"""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
from typing import Any, Final

import typing_extensions
from typing_extensions import LiteralString, TypeAliasType, TypeIs, deprecated

__all__ = (
    'DEPRECATED_ALIASES',
    'NoneType',
    'is_annotated',
    'is_any',
    'is_classvar',
    'is_concatenate',
    'is_deprecated',
    'is_final',
    'is_generic',
    'is_literal',
    'is_literalstring',
    'is_namedtuple',
    'is_never',
    'is_newtype',
    'is_nodefault',
    '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 :paramref:`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)

    if in_typing and in_typing_extensions:
        if getattr(typing, member) is getattr(typing_extensions, member):
            check_code = f'obj is typing.{member}'
        else:
            check_code = f'obj is typing.{member} or obj is typing_extensions.{member}'
    elif in_typing and not in_typing_extensions:
        check_code = f'obj is typing.{member}'
    elif not in_typing and in_typing_extensions:
        check_code = f'obj is typing_extensions.{member}'
    else:
        check_code = 'False'

    func_code = dedent(f"""
    def {function_name}(obj: Any, /) -> bool:
        return {check_code}
    """)

    locals_: dict[str, Any] = {}
    globals_: dict[str, Any] = {'Any': Any, 'typing': typing, 'typing_extensions': typing_extensions}
    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)

    if in_typing and in_typing_extensions:
        if getattr(typing, member) is getattr(typing_extensions, member):
            check_code = f'isinstance(obj, typing.{member})'
        else:
            check_code = f'isinstance(obj, (typing.{member}, typing_extensions.{member}))'
    elif in_typing and not in_typing_extensions:
        check_code = f'isinstance(obj, typing.{member})'
    elif not in_typing and in_typing_extensions:
        check_code = f'isinstance(obj, typing_extensions.{member})'
    else:
        check_code = 'False'

    func_code = dedent(f"""
    def {function_name}(obj: Any, /) -> 'TypeIs[{member}]':
        return {check_code}
    """)

    locals_: dict[str, Any] = {}
    globals_: dict[str, Any] = {'Any': Any, 'typing': typing, 'typing_extensions': typing_extensions}
    exec(func_code, globals_, locals_)
    return locals_[function_name]


if sys.version_info >= (3, 10):
    from types import NoneType
else:
    NoneType = type(None)

# 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
```
"""

# ForwardRef?

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
```
"""

if sys.version_info >= (3, 10):
    is_newtype = _compile_isinstance_check_function('NewType', 'is_newtype')
else:  # On Python 3.10, `NewType` is a function.

    def is_newtype(obj: Any, /) -> bool:
        return hasattr(obj, '__supertype__')


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_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):

    def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]':
        return isinstance(obj, (warnings.deprecated, typing_extensions.deprecated))

else:

    def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]':
        return isinstance(obj, typing_extensions.deprecated)


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'))
True
```
"""


# Aliases defined in the `typing` module using `typing._SpecialGenericAlias` (itself aliases 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/)."""


# Add the `typing_extensions` aliases:
for alias, target in list(DEPRECATED_ALIASES.items()):
    # Use `alias.__name__` when we drop support for Python 3.9
    if (te_alias := getattr(typing_extensions, alias._name, None)) is not None:
        DEPRECATED_ALIASES[te_alias] = target
