OLD | NEW |
1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. | 1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr | 2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
3 # | 3 # |
4 # This file is part of logilab-common. | 4 # This file is part of logilab-common. |
5 # | 5 # |
6 # logilab-common is free software: you can redistribute it and/or modify it unde
r | 6 # logilab-common is free software: you can redistribute it and/or modify it unde
r |
7 # the terms of the GNU Lesser General Public License as published by the Free | 7 # the terms of the GNU Lesser General Public License as published by the Free |
8 # Software Foundation, either version 2.1 of the License, or (at your option) an
y | 8 # Software Foundation, either version 2.1 of the License, or (at your option) an
y |
9 # later version. | 9 # later version. |
10 # | 10 # |
11 # logilab-common is distributed in the hope that it will be useful, but WITHOUT | 11 # logilab-common is distributed in the hope that it will be useful, but WITHOUT |
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | 12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more | 13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
14 # details. | 14 # details. |
15 # | 15 # |
16 # You should have received a copy of the GNU Lesser General Public License along | 16 # You should have received a copy of the GNU Lesser General Public License along |
17 # with logilab-common. If not, see <http://www.gnu.org/licenses/>. | 17 # with logilab-common. If not, see <http://www.gnu.org/licenses/>. |
18 """Deprecation utilities.""" | 18 """Deprecation utilities.""" |
19 | 19 |
20 __docformat__ = "restructuredtext en" | 20 __docformat__ = "restructuredtext en" |
21 | 21 |
22 import sys | 22 import sys |
23 from warnings import warn | 23 from warnings import warn |
24 | 24 |
25 from logilab.common.changelog import Version | 25 class class_deprecated(type): |
| 26 """metaclass to print a warning on instantiation of a deprecated class""" |
26 | 27 |
| 28 def __call__(cls, *args, **kwargs): |
| 29 msg = getattr(cls, "__deprecation_warning__", |
| 30 "%(cls)s is deprecated") % {'cls': cls.__name__} |
| 31 warn(msg, DeprecationWarning, stacklevel=2) |
| 32 return type.__call__(cls, *args, **kwargs) |
27 | 33 |
28 class DeprecationWrapper(object): | |
29 """proxy to print a warning on access to any attribute of the wrapped object | |
30 """ | |
31 def __init__(self, proxied, msg=None): | |
32 self._proxied = proxied | |
33 self._msg = msg | |
34 | |
35 def __getattr__(self, attr): | |
36 warn(self._msg, DeprecationWarning, stacklevel=2) | |
37 return getattr(self._proxied, attr) | |
38 | |
39 def __setattr__(self, attr, value): | |
40 if attr in ('_proxied', '_msg'): | |
41 self.__dict__[attr] = value | |
42 else: | |
43 warn(self._msg, DeprecationWarning, stacklevel=2) | |
44 setattr(self._proxied, attr, value) | |
45 | |
46 | |
47 class DeprecationManager(object): | |
48 """Manage the deprecation message handling. Messages are dropped for | |
49 versions more recent than the 'compatible' version. Example:: | |
50 | |
51 deprecator = deprecation.DeprecationManager("module_name") | |
52 deprecator.compatibility('1.3') | |
53 | |
54 deprecator.warn('1.2', "message.") | |
55 | |
56 @deprecator.deprecated('1.2', 'Message') | |
57 def any_func(): | |
58 pass | |
59 | |
60 class AnyClass(object): | |
61 __metaclass__ = deprecator.class_deprecated('1.2') | |
62 """ | |
63 def __init__(self, module_name=None): | |
64 """ | |
65 """ | |
66 self.module_name = module_name | |
67 self.compatible_version = None | |
68 | |
69 def compatibility(self, compatible_version): | |
70 """Set the compatible version. | |
71 """ | |
72 self.compatible_version = Version(compatible_version) | |
73 | |
74 def deprecated(self, version=None, reason=None, stacklevel=2, name=None, doc
=None): | |
75 """Display a deprecation message only if the version is older than the | |
76 compatible version. | |
77 """ | |
78 def decorator(func): | |
79 message = reason or 'The function "%s" is deprecated' | |
80 if '%s' in message: | |
81 message %= func.__name__ | |
82 def wrapped(*args, **kwargs): | |
83 self.warn(version, message, stacklevel+1) | |
84 return func(*args, **kwargs) | |
85 return wrapped | |
86 return decorator | |
87 | |
88 def class_deprecated(self, version=None): | |
89 class metaclass(type): | |
90 """metaclass to print a warning on instantiation of a deprecated cla
ss""" | |
91 | |
92 def __call__(cls, *args, **kwargs): | |
93 msg = getattr(cls, "__deprecation_warning__", | |
94 "%(cls)s is deprecated") % {'cls': cls.__name__} | |
95 self.warn(version, msg, stacklevel=3) | |
96 return type.__call__(cls, *args, **kwargs) | |
97 return metaclass | |
98 | |
99 def moved(self, version, modpath, objname): | |
100 """use to tell that a callable has been moved to a new module. | |
101 | |
102 It returns a callable wrapper, so that when its called a warning is prin
ted | |
103 telling where the object can be found, import is done (and not before) a
nd | |
104 the actual object is called. | |
105 | |
106 NOTE: the usage is somewhat limited on classes since it will fail if the | |
107 wrapper is use in a class ancestors list, use the `class_moved` function | |
108 instead (which has no lazy import feature though). | |
109 """ | |
110 def callnew(*args, **kwargs): | |
111 from logilab.common.modutils import load_module_from_name | |
112 message = "object %s has been moved to module %s" % (objname, modpat
h) | |
113 self.warn(version, message) | |
114 m = load_module_from_name(modpath) | |
115 return getattr(m, objname)(*args, **kwargs) | |
116 return callnew | |
117 | |
118 def class_renamed(self, version, old_name, new_class, message=None): | |
119 clsdict = {} | |
120 if message is None: | |
121 message = '%s is deprecated, use %s' % (old_name, new_class.__name__
) | |
122 clsdict['__deprecation_warning__'] = message | |
123 try: | |
124 # new-style class | |
125 return self.class_deprecated(version)(old_name, (new_class,), clsdic
t) | |
126 except (NameError, TypeError): | |
127 # old-style class | |
128 class DeprecatedClass(new_class): | |
129 """FIXME: There might be a better way to handle old/new-style cl
ass | |
130 """ | |
131 def __init__(self, *args, **kwargs): | |
132 self.warn(version, message, stacklevel=3) | |
133 new_class.__init__(self, *args, **kwargs) | |
134 return DeprecatedClass | |
135 | |
136 def class_moved(self, version, new_class, old_name=None, message=None): | |
137 """nice wrapper around class_renamed when a class has been moved into | |
138 another module | |
139 """ | |
140 if old_name is None: | |
141 old_name = new_class.__name__ | |
142 if message is None: | |
143 message = 'class %s is now available as %s.%s' % ( | |
144 old_name, new_class.__module__, new_class.__name__) | |
145 return self.class_renamed(version, old_name, new_class, message) | |
146 | |
147 def warn(self, version=None, reason="", stacklevel=2): | |
148 """Display a deprecation message only if the version is older than the | |
149 compatible version. | |
150 """ | |
151 if (self.compatible_version is None | |
152 or version is None | |
153 or Version(version) < self.compatible_version): | |
154 if self.module_name and version: | |
155 reason = '[%s %s] %s' % (self.module_name, version, reason) | |
156 elif self.module_name: | |
157 reason = '[%s] %s' % (self.module_name, reason) | |
158 elif version: | |
159 reason = '[%s] %s' % (version, reason) | |
160 warn(reason, DeprecationWarning, stacklevel=stacklevel) | |
161 | |
162 _defaultdeprecator = DeprecationManager() | |
163 | |
164 def deprecated(reason=None, stacklevel=2, name=None, doc=None): | |
165 return _defaultdeprecator.deprecated(None, reason, stacklevel, name, doc) | |
166 | |
167 class_deprecated = _defaultdeprecator.class_deprecated() | |
168 | |
169 def moved(modpath, objname): | |
170 return _defaultdeprecator.moved(None, modpath, objname) | |
171 moved.__doc__ = _defaultdeprecator.moved.__doc__ | |
172 | 34 |
173 def class_renamed(old_name, new_class, message=None): | 35 def class_renamed(old_name, new_class, message=None): |
174 """automatically creates a class which fires a DeprecationWarning | 36 """automatically creates a class which fires a DeprecationWarning |
175 when instantiated. | 37 when instantiated. |
176 | 38 |
177 >>> Set = class_renamed('Set', set, 'Set is now replaced by set') | 39 >>> Set = class_renamed('Set', set, 'Set is now replaced by set') |
178 >>> s = Set() | 40 >>> s = Set() |
179 sample.py:57: DeprecationWarning: Set is now replaced by set | 41 sample.py:57: DeprecationWarning: Set is now replaced by set |
180 s = Set() | 42 s = Set() |
181 >>> | 43 >>> |
182 """ | 44 """ |
183 return _defaultdeprecator.class_renamed(None, old_name, new_class, message) | 45 clsdict = {} |
| 46 if message is None: |
| 47 message = '%s is deprecated, use %s' % (old_name, new_class.__name__) |
| 48 clsdict['__deprecation_warning__'] = message |
| 49 try: |
| 50 # new-style class |
| 51 return class_deprecated(old_name, (new_class,), clsdict) |
| 52 except (NameError, TypeError): |
| 53 # old-style class |
| 54 class DeprecatedClass(new_class): |
| 55 """FIXME: There might be a better way to handle old/new-style class |
| 56 """ |
| 57 def __init__(self, *args, **kwargs): |
| 58 warn(message, DeprecationWarning, stacklevel=2) |
| 59 new_class.__init__(self, *args, **kwargs) |
| 60 return DeprecatedClass |
| 61 |
184 | 62 |
185 def class_moved(new_class, old_name=None, message=None): | 63 def class_moved(new_class, old_name=None, message=None): |
186 return _defaultdeprecator.class_moved(None, new_class, old_name, message) | 64 """nice wrapper around class_renamed when a class has been moved into |
187 class_moved.__doc__ = _defaultdeprecator.class_moved.__doc__ | 65 another module |
| 66 """ |
| 67 if old_name is None: |
| 68 old_name = new_class.__name__ |
| 69 if message is None: |
| 70 message = 'class %s is now available as %s.%s' % ( |
| 71 old_name, new_class.__module__, new_class.__name__) |
| 72 return class_renamed(old_name, new_class, message) |
188 | 73 |
| 74 def deprecated(reason=None, stacklevel=2, name=None, doc=None): |
| 75 """Decorator that raises a DeprecationWarning to print a message |
| 76 when the decorated function is called. |
| 77 """ |
| 78 def deprecated_decorator(func): |
| 79 message = reason or 'The function "%s" is deprecated' |
| 80 if '%s' in message: |
| 81 message = message % func.func_name |
| 82 def wrapped(*args, **kwargs): |
| 83 warn(message, DeprecationWarning, stacklevel=stacklevel) |
| 84 return func(*args, **kwargs) |
| 85 try: |
| 86 wrapped.__name__ = name or func.__name__ |
| 87 except TypeError: # readonly attribute in 2.3 |
| 88 pass |
| 89 wrapped.__doc__ = doc or func.__doc__ |
| 90 return wrapped |
| 91 return deprecated_decorator |
| 92 |
| 93 def moved(modpath, objname): |
| 94 """use to tell that a callable has been moved to a new module. |
| 95 |
| 96 It returns a callable wrapper, so that when its called a warning is printed |
| 97 telling where the object can be found, import is done (and not before) and |
| 98 the actual object is called. |
| 99 |
| 100 NOTE: the usage is somewhat limited on classes since it will fail if the |
| 101 wrapper is use in a class ancestors list, use the `class_moved` function |
| 102 instead (which has no lazy import feature though). |
| 103 """ |
| 104 def callnew(*args, **kwargs): |
| 105 from logilab.common.modutils import load_module_from_name |
| 106 message = "object %s has been moved to module %s" % (objname, modpath) |
| 107 warn(message, DeprecationWarning, stacklevel=2) |
| 108 m = load_module_from_name(modpath) |
| 109 return getattr(m, objname)(*args, **kwargs) |
| 110 return callnew |
| 111 |
| 112 |
OLD | NEW |