[Python-checkins] cpython: Close #19030: inspect.getmembers and inspect.classify_class_attrs
ethan.furman
python-checkins at python.org
Fri Oct 18 09:27:45 CEST 2013
http://hg.python.org/cpython/rev/39b06c3fbe2e
changeset: 86419:39b06c3fbe2e
user: Ethan Furman <ethan at stoneleaf.us>
date: Fri Oct 18 00:27:39 2013 -0700
summary:
Close #19030: inspect.getmembers and inspect.classify_class_attrs
Order of search is now:
1. Try getattr
2. If that throws an exception, check __dict__ directly
3. If still not found, walk the mro looking for the eldest class that has
the attribute (e.g. things returned by __getattr__)
4. If none of that works (e.g. due to a buggy __dir__, __getattr__, etc.
method or missing __slot__ attribute), ignore the attribute entirely.
files:
Doc/library/inspect.rst | 6 +-
Lib/inspect.py | 57 ++++++++------
Lib/test/test_inspect.py | 100 ++++++++++++++++++++++++--
Lib/types.py | 2 +-
Misc/NEWS | 5 +
5 files changed, 131 insertions(+), 39 deletions(-)
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -173,9 +173,9 @@
.. note::
- :func:`getmembers` will only return metaclass attributes when the
- argument is a class and those attributes have been listed in a custom
- :meth:`__dir__`.
+ :func:`getmembers` will only return class attributes defined in the
+ metaclass when the argument is a class and those attributes have been
+ listed in the metaclass' custom :meth:`__dir__`.
.. function:: getmoduleinfo(path)
diff --git a/Lib/inspect.py b/Lib/inspect.py
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -280,18 +280,22 @@
except AttributeError:
pass
for key in names:
- # First try to get the value via __dict__. Some descriptors don't
- # like calling their __get__ (see bug #1785).
- for base in mro:
- if key in base.__dict__ and key not in processed:
- # handle the normal case first; if duplicate entries exist
- # they will be handled second
- value = base.__dict__[key]
- break
- else:
- try:
- value = getattr(object, key)
- except AttributeError:
+ # First try to get the value via getattr. Some descriptors don't
+ # like calling their __get__ (see bug #1785), so fall back to
+ # looking in the __dict__.
+ try:
+ value = getattr(object, key)
+ # handle the duplicate key
+ if key in processed:
+ raise AttributeError
+ except AttributeError:
+ for base in mro:
+ if key in base.__dict__:
+ value = base.__dict__[key]
+ break
+ else:
+ # could be a (currently) missing slot member, or a buggy
+ # __dir__; discard and move on
continue
if not predicate or predicate(value):
results.append((key, value))
@@ -336,7 +340,7 @@
# add any virtual attributes to the list of names
# this may result in duplicate entries if, for example, a virtual
# attribute with the same name as a member property exists
- for base in cls.__bases__:
+ for base in mro:
for k, v in base.__dict__.items():
if isinstance(v, types.DynamicClassAttribute):
names.append(k)
@@ -356,36 +360,43 @@
homecls = None
get_obj = sentinel
dict_obj = sentinel
-
-
if name not in processed:
try:
get_obj = getattr(cls, name)
except Exception as exc:
pass
else:
- homecls = getattr(get_obj, "__class__")
homecls = getattr(get_obj, "__objclass__", homecls)
if homecls not in possible_bases:
# if the resulting object does not live somewhere in the
- # mro, drop it and go with the dict_obj version only
+ # mro, drop it and search the mro manually
homecls = None
- get_obj = sentinel
-
+ last_cls = None
+ last_obj = None
+ for srch_cls in ((cls,) + mro):
+ srch_obj = getattr(srch_cls, name, None)
+ if srch_obj is get_obj:
+ last_cls = srch_cls
+ last_obj = srch_obj
+ if last_cls is not None:
+ homecls = last_cls
for base in possible_bases:
if name in base.__dict__:
dict_obj = base.__dict__[name]
homecls = homecls or base
break
-
+ if homecls is None:
+ # unable to locate the attribute anywhere, most likely due to
+ # buggy custom __dir__; discard and move on
+ continue
# Classify the object or its descriptor.
if get_obj is not sentinel:
obj = get_obj
else:
obj = dict_obj
- if isinstance(obj, staticmethod):
+ if isinstance(dict_obj, staticmethod):
kind = "static method"
- elif isinstance(obj, classmethod):
+ elif isinstance(dict_obj, classmethod):
kind = "class method"
elif isinstance(obj, property):
kind = "property"
@@ -393,10 +404,8 @@
kind = "method"
else:
kind = "data"
-
result.append(Attribute(name, kind, homecls, obj))
processed.add(name)
-
return result
# ----------------------------------------------------------- class helpers
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -126,7 +126,6 @@
def test_get_slot_members(self):
class C(object):
__slots__ = ("a", "b")
-
x = C()
x.a = 42
members = dict(inspect.getmembers(x))
@@ -469,13 +468,13 @@
A broken data descriptor. See bug #1785.
"""
def __get__(*args):
- raise AssertionError("should not __get__ data descriptors")
+ raise AttributeError("broken data descriptor")
def __set__(*args):
raise RuntimeError
def __getattr__(*args):
- raise AssertionError("should not __getattr__ data descriptors")
+ raise AttributeError("broken data descriptor")
class _BrokenMethodDescriptor(object):
@@ -483,10 +482,10 @@
A broken method descriptor. See bug #1785.
"""
def __get__(*args):
- raise AssertionError("should not __get__ method descriptors")
+ raise AttributeError("broken method descriptor")
def __getattr__(*args):
- raise AssertionError("should not __getattr__ method descriptors")
+ raise AttributeError("broken method descriptor")
# Helper for testing classify_class_attrs.
@@ -656,13 +655,77 @@
if isinstance(builtin, type):
inspect.classify_class_attrs(builtin)
- def test_classify_VirtualAttribute(self):
- class VA:
+ def test_classify_DynamicClassAttribute(self):
+ class Meta(type):
+ def __getattr__(self, name):
+ if name == 'ham':
+ return 'spam'
+ return super().__getattr__(name)
+ class VA(metaclass=Meta):
@types.DynamicClassAttribute
def ham(self):
return 'eggs'
- should_find = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham'])
- self.assertIn(should_find, inspect.classify_class_attrs(VA))
+ should_find_dca = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham'])
+ self.assertIn(should_find_dca, inspect.classify_class_attrs(VA))
+ should_find_ga = inspect.Attribute('ham', 'data', VA, 'spam')
+ self.assertIn(should_find_ga, inspect.classify_class_attrs(VA))
+
+ def test_classify_VirtualAttribute(self):
+ class Meta(type):
+ def __dir__(cls):
+ return ['__class__', '__module__', '__name__', 'BOOM']
+ def __getattr__(self, name):
+ if name =='BOOM':
+ return 42
+ return super().__getattr(name)
+ class Class(metaclass=Meta):
+ pass
+ should_find = inspect.Attribute('BOOM', 'data', Class, 42)
+ self.assertIn(should_find, inspect.classify_class_attrs(Class))
+
+ def test_classify_VirtualAttribute_multi_classes(self):
+ class Meta1(type):
+ def __dir__(cls):
+ return ['__class__', '__module__', '__name__', 'one']
+ def __getattr__(self, name):
+ if name =='one':
+ return 1
+ return super().__getattr__(name)
+ class Meta2(type):
+ def __dir__(cls):
+ return ['__class__', '__module__', '__name__', 'two']
+ def __getattr__(self, name):
+ if name =='two':
+ return 2
+ return super().__getattr__(name)
+ class Meta3(Meta1, Meta2):
+ def __dir__(cls):
+ return list(sorted(set(['__class__', '__module__', '__name__', 'three'] +
+ Meta1.__dir__(cls) + Meta2.__dir__(cls))))
+ def __getattr__(self, name):
+ if name =='three':
+ return 3
+ return super().__getattr__(name)
+ class Class1(metaclass=Meta1):
+ pass
+ class Class2(Class1, metaclass=Meta3):
+ pass
+
+ should_find1 = inspect.Attribute('one', 'data', Class1, 1)
+ should_find2 = inspect.Attribute('two', 'data', Class2, 2)
+ should_find3 = inspect.Attribute('three', 'data', Class2, 3)
+ cca = inspect.classify_class_attrs(Class2)
+ for sf in (should_find1, should_find2, should_find3):
+ self.assertIn(sf, cca)
+
+ def test_classify_class_attrs_with_buggy_dir(self):
+ class M(type):
+ def __dir__(cls):
+ return ['__class__', '__name__', 'missing']
+ class C(metaclass=M):
+ pass
+ attrs = [a[0] for a in inspect.classify_class_attrs(C)]
+ self.assertNotIn('missing', attrs)
def test_getmembers_descriptors(self):
class A(object):
@@ -708,11 +771,26 @@
self.assertIn(('f', b.f), inspect.getmembers(b, inspect.ismethod))
def test_getmembers_VirtualAttribute(self):
- class A:
+ class M(type):
+ def __getattr__(cls, name):
+ if name == 'eggs':
+ return 'scrambled'
+ return super().__getattr__(name)
+ class A(metaclass=M):
@types.DynamicClassAttribute
def eggs(self):
return 'spam'
- self.assertIn(('eggs', A.__dict__['eggs']), inspect.getmembers(A))
+ self.assertIn(('eggs', 'scrambled'), inspect.getmembers(A))
+ self.assertIn(('eggs', 'spam'), inspect.getmembers(A()))
+
+ def test_getmembers_with_buggy_dir(self):
+ class M(type):
+ def __dir__(cls):
+ return ['__class__', '__name__', 'missing']
+ class C(metaclass=M):
+ pass
+ attrs = [a[0] for a in inspect.getmembers(C)]
+ self.assertNotIn('missing', attrs)
_global_ref = object()
diff --git a/Lib/types.py b/Lib/types.py
--- a/Lib/types.py
+++ b/Lib/types.py
@@ -117,7 +117,7 @@
self.fset = fset
self.fdel = fdel
# next two lines make DynamicClassAttribute act the same as property
- self.__doc__ = doc or fget.__doc__ or self.__doc__
+ self.__doc__ = doc or fget.__doc__
self.overwrite_doc = doc is None
# support for abstract methods
self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False))
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -135,6 +135,11 @@
- Issue #4366: Fix building extensions on all platforms when --enable-shared
is used.
+- Issue #19030: Fixed `inspect.getmembers` and `inspect.classify_class_attrs`
+ to attempt activating descriptors before falling back to a __dict__ search
+ for faulty descriptors. `inspect.classify_class_attrs` no longer returns
+ Attributes whose home class is None.
+
C API
-----
--
Repository URL: http://hg.python.org/cpython
More information about the Python-checkins
mailing list