1"""Low-level introspection utilities for [`typing`][] members.
2
3The provided functions in this module check against both the [`typing`][] and [`typing_extensions`][]
4variants, if they exists and are different.
5"""
6# ruff: noqa: UP006
7
8import collections.abc
9import contextlib
10import re
11import sys
12import typing
13import warnings
14from textwrap import dedent
15from types import FunctionType, GenericAlias, NoneType
16from typing import Any, Final
17
18import typing_extensions
19from typing_extensions import LiteralString, TypeAliasType, TypeIs, deprecated
20
21__all__ = (
22 'DEPRECATED_ALIASES',
23 'DEPRECATED_ALIASES_IDS',
24 'NoneType',
25 'is_annotated',
26 'is_any',
27 'is_classvar',
28 'is_concatenate',
29 'is_deprecated',
30 'is_final',
31 'is_forwardref',
32 'is_generic',
33 'is_literal',
34 'is_literalstring',
35 'is_namedtuple',
36 'is_never',
37 'is_newtype',
38 'is_nodefault',
39 'is_noextraitems',
40 'is_noreturn',
41 'is_notrequired',
42 'is_paramspec',
43 'is_paramspecargs',
44 'is_paramspeckwargs',
45 'is_readonly',
46 'is_required',
47 'is_self',
48 'is_typealias',
49 'is_typealiastype',
50 'is_typeguard',
51 'is_typeis',
52 'is_typevar',
53 'is_typevartuple',
54 'is_union',
55 'is_unpack',
56)
57
58_IS_PY310 = sys.version_info[:2] == (3, 10)
59
60
61def _compile_identity_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType:
62 """Create a function checking that the function argument is the (unparameterized) typing `member`.
63
64 The function will make sure to check against both the `typing` and `typing_extensions`
65 variants as depending on the Python version, the `typing_extensions` variant might be different.
66 For instance, on Python 3.9:
67
68 ```pycon
69 >>> from typing import Literal as t_Literal
70 >>> from typing_extensions import Literal as te_Literal, get_origin
71
72 >>> t_Literal is te_Literal
73 False
74 >>> get_origin(t_Literal[1])
75 typing.Literal
76 >>> get_origin(te_Literal[1])
77 typing_extensions.Literal
78 ```
79 """
80 in_typing = hasattr(typing, member)
81 in_typing_extensions = hasattr(typing_extensions, member)
82
83 globals_: dict[str, Any] = {'Any': Any}
84
85 if in_typing and in_typing_extensions:
86 # For performance reasons, cache the objects in `globals_` so the generated function avoids
87 # repeated module attribute lookups (and `typing`'s module-level `__getattr__()`):
88 t_obj = getattr(typing, member)
89 te_obj = getattr(typing_extensions, member)
90 if t_obj is te_obj:
91 globals_['_obj'] = t_obj
92 check_code = 'obj is _obj'
93 else:
94 globals_['_t_obj'] = t_obj
95 globals_['_te_obj'] = te_obj
96 check_code = 'obj is _t_obj or obj is _te_obj'
97 elif in_typing and not in_typing_extensions:
98 globals_['_obj'] = getattr(typing, member)
99 check_code = 'obj is _obj'
100 elif not in_typing and in_typing_extensions:
101 globals_['_obj'] = getattr(typing_extensions, member)
102 check_code = 'obj is _obj'
103 else:
104 check_code = 'False'
105
106 func_code = dedent(f"""
107 def {function_name}(obj: Any, /) -> bool:
108 return {check_code}
109 """)
110
111 locals_: dict[str, Any] = {}
112 exec(func_code, globals_, locals_)
113 return locals_[function_name]
114
115
116def _compile_isinstance_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType:
117 """Create a function checking that the function is an instance of the typing `member`.
118
119 The function will make sure to check against both the `typing` and `typing_extensions`
120 variants as depending on the Python version, the `typing_extensions` variant might be different.
121 """
122 in_typing = hasattr(typing, member)
123 in_typing_extensions = hasattr(typing_extensions, member)
124
125 globals_: dict[str, Any] = {'Any': Any}
126
127 if in_typing and in_typing_extensions:
128 # For performance reasons, cache the objects in `globals_` so the generated function avoids
129 # repeated module attribute lookups (and `typing`'s module-level `__getattr__()`):
130 t_obj = getattr(typing, member)
131 te_obj = getattr(typing_extensions, member)
132 if t_obj is te_obj:
133 globals_['_obj'] = t_obj
134 check_code = 'isinstance(obj, _obj)'
135 else:
136 globals_['_objs'] = (t_obj, te_obj)
137 check_code = 'isinstance(obj, _objs)'
138 elif in_typing and not in_typing_extensions:
139 globals_['_obj'] = getattr(typing, member)
140 check_code = 'isinstance(obj, _obj)'
141 elif not in_typing and in_typing_extensions:
142 globals_['_obj'] = getattr(typing_extensions, member)
143 check_code = 'isinstance(obj, _obj)'
144 else:
145 check_code = 'False'
146
147 func_code = dedent(f"""
148 def {function_name}(obj: Any, /) -> 'TypeIs[{member}]':
149 return {check_code}
150 """)
151
152 locals_: dict[str, Any] = {}
153 exec(func_code, globals_, locals_)
154 return locals_[function_name]
155
156
157# Keep this ordered, as per `typing.__all__`:
158
159is_annotated = _compile_identity_check_function('Annotated', 'is_annotated')
160is_annotated.__doc__ = """
161Return whether the argument is the [`Annotated`][typing.Annotated] [special form][].
162
163```pycon
164>>> is_annotated(Annotated)
165True
166>>> is_annotated(Annotated[int, ...])
167False
168```
169"""
170
171is_any = _compile_identity_check_function('Any', 'is_any')
172is_any.__doc__ = """
173Return whether the argument is the [`Any`][typing.Any] [special form][].
174
175```pycon
176>>> is_any(Any)
177True
178```
179"""
180
181is_classvar = _compile_identity_check_function('ClassVar', 'is_classvar')
182is_classvar.__doc__ = """
183Return whether the argument is the [`ClassVar`][typing.ClassVar] [type qualifier][].
184
185```pycon
186>>> is_classvar(ClassVar)
187True
188>>> is_classvar(ClassVar[int])
189>>> False
190```
191"""
192
193is_concatenate = _compile_identity_check_function('Concatenate', 'is_concatenate')
194is_concatenate.__doc__ = """
195Return whether the argument is the [`Concatenate`][typing.Concatenate] [special form][].
196
197```pycon
198>>> is_concatenate(Concatenate)
199True
200>>> is_concatenate(Concatenate[int, P])
201False
202```
203"""
204
205is_final = _compile_identity_check_function('Final', 'is_final')
206is_final.__doc__ = """
207Return whether the argument is the [`Final`][typing.Final] [type qualifier][].
208
209```pycon
210>>> is_final(Final)
211True
212>>> is_final(Final[int])
213False
214```
215"""
216
217
218# Unlikely to have a different version in `typing-extensions`, but keep it consistent.
219# Also note that starting in 3.14, this is an alias to `annotationlib.ForwardRef`, but
220# accessing it from `typing` doesn't seem to be deprecated.
221is_forwardref = _compile_isinstance_check_function('ForwardRef', 'is_forwardref')
222is_forwardref.__doc__ = """
223Return whether the argument is an instance of [`ForwardRef`][typing.ForwardRef].
224
225```pycon
226>>> is_forwardref(ForwardRef('T'))
227True
228```
229"""
230
231
232is_generic = _compile_identity_check_function('Generic', 'is_generic')
233is_generic.__doc__ = """
234Return whether the argument is the [`Generic`][typing.Generic] [special form][].
235
236```pycon
237>>> is_generic(Generic)
238True
239>>> is_generic(Generic[T])
240False
241```
242"""
243
244is_literal = _compile_identity_check_function('Literal', 'is_literal')
245is_literal.__doc__ = """
246Return whether the argument is the [`Literal`][typing.Literal] [special form][].
247
248```pycon
249>>> is_literal(Literal)
250True
251>>> is_literal(Literal["a"])
252False
253```
254"""
255
256
257# `get_origin(Optional[int]) is Union`, so `is_optional()` isn't implemented.
258
259is_paramspec = _compile_isinstance_check_function('ParamSpec', 'is_paramspec')
260is_paramspec.__doc__ = """
261Return whether the argument is an instance of [`ParamSpec`][typing.ParamSpec].
262
263```pycon
264>>> P = ParamSpec('P')
265>>> is_paramspec(P)
266True
267```
268"""
269
270# Protocol?
271
272is_typevar = _compile_isinstance_check_function('TypeVar', 'is_typevar')
273is_typevar.__doc__ = """
274Return whether the argument is an instance of [`TypeVar`][typing.TypeVar].
275
276```pycon
277>>> T = TypeVar('T')
278>>> is_typevar(T)
279True
280```
281"""
282
283is_typevartuple = _compile_isinstance_check_function('TypeVarTuple', 'is_typevartuple')
284is_typevartuple.__doc__ = """
285Return whether the argument is an instance of [`TypeVarTuple`][typing.TypeVarTuple].
286
287```pycon
288>>> Ts = TypeVarTuple('Ts')
289>>> is_typevartuple(Ts)
290True
291```
292"""
293
294is_union = _compile_identity_check_function('Union', 'is_union')
295is_union.__doc__ = """
296Return whether the argument is the [`Union`][typing.Union] [special form][].
297
298This function can also be used to check for the [`Optional`][typing.Optional] [special form][],
299as at runtime, `Optional[int]` is equivalent to `Union[int, None]`.
300
301```pycon
302>>> is_union(Union)
303True
304>>> is_union(Union[int, str])
305False
306```
307
308!!! warning
309 This does not check for unions using the [new syntax][types-union] (e.g. `int | str`).
310"""
311
312
313def is_namedtuple(obj: Any, /) -> bool:
314 """Return whether the argument is a named tuple type.
315
316 This includes [`NamedTuple`][typing.NamedTuple] subclasses and classes created from the
317 [`collections.namedtuple`][] factory function.
318
319 ```pycon
320 >>> class User(NamedTuple):
321 ... name: str
322 ...
323 >>> is_namedtuple(User)
324 True
325 >>> City = collections.namedtuple('City', [])
326 >>> is_namedtuple(City)
327 True
328 >>> is_namedtuple(NamedTuple)
329 False
330 ```
331 """
332 return isinstance(obj, type) and issubclass(obj, tuple) and hasattr(obj, '_fields') # pyright: ignore[reportUnknownArgumentType]
333
334
335# TypedDict?
336
337# BinaryIO? IO? TextIO?
338
339is_literalstring = _compile_identity_check_function('LiteralString', 'is_literalstring')
340is_literalstring.__doc__ = """
341Return whether the argument is the [`LiteralString`][typing.LiteralString] [special form][].
342
343```pycon
344>>> is_literalstring(LiteralString)
345True
346```
347"""
348
349is_never = _compile_identity_check_function('Never', 'is_never')
350is_never.__doc__ = """
351Return whether the argument is the [`Never`][typing.Never] [special form][].
352
353```pycon
354>>> is_never(Never)
355True
356```
357"""
358
359is_newtype = _compile_isinstance_check_function('NewType', 'is_newtype')
360is_newtype.__doc__ = """
361Return whether the argument is a [`NewType`][typing.NewType].
362
363```pycon
364>>> UserId = NewType("UserId", int)
365>>> is_newtype(UserId)
366True
367```
368"""
369
370is_nodefault = _compile_identity_check_function('NoDefault', 'is_nodefault')
371is_nodefault.__doc__ = """
372Return whether the argument is the [`NoDefault`][typing.NoDefault] sentinel object.
373
374```pycon
375>>> is_nodefault(NoDefault)
376True
377```
378"""
379
380is_noextraitems = _compile_identity_check_function('NoExtraItems', 'is_noextraitems')
381is_noextraitems.__doc__ = """
382Return whether the argument is the `NoExtraItems` sentinel object.
383
384```pycon
385>>> is_noextraitems(NoExtraItems)
386True
387```
388"""
389
390is_noreturn = _compile_identity_check_function('NoReturn', 'is_noreturn')
391is_noreturn.__doc__ = """
392Return whether the argument is the [`NoReturn`][typing.NoReturn] [special form][].
393
394```pycon
395>>> is_noreturn(NoReturn)
396True
397>>> is_noreturn(Never)
398False
399```
400"""
401
402is_notrequired = _compile_identity_check_function('NotRequired', 'is_notrequired')
403is_notrequired.__doc__ = """
404Return whether the argument is the [`NotRequired`][typing.NotRequired] [special form][].
405
406```pycon
407>>> is_notrequired(NotRequired)
408True
409```
410"""
411
412is_paramspecargs = _compile_isinstance_check_function('ParamSpecArgs', 'is_paramspecargs')
413is_paramspecargs.__doc__ = """
414Return whether the argument is an instance of [`ParamSpecArgs`][typing.ParamSpecArgs].
415
416```pycon
417>>> P = ParamSpec('P')
418>>> is_paramspecargs(P.args)
419True
420```
421"""
422
423is_paramspeckwargs = _compile_isinstance_check_function('ParamSpecKwargs', 'is_paramspeckwargs')
424is_paramspeckwargs.__doc__ = """
425Return whether the argument is an instance of [`ParamSpecKwargs`][typing.ParamSpecKwargs].
426
427```pycon
428>>> P = ParamSpec('P')
429>>> is_paramspeckwargs(P.kwargs)
430True
431```
432"""
433
434is_readonly = _compile_identity_check_function('ReadOnly', 'is_readonly')
435is_readonly.__doc__ = """
436Return whether the argument is the [`ReadOnly`][typing.ReadOnly] [special form][].
437
438```pycon
439>>> is_readonly(ReadOnly)
440True
441```
442"""
443
444is_required = _compile_identity_check_function('Required', 'is_required')
445is_required.__doc__ = """
446Return whether the argument is the [`Required`][typing.Required] [special form][].
447
448```pycon
449>>> is_required(Required)
450True
451```
452"""
453
454is_self = _compile_identity_check_function('Self', 'is_self')
455is_self.__doc__ = """
456Return whether the argument is the [`Self`][typing.Self] [special form][].
457
458```pycon
459>>> is_self(Self)
460True
461```
462"""
463
464# TYPE_CHECKING?
465
466is_typealias = _compile_identity_check_function('TypeAlias', 'is_typealias')
467is_typealias.__doc__ = """
468Return whether the argument is the [`TypeAlias`][typing.TypeAlias] [special form][].
469
470```pycon
471>>> is_typealias(TypeAlias)
472True
473```
474"""
475
476is_typeguard = _compile_identity_check_function('TypeGuard', 'is_typeguard')
477is_typeguard.__doc__ = """
478Return whether the argument is the [`TypeGuard`][typing.TypeGuard] [special form][].
479
480```pycon
481>>> is_typeguard(TypeGuard)
482True
483```
484"""
485
486is_typeis = _compile_identity_check_function('TypeIs', 'is_typeis')
487is_typeis.__doc__ = """
488Return whether the argument is the [`TypeIs`][typing.TypeIs] [special form][].
489
490```pycon
491>>> is_typeis(TypeIs)
492True
493```
494"""
495
496_is_typealiastype_inner = _compile_isinstance_check_function('TypeAliasType', '_is_typealiastype_inner')
497
498
499if _IS_PY310:
500 # Parameterized PEP 695 type aliases are instances of `types.GenericAlias` in typing_extensions>=4.13.0.
501 # On Python 3.10, with `Alias[int]` being such an instance of `GenericAlias`,
502 # `isinstance(Alias[int], TypeAliasType)` returns `True`.
503 # See https://github.com/python/cpython/issues/89828.
504 def is_typealiastype(obj: Any, /) -> 'TypeIs[TypeAliasType]':
505 return type(obj) is not GenericAlias and _is_typealiastype_inner(obj)
506else:
507 is_typealiastype = _compile_isinstance_check_function('TypeAliasType', 'is_typealiastype')
508
509is_typealiastype.__doc__ = """
510Return whether the argument is a [`TypeAliasType`][typing.TypeAliasType] instance.
511
512```pycon
513>>> type MyInt = int
514>>> is_typealiastype(MyInt)
515True
516>>> MyStr = TypeAliasType("MyStr", str)
517>>> is_typealiastype(MyStr):
518True
519>>> type MyList[T] = list[T]
520>>> is_typealiastype(MyList[int])
521False
522```
523"""
524
525is_unpack = _compile_identity_check_function('Unpack', 'is_unpack')
526is_unpack.__doc__ = """
527Return whether the argument is the [`Unpack`][typing.Unpack] [special form][].
528
529```pycon
530>>> is_unpack(Unpack)
531True
532>>> is_unpack(Unpack[Ts])
533False
534```
535"""
536
537
538if sys.version_info >= (3, 13):
539 _deprecated_types = (warnings.deprecated, typing_extensions.deprecated)
540
541 def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]':
542 return isinstance(obj, _deprecated_types)
543
544else:
545 _deprecated_type = typing_extensions.deprecated
546
547 def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]':
548 return isinstance(obj, _deprecated_type)
549
550
551is_deprecated.__doc__ = """
552Return whether the argument is a [`deprecated`][warnings.deprecated] instance.
553
554This also includes the [`typing_extensions` backport][typing_extensions.deprecated].
555
556```pycon
557>>> is_deprecated(warnings.deprecated('message'))
558True
559>>> is_deprecated(typing_extensions.deprecated('message'))
560True
561```
562"""
563
564
565# Aliases defined in the `typing` module using `typing._SpecialGenericAlias` (itself aliased as `alias()`):
566DEPRECATED_ALIASES: Final[dict[Any, type[Any]]] = {
567 typing.Hashable: collections.abc.Hashable,
568 typing.Awaitable: collections.abc.Awaitable,
569 typing.Coroutine: collections.abc.Coroutine,
570 typing.AsyncIterable: collections.abc.AsyncIterable,
571 typing.AsyncIterator: collections.abc.AsyncIterator,
572 typing.Iterable: collections.abc.Iterable,
573 typing.Iterator: collections.abc.Iterator,
574 typing.Reversible: collections.abc.Reversible,
575 typing.Sized: collections.abc.Sized,
576 typing.Container: collections.abc.Container,
577 typing.Collection: collections.abc.Collection,
578 # type ignore reason: https://github.com/python/typeshed/issues/6257:
579 typing.Callable: collections.abc.Callable, # pyright: ignore[reportAssignmentType, reportUnknownMemberType]
580 typing.AbstractSet: collections.abc.Set,
581 typing.MutableSet: collections.abc.MutableSet,
582 typing.Mapping: collections.abc.Mapping,
583 typing.MutableMapping: collections.abc.MutableMapping,
584 typing.Sequence: collections.abc.Sequence,
585 typing.MutableSequence: collections.abc.MutableSequence,
586 typing.Tuple: tuple,
587 typing.List: list,
588 typing.Deque: collections.deque,
589 typing.Set: set,
590 typing.FrozenSet: frozenset,
591 typing.MappingView: collections.abc.MappingView,
592 typing.KeysView: collections.abc.KeysView,
593 typing.ItemsView: collections.abc.ItemsView,
594 typing.ValuesView: collections.abc.ValuesView,
595 typing.Dict: dict,
596 typing.DefaultDict: collections.defaultdict,
597 typing.OrderedDict: collections.OrderedDict,
598 typing.Counter: collections.Counter,
599 typing.ChainMap: collections.ChainMap,
600 typing.Generator: collections.abc.Generator,
601 typing.AsyncGenerator: collections.abc.AsyncGenerator,
602 typing.Type: type,
603 # Defined in `typing.__getattr__`:
604 typing.Pattern: re.Pattern,
605 typing.Match: re.Match,
606 typing.ContextManager: contextlib.AbstractContextManager,
607 typing.AsyncContextManager: contextlib.AbstractAsyncContextManager,
608 # Skipped: `ByteString` (deprecated, removed in 3.14)
609}
610"""A mapping between the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/)."""
611
612
613DEPRECATED_ALIASES_IDS: Final[dict[int, type[Any]]] = {id(k): v for k, v in DEPRECATED_ALIASES.items()}
614"""A mapping between the [identity][id] of the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/)."""
615
616
617# Add the `typing_extensions` aliases:
618for alias, target in list(DEPRECATED_ALIASES.items()):
619 if (te_alias := getattr(typing_extensions, alias.__name__, None)) is not None:
620 DEPRECATED_ALIASES[te_alias] = target