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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import types
|
import types
|
||||||
import warnings
|
import warnings
|
||||||
@ -88,6 +89,18 @@ def generate_message(prefix, postfix=None, message=None,
|
|||||||
return ''.join(message_components)
|
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):
|
def get_class_name(obj, fully_qualified=True):
|
||||||
"""Get class name for object.
|
"""Get class name for object.
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ def _moved_decorator(kind, new_attribute_name, message=None,
|
|||||||
if attr_postfix:
|
if attr_postfix:
|
||||||
old_attribute_name += attr_postfix
|
old_attribute_name += attr_postfix
|
||||||
|
|
||||||
@six.wraps(f)
|
@six.wraps(f, assigned=_utils.get_assigned(f))
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
base_name = _utils.get_class_name(self, fully_qualified=False)
|
base_name = _utils.get_class_name(self, fully_qualified=False)
|
||||||
if fully_qualified:
|
if fully_qualified:
|
||||||
@ -75,7 +75,7 @@ def moved_function(new_func, old_func_name, old_module_name,
|
|||||||
message=message, version=version,
|
message=message, version=version,
|
||||||
removal_version=removal_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):
|
def old_new_func(*args, **kwargs):
|
||||||
_utils.deprecation(out_message, stacklevel=stacklevel,
|
_utils.deprecation(out_message, stacklevel=stacklevel,
|
||||||
category=category)
|
category=category)
|
||||||
@ -182,7 +182,7 @@ def moved_class(new_class, old_class_name, old_module_name,
|
|||||||
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
|
|
||||||
@six.wraps(f, assigned=("__name__", "__doc__"))
|
@six.wraps(f, assigned=_utils.get_assigned(f))
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
_utils.deprecation(out_message, stacklevel=stacklevel,
|
_utils.deprecation(out_message, stacklevel=stacklevel,
|
||||||
category=category)
|
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
|
Due to limitations of the wrapt library (and python) itself, if this
|
||||||
is applied to subclasses of metaclasses then it likely will not work
|
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
|
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 message: A message to include in the deprecation warning
|
||||||
:param str version: Specify what version the removed function is present in
|
: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
|
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,
|
def removed_module(module, replacement=None, message=None,
|
||||||
version=None, removal_version=None, stacklevel=3,
|
version=None, removal_version=None, stacklevel=3,
|
||||||
category=None):
|
category=None):
|
||||||
|
@ -35,7 +35,7 @@ def renamed_kwarg(old_name, new_name, message=None,
|
|||||||
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
|
|
||||||
@six.wraps(f)
|
@six.wraps(f, assigned=_utils.get_assigned(f))
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if old_name in kwargs:
|
if old_name in kwargs:
|
||||||
_utils.deprecation(out_message,
|
_utils.deprecation(out_message,
|
||||||
|
@ -121,6 +121,18 @@ class EFSF_2(object):
|
|||||||
pass
|
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):
|
class ThingB(object):
|
||||||
@removals.remove()
|
@removals.remove()
|
||||||
def black_tristars(self):
|
def black_tristars(self):
|
||||||
@ -475,6 +487,24 @@ class RemovalTests(test_base.TestCase):
|
|||||||
w = capture[0]
|
w = capture[0]
|
||||||
self.assertEqual(PendingDeprecationWarning, w.category)
|
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):
|
def test_warnings_emitted_instancemethod(self):
|
||||||
zeon = ThingB()
|
zeon = ThingB()
|
||||||
with warnings.catch_warnings(record=True) as capture:
|
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
|
>>> from debtcollector import removals
|
||||||
>>> import warnings
|
>>> import warnings
|
||||||
>>> warnings.simplefilter('always')
|
>>> warnings.simplefilter('always')
|
||||||
>>> @removals.remove
|
>>> @removals.removed_class("Pinto")
|
||||||
... class Pinto(object):
|
... class Pinto(object):
|
||||||
... pass
|
... pass
|
||||||
...
|
...
|
||||||
@ -48,7 +48,7 @@ A basic example to do just this (on a class):
|
|||||||
|
|
||||||
.. testoutput::
|
.. 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):
|
A basic example to do just this (on a classmethod):
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user