Skip to content

Commit

Permalink
Deep dict type-checking x 1.
Browse files Browse the repository at this point in the history
This commit is the epochal first 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 drafts a
gargantuan Python expression intended to be subsequently interpolated
into dynamically generated code deeply type-checking mappings.
(*Fiendish dishes dish out lean fishes!*)
  • Loading branch information
leycec committed Mar 9, 2024
1 parent 5424d27 commit 505177d
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 232 deletions.
77 changes: 65 additions & 12 deletions beartype/_check/code/codemake.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
CODE_PEP484585_GENERIC_CHILD,
CODE_PEP484585_GENERIC_PREFIX,
CODE_PEP484585_GENERIC_SUFFIX,
CODE_PEP484585_MAPPING,
CODE_PEP484585_SEQUENCE_ARGS_1,
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR,
CODE_PEP484585_SUBCLASS,
Expand Down Expand Up @@ -116,8 +117,8 @@
from beartype._util.func.utilfuncscope import add_func_scope_attr
from beartype._util.hint.pep.proposal.pep484585.utilpep484585 import (
is_hint_pep484585_tuple_empty)
from beartype._util.hint.pep.proposal.pep484585.utilpep484585arg import (
get_hint_pep484585_args_1)
from beartype._util.hint.pep.proposal.pep484585.utilpep484585 import (
get_hint_pep484585_args)
from beartype._util.hint.pep.proposal.pep484585.utilpep484585generic import (
get_hint_pep484585_generic_type,
iter_hint_pep484585_generic_bases_unerased_tree,
Expand Down Expand Up @@ -183,6 +184,8 @@ def make_check_expr(
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_SEQUENCE_ARGS_1_format: Callable = (
CODE_PEP484585_SEQUENCE_ARGS_1.format),
CODE_PEP484585_SEQUENCE_ARGS_1_PITH_CHILD_EXPR_format: Callable = (
Expand Down Expand Up @@ -1403,17 +1406,17 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# ignorable arguments. In this case...
):
# Python expression evaluating to the origin type of this
# sequence type hint.
# sequence hint.
hint_curr_expr = add_func_scope_type(
# Origin type of this sequence.
# Origin type of this sequence hint.
cls=get_hint_pep_origin_type_isinstanceable(hint_curr),
func_scope=func_wrapper_scope,
exception_prefix=_EXCEPTION_PREFIX_HINT,
)
# print(f'Sequence type hint {hint_curr} origin type scoped: {hint_curr_expr}')

# Child type hint subscripting this sequence type hint,
# defined as either...
# Child hint subscripting this sequence hint, defined as
# either...
hint_child = (
# If this hint is a variadic tuple, the parent "if"
# statement above has already validated the contents of
Expand All @@ -1425,8 +1428,11 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# which case the contents of this sequence have yet to
# be validated. In this case, inefficiently get the lone
# child hint of this parent hint *WITH* validation.
get_hint_pep484585_args_1(
hint=hint_curr, exception_prefix=_EXCEPTION_PREFIX)
get_hint_pep484585_args(
hint=hint_curr,
args_len=1,
exception_prefix=_EXCEPTION_PREFIX,
)
)

# If this child hint is *NOT* ignorable, deeply type-check
Expand Down Expand Up @@ -1563,11 +1569,58 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# Else, this hint is *NOT* a tuple.
#
# ..........{ MAPPINGS }............
#FIXME: Implement us up, please.
# # If this hint is a standard mapping (e.g.,
# # "typing.Dict[str, int]")...
# # If this hint is a standard mapping (e.g., "dict[str, int]")...
# elif hint_curr_sign in HINT_SIGNS_MAPPING:
# pass
# # Python expression evaluating to the origin type of this
# # mapping hint.
# hint_curr_expr = add_func_scope_type(
# # Origin type of this sequence.
# cls=get_hint_pep_origin_type_isinstanceable(hint_curr),
# func_scope=func_wrapper_scope,
# exception_prefix=_EXCEPTION_PREFIX_HINT,
# )
#
# # Child key and value hints subscripting this mapping hint.
# hint_child_key, hint_child_value = get_hint_pep484585_args(
# hint=hint_curr,
# args_len=2,
# exception_prefix=_EXCEPTION_PREFIX,
# )
#
# #FIXME: Start here tomorrow, please. A good place would be
# #generating the "pith_curr_key_var_name" and
# #"pith_curr_value_var_name" variables as is done above for
# #the "pith_curr_var_name" variable. *sigh*
# # If this child hint is *NOT* ignorable, deeply type-check
# # both the type of the current pith *AND* a randomly indexed
# # item of this pith. Specifically...
# if not is_hint_ignorable(hint_child):
# # Record that a pseudo-random integer is now required.
# is_var_random_int_needed = True
#
# # Code type-checking this pith against this type.
# func_curr_code = CODE_PEP484585_SEQUENCE_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 value of a
# # randomly indexed item of the current pith
# # (i.e., standard sequence) 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)),
# )
# # 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 mapping.
#
# ............{ ANNOTATED }............
# If this hint is a PEP 593-compliant type metahint, this
Expand Down
45 changes: 29 additions & 16 deletions beartype/_check/code/snip/codesnipstr.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@


CODE_PEP572_PITH_ASSIGN_AND = '''
{indent_curr} # Expression assigning this pith and always evaluating to true.
{indent_curr} # Localize this pith as a stupidly fast assignment expression.
{indent_curr} ({pith_curr_assign_expr}) is {pith_curr_var_name} and'''
'''
Code snippet embedding an assignment expression assigning the full Python
Expand Down Expand Up @@ -140,13 +140,28 @@
this parent type has been generated.
'''

# ....................{ HINT ~ pep : (484|585) : mapping }....................
# ....................{ HINT ~ pep : (484|585) : mapping }....................
CODE_PEP484585_MAPPING = '''(
{indent_curr} # True only if this pith is of this mapping type.
{indent_curr} # True only if this pith is of this mapping type *AND*...
{indent_curr} isinstance({pith_curr_assign_expr}, {hint_curr_expr}) and
{indent_curr} # True only if either this pith is empty *OR* this pith is
{indent_curr} # both non-empty and a random item deeply satisfies this hint.
{indent_curr} (not {pith_curr_var_name} or {hint_child_placeholder})
{indent_curr} # True only if either...
{indent_curr} (
{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} )
{indent_curr})'''
'''
:pep:`484`- and :pep:`585`-compliant code snippet type-checking the current pith
Expand All @@ -159,23 +174,21 @@
-------
**This snippet cannot contain ternary conditionals.** See
:data:`.CODE_PEP484585_SEQUENCE_ARGS_1` for further commentary.
'''
There exist numerous means of accessing the first key-value pair of a
dictionary. The approach taken here is well-known to be the fastest, as
documented at this `StackOverflow answer`_.
CODE_PEP484585_MAPPING_KEY_FIRST_PITH_CHILD_EXPR = (
'''next(iter({{pith_curr_var_name}}.keys()))''')
'''
:pep:`484`- and :pep:`585`-compliant Python expression yielding the value of the
first key of the current pith (which, by definition, *must* be a standard
mapping).
.. _StackOverflow answer:
https://stackoverflow.com/a/70490285/2809027
'''

# ....................{ HINT ~ pep : (484|585) : sequence }....................
CODE_PEP484585_SEQUENCE_ARGS_1 = '''(
{indent_curr} # True only if this pith is of this sequence type.
{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 pith is empty *OR* this pith is
{indent_curr} # both non-empty and a random item deeply satisfies this hint.
{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})'''
'''
Expand Down
95 changes: 59 additions & 36 deletions beartype/_check/convert/convreduce.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,8 @@
from beartype._conf.confcls import BeartypeConf
from beartype._data.hint.pep.sign.datapepsigncls import HintSign
from beartype._data.hint.pep.sign.datapepsigns import (
HintSignAnnotated,
HintSignFinal,
HintSignGeneric,
HintSignLiteralString,
HintSignNewType,
HintSignNone,
HintSignNumpyArray,
HintSignPanderaAny,
HintSignPep557DataclassInitVar,
HintSignPep585BuiltinSubscriptedUnknown,
HintSignTypeAlias,
HintSignPep695TypeAlias,
HintSignSelf,
HintSignType,
HintSignTypeGuard,
HintSignTypeVar,
HintSignTypedDict,
HintSignAbstractSet,
HintSignAnnotated,
HintSignAsyncContextManager,
HintSignAsyncGenerator,
HintSignAsyncIterable,
Expand All @@ -60,28 +44,43 @@
HintSignDefaultDict,
HintSignDeque,
HintSignDict,
HintSignFinal,
HintSignFrozenSet,
HintSignGenerator,
HintSignGeneric,
HintSignHashable,
HintSignItemsView,
HintSignIterable,
HintSignIterator,
HintSignKeysView,
HintSignList,
HintSignLiteralString,
HintSignMappingView,
HintSignMapping,
HintSignMatch,
HintSignMutableMapping,
HintSignMutableSequence,
HintSignMutableSet,
HintSignNewType,
HintSignNone,
HintSignNumpyArray,
HintSignOrderedDict,
HintSignPanderaAny,
HintSignPattern,
HintSignPep557DataclassInitVar,
HintSignPep585BuiltinSubscriptedUnknown,
HintSignPep695TypeAlias,
HintSignReversible,
HintSignSelf,
HintSignSequence,
HintSignSet,
HintSignSized,
HintSignTuple,
HintSignType,
HintSignTypeAlias,
HintSignTypeGuard,
HintSignTypeVar,
HintSignTypedDict,
HintSignValuesView,
)
from beartype._data.hint.datahinttyping import TypeStack
Expand Down Expand Up @@ -116,6 +115,7 @@
from beartype._util.hint.pep.proposal.utilpep695 import reduce_hint_pep695
from beartype._util.hint.pep.utilpepget import get_hint_pep_sign_or_none
from beartype._util.hint.pep.utilpepreduce import reduce_hint_pep_unsigned
from beartype._util.utilobject import SENTINEL
from collections.abc import Callable

# ....................{ REDUCERS }....................
Expand Down Expand Up @@ -181,23 +181,46 @@ class variable or method annotated by this hint *or* :data:`None`).
* Else, this hint as is unmodified.
'''

# This possibly contextual hint inefficiently reduced to another hint.
#
# Note that we intentionally reduce lower-level contextual hints *BEFORE*
# higher-level context-free hints. In theory, order of reduction *SHOULD* be
# insignificant; in practice, we suspect unforeseen and unpredictable
# interactions between these two reductions. To reduce the likelihood of
# fire-breathing dragons, we reduce lower-level hints first.
hint = _reduce_hint_uncached(
hint=hint,
conf=conf,
pith_name=pith_name,
cls_stack=cls_stack,
exception_prefix=exception_prefix,
)

# This possibly context-free hint efficiently reduced to another hint.
hint = _reduce_hint_cached(hint, conf, exception_prefix)
# Previously reduced instance of this hint, initialized to the sentinel to
# guarantee that the passed hint is *NEVER* equal to the previously reduced
# instance of this hint unless actually reduced below. This is necessary, as
# "None" is a valid type hint reduced to "type(None)" below.
hint_prev: object = SENTINEL

# Repeatedly reduce this hint to increasingly irreducible hints until this
# hint is no longer reducible.
while True:
# This possibly contextual hint inefficiently reduced to another hint.
#
# Note that we intentionally reduce lower-level contextual hints
# *BEFORE* reducing higher-level context-free hints. In theory, order of
# reduction *SHOULD* be insignificant; in practice, we suspect
# unforeseen and unpredictable interactions between these two
# reductions. To reduce the likelihood of fire-breathing dragons here,
# we reduce lower-level hints first.
hint = _reduce_hint_uncached(
hint=hint,
conf=conf,
pith_name=pith_name,
cls_stack=cls_stack,
exception_prefix=exception_prefix,
)

# This possibly context-free hint efficiently reduced to another hint.
hint = _reduce_hint_cached(hint, conf, exception_prefix)

# If the current and previously reduced instances of this hint are
# identical, the above reductions preserved this hint as is rather than
# reducing this hint, implying this hint to irreducible. In this case,
# stop reducing.
if hint is hint_prev:
break
# Else, the current and previously reduced instances of this hint
# differ, implying this hint to still be reducible. In this case,
# continue reducing.

# Previously reduced instance of this hint.
hint_prev = hint

# Return this possibly reduced hint.
return hint
Expand Down Expand Up @@ -253,7 +276,7 @@ def _reduce_hint_uncached(
'''

# Sign uniquely identifying this hint if this hint is identifiable *OR*
# "None" otherwise.
# "None" otherwise (e.g., if this hint is merely an isinstanceable class).
hint_sign = get_hint_pep_sign_or_none(hint)

# Callable reducing this hint if a callable reducing hints of this sign was
Expand Down Expand Up @@ -330,7 +353,7 @@ def _reduce_hint_cached(
pass

# Sign uniquely identifying this hint if this hint is identifiable *OR*
# "None" otherwise.
# "None" otherwise (e.g., if this hint is merely an isinstanceable class).
hint_sign = get_hint_pep_sign_or_none(hint)

# Callable reducing this hint if a callable reducing hints of this sign was
Expand Down

0 comments on commit 505177d

Please sign in to comment.