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
This commit is contained in:
Joshua Harlow
2014-07-13 21:35:21 -07:00
committed by Joshua Harlow
parent 08b5505419
commit 5c84ddad84
2 changed files with 105 additions and 17 deletions

View File

@@ -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', '<locals>',
'a', '<locals>', '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):

View File

@@ -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)