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

'''
Project-wide **PEP-noncompliant Pandera type hint reducers** (i.e., low-level
callables converting higher-level type hints defined by the third-party
:mod:`pandera` package that do *not* comply with any specific PEP but are
nonetheless shallowly supported by :mod:`beartype` to lower-level type hints
more readily consumable by :mod:`beartype`).

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

# ....................{ IMPORTS                            }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# CAUTION: The top-level of this module should avoid importing from third-party
# optional libraries, both because those libraries cannot be guaranteed to be
# either installed or importable here *AND* because those imports are likely to
# be computationally expensive, particularly for imports transitively importing
# C extensions (e.g., anything from NumPy or SciPy).
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
from beartype.roar import BeartypeDecorHintNonpepPanderaException
from beartype._data.typing.datatypingport import Hint
from beartype._util.cache.utilcachecall import callable_cached

# ....................{ REDUCERS                           }....................
@callable_cached
def reduce_hint_pandera(hint: Hint, exception_prefix: str) -> type:
    '''
    Reduce the passed **PEP-noncompliant Pandera type hint** (i.e.,
    subscription of *any* PEP-noncompliant type hint factory published by the
    third-party :mod:`pandera.typing` type hint factory) to the isinstanceable
    Pandas type subclassed by this hint, which :mod:`beartype` then subsequently
    subjects to shallow type-checking.

    This reducer enables :mod:`beartype` to at least shallowly type-check
    Pandera type hints while allowing Pandera itself to deeply type-check the
    same hints. Pandera publishes its own Pandera-specific PEP-noncompliant
    runtime type-checking decorator :func:`pandera.check_types` that supports
    *only* Pandera-specific PEP-noncompliant :mod:`pandera.typing` type hints.
    Since Pandera users are already accustomed to decorating *all* Pandera-based
    callables (i.e., callables accepting one or more parameters and/or returning
    one or more values annotated by Pandera type hints) with
    :func:`pandera.check_types`, attempting to deeply type-check the same
    objects already type-checked by that decorator would only inefficiently and
    needlessly slow type-checking wrappers generated by the
    :func:`beartype.beartype` decorator. Moreover, doing so is infeasible.
    Pandera type hints are extremely non-standard and thus *not* reasonably
    type-checkable by any standards-compliant static or runtime type-checkers.
    Shallowly type-checking Pandera type hints is still beneficial, however, as:

    * Pandera itself currently fails to shallowly type-check its own type hints.
      That is to say, if a caller passes a string rather than a Pandas data
      frame to a :func:`pandera.check_types`-decorated function, that function
      will silently accept that string rather than raise an exception. *sigh*
    * Functions annotated by one or more Pandera type hints that are (either
      intentionally or accidentally) *not* decorated by
      :func:`pandera.check_types` will still receive at least a modicum of
      shallow type-checking support from :mod:`beartype` itself.

    This reducer is memoized for efficiency.

    Motivation
    ----------
    The core issue with Pandera type hints is somewhat more subtle than the glib
    hand-waving performed above. Yes, Pandera type hints *are* PEP-noncompliant,
    but they're more than just that. Pandera type hints fundamentally contravene
    established semantics for PEP-compliant generics. Generally speaking,
    generics are *not* simply descriptive type hints; they're full-blown classes
    intended to be instantiated as objects throughout the codebase using those
    generics as type hints. The unsubscripted portion of a generic hint is an
    instanceable class (e.g., the "list" in "list[str]" is itself an
    instanceable class). @beartype expects any object annotated by a generic
    type hint to be an instance of that generic: e.g.,

    .. code-block:: pycon

       # A PEP 585-compliant generic.
       >>> class ListOfStrings(list[str]): pass

       # An instance of this generic satisfies this generic used as a type hint.
       >>> from beartype import beartype
       >>> @beartype
       ... def accept_list_of_strings(lst: ListOfStrings): return 'Okie-dokie!'
       >>> accept_list_of_strings(ListOfStrings())
       'Okie-dokie!'

    Pandera type hints violate this expectation. Syntactically, Pandera type
    hints are PEP-compliant generics (of course); semantically, Pandera type
    hints are PEP-noncompliant generics, because the objects they describe
    (i.e., Pandas data frames) are *not* instances of these generics. Pandas
    data frames are instances of the Pandera-agnostic
    "pandas.core.frame.DataFrame" non-generic class rather than Pandera-specific
    "pandera.typing.pandas.DataFrame" generic.

    Pandera type hints *should* have been instead defined as PEP 544-compliant
    protocols that exploit ephemeral duck typing. Since they weren't, downstream
    consumers like @beartype must now pretend that Pandera type hints are the
    Pandas types they semantically alias by reducing the former to the latter.

    Caveats
    -------
    **This reducer does not validate the callable annotated by this Pandera type
    hint to be decorated by the** :func:`pandera.check_types` **decorator.**
    Ideally, this reducer would do so to prevent :mod:`beartype` from emitting
    false positives and negatives from calls to callables for which the user
    accidentally omitted the :func:`pandera.check_types` decorator.
    Unfortunately, order of decoration is arbitrary. :mod:`beartype` has no
    means of distinguishing between these two cases:

    * The valid case in which the user decorated this callable first by
      :func:`beartype.beartype` and then by :func:`pandera.check_types`. In this
      case, :func:`beartype.beartype` runs first and has no efficient means of
      deciding that the :func:`pandera.check_types` will be run immediately
      after -- short of abstract syntax tree (AST) inspection, which would be
      extraordinarily inefficient, non-portable, and fragile.
    * The invalid case in which the user accidentally omitted the
      :func:`pandera.check_types` decorator.

    **This reducer does not validate that this Pandera type hint annotates a
    callable.** Technically, Pandera type hints are invalid in *all* type
    hinting contexts except as callable annotations -- including:

    * As a class type hint.
    * As an attribute assignment type hint.
    * As the type hint passed to a statement-level runtime type-checker (e.g.,
      :func:`beartype.door.is_bearable`).

    Pragmatically, refactoring :mod:`beartype` to inform reducers of whether or
    not the current hint (that may be a nested child type hint of a parent type
    hint) annotates a callable or not would be extraordinarily non-trivial.
    Doing so would require refactoring our low-level:

    * Code generators to accept an additional parameter describing this case.
    * Type hint reducers to transitively pass that same parameter here.

    Since Pandera type hints are already PEP-noncompliant, the only sane
    approach is to continue unconditionally ignoring them. Let us not break
    :mod:`beartype` for the PEP-noncompliant.

    Parameters
    ----------
    hint : Hint
        PEP-noncompliant typed NumPy array to return the data type of.
    exception_prefix : str
        Human-readable substring prefixing raised exception messages.

    Returns
    -------
    type
        Isinstanceable Pandas type subclassed by this Pandera type hint.

    Raises
    ------
    BeartypeDecorHintNonpepPanderaException
        If either:

        * This hint is *not* a PEP 484- or 585-compliant generic.
        * This hint is a PEP 484- or 585-compliant generic *not* subclassing one
          or more Pandas-specific superclasses.
    '''

    # Avoid circular import dependencies.
    from beartype._util.hint.pep.proposal.pep484585.generic.pep484585genget import (
        get_hint_pep484585_generic_base_in_module_first)

    # Find and return the first dataframe-like type subclassed by this
    # Pandera-specific generic type hint.
    return get_hint_pep484585_generic_base_in_module_first(
        hint=hint,  # pyright: ignore
        module_names=_MODULE_NAMES_DATAFRAME,
        exception_cls=BeartypeDecorHintNonpepPanderaException,
        exception_prefix=exception_prefix,
    )

# ....................{ PRIVATE ~ constants                }....................
_MODULE_NAMES_DATAFRAME = frozenset(('pandas', 'polars',))
'''
Fully-qualified names of all third-party packages defining dataframe-like types
supported by Pandera.
'''
