Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/astroid/context.py: 64%
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
10import pprint
11from collections.abc import Iterator, Sequence
12from typing import TYPE_CHECKING, Optional
14from astroid.typing import InferenceResult, SuccessfulInferenceResult
16if TYPE_CHECKING:
17 from astroid import constraint, nodes
18 from astroid.nodes.node_classes import Keyword
19 from astroid.nodes.node_ng import NodeNG
21_InferenceCache = dict[
22 tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"]
23]
25_INFERENCE_CACHE: _InferenceCache = {}
28def _invalidate_cache() -> None:
29 _INFERENCE_CACHE.clear()
32class InferenceContext:
33 """Provide context for inference.
35 Store already inferred nodes to save time
36 Account for already visited nodes to stop infinite recursion
37 """
39 __slots__ = (
40 "_nodes_inferred",
41 "boundnode",
42 "callcontext",
43 "constraints",
44 "extra_context",
45 "lookupname",
46 "path",
47 )
49 max_inferred = 100
51 def __init__(
52 self,
53 path: set[tuple[nodes.NodeNG, str | None]] | None = None,
54 nodes_inferred: list[int] | None = None,
55 ) -> None:
56 if nodes_inferred is None:
57 self._nodes_inferred = [0]
58 else:
59 self._nodes_inferred = nodes_inferred
61 self.path = path or set()
62 """Path of visited nodes and their lookupname.
64 Currently this key is ``(node, context.lookupname)``
65 """
66 self.lookupname: str | None = None
67 """The original name of the node.
69 e.g.
70 foo = 1
71 The inference of 'foo' is nodes.Const(1) but the lookup name is 'foo'
72 """
73 self.callcontext: CallContext | None = None
74 """The call arguments and keywords for the given context."""
75 self.boundnode: SuccessfulInferenceResult | None = None
76 """The bound node of the given context.
78 e.g. the bound node of object.__new__(cls) is the object node
79 """
80 self.extra_context: dict[SuccessfulInferenceResult, InferenceContext] = {}
81 """Context that needs to be passed down through call stacks for call arguments."""
83 self.constraints: dict[str, dict[nodes.If, set[constraint.Constraint]]] = {}
84 """The constraints on nodes."""
86 @property
87 def nodes_inferred(self) -> int:
88 """
89 Number of nodes inferred in this context and all its clones/descendents.
91 Wrap inner value in a mutable cell to allow for mutating a class
92 variable in the presence of __slots__
93 """
94 return self._nodes_inferred[0]
96 @nodes_inferred.setter
97 def nodes_inferred(self, value: int) -> None:
98 self._nodes_inferred[0] = value
100 @property
101 def inferred(self) -> _InferenceCache:
102 """
103 Inferred node contexts to their mapped results.
105 Currently the key is ``(node, lookupname, callcontext, boundnode)``
106 and the value is tuple of the inferred results
107 """
108 return _INFERENCE_CACHE
110 def push(self, node: nodes.NodeNG) -> bool:
111 """Push node into inference path.
113 Allows one to see if the given node has already
114 been looked at for this inference context
115 """
116 name = self.lookupname
117 if (node, name) in self.path:
118 return True
120 self.path.add((node, name))
121 return False
123 def clone(self) -> InferenceContext:
124 """Clone inference path.
126 For example, each side of a binary operation (BinOp)
127 starts with the same context but diverge as each side is inferred
128 so the InferenceContext will need be cloned
129 """
130 # XXX copy lookupname/callcontext ?
131 clone = InferenceContext(self.path.copy(), nodes_inferred=self._nodes_inferred)
132 clone.callcontext = self.callcontext
133 clone.boundnode = self.boundnode
134 clone.extra_context = self.extra_context
135 clone.constraints = self.constraints.copy()
136 return clone
138 @contextlib.contextmanager
139 def restore_path(self) -> Iterator[None]:
140 path = set(self.path)
141 yield
142 self.path = path
144 def is_empty(self) -> bool:
145 return (
146 not self.path
147 and not self.nodes_inferred
148 and not self.callcontext
149 and not self.boundnode
150 and not self.lookupname
151 and not self.callcontext
152 and not self.extra_context
153 and not self.constraints
154 )
156 def __str__(self) -> str:
157 state = (
158 f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}"
159 for field in self.__slots__
160 )
161 return "{}({})".format(type(self).__name__, ",\n ".join(state))
164class CallContext:
165 """Holds information for a call site."""
167 __slots__ = ("args", "callee", "keywords")
169 def __init__(
170 self,
171 args: list[NodeNG],
172 keywords: list[Keyword] | None = None,
173 callee: InferenceResult | None = None,
174 ):
175 self.args = args # Call positional arguments
176 if keywords:
177 arg_value_pairs = [(arg.arg, arg.value) for arg in keywords]
178 else:
179 arg_value_pairs = []
180 self.keywords = arg_value_pairs # Call keyword arguments
181 self.callee = callee # Function being called
184def copy_context(context: InferenceContext | None) -> InferenceContext:
185 """Clone a context if given, or return a fresh context."""
186 if context is not None:
187 return context.clone()
189 return InferenceContext()
192def bind_context_to_node(
193 context: InferenceContext | None, node: SuccessfulInferenceResult
194) -> InferenceContext:
195 """Give a context a boundnode
196 to retrieve the correct function name or attribute value
197 with from further inference.
199 Do not use an existing context since the boundnode could then
200 be incorrectly propagated higher up in the call stack.
201 """
202 context = copy_context(context)
203 context.boundnode = node
204 return context