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
4
5"""Astroid hooks for typing.py support."""
6
7from __future__ import annotations
8
9import textwrap
10import typing
11from collections.abc import Iterator
12from functools import partial
13from typing import Final
14
15from astroid import context, nodes
16from astroid.brain.helpers import register_module_extender
17from astroid.builder import AstroidBuilder, _extract_single_node, extract_node
18from astroid.const import PY312_PLUS, PY313_PLUS, PY314_PLUS, PY315_PLUS
19from astroid.exceptions import (
20 AstroidSyntaxError,
21 AttributeInferenceError,
22 InferenceError,
23 UseInferenceDefault,
24)
25from astroid.inference_tip import inference_tip
26from astroid.manager import AstroidManager
27
28TYPING_TYPEVARS = {"TypeVar", "NewType"}
29TYPING_TYPEVARS_QUALIFIED: Final = {
30 "typing.TypeVar",
31 "typing.NewType",
32 "typing_extensions.TypeVar",
33}
34TYPING_TYPEDDICT_QUALIFIED: Final = {"typing.TypedDict", "typing_extensions.TypedDict"}
35TYPING_TYPE_TEMPLATE = """
36class Meta(type):
37 def __getitem__(self, item):
38 return self
39
40 @property
41 def __args__(self):
42 return ()
43
44class {0}(metaclass=Meta):
45 pass
46"""
47TYPING_MEMBERS = set(getattr(typing, "__all__", []))
48
49TYPING_ALIAS = frozenset(
50 (
51 "typing.Hashable",
52 "typing.Awaitable",
53 "typing.Coroutine",
54 "typing.AsyncIterable",
55 "typing.AsyncIterator",
56 "typing.Iterable",
57 "typing.Iterator",
58 "typing.Reversible",
59 "typing.Sized",
60 "typing.Container",
61 "typing.Collection",
62 "typing.Callable",
63 "typing.AbstractSet",
64 "typing.MutableSet",
65 "typing.Mapping",
66 "typing.MutableMapping",
67 "typing.Sequence",
68 "typing.MutableSequence",
69 "typing.ByteString", # scheduled for removal in 3.17
70 "typing.Tuple",
71 "typing.List",
72 "typing.Deque",
73 "typing.Set",
74 "typing.FrozenSet",
75 "typing.MappingView",
76 "typing.KeysView",
77 "typing.ItemsView",
78 "typing.ValuesView",
79 "typing.ContextManager",
80 "typing.AsyncContextManager",
81 "typing.Dict",
82 "typing.DefaultDict",
83 "typing.OrderedDict",
84 "typing.Counter",
85 "typing.ChainMap",
86 "typing.Generator",
87 "typing.AsyncGenerator",
88 "typing.Type",
89 "typing.Pattern",
90 "typing.Match",
91 )
92)
93
94CLASS_GETITEM_TEMPLATE = """
95@classmethod
96def __class_getitem__(cls, item):
97 return cls
98"""
99
100
101def looks_like_typing_typevar_or_newtype(node) -> bool:
102 func = node.func
103 if isinstance(func, nodes.Attribute):
104 return func.attrname in TYPING_TYPEVARS
105 if isinstance(func, nodes.Name):
106 return func.name in TYPING_TYPEVARS
107 return False
108
109
110def infer_typing_typevar_or_newtype(
111 node: nodes.Call, context_itton: context.InferenceContext | None = None
112) -> Iterator[nodes.ClassDef]:
113 """Infer a typing.TypeVar(...) or typing.NewType(...) call."""
114 try:
115 func = next(node.func.infer(context=context_itton))
116 except (InferenceError, StopIteration) as exc:
117 raise UseInferenceDefault from exc
118
119 if func.qname() not in TYPING_TYPEVARS_QUALIFIED:
120 raise UseInferenceDefault
121 if not node.args:
122 raise UseInferenceDefault
123 # Cannot infer from a dynamic class name (f-string)
124 if isinstance(node.args[0], nodes.JoinedStr):
125 raise UseInferenceDefault
126
127 typename = node.args[0].as_string().strip("'")
128 try:
129 # ``typing`` accepts any string as a name, so don't splice it into the
130 # template: a crafted value such as ``"T(Base): #"`` would break out of
131 # the identifier position and inject bases and a body into the class.
132 # Build the class with a fixed name and assign the real one afterwards.
133 node = extract_node(TYPING_TYPE_TEMPLATE.format("_TypeVar"))
134 except AstroidSyntaxError as exc:
135 raise InferenceError from exc
136 node.name = typename
137 return node.infer(context=context_itton)
138
139
140def _looks_like_typing_subscript(node) -> bool:
141 """Try to figure out if a Subscript node *might* be a typing-related subscript."""
142 if isinstance(node, nodes.Name):
143 return node.name in TYPING_MEMBERS
144 if isinstance(node, nodes.Attribute):
145 return node.attrname in TYPING_MEMBERS
146 if isinstance(node, nodes.Subscript):
147 return _looks_like_typing_subscript(node.value)
148 return False
149
150
151def infer_typing_attr(
152 node: nodes.Subscript, ctx: context.InferenceContext | None = None
153) -> Iterator[nodes.ClassDef]:
154 """Infer a typing.X[...] subscript."""
155 try:
156 value = next(node.value.infer()) # type: ignore[union-attr] # value shouldn't be None for Subscript.
157 except (InferenceError, StopIteration) as exc:
158 raise UseInferenceDefault from exc
159
160 if not value.qname().startswith("typing.") or value.qname() in TYPING_ALIAS:
161 # If typing subscript belongs to an alias handle it separately.
162 raise UseInferenceDefault
163
164 if (
165 PY313_PLUS
166 and isinstance(value, nodes.FunctionDef)
167 and value.qname() == "typing.Annotated"
168 ):
169 # typing.Annotated is a FunctionDef on 3.13+
170 node._explicit_inference = lambda node, context: iter([value])
171 return iter([value])
172
173 if isinstance(value, nodes.ClassDef) and value.qname() in {
174 "typing.Generic",
175 "typing.Annotated",
176 "typing_extensions.Annotated",
177 }:
178 # typing.Generic and typing.Annotated (PY39) are subscriptable
179 # through __class_getitem__. Since astroid can't easily
180 # infer the native methods, replace them for an easy inference tip
181 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE)
182 value.locals["__class_getitem__"] = [func_to_add]
183 if (
184 isinstance(node.parent, nodes.ClassDef)
185 and node in node.parent.bases
186 and getattr(node.parent, "__cache", None)
187 ):
188 # node.parent.slots is evaluated and cached before the inference tip
189 # is first applied. Remove the last result to allow a recalculation of slots
190 cache = node.parent.__cache # type: ignore[attr-defined] # Unrecognized getattr
191 if cache.get(node.parent.slots) is not None:
192 del cache[node.parent.slots]
193 # Avoid re-instantiating this class every time it's seen
194 node._explicit_inference = lambda node, context: iter([value])
195 return iter([value])
196
197 node = extract_node(TYPING_TYPE_TEMPLATE.format(value.qname().split(".")[-1]))
198 return node.infer(context=ctx)
199
200
201def _looks_like_generic_class_pep695(node: nodes.ClassDef) -> bool:
202 """Check if class is using type parameter. Python 3.12+."""
203 return len(node.type_params) > 0
204
205
206def infer_typing_generic_class_pep695(
207 node: nodes.ClassDef, ctx: context.InferenceContext | None = None
208) -> Iterator[nodes.ClassDef]:
209 """Add __class_getitem__ for generic classes. Python 3.12+."""
210 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE)
211 node.locals["__class_getitem__"] = [func_to_add]
212 return iter([node])
213
214
215def _looks_like_typedDict( # pylint: disable=invalid-name
216 node: nodes.FunctionDef | nodes.ClassDef,
217) -> bool:
218 """Check if node is TypedDict FunctionDef."""
219 return node.qname() in TYPING_TYPEDDICT_QUALIFIED
220
221
222def infer_typedDict( # pylint: disable=invalid-name
223 node: nodes.FunctionDef, ctx: context.InferenceContext | None = None
224) -> Iterator[nodes.ClassDef]:
225 """Replace TypedDict FunctionDef with ClassDef."""
226 class_def = nodes.ClassDef(
227 name="TypedDict",
228 lineno=node.lineno,
229 col_offset=node.col_offset,
230 parent=node.parent,
231 end_lineno=node.end_lineno,
232 end_col_offset=node.end_col_offset,
233 )
234 class_def.postinit(bases=[extract_node("dict")], body=[], decorators=None)
235 func_to_add = _extract_single_node("dict")
236 class_def.locals["__call__"] = [func_to_add]
237 # TypedDict subclasses have ``__required_keys__`` and ``__optional_keys__``
238 # class attributes at runtime (e.g. ``MyDict.__required_keys__``), even
239 # though the annotation-only body never declares them.
240 for attr in ("__required_keys__", "__optional_keys__"):
241 func_to_add = _extract_single_node("dict")
242 class_def.locals[attr] = [func_to_add]
243 return iter([class_def])
244
245
246def _looks_like_typing_alias(node: nodes.Call) -> bool:
247 """
248 Returns True if the node corresponds to a call to _alias function.
249
250 For example :
251
252 MutableSet = _alias(collections.abc.MutableSet, T)
253
254 :param node: call node
255 """
256 return (
257 isinstance(node.func, nodes.Name)
258 # TODO: remove _DeprecatedGenericAlias when Py3.14 min
259 and node.func.name in {"_alias", "_DeprecatedGenericAlias"}
260 and len(node.args) == 2
261 and (
262 # _alias function works also for builtins object such as list and dict
263 isinstance(node.args[0], (nodes.Attribute, nodes.Name))
264 )
265 )
266
267
268def _forbid_class_getitem_access(node: nodes.ClassDef) -> None:
269 """Disable the access to __class_getitem__ method for the node in parameters."""
270
271 def full_raiser(origin_func, attr, *args, **kwargs):
272 """
273 Raises an AttributeInferenceError in case of access to __class_getitem__ method.
274 Otherwise, just call origin_func.
275 """
276 if attr == "__class_getitem__":
277 raise AttributeInferenceError("__class_getitem__ access is not allowed")
278 return origin_func(attr, *args, **kwargs)
279
280 try:
281 node.getattr("__class_getitem__")
282 # If we are here, then we are sure to modify an object that does have
283 # __class_getitem__ method (which origin is the protocol defined in
284 # collections module) whereas the typing module considers it should not.
285 # We do not want __class_getitem__ to be found in the classdef
286 partial_raiser = partial(full_raiser, node.getattr)
287 node.getattr = partial_raiser
288 except AttributeInferenceError:
289 pass
290
291
292def infer_typing_alias(
293 node: nodes.Call, ctx: context.InferenceContext | None = None
294) -> Iterator[nodes.ClassDef]:
295 """
296 Infers the call to _alias function
297 Insert ClassDef, with same name as aliased class,
298 in mro to simulate _GenericAlias.
299
300 :param node: call node
301 :param context: inference context
302
303 # TODO: evaluate if still necessary when Py3.12 is minimum
304 """
305 if not (
306 isinstance(node.parent, nodes.Assign)
307 and len(node.parent.targets) == 1
308 and isinstance(node.parent.targets[0], nodes.AssignName)
309 ):
310 raise UseInferenceDefault
311 try:
312 res = next(node.args[0].infer(context=ctx))
313 except StopIteration as e:
314 raise InferenceError(node=node.args[0], context=ctx) from e
315
316 assign_name = node.parent.targets[0]
317
318 class_def = nodes.ClassDef(
319 name=assign_name.name,
320 lineno=assign_name.lineno,
321 col_offset=assign_name.col_offset,
322 parent=node.parent,
323 end_lineno=assign_name.end_lineno,
324 end_col_offset=assign_name.end_col_offset,
325 )
326 if isinstance(res, nodes.ClassDef):
327 # Only add `res` as base if it's a `ClassDef`
328 # This isn't the case for `typing.Pattern` and `typing.Match`
329 class_def.postinit(bases=[res], body=[], decorators=None)
330
331 maybe_type_var = node.args[1]
332 if isinstance(maybe_type_var, nodes.Const) and maybe_type_var.value > 0:
333 # If typing alias is subscriptable, add `__class_getitem__` to ClassDef
334 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE)
335 class_def.locals["__class_getitem__"] = [func_to_add]
336 else:
337 # If not, make sure that `__class_getitem__` access is forbidden.
338 # This is an issue in cases where the aliased class implements it,
339 # but the typing alias isn't subscriptable. E.g., `typing.ByteString` for PY39+
340 _forbid_class_getitem_access(class_def)
341
342 # Avoid re-instantiating this class every time it's seen
343 node._explicit_inference = lambda node, context: iter([class_def])
344 return iter([class_def])
345
346
347def _looks_like_special_alias(node: nodes.Call) -> bool:
348 """Return True if call is for Tuple or Callable alias.
349
350 In PY37 and PY38 the call is to '_VariadicGenericAlias' with 'tuple' as
351 first argument. In PY39+ it is replaced by a call to '_TupleType'.
352
353 PY37: Tuple = _VariadicGenericAlias(tuple, (), inst=False, special=True)
354 PY39: Tuple = _TupleType(tuple, -1, inst=False, name='Tuple')
355
356 PY37: Callable = _VariadicGenericAlias(collections.abc.Callable, (), special=True)
357 PY39: Callable = _CallableType(collections.abc.Callable, 2)
358 """
359 return (
360 isinstance(node.func, nodes.Name)
361 and node.args
362 and (
363 (
364 node.func.name == "_TupleType"
365 and isinstance(node.args[0], nodes.Name)
366 and node.args[0].name == "tuple"
367 )
368 or (
369 node.func.name == "_CallableType"
370 and isinstance(node.args[0], nodes.Attribute)
371 and node.args[0].as_string() == "collections.abc.Callable"
372 )
373 )
374 )
375
376
377def infer_special_alias(
378 node: nodes.Call, ctx: context.InferenceContext | None = None
379) -> Iterator[nodes.ClassDef]:
380 """Infer call to tuple alias as new subscriptable class typing.Tuple."""
381 if not (
382 isinstance(node.parent, nodes.Assign)
383 and len(node.parent.targets) == 1
384 and isinstance(node.parent.targets[0], nodes.AssignName)
385 ):
386 raise UseInferenceDefault
387 try:
388 res = next(node.args[0].infer(context=ctx))
389 except StopIteration as e:
390 raise InferenceError(node=node.args[0], context=ctx) from e
391
392 assign_name = node.parent.targets[0]
393 class_def = nodes.ClassDef(
394 name=assign_name.name,
395 parent=node.parent,
396 lineno=assign_name.lineno,
397 col_offset=assign_name.col_offset,
398 end_lineno=assign_name.end_lineno,
399 end_col_offset=assign_name.end_col_offset,
400 )
401 class_def.postinit(bases=[res], body=[], decorators=None)
402 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE)
403 class_def.locals["__class_getitem__"] = [func_to_add]
404 # Avoid re-instantiating this class every time it's seen
405 node._explicit_inference = lambda node, context: iter([class_def])
406 return iter([class_def])
407
408
409def _looks_like_typing_cast(node: nodes.Call) -> bool:
410 return (isinstance(node.func, nodes.Name) and node.func.name == "cast") or (
411 isinstance(node.func, nodes.Attribute) and node.func.attrname == "cast"
412 )
413
414
415def infer_typing_cast(
416 node: nodes.Call, ctx: context.InferenceContext | None = None
417) -> Iterator[nodes.NodeNG]:
418 """Infer call to cast() returning same type as casted-from var."""
419 if not isinstance(node.func, (nodes.Name, nodes.Attribute)):
420 raise UseInferenceDefault
421
422 try:
423 func = next(node.func.infer(context=ctx))
424 except (InferenceError, StopIteration) as exc:
425 raise UseInferenceDefault from exc
426 if not (
427 isinstance(func, nodes.FunctionDef)
428 and func.qname() == "typing.cast"
429 and len(node.args) == 2
430 ):
431 raise UseInferenceDefault
432
433 return node.args[1].infer(context=ctx)
434
435
436def _typing_transform():
437 code = textwrap.dedent("""
438 class Generic:
439 @classmethod
440 def __class_getitem__(cls, item): return cls
441 class ParamSpec:
442 @property
443 def args(self):
444 return ParamSpecArgs(self)
445 @property
446 def kwargs(self):
447 return ParamSpecKwargs(self)
448 class ParamSpecArgs: ...
449 class ParamSpecKwargs: ...
450 class TypeAlias: ...
451 class Type:
452 @classmethod
453 def __class_getitem__(cls, item): return cls
454 class TypeVar:
455 @classmethod
456 def __class_getitem__(cls, item): return cls
457 class TypeVarTuple: ...
458 class ContextManager:
459 @classmethod
460 def __class_getitem__(cls, item): return cls
461 class AsyncContextManager:
462 @classmethod
463 def __class_getitem__(cls, item): return cls
464 class Pattern:
465 @classmethod
466 def __class_getitem__(cls, item): return cls
467 class Match:
468 @classmethod
469 def __class_getitem__(cls, item): return cls
470 """)
471 if PY314_PLUS:
472 code += textwrap.dedent("""
473 from annotationlib import ForwardRef
474 class Union:
475 @classmethod
476 def __class_getitem__(cls, item): return cls
477 """)
478 if PY315_PLUS:
479 # typing.ByteString was removed from the typing module in Python 3.15
480 # (it was deprecated since 3.12 and present at module level until 3.14).
481 # Inject a stub so code using `typing.ByteString` can still be inferred.
482 code += textwrap.dedent("""
483 class ByteString: ...
484 """)
485 return AstroidBuilder(AstroidManager()).string_build(code)
486
487
488def register(manager: AstroidManager) -> None:
489 manager.register_transform(
490 nodes.Call,
491 inference_tip(infer_typing_typevar_or_newtype),
492 looks_like_typing_typevar_or_newtype,
493 )
494 manager.register_transform(
495 nodes.Subscript, inference_tip(infer_typing_attr), _looks_like_typing_subscript
496 )
497 manager.register_transform(
498 nodes.Call, inference_tip(infer_typing_cast), _looks_like_typing_cast
499 )
500
501 manager.register_transform(
502 nodes.FunctionDef, inference_tip(infer_typedDict), _looks_like_typedDict
503 )
504
505 manager.register_transform(
506 nodes.Call, inference_tip(infer_typing_alias), _looks_like_typing_alias
507 )
508 manager.register_transform(
509 nodes.Call, inference_tip(infer_special_alias), _looks_like_special_alias
510 )
511
512 if PY312_PLUS:
513 register_module_extender(manager, "typing", _typing_transform)
514 manager.register_transform(
515 nodes.ClassDef,
516 inference_tip(infer_typing_generic_class_pep695),
517 _looks_like_generic_class_pep695,
518 )