Add 'removed_class' class decorator
To augment the problems with @remove on classes, see bug #1520397 and bug #1500851 introduce a class decorator that is specifically made for removing existing classes (and it appears to work correctly even under inheritance). Related-Bug: #1520397 Related-Bug: #1500851 Change-Id: I91adbdacc9fc77511d3f0bfb66d558269c49f885
This commit is contained in:
parent
0132731a38
commit
fc0304e2a6
@ -14,6 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import types
|
||||
import warnings
|
||||
@ -88,6 +89,18 @@ def generate_message(prefix, postfix=None, message=None,
|
||||
return ''.join(message_components)
|
||||
|
||||
|
||||
def get_assigned(decorator):
|
||||
"""Helper to fix/workaround https://bugs.python.org/issue3445"""
|
||||
if six.PY3:
|
||||
return functools.WRAPPER_ASSIGNMENTS
|
||||
else:
|
||||
assigned = []
|
||||
for attr_name in functools.WRAPPER_ASSIGNMENTS:
|
||||
if hasattr(decorator, attr_name):
|
||||
assigned.append(attr_name)
|
||||
return tuple(assigned)
|
||||
|
||||
|
||||
def get_class_name(obj, fully_qualified=True):
|
||||
"""Get class name for object.
|
||||
|
||||
|
@ -36,7 +36,7 @@ def _moved_decorator(kind, new_attribute_name, message=None,
|
||||
if attr_postfix:
|
||||
old_attribute_name += attr_postfix
|
||||
|
||||
@six.wraps(f)
|
||||
@six.wraps(f, assigned=_utils.get_assigned(f))
|
||||
def wrapper(self, *args, **kwargs):
|
||||
base_name = _utils.get_class_name(self, fully_qualified=False)
|
||||
if fully_qualified:
|
||||
@ -75,7 +75,7 @@ def moved_function(new_func, old_func_name, old_module_name,
|
||||
message=message, version=version,
|
||||
removal_version=removal_version)
|
||||
|
||||
@six.wraps(new_func)
|
||||
@six.wraps(new_func, assigned=_utils.get_assigned(new_func))
|
||||
def old_new_func(*args, **kwargs):
|
||||
_utils.deprecation(out_message, stacklevel=stacklevel,
|
||||
category=category)
|
||||
@ -182,7 +182,7 @@ def moved_class(new_class, old_class_name, old_module_name,
|
||||
|
||||
def decorator(f):
|
||||
|
||||
@six.wraps(f, assigned=("__name__", "__doc__"))
|
||||
@six.wraps(f, assigned=_utils.get_assigned(f))
|
||||
def wrapper(self, *args, **kwargs):
|
||||
_utils.deprecation(out_message, stacklevel=stacklevel,
|
||||
category=category)
|
||||
|
@ -167,7 +167,8 @@ def remove(f=None, message=None, version=None, removal_version=None,
|
||||
Due to limitations of the wrapt library (and python) itself, if this
|
||||
is applied to subclasses of metaclasses then it likely will not work
|
||||
as expected. More information can be found at bug #1520397 to see if
|
||||
this situation affects your usage of this *universal* decorator.
|
||||
this situation affects your usage of this *universal* decorator, for
|
||||
this specific scenario please use :py:func:`.removed_class` instead.
|
||||
|
||||
:param str message: A message to include in the deprecation warning
|
||||
:param str version: Specify what version the removed function is present in
|
||||
@ -262,6 +263,39 @@ def removed_kwarg(old_name, message=None,
|
||||
return wrapper
|
||||
|
||||
|
||||
def removed_class(cls_name, replacement=None, message=None,
|
||||
version=None, removal_version=None, stacklevel=3,
|
||||
category=None):
|
||||
"""Decorates a class to denote that it will be removed at some point."""
|
||||
|
||||
def _wrap_it(old_init, out_message):
|
||||
|
||||
@six.wraps(old_init, assigned=_utils.get_assigned(old_init))
|
||||
def new_init(self, *args, **kwargs):
|
||||
_utils.deprecation(out_message, stacklevel=stacklevel,
|
||||
category=category)
|
||||
return old_init(self, *args, **kwargs)
|
||||
|
||||
return new_init
|
||||
|
||||
def _check_it(cls):
|
||||
if not inspect.isclass(cls):
|
||||
_qual, type_name = _utils.get_qualified_name(type(cls))
|
||||
raise TypeError("Unexpected class type '%s' (expected"
|
||||
" class type only)" % type_name)
|
||||
|
||||
def _cls_decorator(cls):
|
||||
_check_it(cls)
|
||||
out_message = _utils.generate_message(
|
||||
"Using class '%s' (either directly or via inheritance)"
|
||||
" is deprecated" % cls_name, postfix=None, message=message,
|
||||
version=version, removal_version=removal_version)
|
||||
cls.__init__ = _wrap_it(cls.__init__, out_message)
|
||||
return cls
|
||||
|
||||
return _cls_decorator
|
||||
|
||||
|
||||
def removed_module(module, replacement=None, message=None,
|
||||
version=None, removal_version=None, stacklevel=3,
|
||||
category=None):
|
||||
|
@ -35,7 +35,7 @@ def renamed_kwarg(old_name, new_name, message=None,
|
||||
|
||||
def decorator(f):
|
||||
|
||||
@six.wraps(f)
|
||||
@six.wraps(f, assigned=_utils.get_assigned(f))
|
||||
def wrapper(*args, **kwargs):
|
||||
if old_name in kwargs:
|
||||
_utils.deprecation(out_message,
|
||||
|
@ -121,6 +121,18 @@ class EFSF_2(object):
|
||||
pass
|
||||
|
||||
|
||||
@removals.removed_class("StarLord")
|
||||
class StarLord(object):
|
||||
def __init__(self):
|
||||
self.name = "star"
|
||||
|
||||
|
||||
class StarLordJr(StarLord):
|
||||
def __init__(self, name):
|
||||
super(StarLordJr, self).__init__()
|
||||
self.name = name
|
||||
|
||||
|
||||
class ThingB(object):
|
||||
@removals.remove()
|
||||
def black_tristars(self):
|
||||
@ -475,6 +487,24 @@ class RemovalTests(test_base.TestCase):
|
||||
w = capture[0]
|
||||
self.assertEqual(PendingDeprecationWarning, w.category)
|
||||
|
||||
def test_pending_warnings_emitted_class_direct(self):
|
||||
with warnings.catch_warnings(record=True) as capture:
|
||||
warnings.simplefilter("always")
|
||||
s = StarLord()
|
||||
self.assertEqual(1, len(capture))
|
||||
w = capture[0]
|
||||
self.assertEqual(DeprecationWarning, w.category)
|
||||
self.assertEqual("star", s.name)
|
||||
|
||||
def test_pending_warnings_emitted_class_inherit(self):
|
||||
with warnings.catch_warnings(record=True) as capture:
|
||||
warnings.simplefilter("always")
|
||||
s = StarLordJr("star_jr")
|
||||
self.assertEqual(1, len(capture))
|
||||
w = capture[0]
|
||||
self.assertEqual(DeprecationWarning, w.category)
|
||||
self.assertEqual("star_jr", s.name)
|
||||
|
||||
def test_warnings_emitted_instancemethod(self):
|
||||
zeon = ThingB()
|
||||
with warnings.catch_warnings(record=True) as capture:
|
||||
|
@ -38,7 +38,7 @@ A basic example to do just this (on a class):
|
||||
>>> from debtcollector import removals
|
||||
>>> import warnings
|
||||
>>> warnings.simplefilter('always')
|
||||
>>> @removals.remove
|
||||
>>> @removals.removed_class("Pinto")
|
||||
... class Pinto(object):
|
||||
... pass
|
||||
...
|
||||
@ -48,7 +48,7 @@ A basic example to do just this (on a class):
|
||||
|
||||
.. testoutput::
|
||||
|
||||
__main__:1: DeprecationWarning: Using class 'Pinto' is deprecated
|
||||
__main__:1: DeprecationWarning: Using class 'Pinto' (either directly or via inheritance) is deprecated
|
||||
|
||||
A basic example to do just this (on a classmethod):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user