Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/astroid/context.py: 92%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
5"""Various context related utilities, including inference and call contexts."""
7from __future__ import annotations
9import contextlib
10from collections.abc import Iterator, Sequence
11from typing import TYPE_CHECKING
13from astroid.typing import InferenceResult, SuccessfulInferenceResult
15if TYPE_CHECKING:
16 from astroid import constraint, nodes
18_InferenceCache = dict[
19 tuple["nodes.NodeNG", str | None, str | None, str | None], Sequence["nodes.NodeNG"]
20]
22_INFERENCE_CACHE: _InferenceCache = {}
25def _invalidate_cache() -> None:
26 _INFERENCE_CACHE.clear()
29class InferenceContext:
30 """Provide context for inference.
32 Store already inferred nodes to save time
33 Account for already visited nodes to stop infinite recursion
34 """
36 __slots__ = (
37 "_nodes_inferred",
38 "boundnode",
39 "callcontext",
40 "constraints",
41 "extra_context",
42 "lookupname",
43 "path",
44 )
46 max_inferred = 100
48 def __init__(
49 self,
50 path: set[tuple[nodes.NodeNG, str | None]] | None = None,
51 nodes_inferred: list[int] | None = None,
52 ) -> None:
53 if nodes_inferred is None:
54 self._nodes_inferred = [0]
55 else:
56 self._nodes_inferred = nodes_inferred
58 self.path = path or set()
59 """Path of visited nodes and their lookupname.
61 Currently this key is ``(node, context.lookupname)``
62 """
63 self.lookupname: str | None = None
64 """The original name of the node.
66 e.g.
67 foo = 1
68 The inference of 'foo' is nodes.Const(1) but the lookup name is 'foo'
69 """
70 self.callcontext: CallContext | None = None
71 """The call arguments and keywords for the given context."""
72 self.boundnode: SuccessfulInferenceResult | None = None
73 """The bound node of the given context.
75 e.g. the bound node of object.__new__(cls) is the object node
76 """
77 self.extra_context: dict[SuccessfulInferenceResult, InferenceContext] = {}
78 """Context that needs to be passed down through call stacks for call arguments."""
80 self.constraints: dict[str, dict[nodes.NodeNG, set[constraint.Constraint]]] = {}
81 """The constraints on nodes."""
83 @property
84 def nodes_inferred(self) -> int:
85 """
86 Number of nodes inferred in this context and all its clones/descendents.
88 Wrap inner value in a mutable cell to allow for mutating a class
89 variable in the presence of __slots__
90 """
91 return self._nodes_inferred[0]
93 @nodes_inferred.setter
94 def nodes_inferred(self, value: int) -> None:
95 self._nodes_inferred[0] = value
97 @property
98 def inferred(self) -> _InferenceCache:
99 """
100 Inferred node contexts to their mapped results.
102 Currently the key is ``(node, lookupname, callcontext, boundnode)``
103 and the value is tuple of the inferred results
104 """
105 return _INFERENCE_CACHE
107 def push(self, node: nodes.NodeNG) -> bool:
108 """Push node into inference path.
110 Allows one to see if the given node has already
111 been looked at for this inference context
112 """
113 name = self.lookupname
114 if (node, name) in self.path:
115 return True
117 self.path.add((node, name))
118 return False
120 def clone(self) -> InferenceContext:
121 """Clone inference path.
123 For example, each side of a binary operation (BinOp)
124 starts with the same context but diverge as each side is inferred
125 so the InferenceContext will need be cloned
126 """
127 # XXX copy lookupname/callcontext ?
128 # Bypass __init__: with __slots__ all attributes are written directly
129 # below, so the conditionals/defaults in __init__ would be wasted work
130 # on what is the hottest constructor in inference (~85k calls per
131 # pandas/frame run, see #1115).
132 clone = InferenceContext.__new__(InferenceContext)
133 clone._nodes_inferred = self._nodes_inferred
134 clone.path = self.path.copy()
135 clone.lookupname = None
136 clone.callcontext = self.callcontext
137 clone.boundnode = self.boundnode
138 clone.extra_context = self.extra_context
139 clone.constraints = self.constraints.copy()
140 return clone
142 @contextlib.contextmanager
143 def restore_path(self) -> Iterator[None]:
144 path = set(self.path)
145 yield
146 self.path = path
148 def is_empty(self) -> bool:
149 return (
150 not self.path
151 and not self.nodes_inferred
152 and not self.callcontext
153 and not self.boundnode
154 and not self.lookupname
155 and not self.callcontext
156 and not self.extra_context
157 and not self.constraints
158 )
160 def __str__(self) -> str:
161 import pprint # pylint: disable=import-outside-toplevel
163 state = (
164 f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}"
165 for field in self.__slots__
166 )
167 return "{}({})".format(type(self).__name__, ",\n ".join(state))
170class CallContext:
171 """Holds information for a call site."""
173 __slots__ = ("args", "callee", "keywords")
175 def __init__(
176 self,
177 args: list[nodes.NodeNG],
178 keywords: list[nodes.Keyword] | None = None,
179 callee: InferenceResult | None = None,
180 ):
181 self.args = args # Call positional arguments
182 if keywords:
183 arg_value_pairs = [(arg.arg, arg.value) for arg in keywords]
184 else:
185 arg_value_pairs = []
186 self.keywords = arg_value_pairs # Call keyword arguments
187 self.callee = callee # Function being called
190def copy_context(context: InferenceContext | None) -> InferenceContext:
191 """Clone a context if given, or return a fresh context."""
192 if context is not None:
193 return context.clone()
195 return InferenceContext()
198def bind_context_to_node(
199 context: InferenceContext | None, node: SuccessfulInferenceResult
200) -> InferenceContext:
201 """Give a context a boundnode
202 to retrieve the correct function name or attribute value
203 with from further inference.
205 Do not use an existing context since the boundnode could then
206 be incorrectly propagated higher up in the call stack.
207 """
208 context = copy_context(context)
209 context.boundnode = node
210 return context