"""Low-level introspection utilities for [`typing`][] members. The provided functions in this module check against both the [`typing`][] and [`typing_extensions`][] variants, if they exists and are different. """ # ruff: noqa: UP006 import collections.abc import contextlib import re import sys import typing import warnings from textwrap import dedent from types import FunctionType, GenericAlias, NoneType from typing import Any, Final import typing_extensions from typing_extensions import LiteralString, TypeAliasType, TypeIs, deprecated __all__ = ( 'DEPRECATED_ALIASES', 'DEPRECATED_ALIASES_IDS', 'NoneType', 'is_annotated', 'is_any', 'is_classvar', 'is_concatenate', 'is_deprecated', 'is_final', 'is_forwardref', 'is_generic', 'is_literal', 'is_literalstring', 'is_namedtuple', 'is_never', 'is_newtype', 'is_nodefault', 'is_noextraitems', 'is_noreturn', 'is_notrequired', 'is_paramspec', 'is_paramspecargs', 'is_paramspeckwargs', 'is_readonly', 'is_required', 'is_self', 'is_typealias', 'is_typealiastype', 'is_typeguard', 'is_typeis', 'is_typevar', 'is_typevartuple', 'is_union', 'is_unpack', ) _IS_PY310 = sys.version_info[:2] == (3, 10) def _compile_identity_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType: """Create a function checking that the function argument is the (unparameterized) typing `member`. The function will make sure to check against both the `typing` and `typing_extensions` variants as depending on the Python version, the `typing_extensions` variant might be different. For instance, on Python 3.9: ```pycon >>> from typing import Literal as t_Literal >>> from typing_extensions import Literal as te_Literal, get_origin >>> t_Literal is te_Literal False >>> get_origin(t_Literal[1]) typing.Literal >>> get_origin(te_Literal[1]) typing_extensions.Literal ``` """ in_typing = hasattr(typing, member) in_typing_extensions = hasattr(typing_extensions, member) globals_: dict[str, Any] = {'Any': Any} if in_typing and in_typing_extensions: # For performance reasons, cache the objects in `globals_` so the generated function avoids # repeated module attribute lookups (and `typing`'s module-level `__getattr__()`): t_obj = getattr(typing, member) te_obj = getattr(typing_extensions, member) if t_obj is te_obj: globals_['_obj'] = t_obj check_code = 'obj is _obj' else: globals_['_t_obj'] = t_obj globals_['_te_obj'] = te_obj check_code = 'obj is _t_obj or obj is _te_obj' elif in_typing and not in_typing_extensions: globals_['_obj'] = getattr(typing, member) check_code = 'obj is _obj' elif not in_typing and in_typing_extensions: globals_['_obj'] = getattr(typing_extensions, member) check_code = 'obj is _obj' else: check_code = 'False' func_code = dedent(f""" def {function_name}(obj: Any, /) -> bool: return {check_code} """) locals_: dict[str, Any] = {} exec(func_code, globals_, locals_) return locals_[function_name] def _compile_isinstance_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType: """Create a function checking that the function is an instance of the typing `member`. The function will make sure to check against both the `typing` and `typing_extensions` variants as depending on the Python version, the `typing_extensions` variant might be different. """ in_typing = hasattr(typing, member) in_typing_extensions = hasattr(typing_extensions, member) globals_: dict[str, Any] = {'Any': Any} if in_typing and in_typing_extensions: # For performance reasons, cache the objects in `globals_` so the generated function avoids # repeated module attribute lookups (and `typing`'s module-level `__getattr__()`): t_obj = getattr(typing, member) te_obj = getattr(typing_extensions, member) if t_obj is te_obj: globals_['_obj'] = t_obj check_code = 'isinstance(obj, _obj)' else: globals_['_objs'] = (t_obj, te_obj) check_code = 'isinstance(obj, _objs)' elif in_typing and not in_typing_extensions: globals_['_obj'] = getattr(typing, member) check_code = 'isinstance(obj, _obj)' elif not in_typing and in_typing_extensions: globals_['_obj'] = getattr(typing_extensions, member) check_code = 'isinstance(obj, _obj)' else: check_code = 'False' func_code = dedent(f""" def {function_name}(obj: Any, /) -> 'TypeIs[{member}]': return {check_code} """) locals_: dict[str, Any] = {} exec(func_code, globals_, locals_) return locals_[function_name] # Keep this ordered, as per `typing.__all__`: is_annotated = _compile_identity_check_function('Annotated', 'is_annotated') is_annotated.__doc__ = """ Return whether the argument is the [`Annotated`][typing.Annotated] [special form][]. ```pycon >>> is_annotated(Annotated) True >>> is_annotated(Annotated[int, ...]) False ``` """ is_any = _compile_identity_check_function('Any', 'is_any') is_any.__doc__ = """ Return whether the argument is the [`Any`][typing.Any] [special form][]. ```pycon >>> is_any(Any) True ``` """ is_classvar = _compile_identity_check_function('ClassVar', 'is_classvar') is_classvar.__doc__ = """ Return whether the argument is the [`ClassVar`][typing.ClassVar] [type qualifier][]. ```pycon >>> is_classvar(ClassVar) True >>> is_classvar(ClassVar[int]) >>> False ``` """ is_concatenate = _compile_identity_check_function('Concatenate', 'is_concatenate') is_concatenate.__doc__ = """ Return whether the argument is the [`Concatenate`][typing.Concatenate] [special form][]. ```pycon >>> is_concatenate(Concatenate) True >>> is_concatenate(Concatenate[int, P]) False ``` """ is_final = _compile_identity_check_function('Final', 'is_final') is_final.__doc__ = """ Return whether the argument is the [`Final`][typing.Final] [type qualifier][]. ```pycon >>> is_final(Final) True >>> is_final(Final[int]) False ``` """ # Unlikely to have a different version in `typing-extensions`, but keep it consistent. # Also note that starting in 3.14, this is an alias to `annotationlib.ForwardRef`, but # accessing it from `typing` doesn't seem to be deprecated. is_forwardref = _compile_isinstance_check_function('ForwardRef', 'is_forwardref') is_forwardref.__doc__ = """ Return whether the argument is an instance of [`ForwardRef`][typing.ForwardRef]. ```pycon >>> is_forwardref(ForwardRef('T')) True ``` """ is_generic = _compile_identity_check_function('Generic', 'is_generic') is_generic.__doc__ = """ Return whether the argument is the [`Generic`][typing.Generic] [special form][]. ```pycon >>> is_generic(Generic) True >>> is_generic(Generic[T]) False ``` """ is_literal = _compile_identity_check_function('Literal', 'is_literal') is_literal.__doc__ = """ Return whether the argument is the [`Literal`][typing.Literal] [special form][]. ```pycon >>> is_literal(Literal) True >>> is_literal(Literal["a"]) False ``` """ # `get_origin(Optional[int]) is Union`, so `is_optional()` isn't implemented. is_paramspec = _compile_isinstance_check_function('ParamSpec', 'is_paramspec') is_paramspec.__doc__ = """ Return whether the argument is an instance of [`ParamSpec`][typing.ParamSpec]. ```pycon >>> P = ParamSpec('P') >>> is_paramspec(P) True ``` """ # Protocol? is_typevar = _compile_isinstance_check_function('TypeVar', 'is_typevar') is_typevar.__doc__ = """ Return whether the argument is an instance of [`TypeVar`][typing.TypeVar]. ```pycon >>> T = TypeVar('T') >>> is_typevar(T) True ``` """ is_typevartuple = _compile_isinstance_check_function('TypeVarTuple', 'is_typevartuple') is_typevartuple.__doc__ = """ Return whether the argument is an instance of [`TypeVarTuple`][typing.TypeVarTuple]. ```pycon >>> Ts = TypeVarTuple('Ts') >>> is_typevartuple(Ts) True ``` """ is_union = _compile_identity_check_function('Union', 'is_union') is_union.__doc__ = """ Return whether the argument is the [`Union`][typing.Union] [special form][]. This function can also be used to check for the [`Optional`][typing.Optional] [special form][], as at runtime, `Optional[int]` is equivalent to `Union[int, None]`. ```pycon >>> is_union(Union) True >>> is_union(Union[int, str]) False ``` !!! warning This does not check for unions using the [new syntax][types-union] (e.g. `int | str`). """ def is_namedtuple(obj: Any, /) -> bool: """Return whether the argument is a named tuple type. This includes [`NamedTuple`][typing.NamedTuple] subclasses and classes created from the [`collections.namedtuple`][] factory function. ```pycon >>> class User(NamedTuple): ... name: str ... >>> is_namedtuple(User) True >>> City = collections.namedtuple('City', []) >>> is_namedtuple(City) True >>> is_namedtuple(NamedTuple) False ``` """ return isinstance(obj, type) and issubclass(obj, tuple) and hasattr(obj, '_fields') # pyright: ignore[reportUnknownArgumentType] # TypedDict? # BinaryIO? IO? TextIO? is_literalstring = _compile_identity_check_function('LiteralString', 'is_literalstring') is_literalstring.__doc__ = """ Return whether the argument is the [`LiteralString`][typing.LiteralString] [special form][]. ```pycon >>> is_literalstring(LiteralString) True ``` """ is_never = _compile_identity_check_function('Never', 'is_never') is_never.__doc__ = """ Return whether the argument is the [`Never`][typing.Never] [special form][]. ```pycon >>> is_never(Never) True ``` """ is_newtype = _compile_isinstance_check_function('NewType', 'is_newtype') is_newtype.__doc__ = """ Return whether the argument is a [`NewType`][typing.NewType]. ```pycon >>> UserId = NewType("UserId", int) >>> is_newtype(UserId) True ``` """ is_nodefault = _compile_identity_check_function('NoDefault', 'is_nodefault') is_nodefault.__doc__ = """ Return whether the argument is the [`NoDefault`][typing.NoDefault] sentinel object. ```pycon >>> is_nodefault(NoDefault) True ``` """ is_noextraitems = _compile_identity_check_function('NoExtraItems', 'is_noextraitems') is_noextraitems.__doc__ = """ Return whether the argument is the `NoExtraItems` sentinel object. ```pycon >>> is_noextraitems(NoExtraItems) True ``` """ is_noreturn = _compile_identity_check_function('NoReturn', 'is_noreturn') is_noreturn.__doc__ = """ Return whether the argument is the [`NoReturn`][typing.NoReturn] [special form][]. ```pycon >>> is_noreturn(NoReturn) True >>> is_noreturn(Never) False ``` """ is_notrequired = _compile_identity_check_function('NotRequired', 'is_notrequired') is_notrequired.__doc__ = """ Return whether the argument is the [`NotRequired`][typing.NotRequired] [special form][]. ```pycon >>> is_notrequired(NotRequired) True ``` """ is_paramspecargs = _compile_isinstance_check_function('ParamSpecArgs', 'is_paramspecargs') is_paramspecargs.__doc__ = """ Return whether the argument is an instance of [`ParamSpecArgs`][typing.ParamSpecArgs]. ```pycon >>> P = ParamSpec('P') >>> is_paramspecargs(P.args) True ``` """ is_paramspeckwargs = _compile_isinstance_check_function('ParamSpecKwargs', 'is_paramspeckwargs') is_paramspeckwargs.__doc__ = """ Return whether the argument is an instance of [`ParamSpecKwargs`][typing.ParamSpecKwargs]. ```pycon >>> P = ParamSpec('P') >>> is_paramspeckwargs(P.kwargs) True ``` """ is_readonly = _compile_identity_check_function('ReadOnly', 'is_readonly') is_readonly.__doc__ = """ Return whether the argument is the [`ReadOnly`][typing.ReadOnly] [special form][]. ```pycon >>> is_readonly(ReadOnly) True ``` """ is_required = _compile_identity_check_function('Required', 'is_required') is_required.__doc__ = """ Return whether the argument is the [`Required`][typing.Required] [special form][]. ```pycon >>> is_required(Required) True ``` """ is_self = _compile_identity_check_function('Self', 'is_self') is_self.__doc__ = """ Return whether the argument is the [`Self`][typing.Self] [special form][]. ```pycon >>> is_self(Self) True ``` """ # TYPE_CHECKING? is_typealias = _compile_identity_check_function('TypeAlias', 'is_typealias') is_typealias.__doc__ = """ Return whether the argument is the [`TypeAlias`][typing.TypeAlias] [special form][]. ```pycon >>> is_typealias(TypeAlias) True ``` """ is_typeguard = _compile_identity_check_function('TypeGuard', 'is_typeguard') is_typeguard.__doc__ = """ Return whether the argument is the [`TypeGuard`][typing.TypeGuard] [special form][]. ```pycon >>> is_typeguard(TypeGuard) True ``` """ is_typeis = _compile_identity_check_function('TypeIs', 'is_typeis') is_typeis.__doc__ = """ Return whether the argument is the [`TypeIs`][typing.TypeIs] [special form][]. ```pycon >>> is_typeis(TypeIs) True ``` """ _is_typealiastype_inner = _compile_isinstance_check_function('TypeAliasType', '_is_typealiastype_inner') if _IS_PY310: # Parameterized PEP 695 type aliases are instances of `types.GenericAlias` in typing_extensions>=4.13.0. # On Python 3.10, with `Alias[int]` being such an instance of `GenericAlias`, # `isinstance(Alias[int], TypeAliasType)` returns `True`. # See https://github.com/python/cpython/issues/89828. def is_typealiastype(obj: Any, /) -> 'TypeIs[TypeAliasType]': return type(obj) is not GenericAlias and _is_typealiastype_inner(obj) else: is_typealiastype = _compile_isinstance_check_function('TypeAliasType', 'is_typealiastype') is_typealiastype.__doc__ = """ Return whether the argument is a [`TypeAliasType`][typing.TypeAliasType] instance. ```pycon >>> type MyInt = int >>> is_typealiastype(MyInt) True >>> MyStr = TypeAliasType("MyStr", str) >>> is_typealiastype(MyStr): True >>> type MyList[T] = list[T] >>> is_typealiastype(MyList[int]) False ``` """ is_unpack = _compile_identity_check_function('Unpack', 'is_unpack') is_unpack.__doc__ = """ Return whether the argument is the [`Unpack`][typing.Unpack] [special form][]. ```pycon >>> is_unpack(Unpack) True >>> is_unpack(Unpack[Ts]) False ``` """ if sys.version_info >= (3, 13): _deprecated_types = (warnings.deprecated, typing_extensions.deprecated) def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]': return isinstance(obj, _deprecated_types) else: _deprecated_type = typing_extensions.deprecated def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]': return isinstance(obj, _deprecated_type) is_deprecated.__doc__ = """ Return whether the argument is a [`deprecated`][warnings.deprecated] instance. This also includes the [`typing_extensions` backport][typing_extensions.deprecated]. ```pycon >>> is_deprecated(warnings.deprecated('message')) True >>> is_deprecated(typing_extensions.deprecated('message')) True ``` """ # Aliases defined in the `typing` module using `typing._SpecialGenericAlias` (itself aliased as `alias()`): DEPRECATED_ALIASES: Final[dict[Any, type[Any]]] = { typing.Hashable: collections.abc.Hashable, typing.Awaitable: collections.abc.Awaitable, typing.Coroutine: collections.abc.Coroutine, typing.AsyncIterable: collections.abc.AsyncIterable, typing.AsyncIterator: collections.abc.AsyncIterator, typing.Iterable: collections.abc.Iterable, typing.Iterator: collections.abc.Iterator, typing.Reversible: collections.abc.Reversible, typing.Sized: collections.abc.Sized, typing.Container: collections.abc.Container, typing.Collection: collections.abc.Collection, # type ignore reason: https://github.com/python/typeshed/issues/6257: typing.Callable: collections.abc.Callable, # pyright: ignore[reportAssignmentType, reportUnknownMemberType] typing.AbstractSet: collections.abc.Set, typing.MutableSet: collections.abc.MutableSet, typing.Mapping: collections.abc.Mapping, typing.MutableMapping: collections.abc.MutableMapping, typing.Sequence: collections.abc.Sequence, typing.MutableSequence: collections.abc.MutableSequence, typing.Tuple: tuple, typing.List: list, typing.Deque: collections.deque, typing.Set: set, typing.FrozenSet: frozenset, typing.MappingView: collections.abc.MappingView, typing.KeysView: collections.abc.KeysView, typing.ItemsView: collections.abc.ItemsView, typing.ValuesView: collections.abc.ValuesView, typing.Dict: dict, typing.DefaultDict: collections.defaultdict, typing.OrderedDict: collections.OrderedDict, typing.Counter: collections.Counter, typing.ChainMap: collections.ChainMap, typing.Generator: collections.abc.Generator, typing.AsyncGenerator: collections.abc.AsyncGenerator, typing.Type: type, # Defined in `typing.__getattr__`: typing.Pattern: re.Pattern, typing.Match: re.Match, typing.ContextManager: contextlib.AbstractContextManager, typing.AsyncContextManager: contextlib.AbstractAsyncContextManager, # Skipped: `ByteString` (deprecated, removed in 3.14) } """A mapping between the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/).""" DEPRECATED_ALIASES_IDS: Final[dict[int, type[Any]]] = {id(k): v for k, v in DEPRECATED_ALIASES.items()} """A mapping between the [identity][id] of the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/).""" # Add the `typing_extensions` aliases: for alias, target in list(DEPRECATED_ALIASES.items()): if (te_alias := getattr(typing_extensions, alias.__name__, None)) is not None: DEPRECATED_ALIASES[te_alias] = target