summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog5
-rw-r--r--astroid/bases.py8
-rw-r--r--astroid/scoped_nodes.py59
-rw-r--r--astroid/tests/unittest_scoped_nodes.py81
4 files changed, 146 insertions, 7 deletions
diff --git a/ChangeLog b/ChangeLog
index a5c9511..d17134f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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("""