Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/astroid/brain/brain_typing.py: 53%
144 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"""Astroid hooks for typing.py support."""
7from __future__ import annotations
9import typing
10from collections.abc import Iterator
11from functools import partial
12from typing import Final
14from astroid import context, extract_node, inference_tip
15from astroid.builder import _extract_single_node
16from astroid.const import PY39_PLUS
17from astroid.exceptions import (
18 AttributeInferenceError,
19 InferenceError,
20 UseInferenceDefault,
21)
22from astroid.manager import AstroidManager
23from astroid.nodes.node_classes import (
24 Assign,
25 AssignName,
26 Attribute,
27 Call,
28 Const,
29 JoinedStr,
30 Name,
31 NodeNG,
32 Subscript,
33 Tuple,
34)
35from astroid.nodes.scoped_nodes import ClassDef, FunctionDef
37TYPING_TYPEVARS = {"TypeVar", "NewType"}
38TYPING_TYPEVARS_QUALIFIED: Final = {
39 "typing.TypeVar",
40 "typing.NewType",
41 "typing_extensions.TypeVar",
42}
43TYPING_TYPEDDICT_QUALIFIED: Final = {"typing.TypedDict", "typing_extensions.TypedDict"}
44TYPING_TYPE_TEMPLATE = """
45class Meta(type):
46 def __getitem__(self, item):
47 return self
49 @property
50 def __args__(self):
51 return ()
53class {0}(metaclass=Meta):
54 pass
55"""
56TYPING_MEMBERS = set(getattr(typing, "__all__", []))
58TYPING_ALIAS = frozenset(
59 (
60 "typing.Hashable",
61 "typing.Awaitable",
62 "typing.Coroutine",
63 "typing.AsyncIterable",
64 "typing.AsyncIterator",
65 "typing.Iterable",
66 "typing.Iterator",
67 "typing.Reversible",
68 "typing.Sized",
69 "typing.Container",
70 "typing.Collection",
71 "typing.Callable",
72 "typing.AbstractSet",
73 "typing.MutableSet",
74 "typing.Mapping",
75 "typing.MutableMapping",
76 "typing.Sequence",
77 "typing.MutableSequence",
78 "typing.ByteString",
79 "typing.Tuple",
80 "typing.List",
81 "typing.Deque",
82 "typing.Set",
83 "typing.FrozenSet",
84 "typing.MappingView",
85 "typing.KeysView",
86 "typing.ItemsView",
87 "typing.ValuesView",
88 "typing.ContextManager",
89 "typing.AsyncContextManager",
90 "typing.Dict",
91 "typing.DefaultDict",
92 "typing.OrderedDict",
93 "typing.Counter",
94 "typing.ChainMap",
95 "typing.Generator",
96 "typing.AsyncGenerator",
97 "typing.Type",
98 "typing.Pattern",
99 "typing.Match",
100 )
101)
103CLASS_GETITEM_TEMPLATE = """
104@classmethod
105def __class_getitem__(cls, item):
106 return cls
107"""
110def looks_like_typing_typevar_or_newtype(node) -> bool:
111 func = node.func
112 if isinstance(func, Attribute):
113 return func.attrname in TYPING_TYPEVARS
114 if isinstance(func, Name):
115 return func.name in TYPING_TYPEVARS
116 return False
119def infer_typing_typevar_or_newtype(node, context_itton=None):
120 """Infer a typing.TypeVar(...) or typing.NewType(...) call."""
121 try:
122 func = next(node.func.infer(context=context_itton))
123 except (InferenceError, StopIteration) as exc:
124 raise UseInferenceDefault from exc
126 if func.qname() not in TYPING_TYPEVARS_QUALIFIED:
127 raise UseInferenceDefault
128 if not node.args:
129 raise UseInferenceDefault
130 # Cannot infer from a dynamic class name (f-string)
131 if isinstance(node.args[0], JoinedStr):
132 raise UseInferenceDefault
134 typename = node.args[0].as_string().strip("'")
135 node = extract_node(TYPING_TYPE_TEMPLATE.format(typename))
136 return node.infer(context=context_itton)
139def _looks_like_typing_subscript(node) -> bool:
140 """Try to figure out if a Subscript node *might* be a typing-related subscript."""
141 if isinstance(node, Name):
142 return node.name in TYPING_MEMBERS
143 if isinstance(node, Attribute):
144 return node.attrname in TYPING_MEMBERS
145 if isinstance(node, Subscript):
146 return _looks_like_typing_subscript(node.value)
147 return False
150def infer_typing_attr(
151 node: Subscript, ctx: context.InferenceContext | None = None
152) -> Iterator[ClassDef]:
153 """Infer a typing.X[...] subscript."""
154 try:
155 value = next(node.value.infer()) # type: ignore[union-attr] # value shouldn't be None for Subscript.
156 except (InferenceError, StopIteration) as exc:
157 raise UseInferenceDefault from exc
159 if not value.qname().startswith("typing.") or value.qname() in TYPING_ALIAS:
160 # If typing subscript belongs to an alias handle it separately.
161 raise UseInferenceDefault
163 if isinstance(value, ClassDef) and value.qname() in {
164 "typing.Generic",
165 "typing.Annotated",
166 "typing_extensions.Annotated",
167 }:
168 # typing.Generic and typing.Annotated (PY39) are subscriptable
169 # through __class_getitem__. Since astroid can't easily
170 # infer the native methods, replace them for an easy inference tip
171 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE)
172 value.locals["__class_getitem__"] = [func_to_add]
173 if (
174 isinstance(node.parent, ClassDef)
175 and node in node.parent.bases
176 and getattr(node.parent, "__cache", None)
177 ):
178 # node.parent.slots is evaluated and cached before the inference tip
179 # is first applied. Remove the last result to allow a recalculation of slots
180 cache = node.parent.__cache # type: ignore[attr-defined] # Unrecognized getattr
181 if cache.get(node.parent.slots) is not None:
182 del cache[node.parent.slots]
183 return iter([value])
185 node = extract_node(TYPING_TYPE_TEMPLATE.format(value.qname().split(".")[-1]))
186 return node.infer(context=ctx)
189def _looks_like_typedDict( # pylint: disable=invalid-name
190 node: FunctionDef | ClassDef,
191) -> bool:
192 """Check if node is TypedDict FunctionDef."""
193 return node.qname() in TYPING_TYPEDDICT_QUALIFIED
196def infer_old_typedDict( # pylint: disable=invalid-name
197 node: ClassDef, ctx: context.InferenceContext | None = None
198) -> Iterator[ClassDef]:
199 func_to_add = _extract_single_node("dict")
200 node.locals["__call__"] = [func_to_add]
201 return iter([node])
204def infer_typedDict( # pylint: disable=invalid-name
205 node: FunctionDef, ctx: context.InferenceContext | None = None
206) -> Iterator[ClassDef]:
207 """Replace TypedDict FunctionDef with ClassDef."""
208 class_def = ClassDef(
209 name="TypedDict",
210 lineno=node.lineno,
211 col_offset=node.col_offset,
212 parent=node.parent,
213 end_lineno=node.end_lineno,
214 end_col_offset=node.end_col_offset,
215 )
216 class_def.postinit(bases=[extract_node("dict")], body=[], decorators=None)
217 func_to_add = _extract_single_node("dict")
218 class_def.locals["__call__"] = [func_to_add]
219 return iter([class_def])
222def _looks_like_typing_alias(node: Call) -> bool:
223 """
224 Returns True if the node corresponds to a call to _alias function.
226 For example :
228 MutableSet = _alias(collections.abc.MutableSet, T)
230 :param node: call node
231 """
232 return (
233 isinstance(node.func, Name)
234 and node.func.name == "_alias"
235 and (
236 # _alias function works also for builtins object such as list and dict
237 isinstance(node.args[0], (Attribute, Name))
238 )
239 )
242def _forbid_class_getitem_access(node: ClassDef) -> None:
243 """Disable the access to __class_getitem__ method for the node in parameters."""
245 def full_raiser(origin_func, attr, *args, **kwargs):
246 """
247 Raises an AttributeInferenceError in case of access to __class_getitem__ method.
248 Otherwise, just call origin_func.
249 """
250 if attr == "__class_getitem__":
251 raise AttributeInferenceError("__class_getitem__ access is not allowed")
252 return origin_func(attr, *args, **kwargs)
254 try:
255 node.getattr("__class_getitem__")
256 # If we are here, then we are sure to modify an object that does have
257 # __class_getitem__ method (which origin is the protocol defined in
258 # collections module) whereas the typing module considers it should not.
259 # We do not want __class_getitem__ to be found in the classdef
260 partial_raiser = partial(full_raiser, node.getattr)
261 node.getattr = partial_raiser
262 except AttributeInferenceError:
263 pass
266def infer_typing_alias(
267 node: Call, ctx: context.InferenceContext | None = None
268) -> Iterator[ClassDef]:
269 """
270 Infers the call to _alias function
271 Insert ClassDef, with same name as aliased class,
272 in mro to simulate _GenericAlias.
274 :param node: call node
275 :param context: inference context
276 """
277 if (
278 not isinstance(node.parent, Assign)
279 or not len(node.parent.targets) == 1
280 or not isinstance(node.parent.targets[0], AssignName)
281 ):
282 raise UseInferenceDefault
283 try:
284 res = next(node.args[0].infer(context=ctx))
285 except StopIteration as e:
286 raise InferenceError(node=node.args[0], context=ctx) from e
288 assign_name = node.parent.targets[0]
290 class_def = ClassDef(
291 name=assign_name.name,
292 lineno=assign_name.lineno,
293 col_offset=assign_name.col_offset,
294 parent=node.parent,
295 end_lineno=assign_name.end_lineno,
296 end_col_offset=assign_name.end_col_offset,
297 )
298 if isinstance(res, ClassDef):
299 # Only add `res` as base if it's a `ClassDef`
300 # This isn't the case for `typing.Pattern` and `typing.Match`
301 class_def.postinit(bases=[res], body=[], decorators=None)
303 maybe_type_var = node.args[1]
304 if (
305 not PY39_PLUS
306 and not (isinstance(maybe_type_var, Tuple) and not maybe_type_var.elts)
307 or PY39_PLUS
308 and isinstance(maybe_type_var, Const)
309 and maybe_type_var.value > 0
310 ):
311 # If typing alias is subscriptable, add `__class_getitem__` to ClassDef
312 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE)
313 class_def.locals["__class_getitem__"] = [func_to_add]
314 else:
315 # If not, make sure that `__class_getitem__` access is forbidden.
316 # This is an issue in cases where the aliased class implements it,
317 # but the typing alias isn't subscriptable. E.g., `typing.ByteString` for PY39+
318 _forbid_class_getitem_access(class_def)
320 # Avoid re-instantiating this class every time it's seen
321 node._explicit_inference = lambda node, context: iter([class_def])
322 return iter([class_def])
325def _looks_like_special_alias(node: Call) -> bool:
326 """Return True if call is for Tuple or Callable alias.
328 In PY37 and PY38 the call is to '_VariadicGenericAlias' with 'tuple' as
329 first argument. In PY39+ it is replaced by a call to '_TupleType'.
331 PY37: Tuple = _VariadicGenericAlias(tuple, (), inst=False, special=True)
332 PY39: Tuple = _TupleType(tuple, -1, inst=False, name='Tuple')
334 PY37: Callable = _VariadicGenericAlias(collections.abc.Callable, (), special=True)
335 PY39: Callable = _CallableType(collections.abc.Callable, 2)
336 """
337 return isinstance(node.func, Name) and (
338 not PY39_PLUS
339 and node.func.name == "_VariadicGenericAlias"
340 and (
341 isinstance(node.args[0], Name)
342 and node.args[0].name == "tuple"
343 or isinstance(node.args[0], Attribute)
344 and node.args[0].as_string() == "collections.abc.Callable"
345 )
346 or PY39_PLUS
347 and (
348 node.func.name == "_TupleType"
349 and isinstance(node.args[0], Name)
350 and node.args[0].name == "tuple"
351 or node.func.name == "_CallableType"
352 and isinstance(node.args[0], Attribute)
353 and node.args[0].as_string() == "collections.abc.Callable"
354 )
355 )
358def infer_special_alias(
359 node: Call, ctx: context.InferenceContext | None = None
360) -> Iterator[ClassDef]:
361 """Infer call to tuple alias as new subscriptable class typing.Tuple."""
362 if not (
363 isinstance(node.parent, Assign)
364 and len(node.parent.targets) == 1
365 and isinstance(node.parent.targets[0], AssignName)
366 ):
367 raise UseInferenceDefault
368 try:
369 res = next(node.args[0].infer(context=ctx))
370 except StopIteration as e:
371 raise InferenceError(node=node.args[0], context=ctx) from e
373 assign_name = node.parent.targets[0]
374 class_def = ClassDef(
375 name=assign_name.name,
376 parent=node.parent,
377 lineno=assign_name.lineno,
378 col_offset=assign_name.col_offset,
379 end_lineno=assign_name.end_lineno,
380 end_col_offset=assign_name.end_col_offset,
381 )
382 class_def.postinit(bases=[res], body=[], decorators=None)
383 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE)
384 class_def.locals["__class_getitem__"] = [func_to_add]
385 return iter([class_def])
388def _looks_like_typing_cast(node: Call) -> bool:
389 return isinstance(node, Call) and (
390 isinstance(node.func, Name)
391 and node.func.name == "cast"
392 or isinstance(node.func, Attribute)
393 and node.func.attrname == "cast"
394 )
397def infer_typing_cast(
398 node: Call, ctx: context.InferenceContext | None = None
399) -> Iterator[NodeNG]:
400 """Infer call to cast() returning same type as casted-from var."""
401 if not isinstance(node.func, (Name, Attribute)):
402 raise UseInferenceDefault
404 try:
405 func = next(node.func.infer(context=ctx))
406 except (InferenceError, StopIteration) as exc:
407 raise UseInferenceDefault from exc
408 if (
409 not isinstance(func, FunctionDef)
410 or func.qname() != "typing.cast"
411 or len(node.args) != 2
412 ):
413 raise UseInferenceDefault
415 return node.args[1].infer(context=ctx)
418AstroidManager().register_transform(
419 Call,
420 inference_tip(infer_typing_typevar_or_newtype),
421 looks_like_typing_typevar_or_newtype,
422)
423AstroidManager().register_transform(
424 Subscript, inference_tip(infer_typing_attr), _looks_like_typing_subscript
425)
426AstroidManager().register_transform(
427 Call, inference_tip(infer_typing_cast), _looks_like_typing_cast
428)
430if PY39_PLUS:
431 AstroidManager().register_transform(
432 FunctionDef, inference_tip(infer_typedDict), _looks_like_typedDict
433 )
434else:
435 AstroidManager().register_transform(
436 ClassDef, inference_tip(infer_old_typedDict), _looks_like_typedDict
437 )
439AstroidManager().register_transform(
440 Call, inference_tip(infer_typing_alias), _looks_like_typing_alias
441)
442AstroidManager().register_transform(
443 Call, inference_tip(infer_special_alias), _looks_like_special_alias
444)