Skip to content

Commit

Permalink
Parametrized generic detection x 4.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain detecting parametrized
generics (i.e., user-defined generics subscripted by one or more type
variables) en-route to resolving issue #29, kindly submitted by
indefatigable test engineer and anthropomorphic Siberian Husky @eehusky.
Specifically, this commit successfully resolves this issue for both
Python 3.8 or 3.9 as well, thus concluding one of the most difficult
issue resolutions of my all-too-sadly-middle-aged life of living in a
cabin with two lovely cats that are fat. (*Necromantic allomancy!*)
  • Loading branch information
leycec committed Mar 3, 2021
1 parent 5a03fbc commit d15cba5
Show file tree
Hide file tree
Showing 21 changed files with 343 additions and 284 deletions.
4 changes: 2 additions & 2 deletions beartype/_decor/_code/_pep/_error/_peperrorgeneric.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
get_hint_pep484_generic_base_erased_from_unerased)
from beartype._util.hint.pep.proposal.utilhintpep585 import is_hint_pep585_builtin
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_generic_or_none)
get_hint_pep_generic_type_or_none)
from beartype._util.hint.pep.utilhintpeptest import is_hint_pep_typing
from typing import Generic, Optional

Expand Down Expand Up @@ -49,7 +49,7 @@ def get_cause_or_none_generic(sleuth: CauseSleuth) -> Optional[str]:

# If this hint is *NOT* a class, reduce this hint to the object originating
# this hint if any. See the is_hint_pep484_generic() tester for details.
sleuth.hint = get_hint_pep_origin_type_generic_or_none(sleuth.hint)
sleuth.hint = get_hint_pep_generic_type_or_none(sleuth.hint)
assert isinstance(sleuth.hint, type), f'{repr(sleuth.hint)} not class.'

# If this pith is *NOT* an instance of this generic, defer to the getter
Expand Down
4 changes: 2 additions & 2 deletions beartype/_decor/_code/_pep/_error/_peperrorsequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
HINT_PEP_SIGNS_SEQUENCE_STANDARD,
HINT_PEP_SIGNS_TUPLE,
)
from beartype._util.hint.pep.utilhintpepget import get_hint_pep_origin_type_stdlib
from beartype._util.hint.pep.utilhintpepget import get_hint_pep_stdlib_type
from beartype._util.hint.pep.utilhintpeptest import is_hint_pep_tuple_empty
from beartype._util.hint.utilhinttest import is_hint_ignorable
from beartype._util.text.utiltextrepr import get_object_representation
Expand Down Expand Up @@ -56,7 +56,7 @@ def get_cause_or_none_sequence_standard(sleuth: CauseSleuth) -> Optional[str]:
f'multiple arguments.')

# Non-"typing" class originating this attribute (e.g., "list" for "List").
hint_type_origin = get_hint_pep_origin_type_stdlib(sleuth.hint)
hint_type_origin = get_hint_pep_stdlib_type(sleuth.hint)

# If this pith is *NOT* an instance of this class, defer to the getter
# function handling non-"typing" classes.
Expand Down
4 changes: 2 additions & 2 deletions beartype/_decor/_code/_pep/_error/_peperrortype.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from beartype._util.hint.utilhintget import (
get_hint_forwardref_classname_relative_to_obj)
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_stdlib_or_none)
get_hint_pep_stdlib_type_or_none)
from beartype._util.py.utilpymodule import import_module_attr
from beartype._util.text.utiltextcause import get_cause_object_not_type
from typing import Optional
Expand Down Expand Up @@ -111,7 +111,7 @@ def get_cause_or_none_type_origin(sleuth: CauseSleuth) -> Optional[str]:
assert isinstance(sleuth, CauseSleuth), f'{repr(sleuth)} not cause sleuth.'

# Origin type originating this hint if any *OR* "None" otherwise.
hint_type_origin = get_hint_pep_origin_type_stdlib_or_none(sleuth.hint)
hint_type_origin = get_hint_pep_stdlib_type_or_none(sleuth.hint)

# If this hint does *NOT* originate from such a type, raise an exception.
if hint_type_origin is None:
Expand Down
4 changes: 2 additions & 2 deletions beartype/_decor/_code/_pep/_error/_peperrorunion.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from beartype._util.hint.data.pep.proposal.utilhintdatapep484 import (
HINT_PEP484_SIGNS_UNION)
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_stdlib_or_none)
get_hint_pep_stdlib_type_or_none)
from beartype._util.hint.pep.utilhintpeptest import is_hint_pep
from beartype._util.text.utiltextjoin import join_delimited_disjunction_classes
from beartype._util.text.utiltextmunge import (
Expand Down Expand Up @@ -72,7 +72,7 @@ def get_cause_or_none_union(sleuth: CauseSleuth) -> Optional[str]:
if is_hint_pep(hint_child):
# Non-"typing" class originating this child hint if any *OR* "None"
# otherwise.
hint_child_type_origin = get_hint_pep_origin_type_stdlib_or_none(
hint_child_type_origin = get_hint_pep_stdlib_type_or_none(
hint_child)

# If...
Expand Down
12 changes: 6 additions & 6 deletions beartype/_decor/_code/_pep/_pephint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1759,8 +1759,8 @@
get_hint_pep_args,
get_hint_pep_generic_bases_unerased,
get_hint_pep_sign,
get_hint_pep_origin_type_stdlib,
get_hint_pep_origin_type_generic_or_none,
get_hint_pep_stdlib_type,
get_hint_pep_generic_type_or_none,
)
from beartype._util.hint.pep.utilhintpeptest import (
die_if_hint_pep_unsupported,
Expand All @@ -1773,7 +1773,7 @@
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from beartype._util.text.utiltextmunge import replace_str_substrs
from itertools import count
from typing import Set, Generic, Tuple, NoReturn
from typing import Generic, Tuple, NoReturn

# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']
Expand Down Expand Up @@ -2977,7 +2977,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# whether this origin object is an unsubscripted generic, which
# would then imply this hint to be a subscripted generic. If
# this strikes you as insane, you're not alone.
hint_curr = get_hint_pep_origin_type_generic_or_none(hint_curr)
hint_curr = get_hint_pep_generic_type_or_none(hint_curr)

# Assert this hint to be a class.
assert isinstance(hint_curr, type), (
Expand Down Expand Up @@ -3212,7 +3212,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# Origin type of this hint if any *OR* raise an
# exception -- which should *NEVER* happen, as this
# hint was validated above to be supported.
get_hint_pep_origin_type_stdlib(hint_curr)),
get_hint_pep_stdlib_type(hint_curr)),
)
# Else, this hint is *NOT* its own unsubscripted "typing" attribute
# (e.g., "typing.List") and is thus subscripted by one or more
Expand Down Expand Up @@ -3249,7 +3249,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# Origin type of this attribute if any *OR* raise an
# exception -- which should *NEVER* happen, as all standard
# sequences originate from an origin type.
get_hint_pep_origin_type_stdlib(hint_curr))
get_hint_pep_stdlib_type(hint_curr))

# Assert this sequence is either subscripted by exactly one
# argument *OR* a non-standard sequence (e.g., "typing.Tuple").
Expand Down
14 changes: 7 additions & 7 deletions beartype/_util/hint/pep/proposal/utilhintpep484.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def noop(param_hint_ignorable: Generic[T]) -> T: pass
# "typing.Generic[T]"), return true.
#
# Note that we intentionally avoid calling the
# get_hint_pep_origin_type_stdlib_or_none() function here, which has been
# get_hint_pep_stdlib_type_or_none() function here, which has been
# intentionally designed to exclude PEP-compliant type hints
# originating from "typing" type origins for stability reasons.
if getattr(hint, '__origin__', None) is Generic:
Expand Down Expand Up @@ -208,7 +208,7 @@ def is_hint_pep484_generic(hint: object) -> bool:

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_generic_or_none)
get_hint_pep_generic_type_or_none)

# If this hint is *NOT* a class, this hint is *NOT* an unsubscripted
# generic but could still be a subscripted generic (i.e., generic
Expand All @@ -217,7 +217,7 @@ def is_hint_pep484_generic(hint: object) -> bool:
# enabling the subsequent test to test whether this origin object is an
# unsubscripted generic, which would then imply this hint to be a
# subscripted generic. If this strikes you as insane, you're not alone.
hint = get_hint_pep_origin_type_generic_or_none(hint)
hint = get_hint_pep_generic_type_or_none(hint)

# Return true only if this hint is a subclass of the "typing.Generic"
# abstract base class (ABC), in which case this hint is a user-defined
Expand Down Expand Up @@ -270,13 +270,13 @@ def is_hint_pep484_generic(hint: object) -> bool:

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_generic_or_none)
get_hint_pep_generic_type_or_none)
from beartype._util.hint.pep.utilhintpeptest import (
is_hint_pep_type_typing)

# If this hint is *NOT* a class, reduce this hint to the object
# originating this hint if any. See the above tester for details.
hint = get_hint_pep_origin_type_generic_or_none(hint)
hint = get_hint_pep_generic_type_or_none(hint)

# Return true only if this hint is a subclass *NOT* defined by the
# "typing" module whose class is the "typing.GenericMeta" metaclass, in
Expand Down Expand Up @@ -764,11 +764,11 @@ def get_hint_pep484_generic_bases_unerased(hint: Any) -> tuple:

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_generic_or_none)
get_hint_pep_generic_type_or_none)

# If this hint is *NOT* a class, reduce this hint to the object originating
# this hint if any. See is_hint_pep484_generic() tester for details.
hint = get_hint_pep_origin_type_generic_or_none(hint)
hint = get_hint_pep_generic_type_or_none(hint)

# If this hint is *NOT* a PEP 484-compliant generic, raise an exception.
if not is_hint_pep484_generic(hint):
Expand Down
14 changes: 5 additions & 9 deletions beartype/_util/hint/pep/proposal/utilhintpep544.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,12 @@ def is_hint_pep544_ignorable_or_none(
# 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. Testing whether this sign is that
# superclass suffices to test this condition, as the
# get_hint_pep_sign() function returns that superclass as a sign
# *ONLY* when that superclass is directly parametrized by one or more
# type variables. In *ALL* other cases involving that superclass,
# that function *ALWAYS* returns the standard "typing.Generic"
# superclass for instead -- as generics and protocols are otherwise
# indistinguishable with respect to runtime type-checking.
# "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 hint_sign is Protocol or None
return repr(hint).startswith('typing.Protocol[') or None


def is_hint_pep544_io_generic(hint: object) -> bool:
Expand Down
8 changes: 4 additions & 4 deletions beartype/_util/hint/pep/proposal/utilhintpep585.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ def is_hint_pep585_generic(hint: object) -> bool:

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_generic_or_none)
get_hint_pep_generic_type_or_none)

# If this hint is *NOT* a class, reduce this hint to the object
# originating this hint if any. See the comparable
# is_hint_pep484_generic() tester for further details.
hint = get_hint_pep_origin_type_generic_or_none(hint)
hint = get_hint_pep_generic_type_or_none(hint)

# Tuple of all pseudo-superclasses originally subclassed by the passed
# hint if this hint is a generic *OR* false otherwise.
Expand Down Expand Up @@ -231,11 +231,11 @@ def get_hint_pep585_generic_bases_unerased(hint: Any) -> tuple:

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_generic_or_none)
get_hint_pep_generic_type_or_none)

# If this hint is *NOT* a class, reduce this hint to the object originating
# this hint if any. See the is_hint_pep484_generic() tester for details.
hint = get_hint_pep_origin_type_generic_or_none(hint)
hint = get_hint_pep_generic_type_or_none(hint)

# If this hint is *NOT* a PEP 585-compliant generic, raise an exception.
die_unless_hint_pep585_generic(hint)
Expand Down
55 changes: 28 additions & 27 deletions beartype/_util/hint/pep/utilhintpepget.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
# 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_origin_type_stdlib() getter is intentionally *NOT*
# Note that the get_hint_pep_stdlib_type() 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_builtin(hint):
Expand Down Expand Up @@ -609,32 +609,33 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
# Return this "typing" attribute.
return sign

# ....................{ GETTERS ~ origin : generic }....................
# ....................{ GETTERS ~ type : generic }....................
@callable_cached
def get_hint_pep_origin_type_generic_or_none(hint: Any) -> Optional[type]:
def get_hint_pep_generic_type_or_none(hint: Any) -> Optional[type]:
'''
Origin type originating the passed **generic** (i.e., class superficially
subclassing at least one PEP-compliant type hint that is possibly *not* an
actual class) if any *or* ``None`` otherwise (i.e., if this hint is *not* a
generic).
Either the passed **generic** (i.e., class superficially subclassing at
least one PEP-compliant type hint that is possibly *not* an actual class)
if **unsubscripted** (i.e., indexed by *no* arguments or type variables),
the unsubscripted generic underlying this generic if **subscripted** (i.e.,
indexed by one or more arguments and/or type variables), *or* ``None``
otherwise (i.e., if this hint is *not* a generic).
Specifically, this getter returns:
* If this hint is already a class, this hint as is.
* Else, this hint is *not* a class. In this case:
* If this hint both originates from an **origin type** (i.e.,
non-:mod:`typing` class such that *all* objects satisfying this hint
are instances of that class) *and* is subscripted by one or more
arguments and/or type variables, that origin type.
* Else, ``None`` (i.e., if this hint either does not originate from an
origin type *or* does but is unsubscripted).
This getter is mostly intended to simplify usage of user-defined
subscripted PEP-compliant type hint generics.
* If this hint both originates from an **origin type** (i.e.,
non-:mod:`typing` class such that *all* objects satisfying this hint are
instances of that class), that origin type.
* Else if this hint is already a class, this hint as is.
* Else, ``None``.
This getter is memoized for efficiency.
Caveats
----------
**This getter returns false positives in edge cases.** That is, this getter
returns non-``None`` values for both generics and non-generics. Callers
*must* perform subsequent tests to distinguish these two cases.
Parameters
----------
hint : object
Expand Down Expand Up @@ -666,13 +667,13 @@ def get_hint_pep_origin_type_generic_or_none(hint: Any) -> Optional[type]:
# its origin type. In this case, return this class as is.
elif isinstance(hint, type):
return hint
# Else, this hint is *NOT* a class. In this case, this hint originates
# from *NO* origin type.
# Else, this hint is *NOT* a class. In this case, this hint originates from
# *NO* origin type.
else:
return None

# ....................{ GETTERS ~ origin : stdlib }....................
def get_hint_pep_origin_type_stdlib(hint: object) -> type:
# ....................{ GETTERS ~ type : stdlib }....................
def get_hint_pep_stdlib_type(hint: object) -> type:
'''
**Standard library origin type** (i.e., non-:mod:`typing` class such that
*all* objects satisfying the passed PEP-compliant type hint are instances
Expand Down Expand Up @@ -701,11 +702,11 @@ def get_hint_pep_origin_type_stdlib(hint: object) -> type:
See Also
----------
:func:`get_hint_pep_origin_type_stdlib_or_none`
:func:`get_hint_pep_stdlib_type_or_none`
'''

# Origin type originating this object if any *OR* "None" otherwise.
hint_type_origin = get_hint_pep_origin_type_stdlib_or_none(hint)
hint_type_origin = get_hint_pep_stdlib_type_or_none(hint)

# If this type does *NOT* exist, raise an exception.
if hint_type_origin is None:
Expand All @@ -719,7 +720,7 @@ def get_hint_pep_origin_type_stdlib(hint: object) -> type:
return hint_type_origin


def get_hint_pep_origin_type_stdlib_or_none(hint: Any) -> Optional[type]:
def get_hint_pep_stdlib_type_or_none(hint: Any) -> Optional[type]:
'''
**Standard library origin type** (i.e., non-:mod:`typing` class defined by
Python's standard library such that *all* objects satisfying the passed
Expand Down Expand Up @@ -1042,7 +1043,7 @@ class such that *all* objects satisfying this hint are instances of this
Caveats
----------
**The high-level** :func:`get_hint_pep_origin_type_stdlib_or_none` function should
**The high-level** :func:`get_hint_pep_stdlib_type_or_none` function should
always be called in lieu of this low-level function.** Whereas the former
is guaranteed to return either a class or ``None``, this function enjoys no
such guarantees and instead returns what the caller can only safely assume
Expand Down

0 comments on commit d15cba5

Please sign in to comment.