From 5c84ddad84a70cd658686e4f42604ebdd652f843 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Sun, 13 Jul 2014 21:35:21 -0700 Subject: [PATCH] Use __qualname__ where appropriate The __qualname__ attribute simplifies the determination of an objects class name and callable name and is useful in python 3.x to be able to use since it can correctly identify names better than the python 2.x __name__ attribute can. Adds a few tests in to ensure that the usage of __qualname__ where available functions as expected. See: http://legacy.python.org/dev/peps/pep-3155/ Fixes bug 1341441 Change-Id: Ic6942cbbc8e35d65fb3ac603ff1dfc8e20c194a3 --- taskflow/tests/unit/test_utils.py | 56 ++++++++++++++++++++++++-- taskflow/utils/reflection.py | 66 +++++++++++++++++++++++++------ 2 files changed, 105 insertions(+), 17 deletions(-) diff --git a/taskflow/tests/unit/test_utils.py b/taskflow/tests/unit/test_utils.py index 22da1b8b4..4518f961b 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 b386dfa22..bc5a3223a 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)