Skip to content

Commit

Permalink
Deep dict type-checking x 3.
Browse files Browse the repository at this point in the history
This commit is the epochal next commit in a commit chain deeply
type-checking the first key-value pair of each **mapping type hint**
(e.g., of the form `dict[..., ...]`, `collections.defaultdict[...,
...]`, `collections.abc.Mapping[..., ...]`,
`collections.abc.MutableMapping[..., ...]`,
`collections.abc.OrderedDict[..., ...]`, `typing.Dict[..., ...]`,
`typing.Mapping[..., ...]`, `typing.MutableMapping[..., ...]`, or
`typing.OrderedDict[..., ...]`) in `O(1)` time, en-route to partially
resolving long-standing feature requests #167 kindly submitted sometime
during my most recent past life by ardent typing fiend @langfield *and*
#2021 kindly submitted sometime during the past life immediately
preceding my most recent past life by Equinor ASA bear bro @jondequinor
(Jonas Grønås). Specifically, this commit does literally everything
everyone always wanted. This is the mega-commit, the Big One™, the
moment where @beartype actually does something. @beartype now deeply
type-checks the first key-value pair of each mapping type hint in `O(1)`
time. Superficially, this has been tested to work. Pragmatically,
significantly more testing is warranted. (*Massive missives!*)
  • Loading branch information
leycec committed Mar 12, 2024
1 parent 174fc45 commit 43b415c
Show file tree
Hide file tree
Showing 17 changed files with 539 additions and 254 deletions.
402 changes: 222 additions & 180 deletions beartype/_check/code/codemake.py

Large diffs are not rendered by default.

54 changes: 51 additions & 3 deletions beartype/_check/code/snip/codesnipstr.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from beartype._check.checkmagic import (
VAR_NAME_RANDOM_INT,
)
from collections.abc import Callable

# ....................{ HINT ~ placeholder : child }....................
CODE_HINT_CHILD_PLACEHOLDER_PREFIX = '@['
Expand Down Expand Up @@ -141,7 +142,6 @@
'''

# ....................{ HINT ~ pep : (484|585) : mapping }....................
#FIXME: Refactor us up, please.
CODE_PEP484585_MAPPING = '''(
{indent_curr} # True only if this pith is of this mapping type *AND*...
{indent_curr} isinstance({pith_curr_assign_expr}, {hint_curr_expr}) and
Expand All @@ -150,7 +150,7 @@
{indent_curr} # This mapping is empty *OR*...
{indent_curr} not {pith_curr_var_name} or
{indent_curr} # This mapping is non-empty. In this case...
{indent_curr} ({hint_key_and_or_value})
{indent_curr} ({func_curr_code_key_value})
{indent_curr} )
{indent_curr})'''
'''
Expand Down Expand Up @@ -208,7 +208,7 @@
'''


CODE_PEP484585_MAPPING_KEY_AND_VALUE = CODE_PEP484585_MAPPING_KEY_ONLY + ''' and
CODE_PEP484585_MAPPING_KEY_VALUE = CODE_PEP484585_MAPPING_KEY_ONLY + ''' and
{indent_curr} # Localize the first value of this mapping.
{indent_curr} ({pith_curr_value_var_name} := {pith_curr_var_name}[
{indent_curr} {pith_curr_key_var_name}]) is {pith_curr_value_var_name} and
Expand Down Expand Up @@ -519,3 +519,51 @@
:class:`typing.Annotated` type. While there exist alternate and more readable
means of accomplishing this, this approach is the optimally efficient.
'''

# ..................{ FORMATTERS }..................
# str.format() methods, globalized to avoid inefficient dot lookups elsewhere.
# This is an absurd micro-optimization. *fight me, github developer community*
CODE_PEP484_INSTANCE_format: Callable = (
CODE_PEP484_INSTANCE.format)
CODE_PEP484585_GENERIC_CHILD_format: Callable = (
CODE_PEP484585_GENERIC_CHILD.format)
CODE_PEP484585_MAPPING_format: Callable = (
CODE_PEP484585_MAPPING.format)
CODE_PEP484585_MAPPING_KEY_ONLY_format: Callable = (
CODE_PEP484585_MAPPING_KEY_ONLY.format)
CODE_PEP484585_MAPPING_KEY_VALUE_format: Callable = (
CODE_PEP484585_MAPPING_KEY_VALUE.format)
CODE_PEP484585_MAPPING_VALUE_ONLY_format: Callable = (
CODE_PEP484585_MAPPING_VALUE_ONLY.format)
CODE_PEP484585_SEQUENCE_ARGS_1_format: Callable = (
CODE_PEP484585_SEQUENCE_ARGS_1.format)
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR_format: Callable = (
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR.format)
CODE_PEP484585_SUBCLASS_format: Callable = (
CODE_PEP484585_SUBCLASS.format)
CODE_PEP484585_TUPLE_FIXED_EMPTY_format: Callable = (
CODE_PEP484585_TUPLE_FIXED_EMPTY.format)
CODE_PEP484585_TUPLE_FIXED_LEN_format: Callable = (
CODE_PEP484585_TUPLE_FIXED_LEN.format)
CODE_PEP484585_TUPLE_FIXED_NONEMPTY_CHILD_format: Callable = (
CODE_PEP484585_TUPLE_FIXED_NONEMPTY_CHILD.format)
CODE_PEP484585_TUPLE_FIXED_NONEMPTY_PITH_CHILD_EXPR_format: Callable = (
CODE_PEP484585_TUPLE_FIXED_NONEMPTY_PITH_CHILD_EXPR.format)
CODE_PEP484604_UNION_CHILD_PEP_format: Callable = (
CODE_PEP484604_UNION_CHILD_PEP.format)
CODE_PEP484604_UNION_CHILD_NONPEP_format: Callable = (
CODE_PEP484604_UNION_CHILD_NONPEP.format)
CODE_PEP572_PITH_ASSIGN_AND_format: Callable = (
CODE_PEP572_PITH_ASSIGN_AND.format)
CODE_PEP572_PITH_ASSIGN_EXPR_format: Callable = (
CODE_PEP572_PITH_ASSIGN_EXPR.format)
CODE_PEP586_LITERAL_format: Callable = (
CODE_PEP586_LITERAL.format)
CODE_PEP586_PREFIX_format: Callable = (
CODE_PEP586_PREFIX.format)
CODE_PEP593_VALIDATOR_IS_format: Callable = (
CODE_PEP593_VALIDATOR_IS.format)
CODE_PEP593_VALIDATOR_METAHINT_format: Callable = (
CODE_PEP593_VALIDATOR_METAHINT.format)
CODE_PEP593_VALIDATOR_SUFFIX_format: Callable = (
CODE_PEP593_VALIDATOR_SUFFIX.format)
26 changes: 14 additions & 12 deletions beartype/_check/error/_errorcause.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ def hint(self, hint: Any) -> None:
hint = sanify_hint_child(
hint=hint,
conf=self.conf,
exception_prefix=self.exception_prefix,
cls_stack=self.cls_stack,
pith_name=self.pith_name,
exception_prefix=self.exception_prefix,
)

# If this hint is PEP-compliant...
Expand Down Expand Up @@ -457,17 +457,19 @@ def permute(self, **kwargs) -> 'ViolationCause':
Examples
--------
>>> sleuth = ViolationCause(
... pith=[42,]
... hint=typing.List[int],
... cause_indent='',
... exception_prefix='List of integers',
... )
>>> sleuth_copy = sleuth.permute(pith=[24,])
>>> sleuth_copy.pith
[24,]
>>> sleuth_copy.hint
typing.List[int]
.. code-block:: pycon
>>> sleuth = ViolationCause(
... pith=[42,]
... hint=typing.List[int],
... cause_indent='',
... exception_prefix='List of integers',
... )
>>> sleuth_copy = sleuth.permute(pith=[24,])
>>> sleuth_copy.pith
[24,]
>>> sleuth_copy.hint
typing.List[int]
'''

# For the name of each passed keyword argument...
Expand Down
35 changes: 21 additions & 14 deletions beartype/_check/error/_errordata.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@
Dict,
)
from beartype._data.hint.pep.sign.datapepsigncls import HintSign
from beartype._data.hint.pep.sign.datapepsignset import (
HINT_SIGNS_SEQUENCE_ARGS_1,
HINT_SIGNS_ORIGIN_ISINSTANCEABLE,
HINT_SIGNS_UNION,
)
from beartype._check.error._errorcause import ViolationCause

# ....................{ GLOBALS }....................
Expand Down Expand Up @@ -51,25 +46,33 @@ def _init() -> None:
HintSignTuple,
HintSignType,
)
from beartype._data.hint.pep.sign.datapepsignset import (
HINT_SIGNS_MAPPING,
HINT_SIGNS_ORIGIN_ISINSTANCEABLE,
HINT_SIGNS_SEQUENCE_ARGS_1,
HINT_SIGNS_UNION,
)
from beartype._check.error._errortype import (
find_cause_instance_type_forwardref,
find_cause_subclass_type,
find_cause_type_instance_origin,
)
from beartype._check.error._pep._pep484._errornoreturn import (
find_cause_noreturn)
from beartype._check.error._pep._errorpep484604union import (
from beartype._check.error._pep.errorpep484604union import (
find_cause_union)
from beartype._check.error._pep._pep484585._errorgeneric import (
from beartype._check.error._pep.errorpep586 import (
find_cause_literal)
from beartype._check.error._pep.errorpep593 import (
find_cause_annotated)
from beartype._check.error._pep.pep484.errornoreturn import (
find_cause_noreturn)
from beartype._check.error._pep.pep484585.errorgeneric import (
find_cause_generic)
from beartype._check.error._pep._pep484585._errorsequence import (
from beartype._check.error._pep.pep484585.errormapping import (
find_cause_mapping)
from beartype._check.error._pep.pep484585.errorsequence import (
find_cause_sequence_args_1,
find_cause_tuple,
)
from beartype._check.error._pep._errorpep586 import (
find_cause_literal)
from beartype._check.error._pep._errorpep593 import (
find_cause_annotated)

# Map each originative sign to the appropriate getter *BEFORE* any other
# mappings. This is merely a generalized fallback subsequently replaced by
Expand All @@ -78,6 +81,10 @@ def _init() -> None:
HINT_SIGN_TO_GET_CAUSE_FUNC[pep_sign_origin_isinstanceable] = (
find_cause_type_instance_origin)

# Map each mapping sign to its corresponding getter.
for pep_sign_mapping in HINT_SIGNS_MAPPING:
HINT_SIGN_TO_GET_CAUSE_FUNC[pep_sign_mapping] = find_cause_mapping

# Map each 1-argument sequence sign to its corresponding getter.
for pep_sign_sequence_args_1 in HINT_SIGNS_SEQUENCE_ARGS_1:
HINT_SIGN_TO_GET_CAUSE_FUNC[pep_sign_sequence_args_1] = (
Expand Down
File renamed without changes.
File renamed without changes.
158 changes: 158 additions & 0 deletions beartype/_check/error/_pep/pep484585/errormapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
Beartype :pep:`484`- and :pep:`585`-compliant **mapping type hint violation
describers** (i.e., functions returning human-readable strings explaining
violations of :pep:`484`- and :pep:`585`-compliant mapping type hints).
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
from beartype import BeartypeStrategy
from beartype.typing import (
Iterable,
Tuple,
Hashable,
)
from beartype._data.hint.pep.sign.datapepsignset import (
HINT_SIGNS_MAPPING)
from beartype._check.error._errorcause import ViolationCause
from beartype._check.error._errortype import (
find_cause_type_instance_origin)
from beartype._check.error._util.errorutiltext import (
prefix_pith_type,
represent_pith,
)
from beartype._util.hint.utilhinttest import is_hint_ignorable

# ....................{ FINDERS }....................
def find_cause_mapping(cause: ViolationCause) -> ViolationCause:
'''
Output cause describing whether the pith of the passed input cause either
satisfies or violates the **mapping type hint** (i.e., PEP-compliant type
hint accepting exactly two subscripted arguments constraining *all*
key-value pairs of this pith, which necessarily satisfies the
:class:`collections.abc.Mapping` protocol) of that cause.
Parameters
----------
cause : ViolationCause
Input cause providing this data.
Returns
-------
ViolationCause
Output cause type-checking this data.
'''
assert isinstance(cause, ViolationCause), f'{repr(cause)} not cause.'
assert cause.hint_sign in HINT_SIGNS_MAPPING, (
f'{repr(cause.hint)} not mapping hint.')

# Assert this mapping was subscripted by exactly two arguments. Note that
# the "typing" module should have already guaranteed this on our behalf.
assert len(cause.hint_childs) == 2, (
f'Mapping hint {repr(cause.hint)} subscripted by '
f'{len(cause.hint_childs)} != 2.')
# print(f'Validating mapping {repr(cause.pith)}...')

# Shallow output cause to be returned, type-checking only whether this path
# is an instance of the type originating this hint (e.g., "list" for
# "list[str]").
cause_shallow = find_cause_type_instance_origin(cause)

# If this pith is *NOT* an instance of this type, return this shallow cause.
if cause_shallow.cause_str_or_none is not None:
return cause_shallow
# Else, this pith is an instance of this type and is thus a mapping.
#
# If this mapping is non-empty...
elif cause.pith:
# Child key and value hints subscripting this mapping hint.
hint_key = cause.hint_childs[0]
hint_value = cause.hint_childs[1]

# True only if these hints are unignorable.
hint_key_unignorable = not is_hint_ignorable(hint_key)
hint_value_unignorable = not is_hint_ignorable(hint_value)

# Arbitrary iterator vaguely satisfying the dict.items() protocol,
# yielding zero or more 2-tuples of the form "(key, value)", where:
# * "key" is the key of the current key-value pair.
# * "value" is the value of the current key-value pair.
pith_items: Iterable[Tuple[Hashable, object]] = None # type: ignore[assignment]

# If the only the first key-value pair of this mapping was
# type-checked by the the parent @beartype-generated wrapper
# function in O(1) time, type-check only this key-value pair of this
# mapping in O(1) time as well.
if cause.conf.strategy is BeartypeStrategy.O1:
# First key-value pair of this mapping.
pith_item = next(iter(cause.pith.items()))

# Tuple containing only this pair.
pith_items = (pith_item,)
# print(f'Checking item {pith_item_index} in O(1) time!')
# Else, all keys of this mapping were type-checked by the parent
# @beartype-generated wrapper function in O(n) time. In this case,
# type-check *ALL* indices of this mapping in O(n) time as well.
else:
# Iterator yielding all key-value pairs of this mapping.
pith_items = cause.pith.items()
# print('Checking mapping in O(n) time!')

# For each key-value pair of this mapping...
for pith_key, pith_value in pith_items:
# If this child key hint is unignorable...
if hint_key_unignorable:
# Deep output cause, type-checking whether this key satisfies
# this child key hint.
cause_deep = cause.permute(
pith=pith_key, hint=hint_key).find_cause()

# If this key is the cause of this failure...
if cause_deep.cause_str_or_none is not None:
# Human-readable substring prefixing this failure with
# metadata describing this key.
cause_deep.cause_str_or_none = (
f'{prefix_pith_type(cause.pith)}'
f'key {cause_deep.cause_str_or_none}'
)

# Return this cause.
return cause_deep
# Else, this key is *NOT* the cause of this failure. Silently
# continue to this value.
# Else, this child key hint is ignorable.

# If this child value hint is unignorable...
if hint_value_unignorable:
# Deep output cause, type-checking whether this value satisfies
# this child value hint.
cause_deep = cause.permute(
pith=pith_value, hint=hint_value).find_cause()

# If this value is the cause of this failure...
if cause_deep.cause_str_or_none is not None:
# Human-readable substring prefixing this failure with
# metadata describing this value.
cause_deep.cause_str_or_none = (
f'{prefix_pith_type(cause.pith)}'
f'key {represent_pith(pith_key)} '
f'value {cause_deep.cause_str_or_none}'
)

# Return this cause.
return cause_deep
# Else, this value is *NOT* the cause of this failure. Silently
# continue to the key-value pair.
# Else, this child value hint is ignorable.
# Else, this mapping is empty, in which case all items of this mapping (of
# which there are none) are valid. Just go with it, people.

# Return this cause as is; all items of this mapping are valid, implying
# this mapping to deeply satisfy this hint.
return cause

0 comments on commit 43b415c

Please sign in to comment.