aboutsummaryrefslogtreecommitdiff
path: root/astroid/context.py
diff options
context:
space:
mode:
Diffstat (limited to 'astroid/context.py')
-rw-r--r--astroid/context.py212
1 files changed, 212 insertions, 0 deletions
diff --git a/astroid/context.py b/astroid/context.py
new file mode 100644
index 00000000..4dbebcd7
--- /dev/null
+++ b/astroid/context.py
@@ -0,0 +1,212 @@
+# Copyright (c) 2015-2016, 2018-2020 Claudiu Popa <pcmanticore@gmail.com>
+# Copyright (c) 2015-2016 Ceridwen <ceridwenv@gmail.com>
+# Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
+# Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
+# Copyright (c) 2019-2021 hippo91 <guillaume.peillex@gmail.com>
+# Copyright (c) 2020 Bryce Guinta <bryce.guinta@protonmail.com>
+# Copyright (c) 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
+# Copyright (c) 2021 Daniƫl van Noord <13665637+DanielNoord@users.noreply.github.com>
+# Copyright (c) 2021 David Liu <david@cs.toronto.edu>
+# Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
+# Copyright (c) 2021 Andrew Haigh <hello@nelf.in>
+
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+
+"""Various context related utilities, including inference and call contexts."""
+import contextlib
+import pprint
+from typing import TYPE_CHECKING, List, MutableMapping, Optional, Sequence, Tuple
+
+if TYPE_CHECKING:
+ from astroid.nodes.node_classes import Keyword, NodeNG
+
+
+_INFERENCE_CACHE = {}
+
+
+def _invalidate_cache():
+ _INFERENCE_CACHE.clear()
+
+
+class InferenceContext:
+ """Provide context for inference
+
+ Store already inferred nodes to save time
+ Account for already visited nodes to stop infinite recursion
+ """
+
+ __slots__ = (
+ "path",
+ "lookupname",
+ "callcontext",
+ "boundnode",
+ "extra_context",
+ "_nodes_inferred",
+ )
+
+ max_inferred = 100
+
+ def __init__(self, path=None, nodes_inferred=None):
+ if nodes_inferred is None:
+ self._nodes_inferred = [0]
+ else:
+ self._nodes_inferred = nodes_inferred
+ self.path = path or set()
+ """
+ :type: set(tuple(NodeNG, optional(str)))
+
+ Path of visited nodes and their lookupname
+
+ Currently this key is ``(node, context.lookupname)``
+ """
+ self.lookupname = None
+ """
+ :type: optional[str]
+
+ The original name of the node
+
+ e.g.
+ foo = 1
+ The inference of 'foo' is nodes.Const(1) but the lookup name is 'foo'
+ """
+ self.callcontext = None
+ """
+ :type: optional[CallContext]
+
+ The call arguments and keywords for the given context
+ """
+ self.boundnode = None
+ """
+ :type: optional[NodeNG]
+
+ The bound node of the given context
+
+ e.g. the bound node of object.__new__(cls) is the object node
+ """
+ self.extra_context = {}
+ """
+ :type: dict(NodeNG, Context)
+
+ Context that needs to be passed down through call stacks
+ for call arguments
+ """
+
+ @property
+ def nodes_inferred(self):
+ """
+ Number of nodes inferred in this context and all its clones/decendents
+
+ Wrap inner value in a mutable cell to allow for mutating a class
+ variable in the presence of __slots__
+ """
+ return self._nodes_inferred[0]
+
+ @nodes_inferred.setter
+ def nodes_inferred(self, value):
+ self._nodes_inferred[0] = value
+
+ @property
+ def inferred(
+ self,
+ ) -> MutableMapping[
+ Tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"]
+ ]:
+ """
+ Inferred node contexts to their mapped results
+
+ Currently the key is ``(node, lookupname, callcontext, boundnode)``
+ and the value is tuple of the inferred results
+ """
+ return _INFERENCE_CACHE
+
+ 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
+
+ self.path.add((node, name))
+ 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.copy(), nodes_inferred=self._nodes_inferred)
+ clone.callcontext = self.callcontext
+ clone.boundnode = self.boundnode
+ clone.extra_context = self.extra_context
+ return clone
+
+ @contextlib.contextmanager
+ def restore_path(self):
+ path = set(self.path)
+ yield
+ self.path = path
+
+ def __str__(self):
+ state = (
+ f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}"
+ for field in self.__slots__
+ )
+ return "{}({})".format(type(self).__name__, ",\n ".join(state))
+
+
+class CallContext:
+ """Holds information for a call site."""
+
+ __slots__ = ("args", "keywords", "callee")
+
+ def __init__(
+ self,
+ args: List["NodeNG"],
+ keywords: Optional[List["Keyword"]] = None,
+ callee: Optional["NodeNG"] = None,
+ ):
+ self.args = args # Call positional arguments
+ if keywords:
+ keywords = [(arg.arg, arg.value) for arg in keywords]
+ else:
+ keywords = []
+ self.keywords = keywords # Call keyword arguments
+ self.callee = callee # Function being called
+
+
+def copy_context(context: Optional[InferenceContext]) -> InferenceContext:
+ """Clone a context if given, or return a fresh contexxt"""
+ if context is not None:
+ return context.clone()
+
+ return InferenceContext()
+
+
+def bind_context_to_node(context, node):
+ """Give a context a boundnode
+ to retrieve the correct function name or attribute value
+ with from further inference.
+
+ Do not use an existing context since the boundnode could then
+ be incorrectly propagated higher up in the call stack.
+
+ :param context: Context to use
+ :type context: Optional(context)
+
+ :param node: Node to do name lookups from
+ :type node NodeNG:
+
+ :returns: A new context
+ :rtype: InferenceContext
+ """
+ context = copy_context(context)
+ context.boundnode = node
+ return context