summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog8
-rw-r--r--astroid/bases.py57
-rw-r--r--astroid/interpreter/__init__.py0
-rw-r--r--astroid/interpreter/objectmodel.py530
-rw-r--r--astroid/node_classes.py11
-rw-r--r--astroid/objects.py14
-rw-r--r--astroid/raw_building.py12
-rw-r--r--astroid/scoped_nodes.py68
-rw-r--r--astroid/tests/unittest_object_model.py434
-rw-r--r--astroid/util.py13
10 files changed, 1077 insertions, 70 deletions
diff --git a/ChangeLog b/ChangeLog
index 571673eb..ee5eb6c2 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -22,6 +22,14 @@ Change log for the astroid package (used to be astng)
a namespace package root directory is requested during astroid's import
references.
+ * Introduce a special attributes model
+
+ Through this model, astroid starts knowing special attributes of certain Python objects,
+ such as functions, classes, super objects and so on. This was previously possible before,
+ but now the lookup and the attributes themselves are separated into a new module,
+ objectmodel.py, which describes, in a more comprehensive way, the data model of each
+ object.
+
* Fix a crash which occurred when a method had a same name as a builtin object,
decorated at the same time by that builtin object ( a property for instance)
diff --git a/astroid/bases.py b/astroid/bases.py
index 0f3579e9..126dc44b 100644
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -8,10 +8,17 @@ inference utils.
import collections
import sys
+import six
+
from astroid import context as contextmod
+from astroid import decorators
from astroid import exceptions
from astroid import util
+objectmodel = util.lazy_import('interpreter.objectmodel')
+BUILTINS = six.moves.builtins.__name__
+manager = util.lazy_import('manager')
+MANAGER = manager.AstroidManager()
if sys.version_info >= (3, 0):
BUILTINS = 'builtins'
@@ -111,22 +118,27 @@ def _infer_method_result_truth(instance, method_name, context):
return util.Uninferable
-class Instance(Proxy):
- """A special node representing a class instance."""
+class BaseInstance(Proxy):
+ """An instance base class, which provides lookup methods for potential instances."""
+
+ special_attributes = None
+
+ def display_type(self):
+ return 'Instance of'
def getattr(self, name, context=None, lookupclass=True):
try:
values = self._proxied.instance_attr(name, context)
except exceptions.AttributeInferenceError:
- if name == '__class__':
- return [self._proxied]
+ if self.special_attributes and name in self.special_attributes:
+ return [self.special_attributes.lookup(name)]
+
if lookupclass:
# Class attributes not available through the instance
# unless they are explicitly defined.
- if name in ('__name__', '__bases__', '__mro__', '__subclasses__'):
- return self._proxied.local_attr(name)
return self._proxied.getattr(name, context,
class_context=False)
+
util.reraise(exceptions.AttributeInferenceError(target=self,
attribute=name,
context=context))
@@ -158,8 +170,8 @@ class Instance(Proxy):
try:
# fallback to class.igetattr since it has some logic to handle
# descriptors
- for stmt in self._wrap_attr(self._proxied.igetattr(name, context),
- context):
+ attrs = self._proxied.igetattr(name, context, class_context=False)
+ for stmt in self._wrap_attr(attrs, context):
yield stmt
except exceptions.AttributeInferenceError as error:
util.reraise(exceptions.InferenceError(**vars(error)))
@@ -200,6 +212,12 @@ class Instance(Proxy):
raise exceptions.InferenceError(node=self, caller=caller,
context=context)
+
+class Instance(BaseInstance):
+ """A special node representing a class instance."""
+
+ special_attributes = util.lazy_descriptor(lambda: objectmodel.InstanceModel())
+
def __repr__(self):
return '<Instance of %s.%s at 0x%s>' % (self._proxied.root().name,
self._proxied.name,
@@ -256,6 +274,9 @@ class Instance(Proxy):
class UnboundMethod(Proxy):
"""a special node representing a method not bound to an instance"""
+
+ special_attributes = util.lazy_descriptor(lambda: objectmodel.UnboundMethodModel())
+
def __repr__(self):
frame = self._proxied.parent.frame()
return '<%s %s of %s at 0x%s' % (self.__class__.__name__,
@@ -266,13 +287,13 @@ class UnboundMethod(Proxy):
return False
def getattr(self, name, context=None):
- if name == 'im_func':
- return [self._proxied]
+ if name in self.special_attributes:
+ return [self.special_attributes.lookup(name)]
return self._proxied.getattr(name, context)
def igetattr(self, name, context=None):
- if name == 'im_func':
- return iter((self._proxied,))
+ if name in self.special_attributes:
+ return iter((self.special_attributes.lookup(name), ))
return self._proxied.igetattr(name, context)
def infer_call_result(self, caller, context):
@@ -290,6 +311,9 @@ class UnboundMethod(Proxy):
class BoundMethod(UnboundMethod):
"""a special node representing a method bound to an instance"""
+
+ special_attributes = util.lazy_descriptor(lambda: objectmodel.BoundMethodModel())
+
def __init__(self, proxy, bound):
UnboundMethod.__init__(self, proxy)
self.bound = bound
@@ -387,11 +411,17 @@ class BoundMethod(UnboundMethod):
return True
-class Generator(Instance):
+class Generator(BaseInstance):
"""a special node representing a generator.
Proxied class is set once for all in raw_building.
"""
+
+ special_attributes = util.lazy_descriptor(lambda: objectmodel.GeneratorModel())
+
+ def __init__(self, parent=None):
+ self.parent = parent
+
def callable(self):
return False
@@ -409,4 +439,3 @@ class Generator(Instance):
def __str__(self):
return 'Generator(%s)' % (self._proxied.name)
-
diff --git a/astroid/interpreter/__init__.py b/astroid/interpreter/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/astroid/interpreter/__init__.py
diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py
new file mode 100644
index 00000000..2789c4a9
--- /dev/null
+++ b/astroid/interpreter/objectmodel.py
@@ -0,0 +1,530 @@
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of astroid.
+#
+# astroid is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation, either version 2.1 of the License, or (at your
+# option) any later version.
+#
+# astroid is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with astroid. If not, see <http://www.gnu.org/licenses/>.
+#
+# The code in this file was originally part of logilab-common, licensed under
+# the same license.
+
+"""
+Data object model, as per https://docs.python.org/3/reference/datamodel.html.
+
+This module describes, at least partially, a data object model for some
+of astroid's nodes. The model contains special attributes that nodes such
+as functions, classes, modules etc have, such as __doc__, __class__,
+__module__ etc, being used when doing attribute lookups over nodes.
+
+For instance, inferring `obj.__class__` will first trigger an inference
+of the `obj` variable. If it was succesfully inferred, then an attribute
+`__class__ will be looked for in the inferred object. This is the part
+where the data model occurs. The model is attached to those nodes
+and the lookup mechanism will try to see if attributes such as
+`__class__` are defined by the model or not. If they are defined,
+the model will be requested to return the corresponding value of that
+attribute. Thus the model can be viewed as a special part of the lookup
+mechanism.
+"""
+
+import itertools
+import pprint
+import os
+
+import six
+
+import astroid
+from astroid import context as contextmod
+from astroid import exceptions
+from astroid import node_classes
+
+
+def _dunder_dict(instance, attributes):
+ obj = node_classes.Dict(parent=instance)
+
+ # Convert the keys to node strings
+ keys = [node_classes.Const(value=value, parent=obj)
+ for value in list(attributes.keys())]
+
+ # The original attribute has a list of elements for each key,
+ # but that is not useful for retrieving the special attribute's value.
+ # In this case, we're picking the last value from each list.
+ values = [elem[-1] for elem in attributes.values()]
+
+ obj.postinit(list(zip(keys, values)))
+ return obj
+
+
+class ObjectModel(object):
+
+ def __repr__(self):
+ result = []
+ cname = type(self).__name__
+ string = '%(cname)s(%(fields)s)'
+ alignment = len(cname) + 1
+ for field in sorted(self.attributes()):
+ width = 80 - len(field) - alignment
+ lines = pprint.pformat(field, indent=2,
+ width=width).splitlines(True)
+
+ inner = [lines[0]]
+ for line in lines[1:]:
+ inner.append(' ' * alignment + line)
+ result.append(field)
+
+ return string % {'cname': cname,
+ 'fields': (',\n' + ' ' * alignment).join(result)}
+
+ def __call__(self, instance):
+ self._instance = instance
+ return self
+
+ def __get__(self, instance, cls=None):
+ # ObjectModel needs to be a descriptor so that just doing
+ # `special_attributes = SomeObjectModel` should be enough in the body of a node.
+ # But at the same time, node.special_attributes should return an object
+ # which can be used for manipulating the special attributes. That's the reason
+ # we pass the instance through which it got accessed to ObjectModel.__call__,
+ # returning itself afterwards, so we can still have access to the
+ # underlying data model and to the instance for which it got accessed.
+ return self(instance)
+
+ def __contains__(self, name):
+ return name in self.attributes()
+
+ def attributes(self):
+ """Get the attributes which are exported by this object model."""
+ return [obj[2:] for obj in dir(self) if obj.startswith('py')]
+
+ def lookup(self, name):
+ """Look up the given *name* in the current model
+
+ It should return an AST or an interpreter object,
+ but if the name is not found, then an AttributeInferenceError will be raised.
+ """
+
+ if name in self.attributes():
+ return getattr(self, "py" + name)
+ raise exceptions.AttributeInferenceError(target=self._instance, attribute=name)
+
+
+class ModuleModel(ObjectModel):
+
+ def _builtins(self):
+ builtins = astroid.MANAGER.builtins()
+ return builtins.special_attributes.lookup('__dict__')
+
+ if six.PY3:
+ @property
+ def pybuiltins(self):
+ return self._builtins()
+
+ else:
+ @property
+ def py__builtin__(self):
+ return self._builtins()
+
+ # __path__ is a standard attribute on *packages* not
+ # non-package modules. The only mention of it in the
+ # official 2.7 documentation I can find is in the
+ # tutorial.
+
+ @property
+ def py__path__(self):
+ if not self._instance.package:
+ raise exceptions.AttributeInferenceError(target=self._instance,
+ attribute='__path__')
+
+ path = os.path.dirname(self._instance.file)
+ path_obj = node_classes.Const(value=path, parent=self._instance)
+
+ container = node_classes.List(parent=self._instance)
+ container.postinit([path_obj])
+
+ return container
+
+ @property
+ def py__name__(self):
+ return node_classes.Const(value=self._instance.name,
+ parent=self._instance)
+
+ @property
+ def py__doc__(self):
+ return node_classes.Const(value=self._instance.doc,
+ parent=self._instance)
+
+ @property
+ def py__file__(self):
+ return node_classes.Const(value=self._instance.file,
+ parent=self._instance)
+
+ @property
+ def py__dict__(self):
+ return _dunder_dict(self._instance, self._instance.globals)
+
+ # __package__ isn't mentioned anywhere outside a PEP:
+ # https://www.python.org/dev/peps/pep-0366/
+ @property
+ def py__package__(self):
+ if not self._instance.package:
+ value = ''
+ else:
+ value = self._instance.name
+
+ return node_classes.Const(value=value, parent=self._instance)
+
+ # These are related to the Python 3 implementation of the
+ # import system,
+ # https://docs.python.org/3/reference/import.html#import-related-module-attributes
+
+ @property
+ def py__spec__(self):
+ # No handling for now.
+ return node_classes.Unknown()
+
+ @property
+ def py__loader__(self):
+ # No handling for now.
+ return node_classes.Unknown()
+
+ @property
+ def py__cached__(self):
+ # No handling for now.
+ return node_classes.Unknown()
+
+
+class FunctionModel(ObjectModel):
+
+ @property
+ def py__name__(self):
+ return node_classes.Const(value=self._instance.name,
+ parent=self._instance)
+
+ @property
+ def py__doc__(self):
+ return node_classes.Const(value=self._instance.doc,
+ parent=self._instance)
+
+ @property
+ def py__qualname__(self):
+ return node_classes.Const(value=self._instance.qname(),
+ parent=self._instance)
+
+ @property
+ def py__defaults__(self):
+ func = self._instance
+ if not func.args.defaults:
+ return node_classes.Const(value=None, parent=func)
+
+ defaults_obj = node_classes.Tuple(parent=func)
+ defaults_obj.postinit(func.args.defaults)
+ return defaults_obj
+
+ @property
+ def py__annotations__(self):
+ obj = node_classes.Dict(parent=self._instance)
+
+ if not self._instance.returns:
+ returns = None
+ else:
+ returns = self._instance.returns
+
+ args = self._instance.args
+ annotations = {arg.name: annotation
+ for (arg, annotation) in zip(args.args, args.annotations)
+ if annotation}
+ if args.varargannotation:
+ annotations[args.vararg] = args.varargannotation
+ if args.kwargannotation:
+ annotations[args.kwarg] = args.kwargannotation
+ if returns:
+ annotations['return'] = returns
+
+ items = [(node_classes.Const(key, parent=obj), value)
+ for (key, value) in annotations.items()]
+
+ obj.postinit(items)
+ return obj
+
+ @property
+ def py__dict__(self):
+ return node_classes.Dict(parent=self._instance)
+
+ py__globals__ = py__dict__
+
+ @property
+ def py__kwdefaults__(self):
+
+ def _default_args(args, parent):
+ for arg in args.kwonlyargs:
+ try:
+ default = args.default_value(arg.name)
+ except exceptions.NoDefault:
+ continue
+
+ name = node_classes.Const(arg.name, parent=parent)
+ yield name, default
+
+ args = self._instance.args
+ obj = node_classes.Dict(parent=self._instance)
+ defaults = dict(_default_args(args, obj))
+
+ obj.postinit(list(defaults.items()))
+ return obj
+
+ @property
+ def py__module__(self):
+ return node_classes.Const(self._instance.root().qname())
+
+ @property
+ def py__get__(self):
+ from astroid import bases
+
+ func = self._instance
+
+ class DescriptorBoundMethod(bases.BoundMethod):
+ """Bound method which knows how to understand calling descriptor binding."""
+ def infer_call_result(self, caller, context=None):
+ if len(caller.args) != 2:
+ raise exceptions.InferenceError(
+ "Invalid arguments for descriptor binding",
+ target=self, context=context)
+
+ context = contextmod.copy_context(context)
+ cls = next(caller.args[0].infer(context=context))
+
+ # Rebuild the original value, but with the parent set as the
+ # class where it will be bound.
+ new_func = func.__class__(name=func.name, doc=func.doc,
+ lineno=func.lineno, col_offset=func.col_offset,
+ parent=cls)
+ new_func.postinit(func.args, func.body,
+ func.decorators, func.returns)
+
+ # Build a proper bound method that points to our newly built function.
+ proxy = bases.UnboundMethod(new_func)
+ yield bases.BoundMethod(proxy=proxy, bound=cls)
+
+ return DescriptorBoundMethod(proxy=self._instance, bound=self._instance)
+
+ # These are here just for completion.
+ @property
+ def py__ne__(self):
+ return node_classes.Unknown()
+
+ py__subclasshook__ = py__ne__
+ py__str__ = py__ne__
+ py__sizeof__ = py__ne__
+ py__setattr__ = py__ne__
+ py__repr__ = py__ne__
+ py__reduce__ = py__ne__
+ py__reduce_ex__ = py__ne__
+ py__new__ = py__ne__
+ py__lt__ = py__ne__
+ py__eq__ = py__ne__
+ py__gt__ = py__ne__
+ py__format__ = py__ne__
+ py__delattr__ = py__ne__
+ py__getattribute__ = py__ne__
+ py__hash__ = py__ne__
+ py__init__ = py__ne__
+ py__dir__ = py__ne__
+ py__call__ = py__ne__
+ py__class__ = py__ne__
+ py__closure__ = py__ne__
+ py__code__ = py__ne__
+
+
+class ClassModel(ObjectModel):
+
+ @property
+ def py__module__(self):
+ return node_classes.Const(self._instance.root().qname())
+
+ @property
+ def py__name__(self):
+ return node_classes.Const(self._instance.name)
+
+ @property
+ def py__qualname__(self):
+ return node_classes.Const(self._instance.qname())
+
+ @property
+ def py__doc__(self):
+ return node_classes.Const(self._instance.doc)
+
+ @property
+ def py__mro__(self):
+ if not self._instance.newstyle:
+ raise exceptions.AttributeInferenceError(target=self._instance,
+ attribute='__mro__')
+
+ mro = self._instance.mro()
+ obj = node_classes.Tuple(parent=self._instance)
+ obj.postinit(mro)
+ return obj
+
+ @property
+ def pymro(self):
+ from astroid import bases
+
+ other_self = self
+
+ # Cls.mro is a method and we need to return one in order to have a proper inference.
+ # The method we're returning is capable of inferring the underlying MRO though.
+ class MroBoundMethod(bases.BoundMethod):
+ def infer_call_result(self, caller, context=None):
+ yield other_self.py__mro__
+ return MroBoundMethod(proxy=self._instance, bound=self._instance)
+
+ @property
+ def py__bases__(self):
+ obj = node_classes.Tuple()
+ context = contextmod.InferenceContext()
+ elts = list(self._instance._inferred_bases(context))
+ obj.postinit(elts=elts)
+ return obj
+
+ @property
+ def py__class__(self):
+ from astroid import helpers
+ return helpers.object_type(self._instance)
+
+ @property
+ def py__subclasses__(self):
+ """Get the subclasses of the underlying class
+
+ This looks only in the current module for retrieving the subclasses,
+ thus it might miss a couple of them.
+ """
+ from astroid import bases
+ from astroid import scoped_nodes
+
+ if not self._instance.newstyle:
+ raise exceptions.AttributeInferenceError(target=self._instance,
+ attribute='__subclasses__')
+
+ qname = self._instance.qname()
+ root = self._instance.root()
+ # TODO:
+ classes = [cls for cls in root.nodes_of_class(scoped_nodes.ClassDef)
+ if cls != self._instance and cls.is_subtype_of(qname)]
+
+ obj = node_classes.List(parent=self._instance)
+ obj.postinit(classes)
+
+ class SubclassesBoundMethod(bases.BoundMethod):
+ def infer_call_result(self, caller, context=None):
+ yield obj
+
+ return SubclassesBoundMethod(proxy=self._instance, bound=self._instance)
+
+ @property
+ def py__dict__(self):
+ return node_classes.Dict(parent=self._instance)
+
+
+class SuperModel(ObjectModel):
+
+ @property
+ def py__thisclass__(self):
+ return self._instance.mro_pointer
+
+ @property
+ def py__self_class__(self):
+ return self._instance._self_class
+
+ @property
+ def py__self__(self):
+ return self._instance.type
+
+ @property
+ def py__class__(self):
+ return self._instance._proxied
+
+
+class UnboundMethodModel(ObjectModel):
+
+ @property
+ def py__class__(self):
+ from astroid import helpers
+ return helpers.object_type(self._instance)
+
+ @property
+ def py__func__(self):
+ return self._instance._proxied
+
+ @property
+ def py__self__(self):
+ return node_classes.Const(value=None, parent=self._instance)
+
+ pyim_func = py__func__
+ pyim_class = py__class__
+ pyim_self = py__self__
+
+
+class BoundMethodModel(FunctionModel):
+
+ @property
+ def py__func__(self):
+ return self._instance._proxied._proxied
+
+ @property
+ def py__self__(self):
+ return self._instance.bound
+
+
+class GeneratorModel(FunctionModel):
+
+ def __new__(self, *args, **kwargs):
+ # Append the values from the GeneratorType unto this object.
+ cls = super(GeneratorModel, self).__new__(self, *args, **kwargs)
+ generator = astroid.MANAGER.astroid_cache[six.moves.builtins.__name__]['generator']
+ for name, values in generator.locals.items():
+ print(name, values)
+ method = values[0]
+ patched = lambda self, meth=method: meth
+
+ setattr(type(cls), 'py' + name, property(patched))
+
+ return cls
+
+ @property
+ def py__name__(self):
+ return node_classes.Const(value=self._instance.parent.name,
+ parent=self._instance)
+
+ @property
+ def py__doc__(self):
+ return node_classes.Const(value=self._instance.parent.doc,
+ parent=self._instance)
+
+
+class InstanceModel(ObjectModel):
+
+ @property
+ def py__class__(self):
+ return self._instance._proxied
+
+ @property
+ def py__module__(self):
+ return node_classes.Const(self._instance.root().qname())
+
+ @property
+ def py__doc__(self):
+ return node_classes.Const(self._instance.doc)
+
+ @property
+ def py__dict__(self):
+ return _dunder_dict(self._instance, self._instance.instance_attrs)
diff --git a/astroid/node_classes.py b/astroid/node_classes.py
index a3effaa2..05bfd6d8 100644
--- a/astroid/node_classes.py
+++ b/astroid/node_classes.py
@@ -1857,6 +1857,17 @@ class DictUnpack(NodeNG):
"""Represents the unpacking of dicts into dicts using PEP 448."""
+class Unknown(NodeNG):
+ '''This node represents a node in a constructed AST where
+ introspection is not possible. At the moment, it's only used in
+ the args attribute of FunctionDef nodes where function signature
+ introspection failed.
+ '''
+ def infer(self, context=None, **kwargs):
+ '''Inference on an Unknown node immediately terminates.'''
+ yield util.Uninferable
+
+
# constants ##############################################################
CONST_CLS = {
diff --git a/astroid/objects.py b/astroid/objects.py
index 1adc29ef..6597f3e8 100644
--- a/astroid/objects.py
+++ b/astroid/objects.py
@@ -23,6 +23,7 @@ from astroid import util
BUILTINS = six.moves.builtins.__name__
+objectmodel = util.lazy_import('interpreter.objectmodel')
class FrozenSet(node_classes._BaseContainer):
@@ -52,6 +53,8 @@ class Super(node_classes.NodeNG):
*self_class* is the class where the super call is, while
*scope* is the function where the super call is.
"""
+ special_attributes = util.lazy_descriptor(lambda: objectmodel.SuperModel())
+
# pylint: disable=super-init-not-called
def __init__(self, mro_pointer, mro_type, self_class, scope):
self.type = mro_type
@@ -59,12 +62,6 @@ class Super(node_classes.NodeNG):
self._class_based = False
self._self_class = self_class
self._scope = scope
- self._model = {
- '__thisclass__': self.mro_pointer,
- '__self_class__': self._self_class,
- '__self__': self.type,
- '__class__': self._proxied,
- }
def _infer(self, context=None):
yield self
@@ -120,9 +117,8 @@ class Super(node_classes.NodeNG):
def igetattr(self, name, context=None):
"""Retrieve the inferred values of the given attribute name."""
- local_name = self._model.get(name)
- if local_name:
- yield local_name
+ if name in self.special_attributes:
+ yield self.special_attributes.lookup(name)
return
try:
diff --git a/astroid/raw_building.py b/astroid/raw_building.py
index 4a1ab0a8..edfdaab7 100644
--- a/astroid/raw_building.py
+++ b/astroid/raw_building.py
@@ -396,3 +396,15 @@ _GeneratorType = nodes.ClassDef(types.GeneratorType.__name__, types.GeneratorTyp
_GeneratorType.parent = MANAGER.astroid_cache[six.moves.builtins.__name__]
bases.Generator._proxied = _GeneratorType
Astroid_BUILDER.object_build(bases.Generator._proxied, types.GeneratorType)
+
+_builtins = MANAGER.astroid_cache[six.moves.builtins.__name__]
+BUILTIN_TYPES = (types.GetSetDescriptorType, types.GeneratorType,
+ types.MemberDescriptorType, type(None), type(NotImplemented),
+ types.FunctionType, types.MethodType,
+ types.BuiltinFunctionType, types.ModuleType, types.TracebackType)
+for _type in BUILTIN_TYPES:
+ if _type.__name__ not in _builtins:
+ cls = nodes.ClassDef(_type.__name__, _type.__doc__)
+ cls.parent = MANAGER.astroid_cache[six.moves.builtins.__name__]
+ Astroid_BUILDER.object_build(cls, _type)
+ _builtins[_type.__name__] = cls
diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py
index e520a664..97f4bc93 100644
--- a/astroid/scoped_nodes.py
+++ b/astroid/scoped_nodes.py
@@ -17,6 +17,8 @@ import six
from astroid import bases
from astroid import context as contextmod
from astroid import exceptions
+from astroid import decorators as decorators_mod
+from astroid.interpreter import objectmodel
from astroid import manager
from astroid import mixins
from astroid import node_classes
@@ -80,21 +82,6 @@ def function_to_method(n, klass):
return n
-def std_special_attributes(self, name, add_locals=True):
- if add_locals:
- obj_locals = self.locals
- else:
- obj_locals = {}
- if name == '__name__':
- return [node_classes.const_factory(self.name)] + obj_locals.get(name, [])
- if name == '__doc__':
- return [node_classes.const_factory(self.doc)] + obj_locals.get(name, [])
- if name == '__dict__':
- return [node_classes.Dict()] + obj_locals.get(name, [])
- # TODO: missing context
- raise exceptions.AttributeInferenceError(target=self, attribute=name)
-
-
MANAGER = manager.AstroidManager()
def builtin_lookup(name):
"""lookup a name into the builtin module
@@ -258,10 +245,10 @@ class Module(LocalsDictNodeNG):
# Future imports
future_imports = None
+ special_attributes = objectmodel.ModuleModel()
# names of python special attributes (handled by getattr impl.)
- special_attributes = set(('__name__', '__doc__', '__file__', '__path__',
- '__dict__'))
+
# names of module attributes available through the global scope
scope_attrs = set(('__name__', '__doc__', '__file__', '__path__'))
@@ -340,13 +327,7 @@ class Module(LocalsDictNodeNG):
def getattr(self, name, context=None, ignore_locals=False):
result = []
if name in self.special_attributes:
- if name == '__file__':
- result = ([node_classes.const_factory(self.file)] +
- self.locals.get(name, []))
- elif name == '__path__' and self.package:
- result = [node_classes.List()] + self.locals.get(name, [])
- else:
- result = std_special_attributes(self, name)
+ result = [self.special_attributes.lookup(name)]
elif not ignore_locals and name in self.locals:
result = self.locals[name]
elif self.package:
@@ -716,7 +697,7 @@ class FunctionDef(node_classes.Statement, Lambda):
else:
_astroid_fields = ('decorators', 'args', 'body')
decorators = None
- special_attributes = set(('__name__', '__doc__', '__dict__'))
+ special_attributes = objectmodel.FunctionModel()
is_function = True
# attributes below are set by the builder module or by raw factories
_other_fields = ('name', 'doc')
@@ -864,11 +845,11 @@ class FunctionDef(node_classes.Statement, Lambda):
"""this method doesn't look in the instance_attrs dictionary since it's
done by an Instance proxy at inference time.
"""
- if name == '__module__':
- return [node_classes.const_factory(self.root().qname())]
if name in self.instance_attrs:
return self.instance_attrs[name]
- return std_special_attributes(self, name, False)
+ if name in self.special_attributes:
+ return [self.special_attributes.lookup(name)]
+ raise exceptions.AttributeInferenceError(target=self, attribute=name)
def igetattr(self, name, context=None):
"""Inferred getattr, which returns an iterator of inferred statements."""
@@ -942,8 +923,7 @@ class FunctionDef(node_classes.Statement, Lambda):
def infer_call_result(self, caller, context=None):
"""infer what a function is returning when called"""
if self.is_generator():
- result = bases.Generator()
- result.parent = self
+ result = bases.Generator(self)
yield result
return
# This is really a gigantic hack to work around metaclass generators
@@ -1091,8 +1071,8 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG,
_astroid_fields = ('decorators', 'bases', 'body') # name
decorators = None
- special_attributes = set(('__name__', '__doc__', '__dict__', '__module__',
- '__bases__', '__mro__', '__subclasses__'))
+ special_attributes = objectmodel.ClassModel()
+
_type = None
_metaclass_hack = False
hide = False
@@ -1404,20 +1384,14 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG,
"""
values = self.locals.get(name, [])
- if name in self.special_attributes:
- if name == '__module__':
- return [node_classes.const_factory(self.root().qname())] + values
+ if name in self.special_attributes and class_context:
+ result = [self.special_attributes.lookup(name)]
if name == '__bases__':
- node = node_classes.Tuple()
- elts = list(self._inferred_bases(context))
- node.postinit(elts=elts)
- return [node] + values
- if name == '__mro__' and self.newstyle:
- mro = self.mro()
- node = node_classes.Tuple()
- node.postinit(elts=mro)
- return [node]
- return std_special_attributes(self, name)
+ # Need special treatment, since they are mutable
+ # and we need to return all the values.
+ result += values
+ return result
+
# don't modify the list in self.locals!
values = list(values)
for classnode in self.ancestors(recurs=True, context=context):
@@ -1472,7 +1446,7 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG,
else:
yield bases.BoundMethod(attr, self)
- def igetattr(self, name, context=None):
+ def igetattr(self, name, context=None, class_context=True):
"""inferred getattr, need special treatment in class to handle
descriptors
"""
@@ -1481,7 +1455,7 @@ class ClassDef(mixins.FilterStmtsMixin, LocalsDictNodeNG,
context = contextmod.copy_context(context)
context.lookupname = name
try:
- for inferred in bases._infer_stmts(self.getattr(name, context),
+ for inferred in bases._infer_stmts(self.getattr(name, context, class_context=class_context),
context, frame=self):
# yield Uninferable object instead of descriptors when necessary
if (not isinstance(inferred, node_classes.Const)
diff --git a/astroid/tests/unittest_object_model.py b/astroid/tests/unittest_object_model.py
new file mode 100644
index 00000000..77ca2f06
--- /dev/null
+++ b/astroid/tests/unittest_object_model.py
@@ -0,0 +1,434 @@
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of astroid.
+#
+# astroid is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation, either version 2.1 of the License, or (at your
+# option) any later version.
+#
+# astroid is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with astroid. If not, see <http://www.gnu.org/licenses/>.
+#
+# The code in this file was originally part of logilab-common, licensed under
+# the same license.
+
+import unittest
+import types
+import xml
+
+import six
+
+import astroid
+from astroid import exceptions
+from astroid import MANAGER
+from astroid import test_utils
+
+
+BUILTINS = MANAGER.astroid_cache[six.moves.builtins.__name__]
+
+
+class InstanceModelTest(unittest.TestCase):
+
+ def test_instance_special_model(self):
+ ast_nodes = test_utils.extract_node('''
+ class A:
+ "test"
+ def __init__(self):
+ self.a = 42
+ a = A()
+ a.__class__ #@
+ a.__module__ #@
+ a.__doc__ #@
+ a.__dict__ #@
+ ''', module_name='collections')
+
+ cls = next(ast_nodes[0].infer())
+ self.assertIsInstance(cls, astroid.ClassDef)
+ self.assertEqual(cls.name, 'A')
+
+ module = next(ast_nodes[1].infer())
+ self.assertIsInstance(module, astroid.Const)
+ self.assertEqual(module.value, 'collections')
+
+ doc = next(ast_nodes[2].infer())
+ self.assertIsInstance(doc, astroid.Const)
+ self.assertEqual(doc.value, 'test')
+
+ dunder_dict = next(ast_nodes[3].infer())
+ self.assertIsInstance(dunder_dict, astroid.Dict)
+ attr = next(dunder_dict.getitem('a').infer())
+ self.assertIsInstance(attr, astroid.Const)
+ self.assertEqual(attr.value, 42)
+
+ @unittest.expectedFailure
+ def test_instance_local_attributes_overrides_object_model(self):
+ # The instance lookup needs to be changed in order for this to work.
+ ast_node = test_utils.extract_node('''
+ class A:
+ @property
+ def __dict__(self):
+ return []
+ A().__dict__
+ ''')
+ inferred = next(ast_node.infer())
+ self.assertIsInstance(inferred, astroid.List)
+ self.assertEqual(inferred.elts, [])
+
+
+class BoundMethodModelTest(unittest.TestCase):
+
+ def test_bound_method_model(self):
+ ast_nodes = test_utils.extract_node('''
+ class A:
+ def test(self): pass
+ a = A()
+ a.test.__func__ #@
+ a.test.__self__ #@
+ ''')
+
+ func = next(ast_nodes[0].infer())
+ self.assertIsInstance(func, astroid.FunctionDef)
+ self.assertEqual(func.name, 'test')
+
+ self_ = next(ast_nodes[1].infer())
+ self.assertIsInstance(self_, astroid.Instance)
+ self.assertEqual(self_.name, 'A')
+
+
+class UnboundMethodModelTest(unittest.TestCase):
+
+ def test_unbound_method_model(self):
+ ast_nodes = test_utils.extract_node('''
+ class A:
+ def test(self): pass
+ t = A.test
+ t.__class__ #@
+ t.__func__ #@
+ t.__self__ #@
+ t.im_class #@
+ t.im_func #@
+ t.im_self #@
+ ''')
+
+ cls = next(ast_nodes[0].infer())
+ self.assertIsInstance(cls, astroid.ClassDef)
+ if six.PY2:
+ unbound = BUILTINS.locals[types.MethodType.__name__][0]
+ else:
+ unbound = BUILTINS.locals[types.FunctionType.__name__][0]
+ self.assertEqual(cls.name, unbound.name)
+
+ func = next(ast_nodes[1].infer())
+ self.assertIsInstance(func, astroid.FunctionDef)
+ self.assertEqual(func.name, 'test')
+
+ self_ = next(ast_nodes[2].infer())
+ self.assertIsInstance(self_, astroid.Const)
+ self.assertIsNone(self_.value)
+
+ self.assertEqual(cls.name, next(ast_nodes[3].infer()).name)
+ self.assertEqual(func, next(ast_nodes[4].infer()))
+ self.assertIsNone(next(ast_nodes[5].infer()).value)
+
+
+class ClassModelTest(unittest.TestCase):
+
+ @test_utils.require_version(maxver='3.0')
+ def test__mro__old_style(self):
+ ast_node = test_utils.extract_node('''
+ class A:
+ pass
+ A.__mro__
+ ''')
+ with self.assertRaises(exceptions.InferenceError):
+ next(ast_node.infer())
+
+ @test_utils.require_version(maxver='3.0')
+ def test__subclasses__old_style(self):
+ ast_node = test_utils.extract_node('''
+ class A:
+ pass
+ A.__subclasses__
+ ''')
+ with self.assertRaises(exceptions.InferenceError):
+ next(ast_node.infer())
+
+ def test_class_model(self):
+ ast_nodes = test_utils.extract_node('''
+ class A(object):
+ "test"
+
+ class B(A): pass
+ class C(A): pass
+
+ A.__module__ #@
+ A.__name__ #@
+ A.__qualname__ #@
+ A.__doc__ #@
+ A.__mro__ #@
+ A.mro() #@
+ A.__bases__ #@
+ A.__class__ #@
+ A.__dict__ #@
+ A.__subclasses__() #@
+ ''', module_name='collections')
+
+ module = next(ast_nodes[0].infer())
+ self.assertIsInstance(module, astroid.Const)
+ self.assertEqual(module.value, 'collections')
+
+ name = next(ast_nodes[1].infer())
+ self.assertIsInstance(name, astroid.Const)
+ self.assertEqual(name.value, 'A')
+
+ qualname = next(ast_nodes[2].infer())
+ self.assertIsInstance(qualname, astroid.Const)
+ self.assertEqual(qualname.value, 'collections.A')
+
+ doc = next(ast_nodes[3].infer())
+ self.assertIsInstance(doc, astroid.Const)
+ self.assertEqual(doc.value, 'test')
+
+ mro = next(ast_nodes[4].infer())
+ self.assertIsInstance(mro, astroid.Tuple)
+ self.assertEqual([cls.name for cls in mro.elts],
+ ['A', 'object'])
+
+ called_mro = next(ast_nodes[5].infer())
+ self.assertEqual(called_mro.elts, mro.elts)
+
+ bases = next(ast_nodes[6].infer())
+ self.assertIsInstance(bases, astroid.Tuple)
+ self.assertEqual([cls.name for cls in bases.elts],
+ ['object'])
+
+ cls = next(ast_nodes[7].infer())
+ self.assertIsInstance(cls, astroid.ClassDef)
+ self.assertEqual(cls.name, 'type')
+
+ cls_dict = next(ast_nodes[8].infer())
+ self.assertIsInstance(cls_dict, astroid.Dict)
+
+ subclasses = next(ast_nodes[9].infer())
+ self.assertIsInstance(subclasses, astroid.List)
+ self.assertEqual([cls.name for cls in subclasses.elts], ['B', 'C'])
+
+
+class ModuleModelTest(unittest.TestCase):
+
+ def test__path__not_a_package(self):
+ ast_node = test_utils.extract_node('''
+ import sys
+ sys.__path__ #@
+ ''')
+ with self.assertRaises(exceptions.InferenceError):
+ next(ast_node.infer())
+
+ def test_module_model(self):
+ ast_nodes = test_utils.extract_node('''
+ import xml
+ xml.__path__ #@
+ xml.__name__ #@
+ xml.__doc__ #@
+ xml.__file__ #@
+ xml.__spec__ #@
+ xml.__loader__ #@
+ xml.__cached__ #@
+ xml.__package__ #@
+ xml.__dict__ #@
+ ''')
+
+ path = next(ast_nodes[0].infer())
+ self.assertIsInstance(path, astroid.List)
+ self.assertIsInstance(path.elts[0], astroid.Const)
+ self.assertEqual(path.elts[0].value, xml.__path__[0])
+
+ name = next(ast_nodes[1].infer())
+ self.assertIsInstance(name, astroid.Const)
+ self.assertEqual(name.value, 'xml')
+
+ doc = next(ast_nodes[2].infer())
+ self.assertIsInstance(doc, astroid.Const)
+ self.assertEqual(doc.value, xml.__doc__)
+
+ file_ = next(ast_nodes[3].infer())
+ self.assertIsInstance(file_, astroid.Const)
+ self.assertEqual(file_.value, xml.__file__.replace(".pyc", ".py"))
+
+ for ast_node in ast_nodes[4:7]:
+ inferred = next(ast_node.infer())
+ self.assertIs(inferred, astroid.Uninferable)
+
+ package = next(ast_nodes[7].infer())
+ self.assertIsInstance(package, astroid.Const)
+ self.assertEqual(package.value, 'xml')
+
+ dict_ = next(ast_nodes[8].infer())
+ self.assertIsInstance(dict_, astroid.Dict)
+
+
+class FunctionModelTest(unittest.TestCase):
+
+ def test_partial_descriptor_support(self):
+ bound, result = test_utils.extract_node('''
+ class A(object): pass
+ def test(self): return 42
+ f = test.__get__(A(), A)
+ f #@
+ f() #@
+ ''')
+ bound = next(bound.infer())
+ self.assertIsInstance(bound, astroid.BoundMethod)
+ self.assertEqual(bound._proxied._proxied.name, 'test')
+ result = next(result.infer())
+ self.assertIsInstance(result, astroid.Const)
+ self.assertEqual(result.value, 42)
+
+ @unittest.expectedFailure
+ def test_descriptor_not_inferrring_self(self):
+ # We can't infer __get__(X, Y)() when the bounded function
+ # uses self, because of the tree's parent not being propagating good enough.
+ result = test_utils.extract_node('''
+ class A(object):
+ x = 42
+ def test(self): return self.x
+ f = test.__get__(A(), A)
+ f() #@
+ ''')
+ result = next(result.infer())
+ self.assertIsInstance(result, astroid.Const)
+ self.assertEqual(result.value, 42)
+
+ def test_descriptors_binding_invalid(self):
+ ast_nodes = test_utils.extract_node('''
+ class A: pass
+ def test(self): return 42
+ test.__get__()() #@
+ test.__get__(1)() #@
+ test.__get__(2, 3, 4) #@
+ ''')
+ for node in ast_nodes:
+ with self.assertRaises(exceptions.InferenceError):
+ next(node.infer())
+
+ def test_function_model(self):
+ ast_nodes = test_utils.extract_node('''
+ def func(a=1, b=2):
+ """test"""
+ func.__name__ #@
+ func.__doc__ #@
+ func.__qualname__ #@
+ func.__module__ #@
+ func.__defaults__ #@
+ func.__dict__ #@
+ func.__globals__ #@
+ func.__code__ #@
+ func.__closure__ #@
+ ''', module_name='collections')
+
+ name = next(ast_nodes[0].infer())
+ self.assertIsInstance(name, astroid.Const)
+ self.assertEqual(name.value, 'func')
+
+ doc = next(ast_nodes[1].infer())
+ self.assertIsInstance(doc, astroid.Const)
+ self.assertEqual(doc.value, 'test')
+
+ qualname = next(ast_nodes[2].infer())
+ self.assertIsInstance(qualname, astroid.Const)
+ self.assertEqual(qualname.value, 'collections.func')
+
+ module = next(ast_nodes[3].infer())
+ self.assertIsInstance(module, astroid.Const)
+ self.assertEqual(module.value, 'collections')
+
+ defaults = next(ast_nodes[4].infer())
+ self.assertIsInstance(defaults, astroid.Tuple)
+ self.assertEqual([default.value for default in defaults.elts], [1, 2])
+
+ dict_ = next(ast_nodes[5].infer())
+ self.assertIsInstance(dict_, astroid.Dict)
+
+ globals_ = next(ast_nodes[6].infer())
+ self.assertIsInstance(globals_, astroid.Dict)
+
+ for ast_node in ast_nodes[7:9]:
+ self.assertIs(next(ast_node.infer()), astroid.Uninferable)
+
+ @test_utils.require_version(minver='3.0')
+ def test_empty_return_annotation(self):
+ ast_node = test_utils.extract_node('''
+ def test(): pass
+ test.__annotations__
+ ''')
+ annotations = next(ast_node.infer())
+ self.assertIsInstance(annotations, astroid.Dict)
+ self.assertEqual(len(annotations.items), 0)
+
+ @test_utils.require_version(minver='3.0')
+ def test_annotations_kwdefaults(self):
+ ast_node = test_utils.extract_node('''
+ def test(a: 1, *args: 2, f:4='lala', **kwarg:3)->2: pass
+ test.__annotations__ #@
+ test.__kwdefaults__ #@
+ ''')
+ annotations = next(ast_node[0].infer())
+ self.assertIsInstance(annotations, astroid.Dict)
+ self.assertIsInstance(annotations.getitem('return'), astroid.Const)
+ self.assertEqual(annotations.getitem('return').value, 2)
+ self.assertIsInstance(annotations.getitem('a'), astroid.Const)
+ self.assertEqual(annotations.getitem('a').value, 1)
+ self.assertEqual(annotations.getitem('args').value, 2)
+ self.assertEqual(annotations.getitem('kwarg').value, 3)
+
+ # Currently not enabled.
+ # self.assertEqual(annotations.getitem('f').value, 4)
+
+ kwdefaults = next(ast_node[1].infer())
+ self.assertIsInstance(kwdefaults, astroid.Dict)
+ # self.assertEqual(kwdefaults.getitem('f').value, 'lala')
+
+
+class GeneratorModelTest(unittest.TestCase):
+
+ def test_model(self):
+ ast_nodes = test_utils.extract_node('''
+ def test():
+ "a"
+ yield
+
+ gen = test()
+ gen.__name__ #@
+ gen.__doc__ #@
+ gen.gi_code #@
+ gen.gi_frame #@
+ gen.send #@
+ ''')
+
+ name = next(ast_nodes[0].infer())
+ self.assertEqual(name.value, 'test')
+
+ doc = next(ast_nodes[1].infer())
+ self.assertEqual(doc.value, 'a')
+
+ gi_code = next(ast_nodes[2].infer())
+ self.assertIsInstance(gi_code, astroid.ClassDef)
+ self.assertEqual(gi_code.name, 'gi_code')
+
+ gi_frame = next(ast_nodes[3].infer())
+ self.assertIsInstance(gi_frame, astroid.ClassDef)
+ self.assertEqual(gi_frame.name, 'gi_frame')
+
+ send = next(ast_nodes[4].infer())
+ self.assertIsInstance(send, astroid.BoundMethod)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/astroid/util.py b/astroid/util.py
index 5c506b59..1111202f 100644
--- a/astroid/util.py
+++ b/astroid/util.py
@@ -8,10 +8,23 @@
import sys
import warnings
+import importlib
import lazy_object_proxy
import six
+def lazy_descriptor(obj):
+ class DescriptorProxy(lazy_object_proxy.Proxy):
+ def __get__(self, instance, owner=None):
+ return self.__class__.__get__(self, instance)
+ return DescriptorProxy(obj)
+
+
+def lazy_import(module_name):
+ return lazy_object_proxy.Proxy(
+ lambda: importlib.import_module('.' + module_name, 'astroid'))
+
+
def reraise(exception):
'''Reraises an exception with the traceback from the current exception
block.'''