def get_hint_pep_type_origin_or_none(hint: object) -> 'Optional[type]': # Sign uniquely identifying this hint. hint_sign = get_hint_pep_sign(hint) # If this sign originates from an origin type... if hint_sign in HINT_PEP_SIGNS_TYPE_ORIGIN: # Return either... return ( # If this hint is PEP 585-compliant type hint, this sign. By # definition, the sign uniquely identifying *EVERY* PEP # 585-compliant type hint is the origin type originating that # hint (e.g., "list" for "list[str]"). hint_sign if is_hint_pep585(hint) else # Else, this hint is *NOT* a PEP 585-compliant type hint. By # elimination, this hint *MUST* be a PEP 484-compliant type # hint. In this case, return the origin type originating this # hint (e.g., "list" for "typing.List[str]"). hint.__origin__ ) # Else, this sign does *NOT* originate from an origin type. In this # case, return "None". return None
def test_is_hint_pep585() -> None: ''' Test `PEP 585`_ support implemented in the :func:`beartype.beartype` decorator. ''' # Defer heavyweight imports. from beartype._util.hint.pep.proposal.utilhintpep585 import is_hint_pep585 from beartype_test.unit.data.hint.pep.data_hintpep import HINTS_PEP_META # Assert this tester accepts only PEP 585-compliant type hints. for hint_pep_meta in HINTS_PEP_META: assert is_hint_pep585(hint_pep_meta.hint) is (hint_pep_meta.is_pep585)
def get_hint_pep_sign(hint: object) -> dict: ''' **Sign** (i.e., arbitrary object) uniquely identifying the passed PEP-compliant type hint if this hint is PEP-compliant *or* raise an exception otherwise (i.e., if this hint is *not* PEP-compliant). This getter function associates the passed hint with a public attribute of the :mod:`typing` module effectively acting as a superclass of this hint and thus uniquely identifying the "type" of this hint in the broadest sense of the term "type". These attributes are typically *not* actual types, as most actual :mod:`typing` types are private, fragile, and prone to extreme violation (or even removal) between major Python versions. Nonetheless, these attributes are sufficiently unique to enable callers to distinguish between numerous broad categories of :mod:`typing` behaviour and logic. Specifically, this function returns either: * If this hint is a `PEP 585`_-compliant **builtin** (e.g., C-based type hint instantiated by subscripting either a concrete builtin container class like :class:`list` or :class:`tuple` *or* an abstract base class (ABC) declared by the :mod:`collections.abc` submodule like :class:`collections.abc.Iterable` or :class:`collections.abc.Sequence`), :class:`beartype.cave.HintPep585Type`. * If this hint is a **generic** (i.e., subclasses of the :class:`typing.Generic` abstract base class (ABC)), :class:`typing.Generic`. Note this includes `PEP 544`-compliant **protocols** (i.e., subclasses of the :class:`typing.Protocol` ABC), which implicitly subclass the :class:`typing.Generic` ABC as well. * If this hint is any other class declared by either the :mod:`typing` module (e.g., :class:`typing.TypeVar`) *or* the :mod:`beartype.cave` submodule (e.g., :class:`beartype.cave.HintPep585Type`), that class. * If this hint is a **forward reference** (i.e., string or instance of the concrete :class:`typing.ForwardRef` class), :class:`typing.ForwardRef`. * If this hint is a **type variable** (i.e., instance of the concrete :class:`typing.TypeVar` class), :class:`typing.TypeVar`. * Else, the unsubscripted :mod:`typing` attribute dynamically retrieved by inspecting this hint's **object representation** (i.e., the non-human-readable string returned by the :func:`repr` builtin). This getter function is memoized for efficiency. Motivation ---------- Both `PEP 484`_ and the :mod:`typing` module implementing `PEP 484`_ are functionally deficient with respect to their public APIs. Neither provide external callers any means of deciding the categories of arbitrary PEP-compliant type hints. For example, there exists no general-purpose means of identifying a parametrized subtype (e.g., ``typing.List[int]``) as a parametrization of its unparameterized base type (e.g., ``type.List``). Thus this function, which "fills in the gaps" by implementing this oversight. Parameters ---------- hint : object Object to be inspected. Returns ---------- dict Sign uniquely identifying this hint. Raises ---------- AttributeError If this object's representation is prefixed by the substring ``"typing."``, but the remainder of that representation up to but *not* including the first "[" character in that representation (e.g., ``"Dict"`` for the object ``typing.Dict[str, Tuple[int, ...]]``) is *not* an attribute of the :mod:`typing` module. BeartypeDecorHintPepException If this object is *not* a PEP-compliant type hint. BeartypeDecorHintPepSignException If this object is a PEP-compliant type hint *not* uniquely identifiable by a sign. Examples ---------- >>> import typing >>> from beartype._util.hint.pep.utilhintpepget import ( ... get_hint_pep_sign) >>> get_hint_pep_sign(typing.Any) typing.Any >>> get_hint_pep_sign(typing.Union[str, typing.Sequence[int]]) typing.Union >>> T = typing.TypeVar('T') >>> get_hint_pep_sign(T) typing.TypeVar >>> class Genericity(typing.Generic[T]): pass >>> get_hint_pep_sign(Genericity) typing.Generic >>> class Duplicity(typing.Iterable[T], typing.Container[T]): pass >>> get_hint_pep_sign(Duplicity) typing.Iterable .. _PEP 484: https://www.python.org/dev/peps/pep-0484 .. _PEP 585: https://www.python.org/dev/peps/pep-0585 ''' # Avoid circular import dependencies. from beartype._util.hint.utilhinttest import is_hint_forwardref from beartype._util.hint.pep.utilhintpeptest import ( die_unless_hint_pep, is_hint_pep_generic, is_hint_pep_typevar, ) # If this hint is *NOT* PEP-compliant, raise an exception. # # Note that we intentionally avoid calling the # die_if_hint_pep_unsupported() function here, which calls the # is_hint_pep_supported() function, which calls this function. die_unless_hint_pep(hint) # Else, this hint is PEP-compliant. # If this hint is a PEP-compliant generic (i.e., class # superficially subclassing at least one non-class PEP-compliant object), # return the "typing.Generic" abstract base class (ABC) generically -- get # it? -- identifying PEP-compliant generics. Note that: # * *ALL* PEP 484-compliant generics and PEP 544-compliant protocols are # guaranteed by the "typing" module to subclass this ABC regardless of # whether those generics originally did so explicitly. How? By type # erasure, the gift that keeps on giving: # >>> import typing as t # >>> class MuhList(t.List): pass # >>> MuhList.__orig_bases__ # (typing.List) # >>> MuhList.__mro__ # (__main__.MuhList, list, typing.Generic, object) # * *NO* PEP 585-compliant generics subclass this ABC unless those generics # are also either PEP 484- or 544-compliant. Indeed, PEP 585-compliant # generics subclass *NO* common superclass. # # Ergo, this ABC uniquely identifies many but *NOT* all generics. Although # non-ideal, the failure of PEP 585-compliant generics to subclass a common # superclass leaves us with little alternative. # # Note that generics *CANNOT* be detected by the general-purpose logic # performed below, as this ABC does *NOT* define a __repr__() dunder method # returning a string prefixed by the "typing." substring. if is_hint_pep_generic(hint): return Generic # Else, this hint is *NOT* a generic. # # If this hint is a PEP 585-compliant type hint, return the origin type # originating this hint (e.g., "list" for "list[str]"). # # Note that the get_hint_pep_type_origin() getter is intentionally *NOT* # called here. Why? Because doing so would induce an infinite recursive # loop, since that function internally calls this function. *sigh* elif is_hint_pep585(hint): return hint.__origin__ # Else, this hint is *NOT* a PEP 585-compliant type hint. # # If this hint is... elif ( # A class... isinstance(hint, type) and # *NOT* subscripted by one or more child hints or type variables... not (get_hint_pep_args(hint) or get_hint_pep_typevars(hint)) # Then this class is a standard class. In this case... # # Note that this is principally useful under Python 3.6, which # idiosyncratically defines subscriptions of "typing" objects as classes: # >>> import typing # >>> isinstance(typing.List[int], type) # True # <-- this is balls cray-cray, too. # # While we *COULD* technically fence this conditional behind a Python 3.6 # check, doing so would render this getter less robust against unwarranted # stdlib changes. Ergo, we preserve this as is for forward-proofing. ): # If this class is *NOT* explicitly allowed as a sign, raise an # exception. if hint not in HINT_PEP_SIGNS_TYPE: raise BeartypeDecorHintPepSignException( f'Unsubscripted non-generic class {repr(hint)} invalid as ' f'sign (i.e., not in {repr(HINT_PEP_SIGNS_TYPE)}).' ) # Else, this class is explicitly allowed as a sign. # Return this class as is. return hint # Else, this hint is either not a class *OR* or class subscripted by one or # more child hints or type variables and is thus *NOT* a standard class. In # either case, continue (i.e., attempt to handle this class as a PEP # 484-compliant type hint defined by the "typing" module). # # If this hint is a forward reference, return the class of all PEP # 484-compliant (but *NOT* PEP 585-compliant) forward references. # # Note that PEP 484-compliant forward references *CANNOT* be detected by # the general-purpose logic performed below, as the ForwardRef.__repr__() # dunder method returns a standard representation rather than a string # prefixed by the module name "typing." -- unlike most "typing" objects: # >>> import typing as t # >>> repr(t.ForwardRef('str')) # "ForwardRef('str')" elif is_hint_forwardref(hint): return HINT_PEP484_BASE_FORWARDREF # If this hint is a PEP 484-compliant new type, return the closure factory # function responsible for creating these types. # # Note that these types *CANNOT* be detected by the general-purpose logic # performed below, as the __repr__() dunder methods of the closures created # and returned by the NewType() closure factory function returns a # standard representation rather than a string prefixed by the module name # "typing." -- unlike most "typing" objects: # >>> import typing as t # >>> repr(t.NewType('FakeStr', str)) # '<function NewType.<locals>.new_type at 0x7fca39388050>' elif is_hint_pep484_newtype(hint): return NewType # If this hint is a type variable, return the class of all type variables. # # Note that type variables *CANNOT* be detected by the general-purpose # logic performed below, as the TypeVar.__repr__() dunder method insanely # returns a string prefixed by the non-human-readable character "~" rather # than the module name "typing." -- unlike most "typing" objects: # >>> import typing as t # >>> repr(t.TypeVar('T')) # ~T # # Of course, that brazenly violates Pythonic standards. __repr__() is # generally assumed to return an evaluatable Python expression that, when # evaluated, instantiates an object equal to the original object. Instead, # this API was designed by incorrigible monkeys who profoundly hate the # Python language. This is why we can't have sane things. elif is_hint_pep_typevar(hint): return TypeVar #FIXME: Drop this like hot lead after dropping Python 3.6 support. # If the active Python interpreter targets Python 3.6 *AND* this hint is a # poorly designed Python 3.6-specific "type alias", this hint is a # subscription of either the "typing.Match" or "typing.Pattern" objects. In # this case, this hint declares a non-standard "name" instance variable # whose value is either the literal string "Match" or "Pattern". Return the # "typing" attribute with this name *OR* implicitly raise an # "AttributeError" exception if something goes horribly awry. # # Gods... this is horrible. Thanks for nuthin', Python 3.6. elif IS_PYTHON_3_6 and isinstance(hint, typing._TypeAlias): return getattr(typing, hint.name) # Else, this hint *MUST* be a standard PEP 484-compliant type hint defined # by the "typing" module. # Machine-readable string representation of this hint also serving as the # fully-qualified name of the public "typing" attribute uniquely associated # with this hint (e.g., "typing.Tuple[str, ...]"). # # Although the "typing" module provides *NO* sane public API, it does # reliably implement the __repr__() dunder method across most objects and # types to return a string prefixed "typing." regardless of Python version. # Ergo, this string is effectively the *ONLY* sane means of deciding which # broad category of behaviour an arbitrary PEP 484-compliant type hint # conforms to. sign_name = repr(hint) # If this representation is *NOT* prefixed by "typing,", this hint does # *NOT* originate from the "typing" module and is thus *NOT* PEP-compliant. # But by the validation above, this hint is PEP-compliant. Since this # invokes a world-shattering paradox, raise an exception if not sign_name.startswith('typing.'): raise BeartypeDecorHintPepSignException( f'PEP 484 type hint {repr(hint)} ' f'representation "{sign_name}" not prefixed by "typing.".' ) # Strip the now-harmful "typing." prefix from this representation. # Preserving this prefix would raise an "AttributeError" exception from # the subsequent call to the getattr() builtin. sign_name = sign_name[7:] # hardcode us up the bomb # 0-based index of the first "[" delimiter in this representation if # any *OR* -1 otherwise. sign_name_bracket_index = sign_name.find('[') # If this representation contains such a delimiter, this is a subscripted # type hint. In this case, reduce this representation to its unsubscripted # form by truncating the suffixing parametrization from this representation # (e.g., from "typing.Union[str, typing.Sequence[int]]" to merely # "typing.Union"). # # Note that this is the common case and thus explicitly tested first. if sign_name_bracket_index > 0: sign_name = sign_name[:sign_name_bracket_index] # Else, this representation contains no such delimiter and is thus already # unsubscripted. In this case, preserve this representation as is. # If this name erroneously refers to a non-existing "typing" attribute, # rewrite this name to refer to the actual existing "typing" attribute # corresponding to this sign (e.g., from the non-existing # "typing.AbstractContextManager" attribute to the existing # "typing.ContextManager" attribute). sign_name = _HINT_PEP_TYPING_NAME_BAD_TO_GOOD.get(sign_name, sign_name) # "typing" attribute with this name if any *OR* "None" otherwise. sign = getattr(typing, sign_name, None) # If this "typing" attribute does *NOT* exist... if sign is None: raise BeartypeDecorHintPepSignException( f'PEP 484 type hint {repr(hint)} ' f'attribute "typing.{sign_name}" not found.' ) # Else, this "typing" attribute exists. # Return this "typing" attribute. return sign