Skip to content

Commit

Permalink
Reiterables x 1.
Browse files Browse the repository at this point in the history
This commit is the first in a commit chain deeply type-checking
**reiterables** (i.e., collections satisfying the
`collections.abc.Collection` protocol with guaranteed `O(1)` read-only
access to *only* the first collection item), en-route to *finally*
resolving feature request #167 kindly submitted by the perennial
brilliant @langfield (...*how I miss that awesome guy!*) several
lifetimes ago back when I was probably a wandering vagabond Buddhist
monk with a bad attitude, a begging bowl the size of my emaciated torso,
and an honestly pretty cool straw hat that glinted dangerously in the
firelight. Note that reiterables include *all* containers matched by one
or more of the following PEP 484- or 585-compliant type hints:

* `frozenset[...]`.
* `set[...]`.
* `collections.deque[...]`.
* `collections.abc.Collection[...]`.
* `collections.abc.KeysView[...]`.
* `collections.abc.MutableSet[...]`.
* `collections.abc.Set[...]`.
* `collections.abc.ValuesView[...]`.
* `typing.AbstractSet[...]`.
* `typing.Collection[...]`.
* `typing.Deque[...]`.
* `typing.FrozenSet[...]`.
* `typing.KeysView[...]`.
* `typing.MutableSet[...]`.
* `typing.Set[...]`.
* `typing.ValuesView[...]`.

Specifically, this commit almost fully implements this deep
type-checking. Nothing's tested, so it's probably totally busted. But...
just wow. Should have done this two years ago, huh? This is why we can't
have good type-checking: @leycec gets distracted by shiny glittery stuff
way too easily. (*Lackadaisical lacky licking a licorice chalice!*)
  • Loading branch information
leycec committed May 18, 2024
1 parent ba3bb42 commit d1d6f55
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 15 deletions.
2 changes: 1 addition & 1 deletion beartype/_check/code/codemagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

# ....................{ IMPORTS }....................
from beartype._data.error.dataerrmagic import EXCEPTION_PLACEHOLDER
from itertools import count
# from itertools import count

# ....................{ EXCEPTION }....................
EXCEPTION_PREFIX_FUNC_WRAPPER_LOCAL = (
Expand Down
99 changes: 86 additions & 13 deletions beartype/_check/code/codemake.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
CODE_PEP484585_MAPPING_KEY_ONLY_PITH_CHILD_EXPR_format,
CODE_PEP484585_MAPPING_VALUE_ONLY_PITH_CHILD_EXPR_format,
CODE_PEP484585_MAPPING_KEY_VALUE_PITH_CHILD_EXPR_format,
CODE_PEP484585_REITERABLE_ARGS_1_format,
CODE_PEP484585_REITERABLE_ARGS_1_PITH_CHILD_EXPR_format,
CODE_PEP484585_SEQUENCE_ARGS_1_format,
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR_format,
CODE_PEP484585_SUBCLASS_format,
Expand Down Expand Up @@ -96,13 +98,13 @@
HintSignForwardRef,
HintSignGeneric,
HintSignLiteral,
# HintSignNone,
HintSignTuple,
HintSignType,
)
from beartype._data.hint.pep.sign.datapepsignset import (
HINT_SIGNS_MAPPING,
HINT_SIGNS_ORIGIN_ISINSTANCEABLE,
HINT_SIGNS_REITERABLE_ARGS_1,
HINT_SIGNS_SEQUENCE_ARGS_1,
HINT_SIGNS_SUPPORTED_DEEP,
HINT_SIGNS_UNION,
Expand Down Expand Up @@ -785,13 +787,15 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# "None"), this assignment initializes this local to the new
# set instantiated by this call; else, this assignment
# preserves this local set as is.
hint_curr_expr, hint_refs_type_basename = (
express_func_scope_type_ref(
forwardref=hint_curr,
forwardrefs_class_basename=hint_refs_type_basename,
func_scope=func_wrapper_scope,
exception_prefix=EXCEPTION_PREFIX,
))
(
hint_curr_expr,
hint_refs_type_basename,
) = express_func_scope_type_ref(
forwardref=hint_curr,
forwardrefs_class_basename=hint_refs_type_basename,
func_scope=func_wrapper_scope,
exception_prefix=EXCEPTION_PREFIX,
)

# Code type-checking the current pith against this class.
func_curr_code = CODE_PEP484_INSTANCE_format(
Expand Down Expand Up @@ -1378,7 +1382,8 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# ignorable arguments. In this case...
):
# Python expression evaluating to the origin type of this
# sequence hint.
# sequence hint as a hidden beartype-specific parameter
# injected into the signature of this wrapper function.
hint_curr_expr = add_func_scope_type(
# Origin type of this sequence hint.
cls=get_hint_pep_origin_type_isinstanceable(hint_curr),
Expand Down Expand Up @@ -1432,10 +1437,9 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
pith_curr_var_name=pith_curr_var_name,
hint_curr_expr=hint_curr_expr,
hint_child_placeholder=_enqueue_hint_child(
# Python expression yielding the value of a
# randomly indexed item of the current pith
# (i.e., standard sequence) to be
# type-checked against this child hint.
# Python expression yielding a randomly indexed
# item of this pith to be type-checked against
# this child hint.
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR_format(
pith_curr_var_name=pith_curr_var_name)),
)
Expand Down Expand Up @@ -1720,6 +1724,75 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
)
# Else, this hint is *NOT* a mapping.
#
# ..........{ REITERABLES }............
# If this hint is a single-argument reiterable (e.g.,
# "set[str]")...
elif hint_curr_sign in HINT_SIGNS_REITERABLE_ARGS_1:
# Python expression evaluating to the origin type of this
# reiterable hint as a hidden beartype-specific parameter
# injected into the signature of this wrapper function.
hint_curr_expr = add_func_scope_type(
# Origin type of this reiterable hint.
cls=get_hint_pep_origin_type_isinstanceable(hint_curr),
func_scope=func_wrapper_scope,
exception_prefix=EXCEPTION_PREFIX_FUNC_WRAPPER_LOCAL,
)

# Unignorable sane child hint sanified from this possibly
# ignorable insane child hint *OR* "None" otherwise (i.e.,
# if this child hint is ignorable).
hint_child = sanify_hint_child_if_unignorable_or_none(
# Possibly ignorable insane child hint subscripting this
# parent reiterable hint, validated to be the *ONLY*
# child hint subscripting this parent reiterable hint.
hint=get_hint_pep484585_args(
hint=hint_curr,
args_len=1,
exception_prefix=EXCEPTION_PREFIX,
),
conf=conf,
cls_stack=cls_stack,
exception_prefix=EXCEPTION_PREFIX,
)

# If this child hint is unignorable, deeply type-check both
# the type of the current pith *AND* the first item of this
# pith. Specifically...
if hint_child is not None:
# Code type-checking this pith against this type.
func_curr_code = CODE_PEP484585_REITERABLE_ARGS_1_format(
indent_curr=indent_curr,
pith_curr_assign_expr=pith_curr_assign_expr,
pith_curr_var_name=pith_curr_var_name,
hint_curr_expr=hint_curr_expr,
hint_child_placeholder=_enqueue_hint_child(
# Python expression yielding the first item of
# this pith to be type-checked against this
# child hint.
CODE_PEP484585_REITERABLE_ARGS_1_PITH_CHILD_EXPR_format(
pith_curr_var_name=pith_curr_var_name)),
)
#FIXME: We're repeating this same block *OVER* and *OVER*
#again, plainly violating DRY. The solution is probably to:
#* Far above, initialize "func_curr_code = None" at the
# start of each iteration.
#* Far below, simply define this fallback:
# if func_curr_code is None:
# func_curr_code = CODE_PEP484_INSTANCE_format(
# pith_curr_expr=pith_curr_expr,
# hint_curr_expr=hint_curr_expr,
# )

# Else, this child hint is ignorable. In this case, fallback
# to trivial code shallowly type-checking this pith as an
# instance of this origin type.
else:
func_curr_code = CODE_PEP484_INSTANCE_format(
pith_curr_expr=pith_curr_expr,
hint_curr_expr=hint_curr_expr,
)
# Else, this hint is *NOT* a single-argument reiterable.
#
# ............{ ANNOTATED }............
# If this hint is a PEP 593-compliant type metahint, this
# metahint is guaranteed by the reduction performed above to be
Expand Down
32 changes: 32 additions & 0 deletions beartype/_check/code/snip/codesnipstr.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,34 @@
``dict[object, str]``, ``dict[str, object]``).
'''

# ....................{ HINT ~ pep : (484|585) : reiterable }....................
CODE_PEP484585_REITERABLE_ARGS_1 = '''(
{indent_curr} # True only if this pith is of this reiterable type *AND*...
{indent_curr} isinstance({pith_curr_assign_expr}, {hint_curr_expr}) and
{indent_curr} # True only if either this reiterable is empty *OR* this reiterable
{indent_curr} # is both non-empty and the first item satisfies this hint.
{indent_curr} (not {pith_curr_var_name} or {hint_child_placeholder})
{indent_curr})'''
'''
:pep:`484`- and :pep:`585`-compliant code snippet type-checking the current pith
against a parent **standard reiterable type** (i.e., type hint subscripted by
exactly one child type hint constraining *all* items of this pith, which
necessarily satisfies the :class:`collections.abc.Collection` protocol with
guaranteed :math:`O(1)` read-only access to *only* the first collection item).
See the
:data:`beartype._data.hint.pep.sign.datapepsignset.HINT_SIGNS_REITERABLE_ARGS_1`
docstring for further commentary on reiterables.
'''


CODE_PEP484585_REITERABLE_ARGS_1_PITH_CHILD_EXPR = (
'''next(iter({pith_curr_var_name}))''')
'''
:pep:`484`- and :pep:`585`-compliant Python expression efficiently yielding the
first item of the current reiterable pith.
'''

# ....................{ HINT ~ pep : (484|585) : sequence }....................
CODE_PEP484585_SEQUENCE_ARGS_1 = '''(
{indent_curr} # True only if this pith is of this sequence type *AND*...
Expand Down Expand Up @@ -572,6 +600,10 @@
CODE_PEP484585_MAPPING_VALUE_ONLY_PITH_CHILD_EXPR.format)
CODE_PEP484585_MAPPING_KEY_VALUE_PITH_CHILD_EXPR_format: Callable = (
CODE_PEP484585_MAPPING_KEY_VALUE_PITH_CHILD_EXPR.format)
CODE_PEP484585_REITERABLE_ARGS_1_format: Callable = (
CODE_PEP484585_REITERABLE_ARGS_1.format)
CODE_PEP484585_REITERABLE_ARGS_1_PITH_CHILD_EXPR_format: Callable = (
CODE_PEP484585_REITERABLE_ARGS_1_PITH_CHILD_EXPR.format)
CODE_PEP484585_SEQUENCE_ARGS_1_format: Callable = (
CODE_PEP484585_SEQUENCE_ARGS_1.format)
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR_format: Callable = (
Expand Down
2 changes: 1 addition & 1 deletion beartype/_data/hint/pep/datapeprepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
HintSignParamSpec,
# HintSignParamSpecArgs,
HintSignPep557DataclassInitVar,
HintSignTypeAlias,
# HintSignTypeAlias,
HintSignPep695TypeAlias,
# HintSignProtocol,
HintSignReversible,
Expand Down
60 changes: 60 additions & 0 deletions beartype/_data/hint/pep/sign/datapepsignset.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,22 @@
'''


#FIXME: Consider also adding the following views here:
# HintSignItemsView,
# HintSignMappingView,
#
#That said, we're *NOT* quite sure how these views are actually structured. One
#or both might present data as 2-tuples of "(key, value)" pairs rather than the
#standard "dict" API, in which case we definitely should *NOT* add these views
#here. Instead, we'll need to unroll distinct type-checking logic for these
#views. Of course, nobody's actually requested this yet. In fact, nobody even
#appears to know or care about the existence of these data structures. They're
#probably the least popular and least well-known data structures in the Python
#pantheon. In other words, they're the ultimate high-hanging fruit. Do this only
#if somebody complains *OR* we're outrageously bored. So.... never.
#FIXME: Also add the "HintSignChainMap" sign here. Unlike the views listed
#above, the "collections.ChainMap" data structure almost certainly presents a
#similar API to that of "dict" and can thus be type-checked similarly. (Maybe.)
HINT_SIGNS_MAPPING = frozenset((
# ..................{ PEP (484|585) }..................
HintSignDefaultDict,
Expand All @@ -212,6 +228,50 @@
'''


HINT_SIGNS_REITERABLE_ARGS_1 = frozenset((
# ..................{ PEP (484|585) }..................
HintSignAbstractSet,
HintSignCollection,
HintSignFrozenSet,
HintSignKeysView,
HintSignMutableSet,
HintSignSet,
HintSignValuesView,

#FIXME: Deques are actually somewhat more than merely single-argument
#reiterables. They provide efficient access to both the first *AND* last
#deque items. Ergo, both should be type-checked. The current approach only
#type-checkes the first deque item. That's certainly better than nothing,
#but we can (and should) do better. *sigh*
HintSignDeque,
))
'''
Frozen set of all **standard reiterable signs** (i.e., arbitrary objects
uniquely identifying :pep:`484`- and :pep:`585`-compliant type hints subscripted
by exactly one child type hint constraining *all* items of compliant
collections, which necessarily satisfy the :class:`collections.abc.Collection`
protocol with guaranteed :math:`O(1)` read-only access to *only* the first
collection item).
For disambiguity, we prefer the :mod:`beartype`-specific term "reiterable" to
the standard term "collection" in this context. Why? Because numerous other data
structures (e.g., mappings, sequences) are also technically collections but
*not* matched by this frozen set. Why? Because this frozen set only matches the
proper subset of all collections *not* matched by any other such frozen set.
Equivalently, this frozen set only matches the proper subset of all collections
that are **reiterable** (i.e., that may be safely reiterated multiple times,
where "safely" implies side effect-free idempotency). Each call of the:
* :func:`iter` builtin passed the same reiterable effectively creates and
returns the same iterator.
* :func:`next` builtin passed the same reiterable deterministically returns the
same items in the same order.
Reiterable items are thus preserved (rather than modified) by reiteration.
'''


HINT_SIGNS_SEQUENCE_ARGS_1 = frozenset((
# ..................{ PEP (484|585) }..................
HintSignByteString,
Expand Down

0 comments on commit d1d6f55

Please sign in to comment.