<reponame>qiujiangkun/beartype
#!/usr/bin/env python3
# --------------------( LICENSE                           )--------------------
# Copyright (c) 2014-2022 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide :pep:`544`-compliant type hint utilities.

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS                           }....................
from abc import abstractmethod
from beartype.roar import BeartypeDecorHintPep544Exception
from beartype._data.hint.pep.sign.datapepsigncls import HintSign
from beartype._util.cls.utilclstest import is_type_builtin, is_type_subclass
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from typing import (
    Any,
    BinaryIO,
    Dict,
    IO,
    Optional,
    TextIO,
)

# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ PRIVATE ~ mappings                }....................
_HINTS_PEP484_IO_GENERIC = frozenset((IO, BinaryIO, TextIO,))
'''
Frozen set of all :mod:`typing` **IO generic base class** (i.e., either
:class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by the
:mod:`typing` module).
'''


# Conditionally initialized by the _init() function below.
_HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL: Dict[type, Any] = {}
'''
Dictionary mapping from each :mod:`typing` **IO generic base class** (i.e.,
either :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined
by the :mod:`typing` module) to the associated :mod:`beartype` **IO protocol**
(i.e., either :class:`_Pep544IO` itself *or* a subclass of :class:`_Pep544IO`
defined by this submodule).
'''

# ....................{ PRIVATE ~ classes                 }....................
# Conditionally initialized by the _init() function below.
_Pep544IO: Any = None
'''
:pep:`544`-compliant protocol base class for :class:`_Pep544TextIO` and
:class:`_Pep544BinaryIO`.

This is an abstract, generic version of the return of open().

NOTE: This does not distinguish between the different possible classes (text
vs. binary, read vs. write vs. read/write, append-only, unbuffered). The TextIO
and BinaryIO subclasses below capture the distinctions between text vs. binary,
which is pervasive in the interface; however we currently do not offer a way to
track the other distinctions in the type system.

Design
----------
This base class intentionally duplicates the contents of the existing
:class:`typing.IO` generic base class by substituting the useless
:class:`typing.Generic` superclass of the latter with the useful
:class:`typing.Protocol` superclass of the former. Why? Because *no* stdlib
classes excluding those defined by the :mod:`typing` module itself subclass
:class:`typing.IO`. However, :class:`typing.IO` leverages neither the
:class:`abc.ABCMeta` metaclass *nor* the :class:`typing.Protocol` superclass
needed to support structural subtyping. Therefore, *no* stdlib objects
(including those returned by the :func:`open` builtin) satisfy either
:class:`typing.IO` itself or any subclasses of :class:`typing.IO` (e.g.,
:class:`typing.BinaryIO`, :class:`typing.TextIO`). Therefore,
:class:`typing.IO` and all subclasses thereof are functionally useless for all
practical intents. The conventional excuse `given by Python maintainers to
justify this abhorrent nonsensicality is as follows <typeshed_>`__:

    There are a lot of "file-like" classes, and the typing IO classes are meant
    as "protocols" for general files, but they cannot actually be protocols
    because the file protocol isn't very well defined—there are lots of methods
    that exist on some but not all filelike classes.

Like most :mod:`typing`-oriented confabulation, that, of course, is bollocks.
Refactoring the family of :mod:`typing` IO classes from inveterate generics
into pragmatic protocols is both technically trivial and semantically useful,
because that is exactly what :mod:`beartype` does. It works. It necessitates
modifying three lines of existing code. It preserves backward compatibility. In
short, it should have been done a decade ago. If the file protocol "isn't very
well defined," the solution is to define that protocol with a rigorous type
hierarchy satisfying all possible edge cases. The solution is *not* to pretend
that no solutions exist, that the existing non-solution suffices, and instead
do nothing. Welcome to :mod:`typing`, where no one cares that nothing works as
advertised (or at all)... *and no one ever will.*

.. _typeshed:
   https://github.com/python/typeshed/issues/3225#issuecomment-529277448
'''


# Conditionally initialized by the _init() function below.
_Pep544BinaryIO: Any = None
'''
Typed version of the return of open() in binary mode.
'''


# Conditionally initialized by the _init() function below.
_Pep544TextIO: Any = None
'''
Typed version of the return of open() in text mode.
'''

# ....................{ TESTERS                           }....................
# If the active Python interpreter targets at least Python >= 3.8 and thus
# supports PEP 544, define these functions appropriately.
if IS_PYTHON_AT_LEAST_3_8:
    def is_hint_pep544_ignorable_or_none(
        hint: object, hint_sign: HintSign) -> Optional[bool]:

        # Return either:
        # * If this hint is the "typing.Protocol" superclass directly
        #   parametrized by one or more type variables (e.g.,
        #   "typing.Protocol[S, T]"), true. For unknown and presumably
        #   uninteresting reasons, *ALL* possible objects satisfy this
        #   superclass. Ergo, this superclass and *ALL* parametrizations of
        #   this superclass are synonymous with the "object" root superclass.
        # * Else, "None".
        return repr(hint).startswith('typing.Protocol[') or None


    def is_hint_pep484_generic_io(hint: object) -> bool:

        # Avoid circular import dependencies.
        from beartype._util.hint.pep.utilpepget import (
            get_hint_pep_origin_or_none)

        # Return true only if this hint is either...
        return (
            # An unsubscripted PEP 484-compliant IO generic base class
            # (e.g., "typing.IO") *OR*....
            (isinstance(hint, type) and hint in _HINTS_PEP484_IO_GENERIC) or
            # A subscripted PEP 484-compliant IO generic base class
            # (e.g., "typing.IO[str]") *OR*....
            get_hint_pep_origin_or_none(hint) in _HINTS_PEP484_IO_GENERIC
        )


    def is_hint_pep544_protocol(hint: object) -> bool:

        # Defer version-dependent imports.
        from typing import Protocol

        # Return true only if this hint is...
        return (
            # A PEP 544-compliant protocol *AND*...
            is_type_subclass(hint, Protocol) and  # type: ignore[arg-type]
            # *NOT* a builtin type. For unknown reasons, some but *NOT* all
            # builtin types erroneously present themselves to be PEP
            # 544-compliant protocols under Python >= 3.8: e.g.,
            #     >>> from typing import Protocol
            #     >>> isinstance(str, Protocol)
            #     False        # <--- this makes sense
            #     >>> isinstance(int, Protocol)
            #     True         # <--- this makes no sense whatsoever
            #
            # Since builtin types are obviously *NOT* PEP 544-compliant
            # protocols, explicitly exclude all such types. Why, Guido? Why?
            not (isinstance(hint, type) and is_type_builtin(hint))
        )


# Else, the active Python interpreter targets at most Python < 3.8 and thus
# fails to support PEP 544. In this case, fallback to declaring this function
# to unconditionally return False.
else:
    def is_hint_pep544_ignorable_or_none(
        hint: object, hint_sign: HintSign) -> Optional[bool]:
        return None


    def is_hint_pep484_generic_io(hint: object) -> bool:
        return False


    def is_hint_pep544_protocol(hint: object) -> bool:
        return False

# ....................{ TESTERS ~ doc                     }....................
is_hint_pep544_ignorable_or_none.__doc__ = '''
    ``True`` only if the passed object is a :pep:`544`-compliant **ignorable
    type hint,** ``False`` only if this object is a :pep:`544`-compliant
    unignorable type hint, and ``None`` if this object is *not*
    :pep:`544`-compliant.

    Specifically, this tester function returns ``True`` only if this object is
    a deeply ignorable :pep:`544`-compliant type hint, including:

    * A parametrization of the :class:`typing.Protocol` abstract base class
      (ABC) by one or more type variables. As the name implies, this ABC is
      generic and thus fails to impose any meaningful constraints. Since a type
      variable in and of itself also fails to impose any meaningful
      constraints, these parametrizations are safely ignorable in all possible
      contexts: e.g.,

      .. code-block:: python

         from typing import Protocol, TypeVar
         T = TypeVar('T')
         def noop(param_hint_ignorable: Protocol[T]) -> T: pass

    This tester is intentionally *not* memoized (e.g., by the
    :func:`callable_cached` decorator), as this tester is only safely callable
    by the memoized parent
    :func:`beartype._util.hint.utilhinttest.is_hint_ignorable` tester.

    Parameters
    ----------
    hint : object
        Type hint to be inspected.
    hint_sign : HintSign
        **Sign** (i.e., arbitrary object uniquely identifying this hint).

    Returns
    ----------
    Optional[bool]
        Either:

        * If this object is :pep:`544`-compliant:

          * If this object is a ignorable, ``True``.
          * Else, ``False``.

        * If this object is *not* :pep:`544`-compliant, ``None``.
    '''


is_hint_pep484_generic_io.__doc__ = '''
    ``True`` only if the passed object is a functionally useless
    :pep:`484`-compliant :mod:`typing` **IO generic superclass** (i.e., either
    :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by
    the :mod:`typing` module effectively unusable at runtime due to botched
    implementation details) that is losslessly replaceable with a useful
    :pep:`544`-compliant :mod:`beartype` **IO protocol** (i.e., either
    :class:`beartype._util.hint.pep.proposal.utilpep544._Pep544IO` itself
    *or* a subclass of that class defined by this submodule intentionally
    designed to be usable at runtime).

    This tester is intentionally *not* memoized (e.g., by the
    :func:`callable_cached` decorator), as the implementation trivially reduces
    to an efficient one-liner.

    Parameters
    ----------
    hint : object
        Object to be inspected.

    Returns
    ----------
    bool
        ``True`` only if this object is a :pep:`484`-compliant IO generic base
        class.

    See Also
    ----------
    :class:`beartype._util.hint.pep.proposal.utilpep544._Pep544IO`
        Further commentary.
    '''


is_hint_pep544_protocol.__doc__ = '''
    ``True`` only if the passed object is a :pep:`544`-compliant **protocol**
    (i.e., subclass of the :class:`typing.Protocol` superclass).

    This tester is intentionally *not* memoized (e.g., by the
    :func:`callable_cached` decorator), as the implementation trivially reduces
    to an efficient one-liner.

    Parameters
    ----------
    hint : object
        Object to be inspected.

    Returns
    ----------
    bool
        ``True`` only if this object is a :pep:`544`-compliant protocol.
    '''

# ....................{ REDUCERS                          }....................
def reduce_hint_pep484_generic_io_to_pep544_protocol(
    hint: Any, exception_prefix: str) -> Any:
    '''
    :pep:`544`-compliant :mod:`beartype` **IO protocol** (i.e., either
    :class:`beartype._util.hint.pep.proposal.utilpep544._Pep544IO`
    itself *or* a subclass of that class defined by this submodule
    intentionally designed to be usable at runtime) corresponding to the passed
    :pep:`484`-compliant :mod:`typing` **IO generic base class** (i.e., either
    :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by
    the :mod:`typing` module effectively unusable at runtime due to botched
    implementation details).

    This reducer is intentionally *not* memoized (e.g., by the
    :func:`callable_cached` decorator), as the implementation trivially reduces
    to an efficient one-liner.

    Parameters
    ----------
    hint : type
        :pep:`484`-compliant :mod:`typing` IO generic base class to be replaced
        by the corresponding :pep:`544`-compliant :mod:`beartype` IO protocol.
    exception_prefix : str
        Human-readable label prefixing the representation of this object in the
        exception message.

    Returns
    ----------
    Protocol
        :pep:`544`-compliant :mod:`beartype` IO protocol corresponding to this
        :pep:`484`-compliant :mod:`typing` IO generic base class.

    Raises
    ----------
    BeartypeDecorHintPep544Exception
        If this object is *not* a :pep:`484`-compliant IO generic base class.
    '''

    # If this object is *NOT* a PEP 484-compliant "typing" IO generic,
    # raise an exception.
    if not is_hint_pep484_generic_io(hint):
        raise BeartypeDecorHintPep544Exception(
            f'{exception_prefix}type hint {repr(hint)} not '
            f'PEP 484 IO generic base class '
            f'(i.e., "typing.IO", "typing.BinaryIO", or "typing.TextIO").'
        )
    # Else, this object is *NOT* a PEP 484-compliant "typing" IO generic.
    #
    # If this dictionary has yet to be initialized, this submodule has yet to
    # be initialized. In this case, do so.
    #
    # Note that this initialization is intentionally deferred until required.
    # Why? Because this initialization performs somewhat space- and
    # time-intensive work -- including importation of the "beartype.vale"
    # subpackage, which we strictly prohibit importing from global scope.
    elif not _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL:
        _init()
    # In any case, this dictionary is now initialized.

    # PEP 544-compliant IO protocol implementing this PEP 484-compliant IO
    # generic if any *OR* "None" otherwise.
    pep544_protocol = _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL.get(hint)

    # If *NO* such protocol implements this generic...
    if pep544_protocol is None:
        # Avoid circular import dependencies.
        from beartype._util.hint.pep.utilpepget import (
            get_hint_pep_origin_or_none,
            get_hint_pep_typevars,
        )

        # Tuple of zero or more type variables parametrizing this hint.
        hint_typevars = get_hint_pep_typevars(hint)

        #FIXME: Unit test us up, please.
        # If this hint is unparametrized, raise an exception.
        if not hint_typevars:
            raise BeartypeDecorHintPep544Exception(
                f'{exception_prefix}PEP 484 IO generic base class '
                f'{repr(hint)} invalid (i.e., not subscripted (indexed) by '
                f'either "str", "bytes", "typing.Any", or "typing.AnyStr").'
            )
        # Else, this hint is parametrized and thus defines the "__origin__"
        # dunder attribute whose value is the type originating this hint.

        #FIXME: Attempt to actually handle this type variable, please.
        # Reduce this parametrized hint (e.g., "typing.IO[typing.AnyStr]") to
        # the equivalent unparametrized hint (e.g., "typing.IO"), effectively
        # ignoring the type variable parametrizing this hint.
        hint_unparametrized: type = get_hint_pep_origin_or_none(hint)  # type: ignore[assignment]

        # PEP 544-compliant IO protocol implementing this unparametrized PEP
        # 484-compliant IO generic. For efficiency, we additionally cache this
        # mapping under the original parametrized hint to minimize the cost of
        # similar reductions under subsequent annotations.
        pep544_protocol = \
            _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL[hint] = \
            _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL[hint_unparametrized]

    # Return this protocol.
    return pep544_protocol

# ....................{ INITIALIZERS                      }....................
def _init() -> None:
    '''
    Initialize this submodule.
    '''

    # ..................{ VERSIONS                          }..................
    # If the active Python interpreter only targets Python < 3.8 and thus fails
    # to support PEP 544, silently reduce to a noop.
    if not IS_PYTHON_AT_LEAST_3_8:
        return
    # Else, the active Python interpreter targets Python >= 3.8 and thus
    # supports PEP 593.

    # ..................{ IMPORTS                           }..................
    # Defer Python version-specific imports.
    from beartype._util.mod.lib.utiltyping import import_typing_attr_or_none
    from beartype.typing import List
    from typing import (
        AnyStr,
        Protocol,
        runtime_checkable,
    )

    # ..................{ GLOBALS                           }..................
    # Global attributes to be redefined below.
    global \
        _Pep544BinaryIO, \
        _Pep544IO, \
        _Pep544TextIO

    # ..................{ PROTOCOLS ~ protocol              }..................
    # Note that these classes are intentionally *NOT* declared at global scope;
    # instead, these classes are declared *ONLY* if the active Python
    # interpreter targets Python >= 3.8.

    # PEP-compliant type hint matching file handles opened in either text or
    # binary mode.
    @runtime_checkable
    class _Pep544IO(Protocol[AnyStr]):
        # The body of this class is copied wholesale from the existing
        # non-functional "typing.IO" class.

        __slots__: tuple = ()

        @property
        @abstractmethod
        def mode(self) -> str:
            pass

        @property
        @abstractmethod
        def name(self) -> str:
            pass

        @abstractmethod
        def close(self) -> None:
            pass

        @property
        @abstractmethod
        def closed(self) -> bool:
            pass

        @abstractmethod
        def fileno(self) -> int:
            pass

        @abstractmethod
        def flush(self) -> None:
            pass

        @abstractmethod
        def isatty(self) -> bool:
            pass

        @abstractmethod
        def read(self, n: int = -1) -> AnyStr:
            pass

        @abstractmethod
        def readable(self) -> bool:
            pass

        @abstractmethod
        def readline(self, limit: int = -1) -> AnyStr:
            pass

        @abstractmethod
        def readlines(self, hint: int = -1) -> List[AnyStr]:
            pass

        @abstractmethod
        def seek(self, offset: int, whence: int = 0) -> int:
            pass

        @abstractmethod
        def seekable(self) -> bool:
            pass

        @abstractmethod
        def tell(self) -> int:
            pass

        @abstractmethod
        def truncate(self, size: Optional[int] = None) -> int:
            pass

        @abstractmethod
        def writable(self) -> bool:
            pass

        @abstractmethod
        def write(self, s: AnyStr) -> int:
            pass

        @abstractmethod
        def writelines(self, lines: List[AnyStr]) -> None:
            pass

        @abstractmethod
        def __enter__(self) -> '_Pep544IO[AnyStr]':
            pass

        @abstractmethod
        def __exit__(self, cls, value, traceback) -> None:
            pass


    # PEP-compliant type hint matching file handles opened in text rather than
    # binary mode.
    #
    # Note that PEP 544 explicitly requires *ALL* protocols (including
    # protocols subclassing protocols) to explicitly subclass the "Protocol"
    # superclass, in violation of both sanity and usability. (Thanks, guys.)
    @runtime_checkable
    class _Pep544TextIO(_Pep544IO[str], Protocol):
        # The body of this class is copied wholesale from the existing
        # non-functional "typing.TextIO" class.

        __slots__: tuple = ()

        @property
        @abstractmethod
        def buffer(self) -> _Pep544BinaryIO:
            pass

        @property
        @abstractmethod
        def encoding(self) -> str:
            pass

        @property
        @abstractmethod
        def errors(self) -> Optional[str]:
            pass

        @property
        @abstractmethod
        def line_buffering(self) -> bool:
            pass

        @property
        @abstractmethod
        def newlines(self) -> Any:
            pass

        @abstractmethod
        def __enter__(self) -> '_Pep544TextIO':
            pass

    # ..................{ PROTOCOLS ~ validator             }..................
    # PEP-compliant type hint matching file handles opened in binary rather
    # than text mode.
    #
    # If PEP 593 (e.g., "typing.Annotated") and thus beartype validators are
    # unusable, this hint falls back to ambiguously matching the abstract
    # "typing.IO" protocol ABC. This will yield false positives (i.e., fail to
    # raise exceptions) for @beartype-decorated callables annotated as
    # accepting binary file handles erroneously passed text file handles, which
    # is non-ideal but certainly preferable to raising exceptions at decoration
    # time on each such callable.
    #
    # If PEP 593 (e.g., "typing.Annotated") and thus beartype validators are
    # usable, this hint matches the abstract "typing.IO" protocol ABC but *NOT*
    # the concrete "typing.TextIO" subprotocol subclassing that ABC. Whereas
    # the concrete "typing.TextIO" subprotocol unambiguously matches *ONLY*
    # file handles opened in text mode, the concrete "typing.BinaryIO"
    # subprotocol ambiguously matches file handles opened in both text *AND*
    # binary mode. As the following hypothetical "_Pep544BinaryIO" subclass
    # demonstrates, the "typing.IO" and "typing.BinaryIO" APIs are identical
    # except for method annotations:
    #     class _Pep544BinaryIO(_Pep544IO[bytes], Protocol):
    #         # The body of this class is copied wholesale from the existing
    #         # non-functional "typing.BinaryIO" class.
    #
    #         __slots__: tuple = ()
    #
    #         @abstractmethod
    #         def write(self, s: Union[bytes, bytearray]) -> int:
    #             pass
    #
    #         @abstractmethod
    #         def __enter__(self) -> '_Pep544BinaryIO':
    #             pass
    #
    # Sadly, the method annotations that differ between these APIs are
    # insufficient to disambiguate file handles at runtime. Why? Because most
    # file handles are C-based and thus lack *ANY* annotations whatsoever. With
    # respect to C-based file handles, these APIs are therefore identical.
    # Ergo, the "typing.BinaryIO" subprotocol is mostly useless at runtime.
    #
    # Note, however, that file handles are necessarily *ALWAYS* opened in
    # either text or binary mode. This strict dichotomy implies that any file
    # handle (i.e., object matching the "typing.IO" protocol) *NOT* opened in
    # text mode (i.e., not matching the "typing.TextIO" protocol) must
    # necessarily be opened in binary mode instead.
    _Pep544BinaryIO = _Pep544IO

    #FIXME: Safely replace this with "from typing import Annotated" after
    #dropping Python 3.8 support.
    # "typing.Annotated" type hint factory safely imported from whichever of
    # the "typing" or "typing_extensions" modules declares this attribute if
    # one or more do *OR* "None" otherwise (i.e., if none do).
    typing_annotated = import_typing_attr_or_none('Annotated')

    # If this factory is importable.
    if typing_annotated is not None:
        # Defer heavyweight imports.
        from beartype.vale import IsInstance

        # Expand this hint to unambiguously match binary file handles by
        # subscripting this factory with a beartype validator doing so.
        _Pep544BinaryIO = typing_annotated[
            _Pep544IO, ~IsInstance[_Pep544TextIO]]
    # Else, this factory is unimportable. In this case, accept this hint's
    # default ambiguously matching both binary and text files.

    # ..................{ MAPPINGS                          }..................
    # Dictionary mapping from each "typing" IO generic base class to the
    # associated IO protocol defined above.
    #
    # Note this global is intentionally modified in-place rather than
    # reassigned to a new dictionary. Why? Because the higher-level
    # reduce_hint_pep484_generic_io_to_pep544_protocol() function calling this
    # lower-level initializer has already imported this global.
    _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL.update({
        # Unsubscripted mappings.
        IO:        _Pep544IO,
        BinaryIO:  _Pep544BinaryIO,
        TextIO:    _Pep544TextIO,

        # Subscripted mappings, leveraging the useful observation that these
        # classes all self-cache by design: e.g.,
        #     >>> import typing
        #     >>> typing.IO[str] is typing.IO[str]
        #     True
        #
        # Note that we intentionally map:
        # * "IO[Any]" to the unsubscripted "_Pep544IO" rather than the
        #   subscripted "_Pep544IO[Any]". Although the two are semantically
        #   equivalent, the latter is marginally more space- and time-efficient
        #   to generate code for and thus preferable.
        # * "IO[bytes]" to the unsubscripted "_Pep544Binary" rather than the
        #   subscripted "_Pep544IO[bytes]". Why? Because the former applies
        #   meaningful runtime constraints, whereas the latter does *NOT*.
        # * "IO[str]" to the unsubscripted "_Pep544Text" rather than the
        #   subscripted "_Pep544IO[str]" -- for the same reason.
        #
        # Note that we intentionally avoid mapping parametrizations of "IO" by
        # type variables. Since there exist a countably infinite number of
        # such parametrizations, the parent
        # reduce_hint_pep484_generic_io_to_pep544_protocol() function calling
        # this function handles such parametrizations mostly intelligently.
        IO[Any]:   _Pep544IO,
        IO[bytes]: _Pep544BinaryIO,
        IO[str]:   _Pep544TextIO,
    })

#!/usr/bin/env python3
# --------------------( LICENSE                           )--------------------
# Copyright (c) 2014-2022 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide :pep:`544`-compliant type hint utilities.

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS                           }....................
from abc import abstractmethod
from beartype.roar import BeartypeDecorHintPep544Exception
from beartype._data.hint.pep.sign.datapepsigncls import HintSign
from beartype._util.cls.utilclstest import is_type_builtin, is_type_subclass
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from typing import (
    Any,
    BinaryIO,
    Dict,
    IO,
    Optional,
    TextIO,
)

# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ PRIVATE ~ mappings                }....................
_HINTS_PEP484_IO_GENERIC = frozenset((IO, BinaryIO, TextIO,))
'''
Frozen set of all :mod:`typing` **IO generic base class** (i.e., either
:class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by the
:mod:`typing` module).
'''


# Conditionally initialized by the _init() function below.
_HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL: Dict[type, Any] = {}
'''
Dictionary mapping from each :mod:`typing` **IO generic base class** (i.e.,
either :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined
by the :mod:`typing` module) to the associated :mod:`beartype` **IO protocol**
(i.e., either :class:`_Pep544IO` itself *or* a subclass of :class:`_Pep544IO`
defined by this submodule).
'''

# ....................{ PRIVATE ~ classes                 }....................
# Conditionally initialized by the _init() function below.
_Pep544IO: Any = None
'''
:pep:`544`-compliant protocol base class for :class:`_Pep544TextIO` and
:class:`_Pep544BinaryIO`.

This is an abstract, generic version of the return of open().

NOTE: This does not distinguish between the different possible classes (text
vs. binary, read vs. write vs. read/write, append-only, unbuffered). The TextIO
and BinaryIO subclasses below capture the distinctions between text vs. binary,
which is pervasive in the interface; however we currently do not offer a way to
track the other distinctions in the type system.

Design
----------
This base class intentionally duplicates the contents of the existing
:class:`typing.IO` generic base class by substituting the useless
:class:`typing.Generic` superclass of the latter with the useful
:class:`typing.Protocol` superclass of the former. Why? Because *no* stdlib
classes excluding those defined by the :mod:`typing` module itself subclass
:class:`typing.IO`. However, :class:`typing.IO` leverages neither the
:class:`abc.ABCMeta` metaclass *nor* the :class:`typing.Protocol` superclass
needed to support structural subtyping. Therefore, *no* stdlib objects
(including those returned by the :func:`open` builtin) satisfy either
:class:`typing.IO` itself or any subclasses of :class:`typing.IO` (e.g.,
:class:`typing.BinaryIO`, :class:`typing.TextIO`). Therefore,
:class:`typing.IO` and all subclasses thereof are functionally useless for all
practical intents. The conventional excuse `given by Python maintainers to
justify this abhorrent nonsensicality is as follows <typeshed_>`__:

    There are a lot of "file-like" classes, and the typing IO classes are meant
    as "protocols" for general files, but they cannot actually be protocols
    because the file protocol isn't very well defined—there are lots of methods
    that exist on some but not all filelike classes.

Like most :mod:`typing`-oriented confabulation, that, of course, is bollocks.
Refactoring the family of :mod:`typing` IO classes from inveterate generics
into pragmatic protocols is both technically trivial and semantically useful,
because that is exactly what :mod:`beartype` does. It works. It necessitates
modifying three lines of existing code. It preserves backward compatibility. In
short, it should have been done a decade ago. If the file protocol "isn't very
well defined," the solution is to define that protocol with a rigorous type
hierarchy satisfying all possible edge cases. The solution is *not* to pretend
that no solutions exist, that the existing non-solution suffices, and instead
do nothing. Welcome to :mod:`typing`, where no one cares that nothing works as
advertised (or at all)... *and no one ever will.*

.. _typeshed:
   https://github.com/python/typeshed/issues/3225#issuecomment-529277448
'''


# Conditionally initialized by the _init() function below.
_Pep544BinaryIO: Any = None
'''
Typed version of the return of open() in binary mode.
'''


# Conditionally initialized by the _init() function below.
_Pep544TextIO: Any = None
'''
Typed version of the return of open() in text mode.
'''

# ....................{ TESTERS                           }....................
# If the active Python interpreter targets at least Python >= 3.8 and thus
# supports PEP 544, define these functions appropriately.
if IS_PYTHON_AT_LEAST_3_8:
    def is_hint_pep544_ignorable_or_none(
        hint: object, hint_sign: HintSign) -> Optional[bool]:

        # Return either:
        # * If this hint is the "typing.Protocol" superclass directly
        #   parametrized by one or more type variables (e.g.,
        #   "typing.Protocol[S, T]"), true. For unknown and presumably
        #   uninteresting reasons, *ALL* possible objects satisfy this
        #   superclass. Ergo, this superclass and *ALL* parametrizations of
        #   this superclass are synonymous with the "object" root superclass.
        # * Else, "None".
        return repr(hint).startswith('typing.Protocol[') or None


    def is_hint_pep484_generic_io(hint: object) -> bool:

        # Avoid circular import dependencies.
        from beartype._util.hint.pep.utilpepget import (
            get_hint_pep_origin_or_none)

        # Return true only if this hint is either...
        return (
            # An unsubscripted PEP 484-compliant IO generic base class
            # (e.g., "typing.IO") *OR*....
            (isinstance(hint, type) and hint in _HINTS_PEP484_IO_GENERIC) or
            # A subscripted PEP 484-compliant IO generic base class
            # (e.g., "typing.IO[str]") *OR*....
            get_hint_pep_origin_or_none(hint) in _HINTS_PEP484_IO_GENERIC
        )


    def is_hint_pep544_protocol(hint: object) -> bool:

        # Defer version-dependent imports.
        from typing import Protocol

        # Return true only if this hint is...
        return (
            # A PEP 544-compliant protocol *AND*...
            is_type_subclass(hint, Protocol) and  # type: ignore[arg-type]
            # *NOT* a builtin type. For unknown reasons, some but *NOT* all
            # builtin types erroneously present themselves to be PEP
            # 544-compliant protocols under Python >= 3.8: e.g.,
            #     >>> from typing import Protocol
            #     >>> isinstance(str, Protocol)
            #     False        # <--- this makes sense
            #     >>> isinstance(int, Protocol)
            #     True         # <--- this makes no sense whatsoever
            #
            # Since builtin types are obviously *NOT* PEP 544-compliant
            # protocols, explicitly exclude all such types. Why, Guido? Why?
            not (isinstance(hint, type) and is_type_builtin(hint))
        )


# Else, the active Python interpreter targets at most Python < 3.8 and thus
# fails to support PEP 544. In this case, fallback to declaring this function
# to unconditionally return False.
else:
    def is_hint_pep544_ignorable_or_none(
        hint: object, hint_sign: HintSign) -> Optional[bool]:
        return None


    def is_hint_pep484_generic_io(hint: object) -> bool:
        return False


    def is_hint_pep544_protocol(hint: object) -> bool:
        return False

# ....................{ TESTERS ~ doc                     }....................
is_hint_pep544_ignorable_or_none.__doc__ = '''
    ``True`` only if the passed object is a :pep:`544`-compliant **ignorable
    type hint,** ``False`` only if this object is a :pep:`544`-compliant
    unignorable type hint, and ``None`` if this object is *not*
    :pep:`544`-compliant.

    Specifically, this tester function returns ``True`` only if this object is
    a deeply ignorable :pep:`544`-compliant type hint, including:

    * A parametrization of the :class:`typing.Protocol` abstract base class
      (ABC) by one or more type variables. As the name implies, this ABC is
      generic and thus fails to impose any meaningful constraints. Since a type
      variable in and of itself also fails to impose any meaningful
      constraints, these parametrizations are safely ignorable in all possible
      contexts: e.g.,

      .. code-block:: python

         from typing import Protocol, TypeVar
         T = TypeVar('T')
         def noop(param_hint_ignorable: Protocol[T]) -> T: pass

    This tester is intentionally *not* memoized (e.g., by the
    :func:`callable_cached` decorator), as this tester is only safely callable
    by the memoized parent
    :func:`beartype._util.hint.utilhinttest.is_hint_ignorable` tester.

    Parameters
    ----------
    hint : object
        Type hint to be inspected.
    hint_sign : HintSign
        **Sign** (i.e., arbitrary object uniquely identifying this hint).

    Returns
    ----------
    Optional[bool]
        Either:

        * If this object is :pep:`544`-compliant:

          * If this object is a ignorable, ``True``.
          * Else, ``False``.

        * If this object is *not* :pep:`544`-compliant, ``None``.
    '''


is_hint_pep484_generic_io.__doc__ = '''
    ``True`` only if the passed object is a functionally useless
    :pep:`484`-compliant :mod:`typing` **IO generic superclass** (i.e., either
    :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by
    the :mod:`typing` module effectively unusable at runtime due to botched
    implementation details) that is losslessly replaceable with a useful
    :pep:`544`-compliant :mod:`beartype` **IO protocol** (i.e., either
    :class:`beartype._util.hint.pep.proposal.utilpep544._Pep544IO` itself
    *or* a subclass of that class defined by this submodule intentionally
    designed to be usable at runtime).

    This tester is intentionally *not* memoized (e.g., by the
    :func:`callable_cached` decorator), as the implementation trivially reduces
    to an efficient one-liner.

    Parameters
    ----------
    hint : object
        Object to be inspected.

    Returns
    ----------
    bool
        ``True`` only if this object is a :pep:`484`-compliant IO generic base
        class.

    See Also
    ----------
    :class:`beartype._util.hint.pep.proposal.utilpep544._Pep544IO`
        Further commentary.
    '''


is_hint_pep544_protocol.__doc__ = '''
    ``True`` only if the passed object is a :pep:`544`-compliant **protocol**
    (i.e., subclass of the :class:`typing.Protocol` superclass).

    This tester is intentionally *not* memoized (e.g., by the
    :func:`callable_cached` decorator), as the implementation trivially reduces
    to an efficient one-liner.

    Parameters
    ----------
    hint : object
        Object to be inspected.

    Returns
    ----------
    bool
        ``True`` only if this object is a :pep:`544`-compliant protocol.
    '''

# ....................{ REDUCERS                          }....................
def reduce_hint_pep484_generic_io_to_pep544_protocol(
    hint: Any, exception_prefix: str) -> Any:
    '''
    :pep:`544`-compliant :mod:`beartype` **IO protocol** (i.e., either
    :class:`beartype._util.hint.pep.proposal.utilpep544._Pep544IO`
    itself *or* a subclass of that class defined by this submodule
    intentionally designed to be usable at runtime) corresponding to the passed
    :pep:`484`-compliant :mod:`typing` **IO generic base class** (i.e., either
    :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by
    the :mod:`typing` module effectively unusable at runtime due to botched
    implementation details).

    This reducer is intentionally *not* memoized (e.g., by the
    :func:`callable_cached` decorator), as the implementation trivially reduces
    to an efficient one-liner.

    Parameters
    ----------
    hint : type
        :pep:`484`-compliant :mod:`typing` IO generic base class to be replaced
        by the corresponding :pep:`544`-compliant :mod:`beartype` IO protocol.
    exception_prefix : str
        Human-readable label prefixing the representation of this object in the
        exception message.

    Returns
    ----------
    Protocol
        :pep:`544`-compliant :mod:`beartype` IO protocol corresponding to this
        :pep:`484`-compliant :mod:`typing` IO generic base class.

    Raises
    ----------
    BeartypeDecorHintPep544Exception
        If this object is *not* a :pep:`484`-compliant IO generic base class.
    '''

    # If this object is *NOT* a PEP 484-compliant "typing" IO generic,
    # raise an exception.
    if not is_hint_pep484_generic_io(hint):
        raise BeartypeDecorHintPep544Exception(
            f'{exception_prefix}type hint {repr(hint)} not '
            f'PEP 484 IO generic base class '
            f'(i.e., "typing.IO", "typing.BinaryIO", or "typing.TextIO").'
        )
    # Else, this object is *NOT* a PEP 484-compliant "typing" IO generic.
    #
    # If this dictionary has yet to be initialized, this submodule has yet to
    # be initialized. In this case, do so.
    #
    # Note that this initialization is intentionally deferred until required.
    # Why? Because this initialization performs somewhat space- and
    # time-intensive work -- including importation of the "beartype.vale"
    # subpackage, which we strictly prohibit importing from global scope.
    elif not _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL:
        _init()
    # In any case, this dictionary is now initialized.

    # PEP 544-compliant IO protocol implementing this PEP 484-compliant IO
    # generic if any *OR* "None" otherwise.
    pep544_protocol = _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL.get(hint)

    # If *NO* such protocol implements this generic...
    if pep544_protocol is None:
        # Avoid circular import dependencies.
        from beartype._util.hint.pep.utilpepget import (
            get_hint_pep_origin_or_none,
            get_hint_pep_typevars,
        )

        # Tuple of zero or more type variables parametrizing this hint.
        hint_typevars = get_hint_pep_typevars(hint)

        #FIXME: Unit test us up, please.
        # If this hint is unparametrized, raise an exception.
        if not hint_typevars:
            raise BeartypeDecorHintPep544Exception(
                f'{exception_prefix}PEP 484 IO generic base class '
                f'{repr(hint)} invalid (i.e., not subscripted (indexed) by '
                f'either "str", "bytes", "typing.Any", or "typing.AnyStr").'
            )
        # Else, this hint is parametrized and thus defines the "__origin__"
        # dunder attribute whose value is the type originating this hint.

        #FIXME: Attempt to actually handle this type variable, please.
        # Reduce this parametrized hint (e.g., "typing.IO[typing.AnyStr]") to
        # the equivalent unparametrized hint (e.g., "typing.IO"), effectively
        # ignoring the type variable parametrizing this hint.
        hint_unparametrized: type = get_hint_pep_origin_or_none(hint)  # type: ignore[assignment]

        # PEP 544-compliant IO protocol implementing this unparametrized PEP
        # 484-compliant IO generic. For efficiency, we additionally cache this
        # mapping under the original parametrized hint to minimize the cost of
        # similar reductions under subsequent annotations.
        pep544_protocol = \
            _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL[hint] = \
            _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL[hint_unparametrized]

    # Return this protocol.
    return pep544_protocol

# ....................{ INITIALIZERS                      }....................
def _init() -> None:
    '''
    Initialize this submodule.
    '''

    # ..................{ VERSIONS                          }..................
    # If the active Python interpreter only targets Python < 3.8 and thus fails
    # to support PEP 544, silently reduce to a noop.
    if not IS_PYTHON_AT_LEAST_3_8:
        return
    # Else, the active Python interpreter targets Python >= 3.8 and thus
    # supports PEP 593.

    # ..................{ IMPORTS                           }..................
    # Defer Python version-specific imports.
    from beartype._util.mod.lib.utiltyping import import_typing_attr_or_none
    from beartype.typing import List
    from typing import (
        AnyStr,
        Protocol,
        runtime_checkable,
    )

    # ..................{ GLOBALS                           }..................
    # Global attributes to be redefined below.
    global \
        _Pep544BinaryIO, \
        _Pep544IO, \
        _Pep544TextIO

    # ..................{ PROTOCOLS ~ protocol              }..................
    # Note that these classes are intentionally *NOT* declared at global scope;
    # instead, these classes are declared *ONLY* if the active Python
    # interpreter targets Python >= 3.8.

    # PEP-compliant type hint matching file handles opened in either text or
    # binary mode.
    @runtime_checkable
    class _Pep544IO(Protocol[AnyStr]):
        # The body of this class is copied wholesale from the existing
        # non-functional "typing.IO" class.

        __slots__: tuple = ()

        @property
        @abstractmethod
        def mode(self) -> str:
            pass

        @property
        @abstractmethod
        def name(self) -> str:
            pass

        @abstractmethod
        def close(self) -> None:
            pass

        @property
        @abstractmethod
        def closed(self) -> bool:
            pass

        @abstractmethod
        def fileno(self) -> int:
            pass

        @abstractmethod
        def flush(self) -> None:
            pass

        @abstractmethod
        def isatty(self) -> bool:
            pass

        @abstractmethod
        def read(self, n: int = -1) -> AnyStr:
            pass

        @abstractmethod
        def readable(self) -> bool:
            pass

        @abstractmethod
        def readline(self, limit: int = -1) -> AnyStr:
            pass

        @abstractmethod
        def readlines(self, hint: int = -1) -> List[AnyStr]:
            pass

        @abstractmethod
        def seek(self, offset: int, whence: int = 0) -> int:
            pass

        @abstractmethod
        def seekable(self) -> bool:
            pass

        @abstractmethod
        def tell(self) -> int:
            pass

        @abstractmethod
        def truncate(self, size: Optional[int] = None) -> int:
            pass

        @abstractmethod
        def writable(self) -> bool:
            pass

        @abstractmethod
        def write(self, s: AnyStr) -> int:
            pass

        @abstractmethod
        def writelines(self, lines: List[AnyStr]) -> None:
            pass

        @abstractmethod
        def __enter__(self) -> '_Pep544IO[AnyStr]':
            pass

        @abstractmethod
        def __exit__(self, cls, value, traceback) -> None:
            pass


    # PEP-compliant type hint matching file handles opened in text rather than
    # binary mode.
    #
    # Note that PEP 544 explicitly requires *ALL* protocols (including
    # protocols subclassing protocols) to explicitly subclass the "Protocol"
    # superclass, in violation of both sanity and usability. (Thanks, guys.)
    @runtime_checkable
    class _Pep544TextIO(_Pep544IO[str], Protocol):
        # The body of this class is copied wholesale from the existing
        # non-functional "typing.TextIO" class.

        __slots__: tuple = ()

        @property
        @abstractmethod
        def buffer(self) -> _Pep544BinaryIO:
            pass

        @property
        @abstractmethod
        def encoding(self) -> str:
            pass

        @property
        @abstractmethod
        def errors(self) -> Optional[str]:
            pass

        @property
        @abstractmethod
        def line_buffering(self) -> bool:
            pass

        @property
        @abstractmethod
        def newlines(self) -> Any:
            pass

        @abstractmethod
        def __enter__(self) -> '_Pep544TextIO':
            pass

    # ..................{ PROTOCOLS ~ validator             }..................
    # PEP-compliant type hint matching file handles opened in binary rather
    # than text mode.
    #
    # If PEP 593 (e.g., "typing.Annotated") and thus beartype validators are
    # unusable, this hint falls back to ambiguously matching the abstract
    # "typing.IO" protocol ABC. This will yield false positives (i.e., fail to
    # raise exceptions) for @beartype-decorated callables annotated as
    # accepting binary file handles erroneously passed text file handles, which
    # is non-ideal but certainly preferable to raising exceptions at decoration
    # time on each such callable.
    #
    # If PEP 593 (e.g., "typing.Annotated") and thus beartype validators are
    # usable, this hint matches the abstract "typing.IO" protocol ABC but *NOT*
    # the concrete "typing.TextIO" subprotocol subclassing that ABC. Whereas
    # the concrete "typing.TextIO" subprotocol unambiguously matches *ONLY*
    # file handles opened in text mode, the concrete "typing.BinaryIO"
    # subprotocol ambiguously matches file handles opened in both text *AND*
    # binary mode. As the following hypothetical "_Pep544BinaryIO" subclass
    # demonstrates, the "typing.IO" and "typing.BinaryIO" APIs are identical
    # except for method annotations:
    #     class _Pep544BinaryIO(_Pep544IO[bytes], Protocol):
    #         # The body of this class is copied wholesale from the existing
    #         # non-functional "typing.BinaryIO" class.
    #
    #         __slots__: tuple = ()
    #
    #         @abstractmethod
    #         def write(self, s: Union[bytes, bytearray]) -> int:
    #             pass
    #
    #         @abstractmethod
    #         def __enter__(self) -> '_Pep544BinaryIO':
    #             pass
    #
    # Sadly, the method annotations that differ between these APIs are
    # insufficient to disambiguate file handles at runtime. Why? Because most
    # file handles are C-based and thus lack *ANY* annotations whatsoever. With
    # respect to C-based file handles, these APIs are therefore identical.
    # Ergo, the "typing.BinaryIO" subprotocol is mostly useless at runtime.
    #
    # Note, however, that file handles are necessarily *ALWAYS* opened in
    # either text or binary mode. This strict dichotomy implies that any file
    # handle (i.e., object matching the "typing.IO" protocol) *NOT* opened in
    # text mode (i.e., not matching the "typing.TextIO" protocol) must
    # necessarily be opened in binary mode instead.
    _Pep544BinaryIO = _Pep544IO

    #FIXME: Safely replace this with "from typing import Annotated" after
    #dropping Python 3.8 support.
    # "typing.Annotated" type hint factory safely imported from whichever of
    # the "typing" or "typing_extensions" modules declares this attribute if
    # one or more do *OR* "None" otherwise (i.e., if none do).
    typing_annotated = import_typing_attr_or_none('Annotated')

    # If this factory is importable.
    if typing_annotated is not None:
        # Defer heavyweight imports.
        from beartype.vale import IsInstance

        # Expand this hint to unambiguously match binary file handles by
        # subscripting this factory with a beartype validator doing so.
        _Pep544BinaryIO = typing_annotated[
            _Pep544IO, ~IsInstance[_Pep544TextIO]]
    # Else, this factory is unimportable. In this case, accept this hint's
    # default ambiguously matching both binary and text files.

    # ..................{ MAPPINGS                          }..................
    # Dictionary mapping from each "typing" IO generic base class to the
    # associated IO protocol defined above.
    #
    # Note this global is intentionally modified in-place rather than
    # reassigned to a new dictionary. Why? Because the higher-level
    # reduce_hint_pep484_generic_io_to_pep544_protocol() function calling this
    # lower-level initializer has already imported this global.
    _HINT_PEP484_IO_GENERIC_TO_PEP544_PROTOCOL.update({
        # Unsubscripted mappings.
        IO:        _Pep544IO,
        BinaryIO:  _Pep544BinaryIO,
        TextIO:    _Pep544TextIO,

        # Subscripted mappings, leveraging the useful observation that these
        # classes all self-cache by design: e.g.,
        #     >>> import typing
        #     >>> typing.IO[str] is typing.IO[str]
        #     True
        #
        # Note that we intentionally map:
        # * "IO[Any]" to the unsubscripted "_Pep544IO" rather than the
        #   subscripted "_Pep544IO[Any]". Although the two are semantically
        #   equivalent, the latter is marginally more space- and time-efficient
        #   to generate code for and thus preferable.
        # * "IO[bytes]" to the unsubscripted "_Pep544Binary" rather than the
        #   subscripted "_Pep544IO[bytes]". Why? Because the former applies
        #   meaningful runtime constraints, whereas the latter does *NOT*.
        # * "IO[str]" to the unsubscripted "_Pep544Text" rather than the
        #   subscripted "_Pep544IO[str]" -- for the same reason.
        #
        # Note that we intentionally avoid mapping parametrizations of "IO" by
        # type variables. Since there exist a countably infinite number of
        # such parametrizations, the parent
        # reduce_hint_pep484_generic_io_to_pep544_protocol() function calling
        # this function handles such parametrizations mostly intelligently.
        IO[Any]:   _Pep544IO,
        IO[bytes]: _Pep544BinaryIO,
        IO[str]:   _Pep544TextIO,
    })
#!/usr/bin/env python3 # --------------------( LICENSE                           )-------------------- # Copyright (c) 2014-2022 Beartype authors. # See "LICENSE" for further details. Project-wide :pep:`544`-compliant type hint utilities.  This private submodule is *not* intended for importation by downstream callers. # ....................{ IMPORTS                           }.................... # See the "beartype.cave" submodule for further commentary. # ....................{ PRIVATE ~ mappings                }.................... Frozen set of all :mod:`typing` **IO generic base class** (i.e., either :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by the :mod:`typing` module). # Conditionally initialized by the _init() function below. Dictionary mapping from each :mod:`typing` **IO generic base class** (i.e., either :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by the :mod:`typing` module) to the associated :mod:`beartype` **IO protocol** (i.e., either :class:`_Pep544IO` itself *or* a subclass of :class:`_Pep544IO` defined by this submodule). # ....................{ PRIVATE ~ classes                 }.................... # Conditionally initialized by the _init() function below. :pep:`544`-compliant protocol base class for :class:`_Pep544TextIO` and :class:`_Pep544BinaryIO`.  This is an abstract, generic version of the return of open().  NOTE: This does not distinguish between the different possible classes (text vs. binary, read vs. write vs. read/write, append-only, unbuffered). The TextIO and BinaryIO subclasses below capture the distinctions between text vs. binary, which is pervasive in the interface; however we currently do not offer a way to track the other distinctions in the type system.  Design ---------- This base class intentionally duplicates the contents of the existing :class:`typing.IO` generic base class by substituting the useless :class:`typing.Generic` superclass of the latter with the useful :class:`typing.Protocol` superclass of the former. Why? Because *no* stdlib classes excluding those defined by the :mod:`typing` module itself subclass :class:`typing.IO`. However, :class:`typing.IO` leverages neither the :class:`abc.ABCMeta` metaclass *nor* the :class:`typing.Protocol` superclass needed to support structural subtyping. Therefore, *no* stdlib objects (including those returned by the :func:`open` builtin) satisfy either :class:`typing.IO` itself or any subclasses of :class:`typing.IO` (e.g., :class:`typing.BinaryIO`, :class:`typing.TextIO`). Therefore, :class:`typing.IO` and all subclasses thereof are functionally useless for all practical intents. The conventional excuse `given by Python maintainers to justify this abhorrent nonsensicality is as follows <typeshed_>`__:      There are a lot of "file-like" classes, and the typing IO classes are meant     as "protocols" for general files, but they cannot actually be protocols     because the file protocol isn't very well defined—there are lots of methods     that exist on some but not all filelike classes.  Like most :mod:`typing`-oriented confabulation, that, of course, is bollocks. Refactoring the family of :mod:`typing` IO classes from inveterate generics into pragmatic protocols is both technically trivial and semantically useful, because that is exactly what :mod:`beartype` does. It works. It necessitates modifying three lines of existing code. It preserves backward compatibility. In short, it should have been done a decade ago. If the file protocol "isn't very well defined," the solution is to define that protocol with a rigorous type hierarchy satisfying all possible edge cases. The solution is *not* to pretend that no solutions exist, that the existing non-solution suffices, and instead do nothing. Welcome to :mod:`typing`, where no one cares that nothing works as advertised (or at all)... *and no one ever will.*  .. _typeshed:    https://github.com/python/typeshed/issues/3225#issuecomment-529277448 # Conditionally initialized by the _init() function below. Typed version of the return of open() in binary mode. # Conditionally initialized by the _init() function below. Typed version of the return of open() in text mode. # ....................{ TESTERS                           }.................... # If the active Python interpreter targets at least Python >= 3.8 and thus # supports PEP 544, define these functions appropriately. # Return either: # * If this hint is the "typing.Protocol" superclass directly #   parametrized by one or more type variables (e.g., #   "typing.Protocol[S, T]"), true. For unknown and presumably #   uninteresting reasons, *ALL* possible objects satisfy this #   superclass. Ergo, this superclass and *ALL* parametrizations of #   this superclass are synonymous with the "object" root superclass. # * Else, "None". # Avoid circular import dependencies. # Return true only if this hint is either... # An unsubscripted PEP 484-compliant IO generic base class # (e.g., "typing.IO") *OR*.... # A subscripted PEP 484-compliant IO generic base class # (e.g., "typing.IO[str]") *OR*.... # Defer version-dependent imports. # Return true only if this hint is... # A PEP 544-compliant protocol *AND*... # type: ignore[arg-type] # *NOT* a builtin type. For unknown reasons, some but *NOT* all # builtin types erroneously present themselves to be PEP # 544-compliant protocols under Python >= 3.8: e.g., #     >>> from typing import Protocol #     >>> isinstance(str, Protocol) #     False        # <--- this makes sense #     >>> isinstance(int, Protocol) #     True         # <--- this makes no sense whatsoever # # Since builtin types are obviously *NOT* PEP 544-compliant # protocols, explicitly exclude all such types. Why, Guido? Why? # Else, the active Python interpreter targets at most Python < 3.8 and thus # fails to support PEP 544. In this case, fallback to declaring this function # to unconditionally return False. # ....................{ TESTERS ~ doc                     }.................... ``True`` only if the passed object is a :pep:`544`-compliant **ignorable     type hint,** ``False`` only if this object is a :pep:`544`-compliant     unignorable type hint, and ``None`` if this object is *not*     :pep:`544`-compliant.      Specifically, this tester function returns ``True`` only if this object is     a deeply ignorable :pep:`544`-compliant type hint, including:      * A parametrization of the :class:`typing.Protocol` abstract base class       (ABC) by one or more type variables. As the name implies, this ABC is       generic and thus fails to impose any meaningful constraints. Since a type       variable in and of itself also fails to impose any meaningful       constraints, these parametrizations are safely ignorable in all possible       contexts: e.g.,        .. code-block:: python           from typing import Protocol, TypeVar          T = TypeVar('T')          def noop(param_hint_ignorable: Protocol[T]) -> T: pass      This tester is intentionally *not* memoized (e.g., by the     :func:`callable_cached` decorator), as this tester is only safely callable     by the memoized parent     :func:`beartype._util.hint.utilhinttest.is_hint_ignorable` tester.      Parameters     ----------     hint : object         Type hint to be inspected.     hint_sign : HintSign         **Sign** (i.e., arbitrary object uniquely identifying this hint).      Returns     ----------     Optional[bool]         Either:          * If this object is :pep:`544`-compliant:            * If this object is a ignorable, ``True``.           * Else, ``False``.          * If this object is *not* :pep:`544`-compliant, ``None``. ``True`` only if the passed object is a functionally useless     :pep:`484`-compliant :mod:`typing` **IO generic superclass** (i.e., either     :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by     the :mod:`typing` module effectively unusable at runtime due to botched     implementation details) that is losslessly replaceable with a useful     :pep:`544`-compliant :mod:`beartype` **IO protocol** (i.e., either     :class:`beartype._util.hint.pep.proposal.utilpep544._Pep544IO` itself     *or* a subclass of that class defined by this submodule intentionally     designed to be usable at runtime).      This tester is intentionally *not* memoized (e.g., by the     :func:`callable_cached` decorator), as the implementation trivially reduces     to an efficient one-liner.      Parameters     ----------     hint : object         Object to be inspected.      Returns     ----------     bool         ``True`` only if this object is a :pep:`484`-compliant IO generic base         class.      See Also     ----------     :class:`beartype._util.hint.pep.proposal.utilpep544._Pep544IO`         Further commentary. ``True`` only if the passed object is a :pep:`544`-compliant **protocol**     (i.e., subclass of the :class:`typing.Protocol` superclass).      This tester is intentionally *not* memoized (e.g., by the     :func:`callable_cached` decorator), as the implementation trivially reduces     to an efficient one-liner.      Parameters     ----------     hint : object         Object to be inspected.      Returns     ----------     bool         ``True`` only if this object is a :pep:`544`-compliant protocol. # ....................{ REDUCERS                          }.................... :pep:`544`-compliant :mod:`beartype` **IO protocol** (i.e., either     :class:`beartype._util.hint.pep.proposal.utilpep544._Pep544IO`     itself *or* a subclass of that class defined by this submodule     intentionally designed to be usable at runtime) corresponding to the passed     :pep:`484`-compliant :mod:`typing` **IO generic base class** (i.e., either     :class:`typing.IO` itself *or* a subclass of :class:`typing.IO` defined by     the :mod:`typing` module effectively unusable at runtime due to botched     implementation details).      This reducer is intentionally *not* memoized (e.g., by the     :func:`callable_cached` decorator), as the implementation trivially reduces     to an efficient one-liner.      Parameters     ----------     hint : type         :pep:`484`-compliant :mod:`typing` IO generic base class to be replaced         by the corresponding :pep:`544`-compliant :mod:`beartype` IO protocol.     exception_prefix : str         Human-readable label prefixing the representation of this object in the         exception message.      Returns     ----------     Protocol         :pep:`544`-compliant :mod:`beartype` IO protocol corresponding to this         :pep:`484`-compliant :mod:`typing` IO generic base class.      Raises     ----------     BeartypeDecorHintPep544Exception         If this object is *not* a :pep:`484`-compliant IO generic base class. # If this object is *NOT* a PEP 484-compliant "typing" IO generic, # raise an exception. # Else, this object is *NOT* a PEP 484-compliant "typing" IO generic. # # If this dictionary has yet to be initialized, this submodule has yet to # be initialized. In this case, do so. # # Note that this initialization is intentionally deferred until required. # Why? Because this initialization performs somewhat space- and # time-intensive work -- including importation of the "beartype.vale" # subpackage, which we strictly prohibit importing from global scope. # In any case, this dictionary is now initialized. # PEP 544-compliant IO protocol implementing this PEP 484-compliant IO # generic if any *OR* "None" otherwise. # If *NO* such protocol implements this generic... # Avoid circular import dependencies. # Tuple of zero or more type variables parametrizing this hint. #FIXME: Unit test us up, please. # If this hint is unparametrized, raise an exception. # Else, this hint is parametrized and thus defines the "__origin__" # dunder attribute whose value is the type originating this hint. #FIXME: Attempt to actually handle this type variable, please. # Reduce this parametrized hint (e.g., "typing.IO[typing.AnyStr]") to # the equivalent unparametrized hint (e.g., "typing.IO"), effectively # ignoring the type variable parametrizing this hint. # type: ignore[assignment] # PEP 544-compliant IO protocol implementing this unparametrized PEP # 484-compliant IO generic. For efficiency, we additionally cache this # mapping under the original parametrized hint to minimize the cost of # similar reductions under subsequent annotations. # Return this protocol. # ....................{ INITIALIZERS                      }.................... Initialize this submodule. # ..................{ VERSIONS                          }.................. # If the active Python interpreter only targets Python < 3.8 and thus fails # to support PEP 544, silently reduce to a noop. # Else, the active Python interpreter targets Python >= 3.8 and thus # supports PEP 593. # ..................{ IMPORTS                           }.................. # Defer Python version-specific imports. # ..................{ GLOBALS                           }.................. # Global attributes to be redefined below. # ..................{ PROTOCOLS ~ protocol              }.................. # Note that these classes are intentionally *NOT* declared at global scope; # instead, these classes are declared *ONLY* if the active Python # interpreter targets Python >= 3.8. # PEP-compliant type hint matching file handles opened in either text or # binary mode. # The body of this class is copied wholesale from the existing # non-functional "typing.IO" class. # PEP-compliant type hint matching file handles opened in text rather than # binary mode. # # Note that PEP 544 explicitly requires *ALL* protocols (including # protocols subclassing protocols) to explicitly subclass the "Protocol" # superclass, in violation of both sanity and usability. (Thanks, guys.) # The body of this class is copied wholesale from the existing # non-functional "typing.TextIO" class. # ..................{ PROTOCOLS ~ validator             }.................. # PEP-compliant type hint matching file handles opened in binary rather # than text mode. # # If PEP 593 (e.g., "typing.Annotated") and thus beartype validators are # unusable, this hint falls back to ambiguously matching the abstract # "typing.IO" protocol ABC. This will yield false positives (i.e., fail to # raise exceptions) for @beartype-decorated callables annotated as # accepting binary file handles erroneously passed text file handles, which # is non-ideal but certainly preferable to raising exceptions at decoration # time on each such callable. # # If PEP 593 (e.g., "typing.Annotated") and thus beartype validators are # usable, this hint matches the abstract "typing.IO" protocol ABC but *NOT* # the concrete "typing.TextIO" subprotocol subclassing that ABC. Whereas # the concrete "typing.TextIO" subprotocol unambiguously matches *ONLY* # file handles opened in text mode, the concrete "typing.BinaryIO" # subprotocol ambiguously matches file handles opened in both text *AND* # binary mode. As the following hypothetical "_Pep544BinaryIO" subclass # demonstrates, the "typing.IO" and "typing.BinaryIO" APIs are identical # except for method annotations: #     class _Pep544BinaryIO(_Pep544IO[bytes], Protocol): #         # The body of this class is copied wholesale from the existing #         # non-functional "typing.BinaryIO" class. # #         __slots__: tuple = () # #         @abstractmethod #         def write(self, s: Union[bytes, bytearray]) -> int: #             pass # #         @abstractmethod #         def __enter__(self) -> '_Pep544BinaryIO': #             pass # # Sadly, the method annotations that differ between these APIs are # insufficient to disambiguate file handles at runtime. Why? Because most # file handles are C-based and thus lack *ANY* annotations whatsoever. With # respect to C-based file handles, these APIs are therefore identical. # Ergo, the "typing.BinaryIO" subprotocol is mostly useless at runtime. # # Note, however, that file handles are necessarily *ALWAYS* opened in # either text or binary mode. This strict dichotomy implies that any file # handle (i.e., object matching the "typing.IO" protocol) *NOT* opened in # text mode (i.e., not matching the "typing.TextIO" protocol) must # necessarily be opened in binary mode instead. #FIXME: Safely replace this with "from typing import Annotated" after #dropping Python 3.8 support. # "typing.Annotated" type hint factory safely imported from whichever of # the "typing" or "typing_extensions" modules declares this attribute if # one or more do *OR* "None" otherwise (i.e., if none do). # If this factory is importable. # Defer heavyweight imports. # Expand this hint to unambiguously match binary file handles by # subscripting this factory with a beartype validator doing so. # Else, this factory is unimportable. In this case, accept this hint's # default ambiguously matching both binary and text files. # ..................{ MAPPINGS                          }.................. # Dictionary mapping from each "typing" IO generic base class to the # associated IO protocol defined above. # # Note this global is intentionally modified in-place rather than # reassigned to a new dictionary. Why? Because the higher-level # reduce_hint_pep484_generic_io_to_pep544_protocol() function calling this # lower-level initializer has already imported this global. # Unsubscripted mappings. # Subscripted mappings, leveraging the useful observation that these # classes all self-cache by design: e.g., #     >>> import typing #     >>> typing.IO[str] is typing.IO[str] #     True # # Note that we intentionally map: # * "IO[Any]" to the unsubscripted "_Pep544IO" rather than the #   subscripted "_Pep544IO[Any]". Although the two are semantically #   equivalent, the latter is marginally more space- and time-efficient #   to generate code for and thus preferable. # * "IO[bytes]" to the unsubscripted "_Pep544Binary" rather than the #   subscripted "_Pep544IO[bytes]". Why? Because the former applies #   meaningful runtime constraints, whereas the latter does *NOT*. # * "IO[str]" to the unsubscripted "_Pep544Text" rather than the #   subscripted "_Pep544IO[str]" -- for the same reason. # # Note that we intentionally avoid mapping parametrizations of "IO" by # type variables. Since there exist a countably infinite number of # such parametrizations, the parent # reduce_hint_pep484_generic_io_to_pep544_protocol() function calling # this function handles such parametrizations mostly intelligently.