Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 63c141c

Browse files
committed
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.
1 parent 0e0cd46 commit 63c141c

5 files changed

Lines changed: 131 additions & 39 deletions

File tree

Doc/library/inspect.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ attributes:
173173

174174
.. note::
175175

176-
:func:`getmembers` will only return metaclass attributes when the
177-
argument is a class and those attributes have been listed in a custom
178-
:meth:`__dir__`.
176+
:func:`getmembers` will only return class attributes defined in the
177+
metaclass when the argument is a class and those attributes have been
178+
listed in the metaclass' custom :meth:`__dir__`.
179179

180180

181181
.. function:: getmoduleinfo(path)

Lib/inspect.py

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -280,18 +280,22 @@ def getmembers(object, predicate=None):
280280
except AttributeError:
281281
pass
282282
for key in names:
283-
# First try to get the value via __dict__. Some descriptors don't
284-
# like calling their __get__ (see bug #1785).
285-
for base in mro:
286-
if key in base.__dict__ and key not in processed:
287-
# handle the normal case first; if duplicate entries exist
288-
# they will be handled second
289-
value = base.__dict__[key]
290-
break
291-
else:
292-
try:
293-
value = getattr(object, key)
294-
except AttributeError:
283+
# First try to get the value via getattr. Some descriptors don't
284+
# like calling their __get__ (see bug #1785), so fall back to
285+
# looking in the __dict__.
286+
try:
287+
value = getattr(object, key)
288+
# handle the duplicate key
289+
if key in processed:
290+
raise AttributeError
291+
except AttributeError:
292+
for base in mro:
293+
if key in base.__dict__:
294+
value = base.__dict__[key]
295+
break
296+
else:
297+
# could be a (currently) missing slot member, or a buggy
298+
# __dir__; discard and move on
295299
continue
296300
if not predicate or predicate(value):
297301
results.append((key, value))
@@ -336,7 +340,7 @@ def classify_class_attrs(cls):
336340
# add any virtual attributes to the list of names
337341
# this may result in duplicate entries if, for example, a virtual
338342
# attribute with the same name as a member property exists
339-
for base in cls.__bases__:
343+
for base in mro:
340344
for k, v in base.__dict__.items():
341345
if isinstance(v, types.DynamicClassAttribute):
342346
names.append(k)
@@ -356,47 +360,52 @@ def classify_class_attrs(cls):
356360
homecls = None
357361
get_obj = sentinel
358362
dict_obj = sentinel
359-
360-
361363
if name not in processed:
362364
try:
363365
get_obj = getattr(cls, name)
364366
except Exception as exc:
365367
pass
366368
else:
367-
homecls = getattr(get_obj, "__class__")
368369
homecls = getattr(get_obj, "__objclass__", homecls)
369370
if homecls not in possible_bases:
370371
# if the resulting object does not live somewhere in the
371-
# mro, drop it and go with the dict_obj version only
372+
# mro, drop it and search the mro manually
372373
homecls = None
373-
get_obj = sentinel
374-
374+
last_cls = None
375+
last_obj = None
376+
for srch_cls in ((cls,) + mro):
377+
srch_obj = getattr(srch_cls, name, None)
378+
if srch_obj is get_obj:
379+
last_cls = srch_cls
380+
last_obj = srch_obj
381+
if last_cls is not None:
382+
homecls = last_cls
375383
for base in possible_bases:
376384
if name in base.__dict__:
377385
dict_obj = base.__dict__[name]
378386
homecls = homecls or base
379387
break
380-
388+
if homecls is None:
389+
# unable to locate the attribute anywhere, most likely due to
390+
# buggy custom __dir__; discard and move on
391+
continue
381392
# Classify the object or its descriptor.
382393
if get_obj is not sentinel:
383394
obj = get_obj
384395
else:
385396
obj = dict_obj
386-
if isinstance(obj, staticmethod):
397+
if isinstance(dict_obj, staticmethod):
387398
kind = "static method"
388-
elif isinstance(obj, classmethod):
399+
elif isinstance(dict_obj, classmethod):
389400
kind = "class method"
390401
elif isinstance(obj, property):
391402
kind = "property"
392403
elif isfunction(obj) or ismethoddescriptor(obj):
393404
kind = "method"
394405
else:
395406
kind = "data"
396-
397407
result.append(Attribute(name, kind, homecls, obj))
398408
processed.add(name)
399-
400409
return result
401410

402411
# ----------------------------------------------------------- class helpers

Lib/test/test_inspect.py

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ def __getattr__(self, attr):
126126
def test_get_slot_members(self):
127127
class C(object):
128128
__slots__ = ("a", "b")
129-
130129
x = C()
131130
x.a = 42
132131
members = dict(inspect.getmembers(x))
@@ -469,24 +468,24 @@ class _BrokenDataDescriptor(object):
469468
A broken data descriptor. See bug #1785.
470469
"""
471470
def __get__(*args):
472-
raise AssertionError("should not __get__ data descriptors")
471+
raise AttributeError("broken data descriptor")
473472

474473
def __set__(*args):
475474
raise RuntimeError
476475

477476
def __getattr__(*args):
478-
raise AssertionError("should not __getattr__ data descriptors")
477+
raise AttributeError("broken data descriptor")
479478

480479

481480
class _BrokenMethodDescriptor(object):
482481
"""
483482
A broken method descriptor. See bug #1785.
484483
"""
485484
def __get__(*args):
486-
raise AssertionError("should not __get__ method descriptors")
485+
raise AttributeError("broken method descriptor")
487486

488487
def __getattr__(*args):
489-
raise AssertionError("should not __getattr__ method descriptors")
488+
raise AttributeError("broken method descriptor")
490489

491490

492491
# Helper for testing classify_class_attrs.
@@ -656,13 +655,77 @@ def test_classify_builtin_types(self):
656655
if isinstance(builtin, type):
657656
inspect.classify_class_attrs(builtin)
658657

659-
def test_classify_VirtualAttribute(self):
660-
class VA:
658+
def test_classify_DynamicClassAttribute(self):
659+
class Meta(type):
660+
def __getattr__(self, name):
661+
if name == 'ham':
662+
return 'spam'
663+
return super().__getattr__(name)
664+
class VA(metaclass=Meta):
661665
@types.DynamicClassAttribute
662666
def ham(self):
663667
return 'eggs'
664-
should_find = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham'])
665-
self.assertIn(should_find, inspect.classify_class_attrs(VA))
668+
should_find_dca = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham'])
669+
self.assertIn(should_find_dca, inspect.classify_class_attrs(VA))
670+
should_find_ga = inspect.Attribute('ham', 'data', VA, 'spam')
671+
self.assertIn(should_find_ga, inspect.classify_class_attrs(VA))
672+
673+
def test_classify_VirtualAttribute(self):
674+
class Meta(type):
675+
def __dir__(cls):
676+
return ['__class__', '__module__', '__name__', 'BOOM']
677+
def __getattr__(self, name):
678+
if name =='BOOM':
679+
return 42
680+
return super().__getattr(name)
681+
class Class(metaclass=Meta):
682+
pass
683+
should_find = inspect.Attribute('BOOM', 'data', Class, 42)
684+
self.assertIn(should_find, inspect.classify_class_attrs(Class))
685+
686+
def test_classify_VirtualAttribute_multi_classes(self):
687+
class Meta1(type):
688+
def __dir__(cls):
689+
return ['__class__', '__module__', '__name__', 'one']
690+
def __getattr__(self, name):
691+
if name =='one':
692+
return 1
693+
return super().__getattr__(name)
694+
class Meta2(type):
695+
def __dir__(cls):
696+
return ['__class__', '__module__', '__name__', 'two']
697+
def __getattr__(self, name):
698+
if name =='two':
699+
return 2
700+
return super().__getattr__(name)
701+
class Meta3(Meta1, Meta2):
702+
def __dir__(cls):
703+
return list(sorted(set(['__class__', '__module__', '__name__', 'three'] +
704+
Meta1.__dir__(cls) + Meta2.__dir__(cls))))
705+
def __getattr__(self, name):
706+
if name =='three':
707+
return 3
708+
return super().__getattr__(name)
709+
class Class1(metaclass=Meta1):
710+
pass
711+
class Class2(Class1, metaclass=Meta3):
712+
pass
713+
714+
should_find1 = inspect.Attribute('one', 'data', Class1, 1)
715+
should_find2 = inspect.Attribute('two', 'data', Class2, 2)
716+
should_find3 = inspect.Attribute('three', 'data', Class2, 3)
717+
cca = inspect.classify_class_attrs(Class2)
718+
for sf in (should_find1, should_find2, should_find3):
719+
self.assertIn(sf, cca)
720+
721+
def test_classify_class_attrs_with_buggy_dir(self):
722+
class M(type):
723+
def __dir__(cls):
724+
return ['__class__', '__name__', 'missing']
725+
class C(metaclass=M):
726+
pass
727+
attrs = [a[0] for a in inspect.classify_class_attrs(C)]
728+
self.assertNotIn('missing', attrs)
666729

667730
def test_getmembers_descriptors(self):
668731
class A(object):
@@ -708,11 +771,26 @@ def f(self):
708771
self.assertIn(('f', b.f), inspect.getmembers(b, inspect.ismethod))
709772

710773
def test_getmembers_VirtualAttribute(self):
711-
class A:
774+
class M(type):
775+
def __getattr__(cls, name):
776+
if name == 'eggs':
777+
return 'scrambled'
778+
return super().__getattr__(name)
779+
class A(metaclass=M):
712780
@types.DynamicClassAttribute
713781
def eggs(self):
714782
return 'spam'
715-
self.assertIn(('eggs', A.__dict__['eggs']), inspect.getmembers(A))
783+
self.assertIn(('eggs', 'scrambled'), inspect.getmembers(A))
784+
self.assertIn(('eggs', 'spam'), inspect.getmembers(A()))
785+
786+
def test_getmembers_with_buggy_dir(self):
787+
class M(type):
788+
def __dir__(cls):
789+
return ['__class__', '__name__', 'missing']
790+
class C(metaclass=M):
791+
pass
792+
attrs = [a[0] for a in inspect.getmembers(C)]
793+
self.assertNotIn('missing', attrs)
716794

717795

718796
_global_ref = object()

Lib/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def __init__(self, fget=None, fset=None, fdel=None, doc=None):
117117
self.fset = fset
118118
self.fdel = fdel
119119
# next two lines make DynamicClassAttribute act the same as property
120-
self.__doc__ = doc or fget.__doc__ or self.__doc__
120+
self.__doc__ = doc or fget.__doc__
121121
self.overwrite_doc = doc is None
122122
# support for abstract methods
123123
self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False))

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ Library
135135
- Issue #4366: Fix building extensions on all platforms when --enable-shared
136136
is used.
137137

138+
- Issue #19030: Fixed `inspect.getmembers` and `inspect.classify_class_attrs`
139+
to attempt activating descriptors before falling back to a __dict__ search
140+
for faulty descriptors. `inspect.classify_class_attrs` no longer returns
141+
Attributes whose home class is None.
142+
138143
C API
139144
-----
140145

0 commit comments

Comments
 (0)