mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 05:31:20 +00:00 
			
		
		
		
	Issue #19030: final pieces for proper location of various class attributes located in the metaclass.
Okay, hopefully the very last patch for this issue. :/ I realized when playing with Enum that the metaclass attributes weren't always displayed properly. New patch properly locates DynamicClassAttributes, virtual class attributes (returned by __getattr__ and friends), and metaclass class attributes (if they are also in the metaclass __dir__ method). Also had to change one line in pydoc to get this to work. Added tests in test_inspect and test_pydoc to cover these situations.
This commit is contained in:
		
							parent
							
								
									c93dbe2f9b
								
							
						
					
					
						commit
						b0c84cdaac
					
				
					 4 changed files with 226 additions and 31 deletions
				
			
		|  | @ -269,9 +269,9 @@ def getmembers(object, predicate=None): | ||||||
|     results = [] |     results = [] | ||||||
|     processed = set() |     processed = set() | ||||||
|     names = dir(object) |     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 |     # 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: |     try: | ||||||
|         for base in object.__bases__: |         for base in object.__bases__: | ||||||
|             for k, v in base.__dict__.items(): |             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 |     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 |     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) |     mro = getmro(cls) | ||||||
|     metamro = getmro(type(cls)) # for attributes stored in the metaclass |     metamro = getmro(type(cls)) # for attributes stored in the metaclass | ||||||
|     metamro = tuple([cls for cls in metamro if cls not in (type, object)]) |     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) |     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 |     # 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 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) | ||||||
|     result = [] |     result = [] | ||||||
|     processed = set() |     processed = set() | ||||||
|     sentinel = object() | 
 | ||||||
|     for name in names: |     for name in names: | ||||||
|         # Get the object associated with the name, and where it was defined. |         # Get the object associated with the name, and where it was defined. | ||||||
|         # Normal objects will be looked up with both getattr and directly in |         # 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 |         # its class' dict (in case getattr fails [bug #1785], and also to look | ||||||
|         # for a docstring). |         # 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. |         # class's dict. | ||||||
|         # |         # | ||||||
|         # Getting an obj from the __dict__ sometimes reveals more than |         # Getting an obj from the __dict__ sometimes reveals more than | ||||||
|         # using getattr.  Static and class methods are dramatic examples. |         # using getattr.  Static and class methods are dramatic examples. | ||||||
|         homecls = None |         homecls = None | ||||||
|         get_obj = sentinel |         get_obj = None | ||||||
|         dict_obj = sentinel |         dict_obj = None | ||||||
|         if name not in processed: |         if name not in processed: | ||||||
|             try: |             try: | ||||||
|                 if name == '__dict__': |                 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) |                 get_obj = getattr(cls, name) | ||||||
|             except Exception as exc: |             except Exception as exc: | ||||||
|                 pass |                 pass | ||||||
|             else: |             else: | ||||||
|                 homecls = getattr(get_obj, "__objclass__", homecls) |                 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 |                     # if the resulting object does not live somewhere in the | ||||||
|                     # mro, drop it and search the mro manually |                     # mro, drop it and search the mro manually | ||||||
|                     homecls = None |                     homecls = None | ||||||
|                     last_cls = None |                     last_cls = None | ||||||
|                     last_obj = None |                     # first look in the classes | ||||||
|                     for srch_cls in ((cls,) + mro): |                     for srch_cls in class_bases: | ||||||
|                         srch_obj = getattr(srch_cls, name, None) |                         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_cls = srch_cls | ||||||
|                             last_obj = srch_obj |  | ||||||
|                     if last_cls is not None: |                     if last_cls is not None: | ||||||
|                         homecls = last_cls |                         homecls = last_cls | ||||||
|         for base in possible_bases: |         for base in all_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 |                 if homecls not in metamro: | ||||||
|  |                     homecls = base | ||||||
|                 break |                 break | ||||||
|         if homecls is None: |         if homecls is None: | ||||||
|             # unable to locate the attribute anywhere, most likely due to |             # unable to locate the attribute anywhere, most likely due to | ||||||
|             # buggy custom __dir__; discard and move on |             # buggy custom __dir__; discard and move on | ||||||
|             continue |             continue | ||||||
|  |         obj = get_obj or dict_obj | ||||||
|         # Classify the object or its descriptor. |         # Classify the object or its descriptor. | ||||||
|         if get_obj is not sentinel: |  | ||||||
|             obj = get_obj |  | ||||||
|         else: |  | ||||||
|             obj = dict_obj |  | ||||||
|         if isinstance(dict_obj, staticmethod): |         if isinstance(dict_obj, staticmethod): | ||||||
|             kind = "static method" |             kind = "static method" | ||||||
|  |             obj = dict_obj | ||||||
|         elif isinstance(dict_obj, classmethod): |         elif isinstance(dict_obj, classmethod): | ||||||
|             kind = "class method" |             kind = "class method" | ||||||
|         elif isinstance(obj, property): |             obj = dict_obj | ||||||
|  |         elif isinstance(dict_obj, property): | ||||||
|             kind = "property" |             kind = "property" | ||||||
|  |             obj = dict_obj | ||||||
|         elif isfunction(obj) or ismethoddescriptor(obj): |         elif isfunction(obj) or ismethoddescriptor(obj): | ||||||
|             kind = "method" |             kind = "method" | ||||||
|         else: |         else: | ||||||
|  |  | ||||||
|  | @ -1235,8 +1235,9 @@ def spilldata(msg, attrs, predicate): | ||||||
|                         doc = getdoc(value) |                         doc = getdoc(value) | ||||||
|                     else: |                     else: | ||||||
|                         doc = None |                         doc = None | ||||||
|                     push(self.docother(getattr(object, name), |                     push(self.docother( | ||||||
|                                        name, mod, maxlen=70, doc=doc) + '\n') |                         getattr(object, name, None) or homecls.__dict__[name], | ||||||
|  |                         name, mod, maxlen=70, doc=doc) + '\n') | ||||||
|             return attrs |             return attrs | ||||||
| 
 | 
 | ||||||
|         attrs = [(name, kind, cls, value) |         attrs = [(name, kind, cls, value) | ||||||
|  | @ -1258,7 +1259,6 @@ def spilldata(msg, attrs, predicate): | ||||||
|             else: |             else: | ||||||
|                 tag = "inherited from %s" % classname(thisclass, |                 tag = "inherited from %s" % classname(thisclass, | ||||||
|                                                       object.__module__) |                                                       object.__module__) | ||||||
| 
 |  | ||||||
|             # Sort attrs by name. |             # Sort attrs by name. | ||||||
|             attrs.sort() |             attrs.sort() | ||||||
| 
 | 
 | ||||||
|  | @ -1273,6 +1273,7 @@ def spilldata(msg, attrs, predicate): | ||||||
|                                      lambda t: t[1] == 'data descriptor') |                                      lambda t: t[1] == 'data descriptor') | ||||||
|             attrs = spilldata("Data and other attributes %s:\n" % tag, attrs, |             attrs = spilldata("Data and other attributes %s:\n" % tag, attrs, | ||||||
|                               lambda t: t[1] == 'data') |                               lambda t: t[1] == 'data') | ||||||
|  | 
 | ||||||
|             assert attrs == [] |             assert attrs == [] | ||||||
|             attrs = inherited |             attrs = inherited | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -667,9 +667,19 @@ def ham(self): | ||||||
|                 return 'eggs' |                 return 'eggs' | ||||||
|         should_find_dca = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham']) |         should_find_dca = inspect.Attribute('ham', 'data', VA, VA.__dict__['ham']) | ||||||
|         self.assertIn(should_find_dca, inspect.classify_class_attrs(VA)) |         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)) |         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): |     def test_classify_VirtualAttribute(self): | ||||||
|         class Meta(type): |         class Meta(type): | ||||||
|             def __dir__(cls): |             def __dir__(cls): | ||||||
|  | @ -680,7 +690,7 @@ def __getattr__(self, name): | ||||||
|                 return super().__getattr(name) |                 return super().__getattr(name) | ||||||
|         class Class(metaclass=Meta): |         class Class(metaclass=Meta): | ||||||
|             pass |             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)) |         self.assertIn(should_find, inspect.classify_class_attrs(Class)) | ||||||
| 
 | 
 | ||||||
|     def test_classify_VirtualAttribute_multi_classes(self): |     def test_classify_VirtualAttribute_multi_classes(self): | ||||||
|  | @ -711,9 +721,9 @@ class Class1(metaclass=Meta1): | ||||||
|         class Class2(Class1, metaclass=Meta3): |         class Class2(Class1, metaclass=Meta3): | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|         should_find1 = inspect.Attribute('one', 'data', Class1, 1) |         should_find1 = inspect.Attribute('one', 'data', Meta1, 1) | ||||||
|         should_find2 = inspect.Attribute('two', 'data', Class2, 2) |         should_find2 = inspect.Attribute('two', 'data', Meta2, 2) | ||||||
|         should_find3 = inspect.Attribute('three', 'data', Class2, 3) |         should_find3 = inspect.Attribute('three', 'data', Meta3, 3) | ||||||
|         cca = inspect.classify_class_attrs(Class2) |         cca = inspect.classify_class_attrs(Class2) | ||||||
|         for sf in (should_find1, should_find2, should_find3): |         for sf in (should_find1, should_find2, should_find3): | ||||||
|             self.assertIn(sf, cca) |             self.assertIn(sf, cca) | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
| import string | import string | ||||||
| import test.support | import test.support | ||||||
| import time | import time | ||||||
|  | import types | ||||||
| import unittest | import unittest | ||||||
| import xml.etree | import xml.etree | ||||||
| import textwrap | import textwrap | ||||||
|  | @ -208,6 +209,77 @@ class B(builtins.object) | ||||||
| # output pattern for module with bad imports | # output pattern for module with bad imports | ||||||
| badimport_pattern = "problem in %s - ImportError: No module named %r" | 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): | def run_pydoc(module_name, *args, **env): | ||||||
|     """ |     """ | ||||||
|     Runs pydoc on the specified module. Returns the stripped |     Runs pydoc on the specified module. Returns the stripped | ||||||
|  | @ -636,6 +708,108 @@ def test_keywords(self): | ||||||
|         self.assertEqual(sorted(pydoc.Helper.keywords), |         self.assertEqual(sorted(pydoc.Helper.keywords), | ||||||
|                          sorted(keyword.kwlist)) |                          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 | @reap_threads | ||||||
| def test_main(): | def test_main(): | ||||||
|     try: |     try: | ||||||
|  | @ -645,6 +819,7 @@ def test_main(): | ||||||
|                                   PydocServerTest, |                                   PydocServerTest, | ||||||
|                                   PydocUrlHandlerTest, |                                   PydocUrlHandlerTest, | ||||||
|                                   TestHelper, |                                   TestHelper, | ||||||
|  |                                   PydocWithMetaClasses, | ||||||
|                                   ) |                                   ) | ||||||
|     finally: |     finally: | ||||||
|         reap_children() |         reap_children() | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Ethan Furman
						Ethan Furman