diff options
-rw-r--r-- | ChangeLog | 5 | ||||
-rw-r--r-- | astroid/bases.py | 8 | ||||
-rw-r--r-- | astroid/scoped_nodes.py | 59 | ||||
-rw-r--r-- | astroid/tests/unittest_scoped_nodes.py | 81 |
4 files changed, 146 insertions, 7 deletions
@@ -240,6 +240,11 @@ Change log for the astroid package (used to be astng) * Add get_wrapping_class API to scoped_nodes, which can be used to retrieve the class that wraps a node. + + * Class.getattr looks by default in the implicit and the explicit metaclasses, + which is `type` on Python 3. + + Closes issue #114. diff --git a/astroid/bases.py b/astroid/bases.py index a41f885..4fda5d2 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -199,13 +199,15 @@ class Instance(Proxy): # unless they are explicitly defined. if name in ('__name__', '__bases__', '__mro__', '__subclasses__'): return self._proxied.local_attr(name) - return self._proxied.getattr(name, context) + return self._proxied.getattr(name, context, + class_context=False) raise NotFoundError(name) # since we've no context information, return matching class members as # well if lookupclass: try: - return values + self._proxied.getattr(name, context) + return values + self._proxied.getattr(name, context, + class_context=False) except NotFoundError: pass return values @@ -278,7 +280,7 @@ class Instance(Proxy): def callable(self): try: - self._proxied.getattr('__call__') + self._proxied.getattr('__call__', class_context=False) return True except NotFoundError: return False diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py index c61cb63..bc7a1af 100644 --- a/astroid/scoped_nodes.py +++ b/astroid/scoped_nodes.py @@ -1223,12 +1223,18 @@ class Class(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): """return Instance of Class node, else return self""" return bases.Instance(self) - def getattr(self, name, context=None): - """this method doesn't look in the instance_attrs dictionary since it's - done by an Instance proxy at inference time. + def getattr(self, name, context=None, class_context=True): + """Get an attribute from this class, using Python's attribute semantic + This method doesn't look in the instance_attrs dictionary + since it's done by an Instance proxy at inference time. It may return a YES object if the attribute has not been actually - found but a __getattr__ or __getattribute__ method is defined + found but a __getattr__ or __getattribute__ method is defined. + If *class_context* is given, then it's considered that the attribute + is accessed from a class context, e.g. Class.attribute, otherwise + it might have been accessed from an instance as well. + If *class_context* is used in that case, then a lookup in the + implicit metaclass and the explicit metaclass will be done. """ values = self.locals.get(name, []) if name in self.special_attributes: @@ -1247,10 +1253,55 @@ class Class(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): values = list(values) for classnode in self.ancestors(recurs=True, context=context): values += classnode.locals.get(name, []) + + if class_context: + values += self._metaclass_lookup_attribute(name, context) if not values: raise exceptions.NotFoundError(name) return values + def _metaclass_lookup_attribute(self, name, context): + """Search the given name in the implicit and the explicit metaclass.""" + attrs = set() + implicit_meta = self.implicit_metaclass() + metaclass = self.metaclass() + for cls in {implicit_meta, metaclass}: + if cls and cls != self: + cls_attributes = self._get_attribute_from_metaclass( + cls, name, context) + attrs.update(set(cls_attributes)) + return attrs + + def _get_attribute_from_metaclass(self, cls, name, context): + try: + attrs = cls.getattr(name, context=context, + class_context=True) + except exceptions.NotFoundError: + return + + for attr in bases._infer_stmts(attrs, context, frame=cls): + if not isinstance(attr, Function): + yield attr + continue + + if bases._is_property(attr): + # TODO(cpopa): don't use a private API. + for infered in attr.infer_call_result(self, context): + yield infered + continue + if attr.type == 'classmethod': + # If the method is a classmethod, then it will + # be bound to the metaclass, not to the class + # from where the attribute is retrieved. + # get_wrapping_class could return None, so just + # default to the current class. + frame = get_wrapping_class(attr) or self + yield bases.BoundMethod(attr, frame) + elif attr.type == 'staticmethod': + yield attr + else: + yield bases.BoundMethod(attr, self) + def igetattr(self, name, context=None): """inferred getattr, need special treatment in class to handle descriptors diff --git a/astroid/tests/unittest_scoped_nodes.py b/astroid/tests/unittest_scoped_nodes.py index 67e8182..5e9b07e 100644 --- a/astroid/tests/unittest_scoped_nodes.py +++ b/astroid/tests/unittest_scoped_nodes.py @@ -1324,6 +1324,87 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): type_cls = scoped_nodes.builtin_lookup("type")[1][0] self.assertEqual(cls.implicit_metaclass(), type_cls) + def test_implicit_metaclass_lookup(self): + cls = test_utils.extract_node(''' + class A(object): + pass + ''') + instance = cls.instanciate_class() + func = cls.getattr('mro') + self.assertEqual(len(func), 1) + self.assertRaises(NotFoundError, instance.getattr, 'mro') + + def test_metaclass_lookup_using_same_class(self): + # Check that we don't have recursive attribute access for metaclass + cls = test_utils.extract_node(''' + class A(object): pass + ''') + self.assertEqual(len(cls.getattr('mro')), 1) + + def test_metaclass_lookup_inferrence_errors(self): + module = builder.parse(''' + import six + + class Metaclass(type): + foo = lala + + @six.add_metaclass(Metaclass) + class B(object): pass + ''') + cls = module['B'] + self.assertEqual(YES, next(cls.igetattr('foo'))) + + def test_metaclass_lookup(self): + module = builder.parse(''' + import six + + class Metaclass(type): + foo = 42 + @classmethod + def class_method(cls): + pass + def normal_method(cls): + pass + @property + def meta_property(cls): + return 42 + @staticmethod + def static(): + pass + + @six.add_metaclass(Metaclass) + class A(object): + pass + ''') + acls = module['A'] + normal_attr = next(acls.igetattr('foo')) + self.assertIsInstance(normal_attr, nodes.Const) + self.assertEqual(normal_attr.value, 42) + + class_method = next(acls.igetattr('class_method')) + self.assertIsInstance(class_method, BoundMethod) + self.assertEqual(class_method.bound, module['Metaclass']) + + normal_method = next(acls.igetattr('normal_method')) + self.assertIsInstance(normal_method, BoundMethod) + self.assertEqual(normal_method.bound, module['A']) + + # Attribute access for properties: + # from the metaclass is a property object + # from the class that uses the metaclass, the value + # of the property + property_meta = next(module['Metaclass'].igetattr('meta_property')) + self.assertIsInstance(property_meta, UnboundMethod) + wrapping = scoped_nodes.get_wrapping_class(property_meta) + self.assertEqual(wrapping, module['Metaclass']) + + property_class = next(acls.igetattr('meta_property')) + self.assertIsInstance(property_class, nodes.Const) + self.assertEqual(property_class.value, 42) + + static = next(acls.igetattr('static')) + self.assertIsInstance(static, scoped_nodes.Function) + @test_utils.require_version(maxver='3.0') def test_implicit_metaclass_is_none(self): cls = test_utils.extract_node(""" |