blob: 131046e0a5079ad9d5df6acf5f8041116780a9ff [file] [log] [blame]
Matt Riedemann8c5be172014-06-13 04:59:57 -07001# Copyright (c) 2013 OpenStack Foundation
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""
17Helpers for comparing version strings.
18"""
19
20import functools
21import pkg_resources
22
23from tempest.openstack.common.gettextutils import _
24from tempest.openstack.common import log as logging
25
26
27LOG = logging.getLogger(__name__)
28
29
30class deprecated(object):
31 """A decorator to mark callables as deprecated.
32
33 This decorator logs a deprecation message when the callable it decorates is
34 used. The message will include the release where the callable was
35 deprecated, the release where it may be removed and possibly an optional
36 replacement.
37
38 Examples:
39
40 1. Specifying the required deprecated release
41
42 >>> @deprecated(as_of=deprecated.ICEHOUSE)
43 ... def a(): pass
44
45 2. Specifying a replacement:
46
47 >>> @deprecated(as_of=deprecated.ICEHOUSE, in_favor_of='f()')
48 ... def b(): pass
49
50 3. Specifying the release where the functionality may be removed:
51
52 >>> @deprecated(as_of=deprecated.ICEHOUSE, remove_in=+1)
53 ... def c(): pass
54
55 """
56
57 FOLSOM = 'F'
58 GRIZZLY = 'G'
59 HAVANA = 'H'
60 ICEHOUSE = 'I'
61
62 _RELEASES = {
63 'F': 'Folsom',
64 'G': 'Grizzly',
65 'H': 'Havana',
66 'I': 'Icehouse',
67 }
68
69 _deprecated_msg_with_alternative = _(
70 '%(what)s is deprecated as of %(as_of)s in favor of '
71 '%(in_favor_of)s and may be removed in %(remove_in)s.')
72
73 _deprecated_msg_no_alternative = _(
74 '%(what)s is deprecated as of %(as_of)s and may be '
75 'removed in %(remove_in)s. It will not be superseded.')
76
77 def __init__(self, as_of, in_favor_of=None, remove_in=2, what=None):
78 """Initialize decorator
79
80 :param as_of: the release deprecating the callable. Constants
81 are define in this class for convenience.
82 :param in_favor_of: the replacement for the callable (optional)
83 :param remove_in: an integer specifying how many releases to wait
84 before removing (default: 2)
85 :param what: name of the thing being deprecated (default: the
86 callable's name)
87
88 """
89 self.as_of = as_of
90 self.in_favor_of = in_favor_of
91 self.remove_in = remove_in
92 self.what = what
93
94 def __call__(self, func):
95 if not self.what:
96 self.what = func.__name__ + '()'
97
98 @functools.wraps(func)
99 def wrapped(*args, **kwargs):
100 msg, details = self._build_message()
101 LOG.deprecated(msg, details)
102 return func(*args, **kwargs)
103 return wrapped
104
105 def _get_safe_to_remove_release(self, release):
106 # TODO(dstanek): this method will have to be reimplemented once
107 # when we get to the X release because once we get to the Y
108 # release, what is Y+2?
109 new_release = chr(ord(release) + self.remove_in)
110 if new_release in self._RELEASES:
111 return self._RELEASES[new_release]
112 else:
113 return new_release
114
115 def _build_message(self):
116 details = dict(what=self.what,
117 as_of=self._RELEASES[self.as_of],
118 remove_in=self._get_safe_to_remove_release(self.as_of))
119
120 if self.in_favor_of:
121 details['in_favor_of'] = self.in_favor_of
122 msg = self._deprecated_msg_with_alternative
123 else:
124 msg = self._deprecated_msg_no_alternative
125 return msg, details
126
127
128def is_compatible(requested_version, current_version, same_major=True):
129 """Determine whether `requested_version` is satisfied by
130 `current_version`; in other words, `current_version` is >=
131 `requested_version`.
132
133 :param requested_version: version to check for compatibility
134 :param current_version: version to check against
135 :param same_major: if True, the major version must be identical between
136 `requested_version` and `current_version`. This is used when a
137 major-version difference indicates incompatibility between the two
138 versions. Since this is the common-case in practice, the default is
139 True.
140 :returns: True if compatible, False if not
141 """
142 requested_parts = pkg_resources.parse_version(requested_version)
143 current_parts = pkg_resources.parse_version(current_version)
144
145 if same_major and (requested_parts[0] != current_parts[0]):
146 return False
147
148 return current_parts >= requested_parts