Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/astroid/context.py: 93%
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[
81 str, dict[nodes.If | nodes.IfExp, set[constraint.Constraint]]
82 ] = {}
83 """The constraints on nodes."""
85 @property
86 def nodes_inferred(self) -> int:
87 """
88 Number of nodes inferred in this context and all its clones/descendents.
90 Wrap inner value in a mutable cell to allow for mutating a class
91 variable in the presence of __slots__
92 """
93 return self._nodes_inferred[0]
95 @nodes_inferred.setter
96 def nodes_inferred(self, value: int) -> None:
97 self._nodes_inferred[0] = value
99 @property
100 def inferred(self) -> _InferenceCache:
101 """
102 Inferred node contexts to their mapped results.
104 Currently the key is ``(node, lookupname, callcontext, boundnode)``
105 and the value is tuple of the inferred results
106 """
107 return _INFERENCE_CACHE
109 def push(self, node: nodes.NodeNG) -> bool:
110 """Push node into inference path.
112 Allows one to see if the given node has already
113 been looked at for this inference context
114 """
115 name = self.lookupname
116 if (node, name) in self.path:
117 return True
119 self.path.add((node, name))
120 return False
122 def clone(self) -> InferenceContext:
123 """Clone inference path.
125 For example, each side of a binary operation (BinOp)
126 starts with the same context but diverge as each side is inferred
127 so the InferenceContext will need be cloned
128 """
129 # XXX copy lookupname/callcontext ?
130 clone = InferenceContext(self.path.copy(), nodes_inferred=self._nodes_inferred)
131 clone.callcontext = self.callcontext
132 clone.boundnode = self.boundnode
133 clone.extra_context = self.extra_context
134 clone.constraints = self.constraints.copy()
135 return clone
137 @contextlib.contextmanager
138 def restore_path(self) -> Iterator[None]:
139 path = set(self.path)
140 yield
141 self.path = path
143 def is_empty(self) -> bool:
144 return (
145 not self.path
146 and not self.nodes_inferred
147 and not self.callcontext
148 and not self.boundnode
149 and not self.lookupname
150 and not self.callcontext
151 and not self.extra_context
152 and not self.constraints
153 )
155 def __str__(self) -> str:
156 import pprint # pylint: disable=import-outside-toplevel
158 state = (
159 f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}"
160 for field in self.__slots__
161 )
162 return "{}({})".format(type(self).__name__, ",\n ".join(state))
165class CallContext:
166 """Holds information for a call site."""
168 __slots__ = ("args", "callee", "keywords")
170 def __init__(
171 self,
172 args: list[nodes.NodeNG],
173 keywords: list[nodes.Keyword] | None = None,
174 callee: InferenceResult | None = None,
175 ):
176 self.args = args # Call positional arguments
177 if keywords:
178 arg_value_pairs = [(arg.arg, arg.value) for arg in keywords]
179 else:
180 arg_value_pairs = []
181 self.keywords = arg_value_pairs # Call keyword arguments
182 self.callee = callee # Function being called
185def copy_context(context: InferenceContext | None) -> InferenceContext:
186 """Clone a context if given, or return a fresh context."""
187 if context is not None:
188 return context.clone()
190 return InferenceContext()
193def bind_context_to_node(
194 context: InferenceContext | None, node: SuccessfulInferenceResult
195) -> InferenceContext:
196 """Give a context a boundnode
197 to retrieve the correct function name or attribute value
198 with from further inference.
200 Do not use an existing context since the boundnode could then
201 be incorrectly propagated higher up in the call stack.
202 """
203 context = copy_context(context)
204 context.boundnode = node
205 return context