mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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.
This commit is contained in:
parent
0e0cd46227
commit
63c141cacd
5 changed files with 131 additions and 39 deletions
|
@ -173,9 +173,9 @@ attributes:
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
:func:`getmembers` will only return metaclass attributes when the
|
:func:`getmembers` will only return class attributes defined in the
|
||||||
argument is a class and those attributes have been listed in a custom
|
metaclass when the argument is a class and those attributes have been
|
||||||
:meth:`__dir__`.
|
listed in the metaclass' custom :meth:`__dir__`.
|
||||||
|
|
||||||
|
|
||||||
.. function:: getmoduleinfo(path)
|
.. function:: getmoduleinfo(path)
|
||||||
|
|
|
@ -280,18 +280,22 @@ def getmembers(object, predicate=None):
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
for key in names:
|
for key in names:
|
||||||
# First try to get the value via __dict__. Some descriptors don't
|
# First try to get the value via getattr. Some descriptors don't
|
||||||
# like calling their __get__ (see bug #1785).
|
# like calling their __get__ (see bug #1785), so fall back to
|
||||||
for base in mro:
|
# looking in the __dict__.
|
||||||
if key in base.__dict__ and key not in processed:
|
try:
|
||||||
# handle the normal case first; if duplicate entries exist
|
value = getattr(object, key)
|
||||||
# they will be handled second
|
# handle the duplicate key
|
||||||
value = base.__dict__[key]
|
if key in processed:
|
||||||
break
|
raise AttributeError
|
||||||
else:
|
except AttributeError:
|
||||||
try:
|
for base in mro:
|
||||||
value = getattr(object, key)
|
if key in base.__dict__:
|
||||||
except AttributeError:
|
value = base.__dict__[key]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# could be a (currently) missing slot member, or a buggy
|
||||||
|
# __dir__; discard and move on
|
||||||
continue
|
continue
|
||||||
if not predicate or predicate(value):
|
if not predicate or predicate(value):
|
||||||
results.append((key, value))
|
results.append((key, value))
|
||||||
|
@ -336,7 +340,7 @@ def classify_class_attrs(cls):
|
||||||
# add any virtual attributes to the list of names
|
# add any virtual attributes to the list of names
|
||||||
# this may result in duplicate entries if, for example, a virtual
|
# this may result in duplicate entries if, for example, a virtual
|
||||||
# attribute with the same name as a member property exists
|
# 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():
|
for k, v in base.__dict__.items():
|
||||||
if isinstance(v, types.DynamicClassAttribute):
|
if isinstance(v, types.DynamicClassAttribute):
|
||||||
names.append(k)
|
names.append(k)
|
||||||
|
@ -356,36 +360,43 @@ def classify_class_attrs(cls):
|
||||||
homecls = None
|
homecls = None
|
||||||
get_obj = sentinel
|
get_obj = sentinel
|
||||||
dict_obj = sentinel
|
dict_obj = sentinel
|
||||||
|
|
||||||
|
|
||||||
if name not in processed:
|
if name not in processed:
|
||||||
try:
|
try:
|
||||||
get_obj = getattr(cls, name)
|
get_obj = getattr(cls, name)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
homecls = getattr(get_obj, "__class__")
|
|
||||||
homecls = getattr(get_obj, "__objclass__", homecls)
|
homecls = getattr(get_obj, "__objclass__", homecls)
|
||||||
if homecls not in possible_bases:
|
if homecls not in possible_bases:
|
||||||
# if the resulting object does not live somewhere in the
|
# 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
|
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:
|
for base in possible_bases:
|
||||||
if name in base.__dict__:
|
if name in base.__dict__:
|
||||||
dict_obj = base.__dict__[name]
|
dict_obj = base.__dict__[name]
|
||||||
homecls = homecls or base
|
homecls = homecls or base
|
||||||
break
|
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.
|
# Classify the object or its descriptor.
|
||||||
if get_obj is not sentinel:
|
if get_obj is not sentinel:
|
||||||
obj = get_obj
|
obj = get_obj
|
||||||
else:
|
else:
|
||||||
obj = dict_obj
|
obj = dict_obj
|
||||||
if isinstance(obj, staticmethod):
|
if isinstance(dict_obj, staticmethod):
|
||||||
kind = "static method"
|
kind = "static method"
|
||||||
elif isinstance(obj, classmethod):
|
elif isinstance(dict_obj, classmethod):
|
||||||
kind = "class method"
|
kind = "class method"
|
||||||
elif isinstance(obj, property):
|
elif isinstance(obj, property):
|
||||||
kind = "property"
|
kind = "property"
|
||||||
|
@ -393,10 +404,8 @@ def classify_class_attrs(cls):
|
||||||
kind = "method"
|
kind = "method"
|
||||||
else:
|
else:
|
||||||
kind = "data"
|
kind = "data"
|
||||||
|
|
||||||
result.append(Attribute(name, kind, homecls, obj))
|
result.append(Attribute(name, kind, homecls, obj))
|
||||||
processed.add(name)
|
processed.add(name)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# ----------------------------------------------------------- class helpers
|
# ----------------------------------------------------------- class helpers
|
||||||
|
|
|
@ -126,7 +126,6 @@ class TestPredicates(IsTestBase):
|
||||||
def test_get_slot_members(self):
|
def test_get_slot_members(self):
|
||||||
class C(object):
|
class C(object):
|
||||||
__slots__ = ("a", "b")
|
__slots__ = ("a", "b")
|
||||||
|
|
||||||
x = C()
|
x = C()
|
||||||
x.a = 42
|
x.a = 42
|
||||||
members = dict(inspect.getmembers(x))
|
members = dict(inspect.getmembers(x))
|
||||||
|
@ -469,13 +468,13 @@ class _BrokenDataDescriptor(object):
|
||||||
A broken data descriptor. See bug #1785.
|
A broken data descriptor. See bug #1785.
|
||||||
"""
|
"""
|
||||||
def __get__(*args):
|
def __get__(*args):
|
||||||
raise AssertionError("should not __get__ data descriptors")
|
raise AttributeError("broken data descriptor")
|
||||||
|
|
||||||
def __set__(*args):
|
def __set__(*args):
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
|
||||||
def __getattr__(*args):
|
def __getattr__(*args):
|
||||||
raise AssertionError("should not __getattr__ data descriptors")
|
raise AttributeError("broken data descriptor")
|
||||||
|
|
||||||
|
|
||||||
class _BrokenMethodDescriptor(object):
|
class _BrokenMethodDescriptor(object):
|
||||||
|
@ -483,10 +482,10 @@ class _BrokenMethodDescriptor(object):
|
||||||
A broken method descriptor. See bug #1785.
|
A broken method descriptor. See bug #1785.
|
||||||
"""
|
"""
|
||||||
def __get__(*args):
|
def __get__(*args):
|
||||||
raise AssertionError("should not __get__ method descriptors")
|
raise AttributeError("broken method descriptor")
|
||||||
|
|
||||||
def __getattr__(*args):
|
def __getattr__(*args):
|
||||||
raise AssertionError("should not __getattr__ method descriptors")
|
raise AttributeError("broken method descriptor")
|
||||||
|
|
||||||
|
|
||||||
# Helper for testing classify_class_attrs.
|
# Helper for testing classify_class_attrs.
|
||||||
|
@ -656,13 +655,77 @@ class TestClassesAndFunctions(unittest.TestCase):
|
||||||
if isinstance(builtin, type):
|
if isinstance(builtin, type):
|
||||||
inspect.classify_class_attrs(builtin)
|
inspect.classify_class_attrs(builtin)
|
||||||
|
|
||||||
def test_classify_VirtualAttribute(self):
|
def test_classify_DynamicClassAttribute(self):
|
||||||
class VA:
|
class Meta(type):
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name == 'ham':
|
||||||
|
return 'spam'
|
||||||
|
return super().__getattr__(name)
|
||||||
|
class VA(metaclass=Meta):
|
||||||
@types.DynamicClassAttribute
|
@types.DynamicClassAttribute
|
||||||
def ham(self):
|
def ham(self):
|
||||||
return 'eggs'
|
return 'eggs'
|
||||||
should_find = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham'])
|
should_find_dca = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham'])
|
||||||
self.assertIn(should_find, inspect.classify_class_attrs(VA))
|
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):
|
def test_getmembers_descriptors(self):
|
||||||
class A(object):
|
class A(object):
|
||||||
|
@ -708,11 +771,26 @@ class TestClassesAndFunctions(unittest.TestCase):
|
||||||
self.assertIn(('f', b.f), inspect.getmembers(b, inspect.ismethod))
|
self.assertIn(('f', b.f), inspect.getmembers(b, inspect.ismethod))
|
||||||
|
|
||||||
def test_getmembers_VirtualAttribute(self):
|
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
|
@types.DynamicClassAttribute
|
||||||
def eggs(self):
|
def eggs(self):
|
||||||
return 'spam'
|
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()
|
_global_ref = object()
|
||||||
|
|
|
@ -117,7 +117,7 @@ class DynamicClassAttribute:
|
||||||
self.fset = fset
|
self.fset = fset
|
||||||
self.fdel = fdel
|
self.fdel = fdel
|
||||||
# next two lines make DynamicClassAttribute act the same as property
|
# 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
|
self.overwrite_doc = doc is None
|
||||||
# support for abstract methods
|
# support for abstract methods
|
||||||
self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False))
|
self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False))
|
||||||
|
|
|
@ -135,6 +135,11 @@ Library
|
||||||
- Issue #4366: Fix building extensions on all platforms when --enable-shared
|
- Issue #4366: Fix building extensions on all platforms when --enable-shared
|
||||||
is used.
|
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
|
C API
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue