From 206d8a296e5751ebaaa96db699ecd8b44351d9d1 Mon Sep 17 00:00:00 2001 From: Bryce Guinta Date: Sat, 27 Jan 2018 18:15:00 -0700 Subject: [PATCH] Fix inference issue with inference path cloning Copy inference path in inference context upon cloning to prevent diverging inference paths causing uninferable results --- astroid/context.py | 33 ++++++++++++++++++++++++++++++++- astroid/decorators.py | 6 +++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/astroid/context.py b/astroid/context.py index 00b0287ba3..2274d67f1a 100644 --- a/astroid/context.py +++ b/astroid/context.py @@ -7,20 +7,43 @@ """Various context related utilities, including inference and call contexts.""" import contextlib +import copy import pprint class InferenceContext(object): + """Provide context for inference + + Store already inferred nodes to save time + Account for already visited nodes to infinite stop infinite recursion + """ + __slots__ = ('path', 'lookupname', 'callcontext', 'boundnode', 'inferred') def __init__(self, path=None, inferred=None): self.path = path or set() + """Path of visited nodes and their lookupname + :type: set(tuple(NodeNG, optional(str)))""" self.lookupname = None self.callcontext = None self.boundnode = None self.inferred = inferred or {} + """ + :type: dict(seq, seq) + + Inferred node contexts to their mapped results + Currently the key is (node, lookupname, callcontext, boundnode) + and the value is tuple of the inferred results + """ def push(self, node): + """Push node into inference path + + :return: True if node is already in context path else False + :rtype: bool + + Allows one to see if the given node has already + been looked at for this inference context""" name = self.lookupname if (node, name) in self.path: return True @@ -29,13 +52,21 @@ def push(self, node): return False def clone(self): + """Clone inference path + + For example, each side of a binary operation (BinOp) + starts with the same context but diverge as each side is inferred + so the InferenceContext will need be cloned""" # XXX copy lookupname/callcontext ? - clone = InferenceContext(self.path, inferred=self.inferred) + clone = InferenceContext(copy.copy(self.path), inferred=self.inferred) clone.callcontext = self.callcontext clone.boundnode = self.boundnode return clone def cache_generator(self, key, generator): + """Cache result of generator into dictionary + + Used to cache inference results""" results = [] for result in generator: results.append(result) diff --git a/astroid/decorators.py b/astroid/decorators.py index a59cab2292..baec43b917 100644 --- a/astroid/decorators.py +++ b/astroid/decorators.py @@ -68,7 +68,11 @@ def __get__(self, inst, objtype=None): def path_wrapper(func): - """return the given infer function wrapped to handle the path""" + """return the given infer function wrapped to handle the path + + Used to stop inference if the node has already been looked + at for a given `InferenceContext` to prevent infinite recursion + """ # TODO: switch this to wrapt after the monkey-patching is fixed (ceridwen) @functools.wraps(func) def wrapped(node, context=None, _func=func, **kwargs):