summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/inspect.py55
-rwxr-xr-xLib/pydoc.py7
-rw-r--r--Lib/test/test_inspect.py20
-rw-r--r--Lib/test/test_pydoc.py175
4 files changed, 226 insertions, 31 deletions
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 2e3a670..edbf927 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -269,9 +269,9 @@ def getmembers(object, predicate=None):
results = []
processed = set()
names = dir(object)
- # add any virtual attributes to the list of names if object is a class
+ # :dd any DynamicClassAttributes to the list of names if object is a class;
# 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 DynamicClassAttribute exists
try:
for base in object.__bases__:
for k, v in base.__dict__.items():
@@ -329,79 +329,88 @@ def classify_class_attrs(cls):
If one of the items in dir(cls) is stored in the metaclass it will now
be discovered and not have None be listed as the class in which it was
- defined.
+ defined. Any items whose home class cannot be discovered are skipped.
"""
mro = getmro(cls)
metamro = getmro(type(cls)) # for attributes stored in the metaclass
metamro = tuple([cls for cls in metamro if cls not in (type, object)])
- possible_bases = (cls,) + mro + metamro
+ class_bases = (cls,) + mro
+ all_bases = class_bases + metamro
names = dir(cls)
- # add any virtual attributes to the list of names
+ # :dd any DynamicClassAttributes 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
+ # attribute with the same name as a DynamicClassAttribute exists.
for base in mro:
for k, v in base.__dict__.items():
if isinstance(v, types.DynamicClassAttribute):
names.append(k)
result = []
processed = set()
- sentinel = object()
+
for name in names:
# Get the object associated with the name, and where it was defined.
# Normal objects will be looked up with both getattr and directly in
# its class' dict (in case getattr fails [bug #1785], and also to look
# for a docstring).
- # For VirtualAttributes on the second pass we only look in the
+ # For DynamicClassAttributes on the second pass we only look in the
# class's dict.
#
# Getting an obj from the __dict__ sometimes reveals more than
# using getattr. Static and class methods are dramatic examples.
homecls = None
- get_obj = sentinel
- dict_obj = sentinel
+ get_obj = None
+ dict_obj = None
if name not in processed:
try:
if name == '__dict__':
- raise Exception("__dict__ is special, we don't want the proxy")
+ raise Exception("__dict__ is special, don't want the proxy")
get_obj = getattr(cls, name)
except Exception as exc:
pass
else:
homecls = getattr(get_obj, "__objclass__", homecls)
- if homecls not in possible_bases:
+ if homecls not in class_bases:
# if the resulting object does not live somewhere in the
# mro, drop it and search the mro manually
homecls = None
last_cls = None
- last_obj = None
- for srch_cls in ((cls,) + mro):
+ # first look in the classes
+ for srch_cls in class_bases:
srch_obj = getattr(srch_cls, name, None)
- if srch_obj is get_obj:
+ if srch_obj == get_obj:
+ last_cls = srch_cls
+ # then check the metaclasses
+ for srch_cls in metamro:
+ try:
+ srch_obj = srch_cls.__getattr__(cls, name)
+ except AttributeError:
+ continue
+ if srch_obj == 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 all_bases:
if name in base.__dict__:
dict_obj = base.__dict__[name]
- homecls = homecls or base
+ if homecls not in metamro:
+ homecls = base
break
if homecls is None:
# unable to locate the attribute anywhere, most likely due to
# buggy custom __dir__; discard and move on
continue
+ obj = get_obj or dict_obj
# Classify the object or its descriptor.
- if get_obj is not sentinel:
- obj = get_obj
- else:
- obj = dict_obj
if isinstance(dict_obj, staticmethod):
kind = "static method"
+ obj = dict_obj
elif isinstance(dict_obj, classmethod):
kind = "class method"
- elif isinstance(obj, property):
+ obj = dict_obj
+ elif isinstance(dict_obj, property):
kind = "property"
+ obj = dict_obj
elif isfunction(obj) or ismethoddescriptor(obj):
kind = "method"
else:
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 174311c..d0240ff 100755
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -1235,8 +1235,9 @@ location listed above.
doc = getdoc(value)
else:
doc = None
- push(self.docother(getattr(object, name),
- name, mod, maxlen=70, doc=doc) + '\n')
+ push(self.docother(
+ getattr(object, name, None) or homecls.__dict__[name],
+ name, mod, maxlen=70, doc=doc) + '\n')
return attrs
attrs = [(name, kind, cls, value)
@@ -1258,7 +1259,6 @@ location listed above.
else:
tag = "inherited from %s" % classname(thisclass,
object.__module__)
-
# Sort attrs by name.
attrs.sort()
@@ -1273,6 +1273,7 @@ location listed above.
lambda t: t[1] == 'data descriptor')
attrs = spilldata("Data and other attributes %s:\n" % tag, attrs,
lambda t: t[1] == 'data')
+
assert attrs == []
attrs = inherited
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index fb6aa6a..9d34904 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -667,9 +667,19 @@ class TestClassesAndFunctions(unittest.TestCase):
return 'eggs'
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')
+ should_find_ga = inspect.Attribute('ham', 'data', Meta, 'spam')
self.assertIn(should_find_ga, inspect.classify_class_attrs(VA))
+ def test_classify_metaclass_class_attribute(self):
+ class Meta(type):
+ fish = 'slap'
+ def __dir__(self):
+ return ['__class__', '__modules__', '__name__', 'fish']
+ class Class(metaclass=Meta):
+ pass
+ should_find = inspect.Attribute('fish', 'data', Meta, 'slap')
+ self.assertIn(should_find, inspect.classify_class_attrs(Class))
+
def test_classify_VirtualAttribute(self):
class Meta(type):
def __dir__(cls):
@@ -680,7 +690,7 @@ class TestClassesAndFunctions(unittest.TestCase):
return super().__getattr(name)
class Class(metaclass=Meta):
pass
- should_find = inspect.Attribute('BOOM', 'data', Class, 42)
+ should_find = inspect.Attribute('BOOM', 'data', Meta, 42)
self.assertIn(should_find, inspect.classify_class_attrs(Class))
def test_classify_VirtualAttribute_multi_classes(self):
@@ -711,9 +721,9 @@ class TestClassesAndFunctions(unittest.TestCase):
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)
+ should_find1 = inspect.Attribute('one', 'data', Meta1, 1)
+ should_find2 = inspect.Attribute('two', 'data', Meta2, 2)
+ should_find3 = inspect.Attribute('three', 'data', Meta3, 3)
cca = inspect.classify_class_attrs(Class2)
for sf in (should_find1, should_find2, should_find3):
self.assertIn(sf, cca)
diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py
index 399f818..3508763 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -11,6 +11,7 @@ import re
import string
import test.support
import time
+import types
import unittest
import xml.etree
import textwrap
@@ -208,6 +209,77 @@ missing_pattern = "no Python documentation found for '%s'"
# output pattern for module with bad imports
badimport_pattern = "problem in %s - ImportError: No module named %r"
+expected_dynamicattribute_pattern = """
+Help on class DA in module %s:
+
+class DA(builtins.object)
+ | Data descriptors defined here:
+ |
+ | __dict__
+ | dictionary for instance variables (if defined)
+ |
+ | __weakref__
+ | list of weak references to the object (if defined)
+ |
+ | ham
+ |
+ | ----------------------------------------------------------------------
+ | Data and other attributes inherited from Meta:
+ |
+ | ham = 'spam'
+""".strip()
+
+expected_virtualattribute_pattern1 = """
+Help on class Class in module %s:
+
+class Class(builtins.object)
+ | Data and other attributes inherited from Meta:
+ |
+ | LIFE = 42
+""".strip()
+
+expected_virtualattribute_pattern2 = """
+Help on class Class1 in module %s:
+
+class Class1(builtins.object)
+ | Data and other attributes inherited from Meta1:
+ |
+ | one = 1
+""".strip()
+
+expected_virtualattribute_pattern3 = """
+Help on class Class2 in module %s:
+
+class Class2(Class1)
+ | Method resolution order:
+ | Class2
+ | Class1
+ | builtins.object
+ |
+ | Data and other attributes inherited from Meta1:
+ |
+ | one = 1
+ |
+ | ----------------------------------------------------------------------
+ | Data and other attributes inherited from Meta3:
+ |
+ | three = 3
+ |
+ | ----------------------------------------------------------------------
+ | Data and other attributes inherited from Meta2:
+ |
+ | two = 2
+""".strip()
+
+expected_missingattribute_pattern = """
+Help on class C in module %s:
+
+class C(builtins.object)
+ | Data and other attributes defined here:
+ |
+ | here = 'present!'
+""".strip()
+
def run_pydoc(module_name, *args, **env):
"""
Runs pydoc on the specified module. Returns the stripped
@@ -636,6 +708,108 @@ class TestHelper(unittest.TestCase):
self.assertEqual(sorted(pydoc.Helper.keywords),
sorted(keyword.kwlist))
+class PydocWithMetaClasses(unittest.TestCase):
+ def test_DynamicClassAttribute(self):
+ class Meta(type):
+ def __getattr__(self, name):
+ if name == 'ham':
+ return 'spam'
+ return super().__getattr__(name)
+ class DA(metaclass=Meta):
+ @types.DynamicClassAttribute
+ def ham(self):
+ return 'eggs'
+ output = StringIO()
+ helper = pydoc.Helper(output=output)
+ helper(DA)
+ expected_text = expected_dynamicattribute_pattern % __name__
+ result = output.getvalue().strip()
+ if result != expected_text:
+ print_diffs(expected_text, result)
+ self.fail("outputs are not equal, see diff above")
+
+ def test_virtualClassAttributeWithOneMeta(self):
+ class Meta(type):
+ def __dir__(cls):
+ return ['__class__', '__module__', '__name__', 'LIFE']
+ def __getattr__(self, name):
+ if name =='LIFE':
+ return 42
+ return super().__getattr(name)
+ class Class(metaclass=Meta):
+ pass
+ output = StringIO()
+ helper = pydoc.Helper(output=output)
+ helper(Class)
+ expected_text = expected_virtualattribute_pattern1 % __name__
+ result = output.getvalue().strip()
+ if result != expected_text:
+ print_diffs(expected_text, result)
+ self.fail("outputs are not equal, see diff above")
+
+ def test_virtualClassAttributeWithTwoMeta(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
+ fail1 = fail2 = False
+ output = StringIO()
+ helper = pydoc.Helper(output=output)
+ helper(Class1)
+ expected_text1 = expected_virtualattribute_pattern2 % __name__
+ result1 = output.getvalue().strip()
+ if result1 != expected_text1:
+ print_diffs(expected_text1, result1)
+ fail1 = True
+ output = StringIO()
+ helper = pydoc.Helper(output=output)
+ helper(Class2)
+ expected_text2 = expected_virtualattribute_pattern3 % __name__
+ result2 = output.getvalue().strip()
+ if result2 != expected_text2:
+ print_diffs(expected_text2, result2)
+ fail2 = True
+ if fail1 or fail2:
+ self.fail("outputs are not equal, see diff above")
+
+ def test_buggy_dir(self):
+ class M(type):
+ def __dir__(cls):
+ return ['__class__', '__name__', 'missing', 'here']
+ class C(metaclass=M):
+ here = 'present!'
+ output = StringIO()
+ helper = pydoc.Helper(output=output)
+ helper(C)
+ expected_text = expected_missingattribute_pattern % __name__
+ result = output.getvalue().strip()
+ if result != expected_text:
+ print_diffs(expected_text, result)
+ self.fail("outputs are not equal, see diff above")
+
@reap_threads
def test_main():
try:
@@ -645,6 +819,7 @@ def test_main():
PydocServerTest,
PydocUrlHandlerTest,
TestHelper,
+ PydocWithMetaClasses,
)
finally:
reap_children()