Skip to content

Commit

Permalink
Deep dict type-checking x 2.
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 *all* **mapping type hints** (e.g., of the form `dict[...,
...]`, `collections.abc.Mapping[..., ...]`,
`collections.abc.MutableMapping[..., ...]`, `typing.Dict[..., ...]`,
`typing.Mapping[..., ...]`, or `typing.MutableMapping[..., ...]`),
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 improves
internal detection of ignorable type hints so as to be robust against
pernicious edge cases. Although essential, there isn't particularly much
to see here. *Next!* (*Flummoxed ox flambé!*)
  • Loading branch information
leycec committed Mar 11, 2024
1 parent 505177d commit 174fc45
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 168 deletions.
197 changes: 116 additions & 81 deletions beartype/_check/code/codemake.py

Large diffs are not rendered by default.

65 changes: 53 additions & 12 deletions beartype/_check/code/snip/codesnipstr.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
'''

# ....................{ 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 @@ -149,18 +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} (
{indent_curr} # Localize the first key of this mapping.
{indent_curr} ({pith_curr_key_var_name} := next(iter(
{indent_curr} {pith_curr_var_name}))) is {pith_curr_key_var_name} and
{indent_curr} # True only if this key satisfies this hint *AND*...
{indent_curr} {hint_key_placeholder} 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
{indent_curr} # True only if this value satisfies this hint.
{indent_curr} {hint_value_placeholder}
{indent_curr} )
{indent_curr} ({hint_key_and_or_value})
{indent_curr} )
{indent_curr})'''
'''
Expand All @@ -183,6 +173,57 @@
https://stackoverflow.com/a/70490285/2809027
'''


CODE_PEP484585_MAPPING_KEY_ONLY = '''
{indent_curr} # Localize the first key of this mapping.
{indent_curr} ({pith_curr_key_var_name} := next(iter(
{indent_curr} {pith_curr_var_name}))) is {pith_curr_key_var_name} and
{indent_curr} # True only if this key satisfies this hint.
{indent_curr} {hint_key_placeholder}'''
'''
:pep:`484`- and :pep:`585`-compliant code snippet type-checking *only* the first
key of the current pith against *only* the key child type hint subscripting a
parent standard mapping type.
This snippet intentionally avoids type-checking values and is thus suitable for
type-checking mappings with ignorable value child type hints (e.g.,
``dict[str, object]``).
'''


CODE_PEP484585_MAPPING_VALUE_ONLY = '''
{indent_curr} # Localize the first value of this mapping.
{indent_curr} ({pith_curr_value_var_name} := next(iter(
{indent_curr} {pith_curr_var_name}.values()))) is {pith_curr_value_var_name} and
{indent_curr} # True only if this value satisfies this hint.
{indent_curr} {hint_value_placeholder}'''
'''
:pep:`484`- and :pep:`585`-compliant code snippet type-checking *only* the first
value of the current pith against *only* the value child type hint subscripting
a parent standard mapping type.
This snippet intentionally avoids type-checking keys and is thus suitable for
type-checking mappings with ignorable key child type hints (e.g.,
``dict[object, str]``).
'''


CODE_PEP484585_MAPPING_KEY_AND_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
{indent_curr} # True only if this value satisfies this hint.
{indent_curr} {hint_value_placeholder}'''
'''
:pep:`484`- and :pep:`585`-compliant code snippet type-checking *only* the first
key-value pair of the current pith against *only* the key and value child type
hints subscripting a parent standard mapping type.
This snippet intentionally type-checks both keys and values is thus unsuitable
for type-checking mappings with ignorable key or value child type hints (e.g.,
``dict[object, str]``, ``dict[str, object]``).
'''

# ....................{ 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
62 changes: 54 additions & 8 deletions beartype/_check/convert/convsanify.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from beartype._util.cache.map.utilmapbig import CacheUnboundedStrong
from beartype._util.hint.pep.proposal.pep484585.utilpep484585func import (
reduce_hint_pep484585_func_return)
from beartype._util.hint.utilhinttest import is_hint_ignorable

# ....................{ SANIFIERS ~ root }....................
#FIXME: Unit test us up, please.
Expand Down Expand Up @@ -263,7 +264,52 @@ def sanify_hint_root_statement(
return hint

# ....................{ SANIFIERS ~ any }....................
def sanify_hint_any(
#FIXME: Unit test us up, please.
def sanify_hint_child_if_unignorable_or_none(*args, **kwargs) -> Any:
'''
Type hint sanified (i.e., sanitized) from the passed **possibly insane child
type hint** (i.e., hint transitively subscripting the root type hint
annotating a parameter or return of the currently decorated callable) if
this hint is both reducible and ignorable, this hint unmodified if this hint
is both irreducible and ignorable, and :data:`None` otherwise (i.e., if this
hint is ignorable).
This high-level sanifier effectively chains the lower-level
:func:`sanify_hilt_child` sanifier and
:func:`beartype._util.hint.utilhinttest.is_hint_ignorable` tester into a
single unified function, streamlining sanification and ignorability
detection throughout the codebase.
Note that a :data:`None` return unambiguously implies this hint to be
ignorable, even if the passed hint is itself :data:`None`. Why? Because if
the passed hint were :data:`None`, then a :pep:`484`-compliant reducer will
internally reduce this hint to ``type(None)``. After reduction, *all* hints
are guaranteed to be non-:data:`None`.
Parameters
----------
All passed arguments are passed as is to the lower-level
:func:`sanify_hilt_child` sanifier.
Returns
-------
object
Either:
* If the passed possibly insane child type hint is ignorable after
reduction to a sane child type hint, :data:`None`.
* Else, the sane child type hint to which this hint reduces.
'''

# Sane child hint sanified from this possibly insane child hint if this hint
# is reducible *OR* this hint as is otherwise (i.e., if irreducible).
hint_child = sanify_hint_child(*args, **kwargs)

# Return either "None" if this hint is ignorable or this hint otherwise.
return None if is_hint_ignorable(hint_child) else hint_child


def sanify_hint_child(
# Mandatory parameters.
hint: object,
conf: BeartypeConf,
Expand All @@ -274,16 +320,16 @@ def sanify_hint_any(
pith_name: Optional[str] = None,
) -> Any:
'''
PEP-compliant type hint sanified (i.e., sanitized) from the passed
**PEP-compliant child type hint** (i.e., hint transitively subscripting the
root type hint annotating a parameter or return of the currently decorated
callable) if this hint is reducible *or* this hint as is otherwise (i.e., if
this hint is *not* irreducible).
Type hint sanified (i.e., sanitized) from the passed **possibly insane child
type hint** (i.e., hint transitively subscripting the root type hint
annotating a parameter or return of the currently decorated callable) if
this hint is reducible *or* this hint unmodified otherwise (i.e., if this
hint is irreducible).
Parameters
----------
hint : object
PEP-compliant type hint to be sanified.
Type hint to be sanified.
conf : BeartypeConf
**Beartype configuration** (i.e., self-caching dataclass encapsulating
all settings configuring type-checking for the passed object).
Expand All @@ -308,7 +354,7 @@ class variable or method annotated by this hint *or* :data:`None`).
Returns
-------
object
PEP-compliant type hint sanified from this hint.
Type hint sanified from this possibly insane child type hint.
'''

# This sanifier covers the proper subset of logic performed by the
Expand Down
4 changes: 2 additions & 2 deletions beartype/_check/error/_errorcause.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
is_hint_pep,
is_hint_pep_args,
)
from beartype._check.convert.convsanify import sanify_hint_any
from beartype._check.convert.convsanify import sanify_hint_child
from beartype._util.hint.utilhinttest import is_hint_ignorable

# ....................{ CLASSES }....................
Expand Down Expand Up @@ -266,7 +266,7 @@ def hint(self, hint: Any) -> None:
# Sanitize this hint if unsupported by @beartype in its current form
# (e.g., "numpy.typing.NDArray[...]") to another form supported by
# @beartype (e.g., "typing.Annotated[numpy.ndarray, beartype.vale.*]").
hint = sanify_hint_any(
hint = sanify_hint_child(
hint=hint,
conf=self.conf,
exception_prefix=self.exception_prefix,
Expand Down
5 changes: 4 additions & 1 deletion beartype/_check/error/_pep/_pep484585/_errorgeneric.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ def find_cause_generic(cause: ViolationCause) -> ViolationCause:
# For each unignorable unerased transitive pseudo-superclass originally
# declared as an erased superclass of this generic...
for hint_child in iter_hint_pep484585_generic_bases_unerased_tree(
hint=cause.hint, exception_prefix=cause.exception_prefix):
hint=cause.hint,
conf=cause.conf,
exception_prefix=cause.exception_prefix,
):
# Deep output cause to be returned, permuted from this input cause.
cause_deep = cause.permute(hint=hint_child).find_cause()
# print(f'tuple pith: {pith_item}\ntuple hint child: {hint_child}')
Expand Down

0 comments on commit 174fc45

Please sign in to comment.