Add a removal decorator

This commit adds a new decorator to mark a function/class as
deprecated in preparation for it's removal.

Change-Id: I4c3cf3ff9abdd7faefeae907a8b73608123fb09d
This commit is contained in:
Matthew Treinish 2015-02-05 19:19:04 -05:00 committed by Joshua Harlow
parent b078a94a5b
commit 497c1b47be
4 changed files with 191 additions and 0 deletions

94
debtcollector/removals.py Normal file
View File

@ -0,0 +1,94 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import functools
import inspect
from oslo_utils import reflection
import wrapt
from debtcollector import _utils
def remove(f=None, message=None, version=None, removal_version=None,
stacklevel=3):
"""Decorates a function, method, or class to emit a 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 removal_version: What version the function will be removed. If
'?' is used this implies an undefined future
version
:param int stacklevel: How many entries deep in the call stack before
ignoring
"""
if f is None:
return functools.partial(remove, message=message,
version=version,
removal_version=removal_version,
stacklevel=stacklevel)
@wrapt.decorator
def wrapper(f, instance, args, kwargs):
try:
# Prefer the py3.x name (if we can get at it...)
f_name = f.__qualname__
qualified = True
if inspect.isclass(f):
_prefix_pre = "Using class"
else:
_prefix_pre = "Using function/method"
except AttributeError:
f_name = f.__name__
qualified = False
if not qualified:
_prefix_pre = "Using function/method"
if instance is None:
# Decorator was used on a class
if inspect.isclass(f):
_prefix_pre = "Using class"
module_name = inspect.getmodule(f).__name__
if module_name == '__main__':
f_name = reflection.get_class_name(
f, fully_qualified=False)
else:
f_name = reflection.get_class_name(
f, fully_qualified=True)
base_name = None
# Decorator was a used on a function
else:
module_name = inspect.getmodule(f).__name__
if module_name != '__main__':
f_name = reflection.get_callable_name(f)
base_name = None
# Decorator was used on a classmethod or instancemethod
else:
base_name = reflection.get_class_name(instance,
fully_qualified=False)
if base_name:
function_name = ".".join([base_name, f_name])
else:
function_name = f_name
else:
function_name = f_name
_prefix = _prefix_pre + " %s is deprecated" % function_name
out_message = _utils.generate_message(
_prefix,
version=version,
removal_version=removal_version,
message=message)
_utils.deprecation(out_message, stacklevel)
return f(*args, **kwargs)
return wrapper(f)

View File

@ -15,6 +15,7 @@
import warnings
from debtcollector import moves
from debtcollector import removals
from debtcollector import renames
from debtcollector.tests import base as test_base
@ -50,6 +51,37 @@ class NewHotness(object):
return 'cold'
@removals.remove()
def crimson_lightning(fake_input=None):
return fake_input
@removals.remove()
def red_comet():
return True
@removals.remove()
class EFSF(object):
pass
class ThingB(object):
@removals.remove()
def black_tristars(self):
pass
@removals.remove()
@classmethod
def white_wolf(cls):
pass
@removals.remove()
@staticmethod
def blue_giant():
pass
OldHotness = moves.moved_class(NewHotness, 'OldHotness', __name__)
@ -147,3 +179,62 @@ class RenamedKwargTest(test_base.TestCase):
warnings.simplefilter("always")
self.assertEqual((1, 2), blip_blop(blop=2))
self.assertEqual(0, len(capture))
class RemovalTests(test_base.TestCase):
def test_function_args(self):
self.assertEqual(666, crimson_lightning(666))
def test_function_noargs(self):
self.assertTrue(red_comet())
def test_warnings_emitted_function_args(self):
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
self.assertEqual(666, crimson_lightning(666))
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)
def test_warnings_emitted_function_noargs(self):
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
self.assertTrue(red_comet())
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)
def test_warnings_emitted_class(self):
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
EFSF()
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)
def test_warnings_emitted_instancemethod(self):
zeon = ThingB()
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
zeon.black_tristars()
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)
def test_warnings_emitted_classmethod(self):
zeon = ThingB()
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
zeon.white_wolf()
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)
def test_warnings_emitted_staticmethod(self):
zeon = ThingB()
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
zeon.blue_giant()
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)

View File

@ -21,3 +21,8 @@ Renames
-------
.. automodule:: debtcollector.renames
Removals
--------
.. automodule:: debtcollector.removals

View File

@ -6,3 +6,4 @@ pbr>=0.6,!=0.7,<1.0
Babel>=1.3
six>=1.7.0
oslo.utils>=1.2.0 # Apache-2.0
wrapt>=1.7.0 # BSD License