Skip to content

Commit

Permalink
Parametrized generic detection x 2.
Browse files Browse the repository at this point in the history
This commit is the next 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 partially implements the probable fix
documented by the prior commit. (*An outlier liaison's outlandish lies!*)
  • Loading branch information
leycec committed Feb 28, 2021
1 parent 6381caa commit de8f6f2
Show file tree
Hide file tree
Showing 12 changed files with 427 additions and 244 deletions.
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_type_origin
from beartype._util.hint.pep.utilhintpepget import get_hint_pep_origin_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_type_origin(sleuth.hint)
hint_type_origin = get_hint_pep_origin_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_type_origin_or_none)
get_hint_pep_origin_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_type_origin_or_none(sleuth.hint)
hint_type_origin = get_hint_pep_origin_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_type_origin_or_none)
get_hint_pep_origin_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_type_origin_or_none(
hint_child_type_origin = get_hint_pep_origin_type_or_none(
hint_child)

# If...
Expand Down
8 changes: 4 additions & 4 deletions beartype/_decor/_code/_pep/_pephint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1716,7 +1716,7 @@
get_hint_pep_args,
get_hint_pep_generic_bases_unerased,
get_hint_pep_sign,
get_hint_pep_type_origin,
get_hint_pep_origin_type,
)
from beartype._util.hint.pep.utilhintpeptest import (
die_if_hint_pep_unsupported,
Expand Down Expand Up @@ -2929,7 +2929,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:

# For each pseudo-superclass subclassed by this generic...
for hint_child in hint_childs:
# print(f'hint_child: {repr(hint_child)} {is_hint_pep_class_typing(hint_child)}')
# print(f'hint_child: {repr(hint_child)} {is_hint_pep_type_typing(hint_child)}')

# If this pseudo-superclass is an actual class, this class
# is effectively ignorable. Why? Because the
Expand Down Expand Up @@ -3146,7 +3146,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_type_origin(hint_curr)),
get_hint_pep_origin_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 @@ -3183,7 +3183,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_type_origin(hint_curr))
get_hint_pep_origin_type(hint_curr))

# Assert this sequence is either subscripted by exactly one
# argument *OR* a non-standard sequence (e.g., "typing.Tuple").
Expand Down
2 changes: 1 addition & 1 deletion beartype/_util/cache/utilcachecall.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def callable_cached(func: Callable) -> Callable:
this package are sufficiently small in size to render bounding irrelevant.
Consider the
:func:`beartype._util.hint.pep.utilhintpeptest.is_hint_pep_class_typing`
:func:`beartype._util.hint.pep.utilhintpeptest.is_hint_pep_type_typing`
function, for example. Each call to that function only accepts a single
class and returns a boolean. Under conservative assumptions of 4 bytes of
storage per class reference and 4 byte of storage per boolean reference,
Expand Down
36 changes: 29 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_type_origin_or_none() function here, which has been
# get_hint_pep_origin_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 @@ -206,6 +206,19 @@ class implicitly replacing all string arguments subscripting :mod:`typing`
if IS_PYTHON_AT_LEAST_3_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_subscripted_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
# subscripted by one or more PEP-compliant child type hints). To
# decide, reduce this hint to the object originating this hint if any,
# 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_subscripted_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
# generic.
Expand Down Expand Up @@ -239,12 +252,14 @@ def is_hint_pep484_generic(hint: object) -> bool:
# passed objects that *CANNOT* be guaranteed to be PEP-compliant.
# Indeed, the high-level is_hint_pep() tester establishing the
# PEP-compliance of arbitrary objects internally calls this lower-level
# tester to do so. Since the get_hint_pep484_generic_bases_unerased_or_none() getter
# internally reduces to returning the tuple of the general-purpose
# tester to do so. Since the
# get_hint_pep484_generic_bases_unerased_or_none() getter internally
# reduces to returning the tuple of the general-purpose
# "__orig_bases__" dunder attribute formalized by PEP 560, testing
# whether that tuple is non-empty or not in no way guarantees this
# object to be a PEP-compliant generic.
return is_object_subclass(hint, Generic) # type: ignore[arg-type]

# Else, the active Python interpreter targets Python 3.6. In this case,
# implement this function specific to this Python version.
else:
Expand All @@ -254,8 +269,14 @@ def is_hint_pep484_generic(hint: object) -> bool:
def is_hint_pep484_generic(hint: object) -> bool:

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_subscripted_or_none)
from beartype._util.hint.pep.utilhintpeptest import (
is_hint_pep_class_typing)
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_subscripted_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 All @@ -282,7 +303,7 @@ def is_hint_pep484_generic(hint: object) -> bool:
# disingenuous "typing" pseudo-generics.
return (
isinstance(hint, GenericMeta) and
not is_hint_pep_class_typing(hint)
not is_hint_pep_type_typing(hint)
)


Expand Down Expand Up @@ -674,7 +695,7 @@ def get_hint_pep484_generic_bases_unerased(hint: Any) -> tuple:
# hint_base = hint_orig_mro[hint_orig_mro_index_curr]
#
# # If this superclass is a typing attribute...
# if is_hint_pep_class_typing(hint_base):
# if is_hint_pep_type_typing(hint_base):
# # Avoid inserting this attribute into the "hint_orig_mro" list.
# # Most typing attributes are *NOT* actual classes and those that
# # are have no meaningful public superclass. Ergo, iteration
Expand Down Expand Up @@ -745,7 +766,8 @@ def get_hint_pep484_generic_bases_unerased(hint: Any) -> tuple:
if not is_hint_pep484_generic(hint):
raise BeartypeDecorHintPep484Exception(
f'PEP type hint "{repr(hint)}" neither '
f'PEP 484 generic nor PEP 544 protocol.')
f'PEP 484 generic nor PEP 544 protocol.'
)
# Else, this hint is a PEP 484-compliant generic.

# Unerased pseudo-superclasses of this generic if any *OR* "None"
Expand Down
11 changes: 11 additions & 0 deletions beartype/_util/hint/pep/proposal/utilhintpep585.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ def is_hint_pep585(hint: object) -> bool:
@callable_cached
def is_hint_pep585_generic(hint: object) -> bool:

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_origin_type_subscripted_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.
if not isinstance(hint, type):
hint = get_hint_pep_origin_type_subscripted_or_none(hint)

# Tuple of all pseudo-superclasses originally subclassed by the passed
# hint if this hint is a generic *OR* false otherwise.
hint_bases_erased = getattr(hint, '__orig_bases__', False)
Expand All @@ -70,6 +80,7 @@ def is_hint_pep585_generic(hint: object) -> bool:
# erasure, the former are *NOT*. The only means of deterministically
# deciding whether or not any given class is a PEP 585-compliant
# generic is as follows:
#
# * That class defines both the __class_getitem__() dunder method *AND*
# the "__orig_bases__" instance variable. Note that this condition in
# and of itself is insufficient to decide PEP 585-compliance as a
Expand Down

0 comments on commit de8f6f2

Please sign in to comment.