Initial import of renames/moves + tests

This adds the initial import (and adjustments to requirements
and code) that was initially targeted to land into oslo.utils
but now lands in this project from the following:

https://review.openstack.org/#/c/140119

This forms the basis of the debtcollector functionality (with
more to come as/when needed).

Change-Id: Icd62622a728525fab48ba4de7ee746d0add73b9b
This commit is contained in:
Joshua Harlow 2015-01-20 13:01:57 -08:00
parent 493c200a05
commit b833207aaa
7 changed files with 330 additions and 30 deletions

61
debtcollector/_utils.py Normal file
View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# 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 warnings
def deprecation(message, stacklevel=None):
"""Warns about some type of deprecation that has been (or will be) made.
This helper function makes it easier to interact with the warnings module
by standardizing the arguments that the warning function recieves so that
it is easier to use.
This should be used to emit warnings to users (users can easily turn these
warnings off/on, see https://docs.python.org/2/library/warnings.html
as they see fit so that the messages do not fill up the users logs with
warnings that they do not wish to see in production) about functions,
methods, attributes or other code that is deprecated and will be removed
in a future release (this is done using these warnings to avoid breaking
existing users of those functions, methods, code; which a library should
avoid doing by always giving at *least* N + 1 release for users to address
the deprecation warnings).
"""
if stacklevel is None:
warnings.warn(message, category=DeprecationWarning)
else:
warnings.warn(message,
category=DeprecationWarning, stacklevel=stacklevel)
def generate_message(prefix, postfix=None, message=None,
version=None, removal_version=None):
"""Helper to generate a common message 'style' for deprecation helpers."""
message_components = [prefix]
if version:
message_components.append(" in version '%s'" % version)
if removal_version:
if removal_version == "?":
message_components.append(" and will be removed in a future"
" version")
else:
message_components.append(" and will be removed in version '%s'"
% removal_version)
if postfix:
message_components.append(postfix)
if message:
message_components.append(": %s" % message)
return ''.join(message_components)

104
debtcollector/moves.py Normal file
View File

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# 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
from oslo_utils import reflection
import six
from debtcollector import _utils
_KIND_MOVED_PREFIX_TPL = "%s '%s' has moved to '%s'"
_CLASS_MOVED_PREFIX_TPL = "Class '%s' has moved to '%s'"
def _moved_decorator(kind, new_attribute_name, message=None,
version=None, removal_version=None, stacklevel=3):
"""Decorates a method/property that was moved to another location."""
def decorator(f):
try:
# Prefer the py3.x name (if we can get at it...)
old_attribute_name = f.__qualname__
fully_qualified = True
except AttributeError:
old_attribute_name = f.__name__
fully_qualified = False
@six.wraps(f)
def wrapper(self, *args, **kwargs):
base_name = reflection.get_class_name(self, fully_qualified=False)
if fully_qualified:
old_name = old_attribute_name
else:
old_name = ".".join((base_name, old_attribute_name))
new_name = ".".join((base_name, new_attribute_name))
prefix = _KIND_MOVED_PREFIX_TPL % (kind, old_name, new_name)
out_message = _utils.generate_message(
prefix, message=message,
version=version, removal_version=removal_version)
_utils.deprecation(out_message, stacklevel=stacklevel)
return f(self, *args, **kwargs)
return wrapper
return decorator
def moved_property(new_attribute_name, message=None,
version=None, removal_version=None, stacklevel=3):
"""Decorates a *instance* property that was moved to another location."""
return _moved_decorator('Property', new_attribute_name, message=message,
version=version, removal_version=removal_version,
stacklevel=stacklevel)
def moved_class(new_class, old_class_name, old_module_name,
message=None, version=None, removal_version=None,
stacklevel=3):
"""Deprecates a class that was moved to another location.
This creates a 'new-old' type that can be used for a
deprecation period that can be inherited from. This will emit warnings
when the old locations class is initialized, telling where the new and
improved location for the old class now is.
"""
old_name = ".".join((old_module_name, old_class_name))
new_name = reflection.get_class_name(new_class)
prefix = _CLASS_MOVED_PREFIX_TPL % (old_name, new_name)
out_message = _utils.generate_message(
prefix, message=message, version=version,
removal_version=removal_version)
def decorator(f):
# Use the older functools until the following is available:
#
# https://bitbucket.org/gutworth/six/issue/105
@functools.wraps(f, assigned=("__name__", "__doc__"))
def wrapper(self, *args, **kwargs):
_utils.deprecation(out_message, stacklevel=stacklevel)
return f(self, *args, **kwargs)
return wrapper
old_class = type(old_class_name, (new_class,), {})
old_class.__module__ = old_module_name
old_class.__init__ = decorator(old_class.__init__)
return old_class

45
debtcollector/renames.py Normal file
View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
#
# 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 six
from debtcollector import _utils
_KWARG_RENAMED_POSTFIX_TPL = ", please use the '%s' argument instead"
_KWARG_RENAMED_PREFIX_TPL = "Using the '%s' argument is deprecated"
def renamed_kwarg(old_name, new_name, message=None,
version=None, removal_version=None, stacklevel=3):
"""Decorates a kwarg accepting function to deprecate a renamed kwarg."""
prefix = _KWARG_RENAMED_PREFIX_TPL % old_name
postfix = _KWARG_RENAMED_POSTFIX_TPL % new_name
out_message = _utils.generate_message(
prefix, postfix=postfix, message=message, version=version,
removal_version=removal_version)
def decorator(f):
@six.wraps(f)
def wrapper(*args, **kwargs):
if old_name in kwargs:
_utils.deprecation(out_message, stacklevel=stacklevel)
return f(*args, **kwargs)
return wrapper
return decorator

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# 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.
"""
test_debtcollector
----------------------------------
Tests for `debtcollector` module.
"""
from debtcollector.tests import base
class TestDebtcollector(base.TestCase):
def test_something(self):
pass

View File

@ -0,0 +1,116 @@
# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved.
#
# 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 warnings
from debtcollector import moves
from debtcollector import renames
from debtcollector.tests import base as test_base
@renames.renamed_kwarg('blip', 'blop')
def blip_blop(blip=1, blop=1):
return (blip, blop)
class WoofWoof(object):
@property
def bark(self):
return 'woof'
@property
@moves.moved_property('bark')
def burk(self):
return self.bark
class NewHotness(object):
def hot(self):
return 'cold'
OldHotness = moves.moved_class(NewHotness, 'OldHotness', __name__)
class MovedInheritableClassTest(test_base.TestCase):
def test_basics(self):
old = OldHotness()
self.assertIsInstance(old, NewHotness)
self.assertEqual('cold', old.hot())
def test_warnings_emitted_creation(self):
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
OldHotness()
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)
def test_existing_refer_subclass(self):
class MyOldThing(OldHotness):
pass
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
MyOldThing()
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)
class MovedPropertyTest(test_base.TestCase):
def test_basics(self):
dog = WoofWoof()
self.assertEqual('woof', dog.burk)
self.assertEqual('woof', dog.bark)
def test_warnings_emitted(self):
dog = WoofWoof()
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
self.assertEqual('woof', dog.burk)
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)
def test_warnings_not_emitted(self):
dog = WoofWoof()
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
self.assertEqual('woof', dog.bark)
self.assertEqual(0, len(capture))
class RenamedKwargTest(test_base.TestCase):
def test_basics(self):
self.assertEqual((1, 1), blip_blop())
self.assertEqual((2, 1), blip_blop(blip=2))
self.assertEqual((1, 2), blip_blop(blop=2))
self.assertEqual((2, 2), blip_blop(blip=2, blop=2))
def test_warnings_emitted(self):
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
self.assertEqual((2, 1), blip_blop(blip=2))
self.assertEqual(1, len(capture))
w = capture[0]
self.assertEqual(DeprecationWarning, w.category)
def test_warnings_not_emitted(self):
with warnings.catch_warnings(record=True) as capture:
warnings.simplefilter("always")
self.assertEqual((1, 2), blip_blop(blop=2))
self.assertEqual(0, len(capture))

View File

@ -3,4 +3,6 @@
# process, which may cause wedges in the gate later.
pbr>=0.6,!=0.7,<1.0
Babel>=1.3
Babel>=1.3
six>=1.7.0
oslo.utils>=1.2.0 # Apache-2.0

View File

@ -12,4 +12,4 @@ oslosphinx
oslotest>=1.1.0.0a1
testrepository>=0.0.18
testscenarios>=0.4
testtools>=0.9.34
testtools>=0.9.34