Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/astroid/context.py: 95%
77 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:53 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:53 +0000
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
12from typing import TYPE_CHECKING, Dict, Optional, Sequence, Tuple
14from astroid.typing import InferenceResult, SuccessfulInferenceResult
16if TYPE_CHECKING:
17 from astroid import constraint, nodes
18 from astroid.nodes.node_classes import Keyword, NodeNG
20_InferenceCache = Dict[
21 Tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"]
22]
24_INFERENCE_CACHE: _InferenceCache = {}
27def _invalidate_cache() -> None:
28 _INFERENCE_CACHE.clear()
31class InferenceContext:
32 """Provide context for inference.
34 Store already inferred nodes to save time
35 Account for already visited nodes to stop infinite recursion
36 """
38 __slots__ = (
39 "path",
40 "lookupname",
41 "callcontext",
42 "boundnode",
43 "extra_context",
44 "constraints",
45 "_nodes_inferred",
46 )
48 max_inferred = 100
50 def __init__(
51 self,
52 path: set[tuple[nodes.NodeNG, str | None]] | None = None,
53 nodes_inferred: list[int] | None = None,
54 ) -> None:
55 if nodes_inferred is None:
56 self._nodes_inferred = [0]
57 else:
58 self._nodes_inferred = nodes_inferred
60 self.path = path or set()
61 """Path of visited nodes and their lookupname.
63 Currently this key is ``(node, context.lookupname)``
64 """
65 self.lookupname: str | None = None
66 """The original name of the node.
68 e.g.
69 foo = 1
70 The inference of 'foo' is nodes.Const(1) but the lookup name is 'foo'
71 """
72 self.callcontext: CallContext | None = None
73 """The call arguments and keywords for the given context."""
74 self.boundnode: SuccessfulInferenceResult | None = None
75 """The bound node of the given context.
77 e.g. the bound node of object.__new__(cls) is the object node
78 """
79 self.extra_context: dict[SuccessfulInferenceResult, InferenceContext] = {}
80 """Context that needs to be passed down through call stacks for call arguments."""
82 self.constraints: dict[str, dict[nodes.If, set[constraint.Constraint]]] = {}
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 state = (
157 f"{field}={pprint.pformat(getattr(self, field), width=80 - len(field))}"
158 for field in self.__slots__
159 )
160 return "{}({})".format(type(self).__name__, ",\n ".join(state))
163class CallContext:
164 """Holds information for a call site."""
166 __slots__ = ("args", "keywords", "callee")
168 def __init__(
169 self,
170 args: list[NodeNG],
171 keywords: list[Keyword] | None = None,
172 callee: InferenceResult | None = None,
173 ):
174 self.args = args # Call positional arguments
175 if keywords:
176 arg_value_pairs = [(arg.arg, arg.value) for arg in keywords]
177 else:
178 arg_value_pairs = []
179 self.keywords = arg_value_pairs # Call keyword arguments
180 self.callee = callee # Function being called
183def copy_context(context: InferenceContext | None) -> InferenceContext:
184 """Clone a context if given, or return a fresh context."""
185 if context is not None:
186 return context.clone()
188 return InferenceContext()
191def bind_context_to_node(
192 context: InferenceContext | None, node: SuccessfulInferenceResult
193) -> InferenceContext:
194 """Give a context a boundnode
195 to retrieve the correct function name or attribute value
196 with from further inference.
198 Do not use an existing context since the boundnode could then
199 be incorrectly propagated higher up in the call stack.
200 """
201 context = copy_context(context)
202 context.boundnode = node
203 return context