diff --git a/taskflow/tests/unit/test_utils.py b/taskflow/tests/unit/test_utils.py index 22da1b8b..4518f961 100644 --- a/taskflow/tests/unit/test_utils.py +++ b/taskflow/tests/unit/test_utils.py @@ -19,6 +19,9 @@ import functools import inspect import sys +import six +import testtools + from taskflow import states from taskflow import test from taskflow.tests import utils as test_utils @@ -111,17 +114,22 @@ class GetCallableNameTest(test.TestCase): def test_method(self): name = reflection.get_callable_name(Class.method) - self.assertEqual(name, '.'.join((__name__, 'method'))) + self.assertEqual(name, '.'.join((__name__, 'Class', 'method'))) def test_instance_method(self): name = reflection.get_callable_name(Class().method) self.assertEqual(name, '.'.join((__name__, 'Class', 'method'))) def test_static_method(self): - # NOTE(imelnikov): static method are just functions, class name - # is not recorded anywhere in them. name = reflection.get_callable_name(Class.static_method) - self.assertEqual(name, '.'.join((__name__, 'static_method'))) + if six.PY3: + self.assertEqual(name, + '.'.join((__name__, 'Class', 'static_method'))) + else: + # NOTE(imelnikov): static method are just functions, class name + # is not recorded anywhere in them. + self.assertEqual(name, + '.'.join((__name__, 'static_method'))) def test_class_method(self): name = reflection.get_callable_name(Class.class_method) @@ -141,6 +149,46 @@ class GetCallableNameTest(test.TestCase): '__call__'))) +# These extended/special case tests only work on python 3, due to python 2 +# being broken/incorrect with regard to these special cases... +@testtools.skipIf(not six.PY3, 'python 3.x is not currently available') +class GetCallableNameTestExtended(test.TestCase): + # Tests items in http://legacy.python.org/dev/peps/pep-3155/ + + class InnerCallableClass(object): + def __call__(self): + pass + + def test_inner_callable_class(self): + obj = self.InnerCallableClass() + name = reflection.get_callable_name(obj.__call__) + expected_name = '.'.join((__name__, 'GetCallableNameTestExtended', + 'InnerCallableClass', '__call__')) + self.assertEqual(expected_name, name) + + def test_inner_callable_function(self): + def a(): + + def b(): + pass + + return b + + name = reflection.get_callable_name(a()) + expected_name = '.'.join((__name__, 'GetCallableNameTestExtended', + 'test_inner_callable_function', '', + 'a', '', 'b')) + self.assertEqual(expected_name, name) + + def test_inner_class(self): + obj = self.InnerCallableClass() + name = reflection.get_callable_name(obj) + expected_name = '.'.join((__name__, + 'GetCallableNameTestExtended', + 'InnerCallableClass')) + self.assertEqual(expected_name, name) + + class NotifierTest(test.TestCase): def test_notify_called(self): diff --git a/taskflow/utils/reflection.py b/taskflow/utils/reflection.py index b386dfa2..bc5a3223 100644 --- a/taskflow/utils/reflection.py +++ b/taskflow/utils/reflection.py @@ -21,6 +21,16 @@ import six from taskflow.openstack.common import importutils +try: + _TYPE_TYPE = types.TypeType +except AttributeError: + _TYPE_TYPE = type + +# See: https://docs.python.org/2/library/__builtin__.html#module-__builtin__ +# and see https://docs.python.org/2/reference/executionmodel.html (and likely +# others)... +_BUILTIN_MODULES = ('builtins', '__builtin__', 'exceptions') + def _get_members(obj, exclude_hidden): """Yields the members of an object, filtering by hidden/not hidden.""" @@ -86,12 +96,27 @@ def get_class_name(obj, fully_qualified=True): """ if not isinstance(obj, six.class_types): obj = type(obj) - if obj.__module__ in ('builtins', '__builtin__', 'exceptions'): - return obj.__name__ - if fully_qualified: - return '.'.join((obj.__module__, obj.__name__)) + try: + built_in = obj.__module__ in _BUILTIN_MODULES + except AttributeError: + pass else: - return obj.__name__ + if built_in: + try: + return obj.__qualname__ + except AttributeError: + return obj.__name__ + pieces = [] + try: + pieces.append(obj.__qualname__) + except AttributeError: + pieces.append(obj.__name__) + if fully_qualified: + try: + pieces.insert(0, obj.__module__) + except AttributeError: + pass + return '.'.join(pieces) def get_all_class_names(obj, up_to=object): @@ -115,21 +140,36 @@ def get_callable_name(function): """ method_self = get_method_self(function) if method_self is not None: - # this is bound method + # This is a bound method. if isinstance(method_self, six.class_types): - # this is bound class method + # This is a bound class method. im_class = method_self else: im_class = type(method_self) - parts = (im_class.__module__, im_class.__name__, - function.__name__) - elif inspect.isfunction(function) or inspect.ismethod(function): - parts = (function.__module__, function.__name__) + try: + parts = (im_class.__module__, function.__qualname__) + except AttributeError: + parts = (im_class.__module__, im_class.__name__, function.__name__) + elif inspect.ismethod(function) or inspect.isfunction(function): + # This could be a function, a static method, a unbound method... + try: + parts = (function.__module__, function.__qualname__) + except AttributeError: + if hasattr(function, 'im_class'): + # This is a unbound method, which exists only in python 2.x + im_class = function.im_class + parts = (im_class.__module__, + im_class.__name__, function.__name__) + else: + parts = (function.__module__, function.__name__) else: im_class = type(function) - if im_class is type: + if im_class is _TYPE_TYPE: im_class = function - parts = (im_class.__module__, im_class.__name__) + try: + parts = (im_class.__module__, im_class.__qualname__) + except AttributeError: + parts = (im_class.__module__, im_class.__name__) return '.'.join(parts)