summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudiu Popa <pcmanticore@gmail.com>2016-06-03 20:26:18 +0100
committerClaudiu Popa <pcmanticore@gmail.com>2016-06-03 20:30:30 +0100
commit2e8c48123419e4aafc91e1f35bc9b3f35541cb68 (patch)
treeb1d97737c87949b623d590d83a7d6977ae8a92cc
parent3e27213914271309a4716662b09fda91fca9efa1 (diff)
downloadastroid-git-2e8c48123419e4aafc91e1f35bc9b3f35541cb68.tar.gz
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.
-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.'''