summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudiu Popa <cpopa@cloudbasesolutions.com>2015-03-11 13:47:04 +0200
committerClaudiu Popa <cpopa@cloudbasesolutions.com>2015-03-11 13:47:04 +0200
commit9b5d9598574f593485a7e2ddff6a5a0e995b62f0 (patch)
tree2841e5f79066eebefcf8d31b087a36a34905efa7
parent910f7527d3260af5749f64177166d11d7318b9b2 (diff)
downloadastroid-9b5d9598574f593485a7e2ddff6a5a0e995b62f0.tar.gz
Change the way how context caching and scoping is done.
This patch reverts some changes added by a couple of changesets, making the context caching and scoping to work as before. The changesets in question are: * 41b3bd589da0549ac061bc4c4b5d379cdbb1e10c Replace copy_context with some dynamic scoping. * 8d3c601 Remove context.lookupname; make it an argument to infer() when appropriate. * partially reverts 048a42c. Fix some deep recursion problems. There were some problems with these changes, which led to horrendous performance when dealing with multiple inference paths of the same names, as seen in these Pylint issues: * https://bitbucket.org/logilab/pylint/issue/395/horrible-performance-related-to-inspect * https://bitbucket.org/logilab/pylint/issue/465/pylint-hangs-when-using-inspectsignature * https://bitbucket.org/logilab/pylint/issue/430/pylint-140-execution-time-and-memory The reverted changes assumed that a context it's only passed to callees and then destroyed, thus InferenceContext.push always returned another inference context, with the updated inference path so far. This is wrong, since contexts are sometimes reused, so the original context, the one before the .push call need to have the same cache key in its path (this is actually what's happening in these mentioned issues, the same object is inferred over and over again, but with different contexts).
-rw-r--r--astroid/bases.py119
-rw-r--r--astroid/inference.py86
-rw-r--r--astroid/node_classes.py5
-rw-r--r--astroid/protocols.py7
-rw-r--r--astroid/scoped_nodes.py65
-rw-r--r--astroid/tests/unittest_inference.py2
-rw-r--r--astroid/tests/unittest_nodes.py6
7 files changed, 143 insertions, 147 deletions
diff --git a/astroid/bases.py b/astroid/bases.py
index f1f4cc4..108c874 100644
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -58,52 +58,28 @@ class Proxy(object):
# Inference ##################################################################
-MISSING = object()
-
-
class InferenceContext(object):
- __slots__ = ('path', 'callcontext', 'boundnode', 'infered')
-
- def __init__(self,
- path=None, callcontext=None, boundnode=None, infered=None):
- if path is None:
- self.path = frozenset()
- else:
- self.path = path
- self.callcontext = callcontext
- self.boundnode = boundnode
- if infered is None:
- self.infered = {}
- else:
- self.infered = infered
-
- def push(self, key):
- # This returns a NEW context with the same attributes, but a new key
- # added to `path`. The intention is that it's only passed to callees
- # and then destroyed; otherwise scope() may not work correctly.
- # The cache will be shared, since it's the same exact dict.
- if key in self.path:
- # End the containing generator
- raise StopIteration
-
- return InferenceContext(
- self.path.union([key]),
- self.callcontext,
- self.boundnode,
- self.infered,
- )
-
- @contextmanager
- def scope(self, callcontext=MISSING, boundnode=MISSING):
- try:
- orig = self.callcontext, self.boundnode
- if callcontext is not MISSING:
- self.callcontext = callcontext
- if boundnode is not MISSING:
- self.boundnode = boundnode
- yield
- finally:
- self.callcontext, self.boundnode = orig
+ __slots__ = ('path', 'lookupname', 'callcontext', 'boundnode', 'infered')
+
+ def __init__(self, path=None, infered=None):
+ self.path = path or set()
+ self.lookupname = None
+ self.callcontext = None
+ self.boundnode = None
+ self.infered = infered or {}
+
+ def push(self, node):
+ name = self.lookupname
+ if (node, name) in self.path:
+ raise StopIteration()
+ self.path.add((node, name))
+
+ def clone(self):
+ # XXX copy lookupname/callcontext ?
+ clone = InferenceContext(self.path, infered=self.infered)
+ clone.callcontext = self.callcontext
+ clone.boundnode = self.boundnode
+ return clone
def cache_generator(self, key, generator):
results = []
@@ -114,28 +90,38 @@ class InferenceContext(object):
self.infered[key] = tuple(results)
return
+ @contextmanager
+ def restore_path(self):
+ path = set(self.path)
+ yield
+ self.path = path
+
+def copy_context(context):
+ if context is not None:
+ return context.clone()
+ else:
+ return InferenceContext()
-def _infer_stmts(stmts, context, frame=None, lookupname=None):
+
+def _infer_stmts(stmts, context, frame=None):
"""return an iterator on statements inferred by each statement in <stmts>
"""
stmt = None
infered = False
- if context is None:
+ if context is not None:
+ name = context.lookupname
+ context = context.clone()
+ else:
+ name = None
context = InferenceContext()
for stmt in stmts:
if stmt is YES:
yield stmt
infered = True
continue
-
- kw = {}
- infered_name = stmt._infer_name(frame, lookupname)
- if infered_name is not None:
- # only returns not None if .infer() accepts a lookupname kwarg
- kw['lookupname'] = infered_name
-
+ context.lookupname = stmt._infer_name(frame, name)
try:
- for infered in stmt.infer(context, **kw):
+ for infered in stmt.infer(context):
yield infered
infered = True
except UnresolvableName:
@@ -197,12 +183,13 @@ class Instance(Proxy):
context = InferenceContext()
try:
# avoid recursively inferring the same attr on the same class
- new_context = context.push((self._proxied, name))
+
+ context.push((self._proxied, name))
# XXX frame should be self._proxied, or not ?
- get_attr = self.getattr(name, new_context, lookupclass=False)
+ get_attr = self.getattr(name, context, lookupclass=False)
return _infer_stmts(
- self._wrap_attr(get_attr, new_context),
- new_context,
+ self._wrap_attr(get_attr, context),
+ context,
frame=self,
)
except NotFoundError:
@@ -210,7 +197,7 @@ class Instance(Proxy):
# fallback to class'igetattr since it has some logic to handle
# descriptors
return self._wrap_attr(self._proxied.igetattr(name, context),
- context)
+ context)
except NotFoundError:
raise InferenceError(name)
@@ -301,9 +288,9 @@ class BoundMethod(UnboundMethod):
return True
def infer_call_result(self, caller, context):
- with context.scope(boundnode=self.bound):
- for infered in self._proxied.infer_call_result(caller, context):
- yield infered
+ context = context.clone()
+ context.boundnode = self.bound
+ return self._proxied.infer_call_result(caller, context)
class Generator(Instance):
@@ -335,8 +322,7 @@ def path_wrapper(func):
"""wrapper function handling context"""
if context is None:
context = InferenceContext()
- context = context.push((node, kwargs.get('lookupname')))
-
+ context.push(node)
yielded = set()
for res in _func(node, context, **kwargs):
# unproxy only true instance, not const, tuple, dict...
@@ -409,7 +395,8 @@ class NodeNG(object):
if not context:
return self._infer(context, **kwargs)
- key = (self, kwargs.get('lookupname'), context.callcontext, context.boundnode)
+ key = (self, context.lookupname,
+ context.callcontext, context.boundnode)
if key in context.infered:
return iter(context.infered[key])
diff --git a/astroid/inference.py b/astroid/inference.py
index f29b3d1..2280704 100644
--- a/astroid/inference.py
+++ b/astroid/inference.py
@@ -28,7 +28,7 @@ from astroid.manager import AstroidManager
from astroid.exceptions import (AstroidError, InferenceError, NoDefault,
NotFoundError, UnresolvableName)
from astroid.bases import (YES, Instance, InferenceContext,
- _infer_stmts, path_wrapper,
+ _infer_stmts, copy_context, path_wrapper,
raise_if_nothing_infered)
from astroid.protocols import (
_arguments_infer_argname,
@@ -175,89 +175,92 @@ def infer_name(self, context=None):
if not stmts:
raise UnresolvableName(self.name)
- return _infer_stmts(stmts, context, frame, self.name)
+ context = context.clone()
+ context.lookupname = self.name
+ return _infer_stmts(stmts, context, frame)
nodes.Name._infer = path_wrapper(infer_name)
nodes.AssName.infer_lhs = infer_name # won't work with a path wrapper
def infer_callfunc(self, context=None):
"""infer a CallFunc node by trying to guess what the function returns"""
- if context is None:
- context = InferenceContext()
+ callcontext = context.clone()
+ callcontext.callcontext = CallContext(self.args, self.starargs, self.kwargs)
+ callcontext.boundnode = None
for callee in self.func.infer(context):
- with context.scope(
- callcontext=CallContext(self.args, self.starargs, self.kwargs),
- boundnode=None,
- ):
- if callee is YES:
- yield callee
- continue
- try:
- if hasattr(callee, 'infer_call_result'):
- for infered in callee.infer_call_result(self, context):
- yield infered
- except InferenceError:
- ## XXX log error ?
- continue
+ if callee is YES:
+ yield callee
+ continue
+ try:
+ if hasattr(callee, 'infer_call_result'):
+ for infered in callee.infer_call_result(self, callcontext):
+ yield infered
+ except InferenceError:
+ ## XXX log error ?
+ continue
nodes.CallFunc._infer = path_wrapper(raise_if_nothing_infered(infer_callfunc))
-def infer_import(self, context=None, asname=True, lookupname=None):
+def infer_import(self, context=None, asname=True):
"""infer an Import node: return the imported module/object"""
- if lookupname is None:
+ name = context.lookupname
+ if name is None:
raise InferenceError()
if asname:
- yield self.do_import_module(self.real_name(lookupname))
+ yield self.do_import_module(self.real_name(name))
else:
- yield self.do_import_module(lookupname)
+ yield self.do_import_module(name)
nodes.Import._infer = path_wrapper(infer_import)
def infer_name_module(self, name):
context = InferenceContext()
- return self.infer(context, asname=False, lookupname=name)
+ context.lookupname = name
+ return self.infer(context, asname=False)
nodes.Import.infer_name_module = infer_name_module
-def infer_from(self, context=None, asname=True, lookupname=None):
+def infer_from(self, context=None, asname=True):
"""infer a From nodes: return the imported module/object"""
- if lookupname is None:
+ name = context.lookupname
+ if name is None:
raise InferenceError()
if asname:
- lookupname = self.real_name(lookupname)
+ name = self.real_name(name)
module = self.do_import_module()
try:
- return _infer_stmts(module.getattr(lookupname, ignore_locals=module is self.root()), context, lookupname=lookupname)
+ context = copy_context(context)
+ context.lookupname = name
+ return _infer_stmts(module.getattr(name, ignore_locals=module is self.root()), context)
except NotFoundError:
- raise InferenceError(lookupname)
+ raise InferenceError(name)
nodes.From._infer = path_wrapper(infer_from)
def infer_getattr(self, context=None):
"""infer a Getattr node by using getattr on the associated object"""
- if not context:
- context = InferenceContext()
for owner in self.expr.infer(context):
if owner is YES:
yield owner
continue
try:
- with context.scope(boundnode=owner):
- for obj in owner.igetattr(self.attrname, context):
- yield obj
+ context.boundnode = owner
+ for obj in owner.igetattr(self.attrname, context):
+ yield obj
+ context.boundnode = None
except (NotFoundError, InferenceError):
- pass
+ context.boundnode = None
except AttributeError:
# XXX method / function
- pass
+ context.boundnode = None
nodes.Getattr._infer = path_wrapper(raise_if_nothing_infered(infer_getattr))
nodes.AssAttr.infer_lhs = raise_if_nothing_infered(infer_getattr) # # won't work with a path wrapper
-def infer_global(self, context=None, lookupname=None):
- if lookupname is None:
+def infer_global(self, context=None):
+ if context.lookupname is None:
raise InferenceError()
try:
- return _infer_stmts(self.root().getattr(lookupname), context)
+ return _infer_stmts(self.root().getattr(context.lookupname), context)
except NotFoundError:
raise InferenceError()
nodes.Global._infer = path_wrapper(infer_global)
@@ -349,10 +352,11 @@ def infer_binop(self, context=None):
nodes.BinOp._infer = path_wrapper(infer_binop)
-def infer_arguments(self, context=None, lookupname=None):
- if lookupname is None:
+def infer_arguments(self, context=None):
+ name = context.lookupname
+ if name is None:
raise InferenceError()
- return _arguments_infer_argname(self, lookupname, context)
+ return _arguments_infer_argname(self, name, context)
nodes.Arguments._infer = infer_arguments
diff --git a/astroid/node_classes.py b/astroid/node_classes.py
index f6061b1..d669aaa 100644
--- a/astroid/node_classes.py
+++ b/astroid/node_classes.py
@@ -24,7 +24,7 @@ import six
from logilab.common.decorators import cachedproperty
from astroid.exceptions import NoDefault
-from astroid.bases import (NodeNG, Statement, Instance,
+from astroid.bases import (NodeNG, Statement, Instance, InferenceContext,
_infer_stmts, YES, BUILTINS)
from astroid.mixins import (BlockRangeMixIn, AssignTypeMixin,
ParentAssignTypeMixin, FromImportMixIn)
@@ -130,7 +130,8 @@ class LookupMixIn(object):
the lookup method
"""
frame, stmts = self.lookup(name)
- return _infer_stmts(stmts, None, frame)
+ context = InferenceContext()
+ return _infer_stmts(stmts, context, frame)
def _filter_stmts(self, stmts, frame, offset):
"""filter statements to remove ignorable statements.
diff --git a/astroid/protocols.py b/astroid/protocols.py
index 5b55411..4c11f9c 100644
--- a/astroid/protocols.py
+++ b/astroid/protocols.py
@@ -24,7 +24,7 @@ import collections
from astroid.exceptions import InferenceError, NoDefault, NotFoundError
from astroid.node_classes import unpack_infer
-from astroid.bases import InferenceContext, \
+from astroid.bases import InferenceContext, copy_context, \
raise_if_nothing_infered, yes_if_nothing_infered, Instance, YES
from astroid.nodes import const_factory
from astroid import nodes
@@ -283,8 +283,7 @@ def _arguments_infer_argname(self, name, context):
# if there is a default value, yield it. And then yield YES to reflect
# we can't guess given argument value
try:
- if context is None:
- context = InferenceContext()
+ context = copy_context(context)
for infered in self.default_value(name).infer(context):
yield infered
yield YES
@@ -296,6 +295,8 @@ def arguments_assigned_stmts(self, node, context, asspath=None):
if context.callcontext:
# reset call context/name
callcontext = context.callcontext
+ context = copy_context(context)
+ context.callcontext = None
return callcontext.infer_argument(self.parent, node.name, context)
return _arguments_infer_argname(self, node.name, context)
nodes.Arguments.assigned_stmts = arguments_assigned_stmts
diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py
index 2bbd2c2..aa3874c 100644
--- a/astroid/scoped_nodes.py
+++ b/astroid/scoped_nodes.py
@@ -40,7 +40,7 @@ from astroid.exceptions import NotFoundError, \
from astroid.node_classes import Const, DelName, DelAttr, \
Dict, From, List, Pass, Raise, Return, Tuple, Yield, YieldFrom, \
LookupMixIn, const_factory as cf, unpack_infer, CallFunc
-from astroid.bases import NodeNG, InferenceContext, Instance,\
+from astroid.bases import NodeNG, InferenceContext, Instance, copy_context, \
YES, Generator, UnboundMethod, BoundMethod, _infer_stmts, \
BUILTINS
from astroid.mixins import FilterStmtsMixin
@@ -376,10 +376,10 @@ class Module(LocalsDictNodeNG):
"""inferred getattr"""
# set lookup name since this is necessary to infer on import nodes for
# instance
- if not context:
- context = InferenceContext()
+ context = copy_context(context)
+ context.lookupname = name
try:
- return _infer_stmts(self.getattr(name, context), context, frame=self, lookupname=name)
+ return _infer_stmts(self.getattr(name, context), context, frame=self)
except NotFoundError:
raise InferenceError(name)
@@ -1084,32 +1084,33 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin):
return
for stmt in self.bases:
- try:
- for baseobj in stmt.infer(context):
- if not isinstance(baseobj, Class):
- if isinstance(baseobj, Instance):
- baseobj = baseobj._proxied
- else:
- # duh ?
- continue
- if not baseobj.hide:
- if baseobj in yielded:
- continue # cf xxx above
- yielded.add(baseobj)
- yield baseobj
- if recurs:
- for grandpa in baseobj.ancestors(recurs=True,
- context=context):
- if grandpa is self:
- # This class is the ancestor of itself.
- break
- if grandpa in yielded:
+ with context.restore_path():
+ try:
+ for baseobj in stmt.infer(context):
+ if not isinstance(baseobj, Class):
+ if isinstance(baseobj, Instance):
+ baseobj = baseobj._proxied
+ else:
+ # duh ?
+ continue
+ if not baseobj.hide:
+ if baseobj in yielded:
continue # cf xxx above
- yielded.add(grandpa)
- yield grandpa
- except InferenceError:
- # XXX log error ?
- continue
+ yielded.add(baseobj)
+ yield baseobj
+ if recurs:
+ for grandpa in baseobj.ancestors(recurs=True,
+ context=context):
+ if grandpa is self:
+ # This class is the ancestor of itself.
+ break
+ if grandpa in yielded:
+ continue # cf xxx above
+ yielded.add(grandpa)
+ yield grandpa
+ except InferenceError:
+ # XXX log error ?
+ continue
def local_attr_ancestors(self, name, context=None):
"""return an iterator on astroid representation of parent classes
@@ -1204,11 +1205,11 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin):
"""
# set lookup name since this is necessary to infer on import nodes for
# instance
- if not context:
- context = InferenceContext()
+ context = copy_context(context)
+ context.lookupname = name
try:
for infered in _infer_stmts(self.getattr(name, context), context,
- frame=self, lookupname=name):
+ frame=self):
# yield YES object instead of descriptors when necessary
if not isinstance(infered, Const) and isinstance(infered, Instance):
try:
diff --git a/astroid/tests/unittest_inference.py b/astroid/tests/unittest_inference.py
index 1a53b05..fad7351 100644
--- a/astroid/tests/unittest_inference.py
+++ b/astroid/tests/unittest_inference.py
@@ -1412,7 +1412,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase):
"""
ast = test_utils.extract_node(code, __name__)
expr = ast.func.expr
- self.assertIs(next(expr.infer()), YES)
+ self.assertRaises(InferenceError, next, expr.infer())
def test_tuple_builtin_inference(self):
code = """
diff --git a/astroid/tests/unittest_nodes.py b/astroid/tests/unittest_nodes.py
index abffda1..3392c03 100644
--- a/astroid/tests/unittest_nodes.py
+++ b/astroid/tests/unittest_nodes.py
@@ -356,8 +356,10 @@ from ..cave import wine\n\n"""
astroid = resources.build_file('data/absimport.py')
ctx = InferenceContext()
# will fail if absolute import failed
- next(astroid['message'].infer(ctx, lookupname='message'))
- m = next(astroid['email'].infer(ctx, lookupname='email'))
+ ctx.lookupname = 'message'
+ next(astroid['message'].infer(ctx))
+ ctx.lookupname = 'email'
+ m = next(astroid['email'].infer(ctx))
self.assertFalse(m.file.startswith(os.path.join('data', 'email.py')))
def test_more_absolute_import(self):