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:
Joshua Harlow 2015-12-21 09:07:08 -08:00
parent 0132731a38
commit fc0304e2a6
6 changed files with 84 additions and 7 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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):

View File

@ -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,

View File

@ -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:

View File

@ -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):