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

Skip to content

Commit e03ea37

Browse files
committed
Close #19030: improvements to inspect and Enum.
inspect.getmembers and inspect.classify_class_attrs now search the metaclass mro for types.DynamicClassAttributes (what use to be called enum._RouteClassAttributeToGetattr); in part this means that these two functions no longer rely solely on dir(). Besides now returning more accurate information, these improvements also allow a more helpful help() on Enum classes.
1 parent 7cba5fd commit e03ea37

4 files changed

Lines changed: 153 additions & 58 deletions

File tree

Lib/enum.py

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,10 @@
11
import sys
22
from collections import OrderedDict
3-
from types import MappingProxyType
3+
from types import MappingProxyType, DynamicClassAttribute
44

55
__all__ = ['Enum', 'IntEnum', 'unique']
66

77

8-
class _RouteClassAttributeToGetattr:
9-
"""Route attribute access on a class to __getattr__.
10-
11-
This is a descriptor, used to define attributes that act differently when
12-
accessed through an instance and through a class. Instance access remains
13-
normal, but access to an attribute through a class will be routed to the
14-
class's __getattr__ method; this is done by raising AttributeError.
15-
16-
"""
17-
def __init__(self, fget=None):
18-
self.fget = fget
19-
if fget.__doc__ is not None:
20-
self.__doc__ = fget.__doc__
21-
22-
def __get__(self, instance, ownerclass=None):
23-
if instance is None:
24-
raise AttributeError()
25-
return self.fget(instance)
26-
27-
def __set__(self, instance, value):
28-
raise AttributeError("can't set attribute")
29-
30-
def __delete__(self, instance):
31-
raise AttributeError("can't delete attribute")
32-
33-
348
def _is_descriptor(obj):
359
"""Returns True if obj is a descriptor, False otherwise."""
3610
return (
@@ -504,12 +478,12 @@ def __hash__(self):
504478
# members are not set directly on the enum class -- __getattr__ is
505479
# used to look them up.
506480

507-
@_RouteClassAttributeToGetattr
481+
@DynamicClassAttribute
508482
def name(self):
509483
"""The name of the Enum member."""
510484
return self._name_
511485

512-
@_RouteClassAttributeToGetattr
486+
@DynamicClassAttribute
513487
def value(self):
514488
"""The value of the Enum member."""
515489
return self._value_

Lib/inspect.py

Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,25 @@ def getmembers(object, predicate=None):
267267
else:
268268
mro = ()
269269
results = []
270-
for key in dir(object):
270+
processed = set()
271+
names = dir(object)
272+
# add any virtual attributes to the list of names if object is a class
273+
# this may result in duplicate entries if, for example, a virtual
274+
# attribute with the same name as a member property exists
275+
try:
276+
for base in object.__bases__:
277+
for k, v in base.__dict__.items():
278+
if isinstance(v, types.DynamicClassAttribute):
279+
names.append(k)
280+
except AttributeError:
281+
pass
282+
for key in names:
271283
# First try to get the value via __dict__. Some descriptors don't
272284
# like calling their __get__ (see bug #1785).
273285
for base in mro:
274-
if key in base.__dict__:
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
275289
value = base.__dict__[key]
276290
break
277291
else:
@@ -281,7 +295,8 @@ def getmembers(object, predicate=None):
281295
continue
282296
if not predicate or predicate(value):
283297
results.append((key, value))
284-
results.sort()
298+
processed.add(key)
299+
results.sort(key=lambda pair: pair[0])
285300
return results
286301

287302
Attribute = namedtuple('Attribute', 'name kind defining_class object')
@@ -298,16 +313,15 @@ def classify_class_attrs(cls):
298313
'class method' created via classmethod()
299314
'static method' created via staticmethod()
300315
'property' created via property()
301-
'method' any other flavor of method
316+
'method' any other flavor of method or descriptor
302317
'data' not a method
303318
304319
2. The class which defined this attribute (a class).
305320
306-
3. The object as obtained directly from the defining class's
307-
__dict__, not via getattr. This is especially important for
308-
data attributes: C.data is just a data object, but
309-
C.__dict__['data'] may be a data descriptor with additional
310-
info, like a __doc__ string.
321+
3. The object as obtained by calling getattr; if this fails, or if the
322+
resulting object does not live anywhere in the class' mro (including
323+
metaclasses) then the object is looked up in the defining class's
324+
dict (found by walking the mro).
311325
312326
If one of the items in dir(cls) is stored in the metaclass it will now
313327
be discovered and not have None be listed as the class in which it was
@@ -316,46 +330,72 @@ def classify_class_attrs(cls):
316330

317331
mro = getmro(cls)
318332
metamro = getmro(type(cls)) # for attributes stored in the metaclass
333+
metamro = tuple([cls for cls in metamro if cls not in (type, object)])
334+
possible_bases = (cls,) + mro + metamro
319335
names = dir(cls)
336+
# add any virtual attributes to the list of names
337+
# this may result in duplicate entries if, for example, a virtual
338+
# attribute with the same name as a member property exists
339+
for base in cls.__bases__:
340+
for k, v in base.__dict__.items():
341+
if isinstance(v, types.DynamicClassAttribute):
342+
names.append(k)
320343
result = []
344+
processed = set()
345+
sentinel = object()
321346
for name in names:
322347
# Get the object associated with the name, and where it was defined.
348+
# Normal objects will be looked up with both getattr and directly in
349+
# its class' dict (in case getattr fails [bug #1785], and also to look
350+
# for a docstring).
351+
# For VirtualAttributes on the second pass we only look in the
352+
# class's dict.
353+
#
323354
# Getting an obj from the __dict__ sometimes reveals more than
324355
# using getattr. Static and class methods are dramatic examples.
325-
# Furthermore, some objects may raise an Exception when fetched with
326-
# getattr(). This is the case with some descriptors (bug #1785).
327-
# Thus, we only use getattr() as a last resort.
328356
homecls = None
329-
for base in (cls,) + mro + metamro:
357+
get_obj = sentinel
358+
dict_obj = sentinel
359+
360+
361+
if name not in processed:
362+
try:
363+
get_obj = getattr(cls, name)
364+
except Exception as exc:
365+
pass
366+
else:
367+
homecls = getattr(get_obj, "__class__")
368+
homecls = getattr(get_obj, "__objclass__", homecls)
369+
if homecls not in possible_bases:
370+
# if the resulting object does not live somewhere in the
371+
# mro, drop it and go with the dict_obj version only
372+
homecls = None
373+
get_obj = sentinel
374+
375+
for base in possible_bases:
330376
if name in base.__dict__:
331-
obj = base.__dict__[name]
332-
homecls = base
377+
dict_obj = base.__dict__[name]
378+
homecls = homecls or base
333379
break
334-
else:
335-
obj = getattr(cls, name)
336-
homecls = getattr(obj, "__objclass__", homecls)
337380

338-
# Classify the object.
381+
# Classify the object or its descriptor.
382+
if get_obj is not sentinel:
383+
obj = get_obj
384+
else:
385+
obj = dict_obj
339386
if isinstance(obj, staticmethod):
340387
kind = "static method"
341388
elif isinstance(obj, classmethod):
342389
kind = "class method"
343390
elif isinstance(obj, property):
344391
kind = "property"
345-
elif ismethoddescriptor(obj):
392+
elif isfunction(obj) or ismethoddescriptor(obj):
346393
kind = "method"
347-
elif isdatadescriptor(obj):
348-
kind = "data"
349394
else:
350-
obj_via_getattr = getattr(cls, name)
351-
if (isfunction(obj_via_getattr) or
352-
ismethoddescriptor(obj_via_getattr)):
353-
kind = "method"
354-
else:
355-
kind = "data"
356-
obj = obj_via_getattr
395+
kind = "data"
357396

358397
result.append(Attribute(name, kind, homecls, obj))
398+
processed.add(name)
359399

360400
return result
361401

Lib/test/test_inspect.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,14 @@ def test_classify_builtin_types(self):
652652
if isinstance(builtin, type):
653653
inspect.classify_class_attrs(builtin)
654654

655+
def test_classify_VirtualAttribute(self):
656+
class VA:
657+
@types.DynamicClassAttribute
658+
def ham(self):
659+
return 'eggs'
660+
should_find = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham'])
661+
self.assertIn(should_find, inspect.classify_class_attrs(VA))
662+
655663
def test_getmembers_descriptors(self):
656664
class A(object):
657665
dd = _BrokenDataDescriptor()
@@ -695,6 +703,13 @@ def f(self):
695703
self.assertIn(('f', b.f), inspect.getmembers(b))
696704
self.assertIn(('f', b.f), inspect.getmembers(b, inspect.ismethod))
697705

706+
def test_getmembers_VirtualAttribute(self):
707+
class A:
708+
@types.DynamicClassAttribute
709+
def eggs(self):
710+
return 'spam'
711+
self.assertIn(('eggs', A.__dict__['eggs']), inspect.getmembers(A))
712+
698713

699714
_global_ref = object()
700715
class TestGetClosureVars(unittest.TestCase):
@@ -1082,6 +1097,15 @@ class Thing(object):
10821097

10831098
self.assertEqual(inspect.getattr_static(Thing, 'x'), Thing.x)
10841099

1100+
def test_classVirtualAttribute(self):
1101+
class Thing(object):
1102+
@types.DynamicClassAttribute
1103+
def x(self):
1104+
return self._x
1105+
_x = object()
1106+
1107+
self.assertEqual(inspect.getattr_static(Thing, 'x'), Thing.__dict__['x'])
1108+
10851109
def test_inherited_classattribute(self):
10861110
class Thing(object):
10871111
x = object()

Lib/types.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,60 @@ def _calculate_meta(meta, bases):
9999
"must be a (non-strict) subclass "
100100
"of the metaclasses of all its bases")
101101
return winner
102+
103+
class DynamicClassAttribute:
104+
"""Route attribute access on a class to __getattr__.
105+
106+
This is a descriptor, used to define attributes that act differently when
107+
accessed through an instance and through a class. Instance access remains
108+
normal, but access to an attribute through a class will be routed to the
109+
class's __getattr__ method; this is done by raising AttributeError.
110+
111+
This allows one to have properties active on an instance, and have virtual
112+
attributes on the class with the same name (see Enum for an example).
113+
114+
"""
115+
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
116+
self.fget = fget
117+
self.fset = fset
118+
self.fdel = fdel
119+
# next two lines make DynamicClassAttribute act the same as property
120+
self.__doc__ = doc or fget.__doc__ or self.__doc__
121+
self.overwrite_doc = doc is None
122+
# support for abstract methods
123+
self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False))
124+
125+
def __get__(self, instance, ownerclass=None):
126+
if instance is None:
127+
if self.__isabstractmethod__:
128+
return self
129+
raise AttributeError()
130+
elif self.fget is None:
131+
raise AttributeError("unreadable attribute")
132+
return self.fget(instance)
133+
134+
def __set__(self, instance, value):
135+
if self.fset is None:
136+
raise AttributeError("can't set attribute")
137+
self.fset(instance, value)
138+
139+
def __delete__(self, instance):
140+
if self.fdel is None:
141+
raise AttributeError("can't delete attribute")
142+
self.fdel(instance)
143+
144+
def getter(self, fget):
145+
fdoc = fget.__doc__ if self.overwrite_doc else None
146+
result = type(self)(fget, self.fset, self.fdel, fdoc or self.__doc__)
147+
result.overwrite_doc = self.overwrite_doc
148+
return result
149+
150+
def setter(self, fset):
151+
result = type(self)(self.fget, fset, self.fdel, self.__doc__)
152+
result.overwrite_doc = self.overwrite_doc
153+
return result
154+
155+
def deleter(self, fdel):
156+
result = type(self)(self.fget, self.fset, fdel, self.__doc__)
157+
result.overwrite_doc = self.overwrite_doc
158+
return result

0 commit comments

Comments
 (0)