Skip to content

Commit

Permalink
Reiterables x 8.
Browse files Browse the repository at this point in the history
This commit is the next 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 continues reducing problematic DRY (Don't
Repeat Yourself) violations in @beartype's internal type-checking code
generator. (*Etude of interminable terminus of vermin in verisimilitude!*)
  • Loading branch information
leycec committed Jun 18, 2024
1 parent 0704e95 commit 4ee4191
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 380 deletions.
254 changes: 65 additions & 189 deletions beartype/_check/code/codemake.py

Large diffs are not rendered by default.

104 changes: 1 addition & 103 deletions beartype/_check/code/snip/codesnipstr.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@
'''

# ....................{ IMPORTS }....................
from beartype.typing import Dict
from beartype._check.checkmagic import VAR_NAME_RANDOM_INT
from beartype._data.hint.datahinttyping import (
CallableStrFormat,
HintSignToCallableStrFormat,
)
from beartype._data.hint.pep.sign.datapepsigncls import HintSign
from beartype._data.hint.datahinttyping import CallableStrFormat

# ....................{ HINT ~ placeholder : child }....................
CODE_HINT_CHILD_PLACEHOLDER_PREFIX = '@['
Expand Down Expand Up @@ -246,94 +240,6 @@
``dict[object, str]``, ``dict[str, object]``).
'''

# ....................{ HINT ~ pep : (484|585) : reiterable }....................
#FIXME: Excise after enabling the
#"HINT_SIGN_PEP484585_CONTAINER_ARGS_1_TO_CODE_format" global, please.
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 Also
--------
:data:`beartype._data.hint.pep.sign.datapepsignset.HINT_SIGNS_REITERABLE_ARGS_1`
Further commentary on reiterables.
'''


#FIXME: Excise after enabling the
#"HINT_SIGN_PEP484585_CONTAINER_ARGS_1_TO_CODE_format" global, please.
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 }....................
#FIXME: Excise after enabling the
#"HINT_SIGN_PEP484585_CONTAINER_ARGS_1_TO_CODE_format" global, please.
CODE_PEP484585_SEQUENCE_ARGS_1 = '''(
{indent_curr} # True only if this pith is of this sequence type *AND*...
{indent_curr} isinstance({pith_curr_assign_expr}, {hint_curr_expr}) and
{indent_curr} # True only if either this sequence is empty *OR* this sequence
{indent_curr} # is both non-empty and a random 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 sequence 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.Sequence` protocol with
guaranteed :math:`O(1)` indexation across all sequence items).
Caveats
-------
**This snippet cannot contain ternary conditionals.** For unknown reasons
suggesting a critical defect in the current implementation of Python 3.8's
assignment expressions, this snippet raises :class:`UnboundLocalError`
exceptions resembling the following when this snippet contains one or more
ternary conditionals:
UnboundLocalError: local variable '__beartype_pith_1' referenced before assignment
In particular, the initial draft of this snippet guarded against empty
sequences with a seemingly reasonable ternary conditional:
.. code-block:: python
CODE_PEP484585_SEQUENCE_ARGS_1 = \'\'\'(
{indent_curr} isinstance({pith_curr_assign_expr}, {hint_curr_expr}) and
{indent_curr} {hint_child_placeholder} if {pith_curr_var_name} else True
{indent_curr})\'\'\'
That should behave as expected, but doesn't, presumably due to obscure scoping
rules and a non-intuitive implementation of ternary conditionals in CPython.
Ergo, the current version of this snippet guards against empty sequences with
disjunctions and conjunctions (i.e., ``or`` and ``and`` operators) instead.
Happily, the current version is more efficient than the equivalent approach
based on ternary conditional (albeit slightly less intuitive).
'''


#FIXME: Excise after enabling the
#"HINT_SIGN_PEP484585_CONTAINER_ARGS_1_TO_CODE_format" global, please.
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR = (
f'''{{pith_curr_var_name}}[{VAR_NAME_RANDOM_INT} % len({{pith_curr_var_name}})]''')
'''
:pep:`484`- and :pep:`585`-compliant Python expression yielding the value of a
randomly indexed item of the current sequence pith.
'''

# ....................{ HINT ~ pep : (484|585) : tuple }....................
CODE_PEP484585_TUPLE_FIXED_PREFIX = '''(
{indent_curr} # True only if this pith is a tuple.
Expand Down Expand Up @@ -613,14 +519,6 @@
CODE_PEP484585_MAPPING_VALUE_ONLY_PITH_CHILD_EXPR.format)
CODE_PEP484585_MAPPING_KEY_VALUE_PITH_CHILD_EXPR_format: CallableStrFormat = (
CODE_PEP484585_MAPPING_KEY_VALUE_PITH_CHILD_EXPR.format)
CODE_PEP484585_REITERABLE_ARGS_1_format: CallableStrFormat = (
CODE_PEP484585_REITERABLE_ARGS_1.format)
CODE_PEP484585_REITERABLE_ARGS_1_PITH_CHILD_EXPR_format: CallableStrFormat = (
CODE_PEP484585_REITERABLE_ARGS_1_PITH_CHILD_EXPR.format)
CODE_PEP484585_SEQUENCE_ARGS_1_format: CallableStrFormat = (
CODE_PEP484585_SEQUENCE_ARGS_1.format)
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR_format: CallableStrFormat = (
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR.format)
CODE_PEP484585_SUBCLASS_format: CallableStrFormat = (
CODE_PEP484585_SUBCLASS.format)
CODE_PEP484585_TUPLE_FIXED_EMPTY_format: CallableStrFormat = (
Expand Down
8 changes: 3 additions & 5 deletions beartype/_check/error/_pep/pep484585/errpep484585sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,8 @@ def find_cause_sequence_args_1(cause: ViolationCause) -> ViolationCause:
# Assert this hint was subscripted by the expected number of child type
# hints. Note that prior logic should have already guaranteed this.
assert len(cause.hint_childs) in hints_child_len_expected, (
f'Sequence hint {repr(cause.hint)} subscripted by '
f'{len(cause.hint_childs)} != {hints_child_len_expected} '
f'child type hints.'
f'Sequence type hint {repr(cause.hint)} number of child type hints '
f'{len(cause.hint_childs)} not in {hints_child_len_expected}.'
)

# First child hint subscripting this parent sequence hint. All remaining
Expand Down Expand Up @@ -137,8 +136,7 @@ def find_cause_sequence_args_1(cause: ViolationCause) -> ViolationCause:
# Deep output cause describing the failure of this item to satisfy this
# child hint if this item violates this child hint *OR* "None" otherwise
# (i.e., if this item satisfies this child hint).
cause_deep = cause.permute(
pith=pith_item, hint=hint_child).find_cause()
cause_deep = cause.permute(pith=pith_item, hint=hint_child).find_cause()

# If this item is the cause of this failure...
if cause_deep.cause_str_or_none is not None:
Expand Down
43 changes: 43 additions & 0 deletions beartype/_check/error/errget.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,44 @@
'''

# ....................{ TODO }....................
#FIXME: [DESIGN] The external
#"beartype._check.checkmagic.ARG_NAME_GET_VIOLATION" hidden parameter is
#inefficient in both time and space. Now that we unconditionally pass
#"ARG_NAME_CHECK_META" to *ALL* wrapper functions, we should instead:
#* Rename "ARG_NAME_CHECK_META" to "ARG_NAME_ERROR_META".
#* Define a new "beartype._check.error.errmeta" submodule.
#* Rename the existing "beartype._check.metadata.metacheck.BeartypeCheckMeta"
# dataclass into a new "beartype._check.error.errmeta.BeartypeErrorMeta"
# full-blown class.
#* Remove the "beartype._check.metadata.metacheck" submodule.
#* Refactor *ALL* public methods defined below into methods of that class --
# which then simply accesses the instance variables of this class rather than
# having those instance variables be externally passed. To do so sanely, we
# might consider:
#* Refactor the get_func_pith_violation() function defined below into a new
# get_func_pith_violation() method of "BeartypeErrorMeta" with signature
# resembling:
# class BeartypeErrorMeta(object):
# ...
# def get_func_pith_violation(
# self,
#
# # Mandatory parameters.
# pith_name: str,
# pith_value: object,
#
# # Optional keyword parameters.
# **kwargs
# ) -> Exception:
#* Remove the "beartype._check.checkmagic.ARG_NAME_GET_VIOLATION" global.
#* Refactor all snippets referencing "ARG_NAME_GET_VIOLATION" to instead
# reference "ARG_NAME_ERROR_META".
#
#Oh... wait. That last point is the sticking point, huh? Refactoring those
#snippets isn't quite so easy, sadly. We reference "ARG_NAME_GET_VIOLATION" for
#a reason. Doing so allows us to trivially switch between various competing
#getter functions defined by this submodule. Oh, well. We shrug! *shrug*

#FIXME: [ACCESS] Generalizing the "random_int" concept (i.e., the optional
#"random_int" parameter accepted by the get_func_pith_violation() function) that
#enables O(1) exception handling to containers that do *NOT* provide efficient
Expand Down Expand Up @@ -63,6 +101,11 @@
# over all container items in O(n) brute-force time. Obviously, extreme care
# must be taken here to ensure that this exception handling algorithm visits
# containers in the exact same order as visited by our testing algorithm.
#FIXME: *UHH.* I honestly have *NO* idea what any of the above is on about. It's
#likely we overthought the above commentary to extreme overkill. Notably,
#@beartype does (in fact) now deeply type-check both maps and sets. Works great,
#actually. No need for any of the above insanity, either. Let's re-read this
#and, if bollocks, excise all of the above. Overkill, thy name is that "FIXME:".

#FIXME: [COLOR] The call to the strip_text_ansi() function below is inefficient
#and thus non-ideal. Since efficiency isn't a pressing concern in an exception
Expand Down
Loading

0 comments on commit 4ee4191

Please sign in to comment.