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