diff options
-rw-r--r-- | ChangeLog | 8 | ||||
-rw-r--r-- | astroid/bases.py | 57 | ||||
-rw-r--r-- | astroid/interpreter/__init__.py | 0 | ||||
-rw-r--r-- | astroid/interpreter/objectmodel.py | 530 | ||||
-rw-r--r-- | astroid/node_classes.py | 11 | ||||
-rw-r--r-- | astroid/objects.py | 14 | ||||
-rw-r--r-- | astroid/raw_building.py | 12 | ||||
-rw-r--r-- | astroid/scoped_nodes.py | 68 | ||||
-rw-r--r-- | astroid/tests/unittest_object_model.py | 434 | ||||
-rw-r--r-- | astroid/util.py | 13 |
10 files changed, 1077 insertions, 70 deletions
@@ -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.''' |