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