Skip to content

Commit

Permalink
beartype.vale error granularity x 1.
Browse files Browse the repository at this point in the history
This commit is the first in a commit chain improving the granularity
(i.e., specificity) of exception messages raised by high-level beartype
validators synthesized from lower-level beartype validators (e.g., via
overloaded set theoretic operators like `|`, `&`, and `~`), en-route to
resolving issue #72 kindly submitted by the unwreckable type-hinting
guru Derek Wan (@dycw). Specifically, this commit drafts and documents
(but has yet to meaningfully implement) a new
`BeartypeValidator.get_diagnosis()` getter method returning a detailed
pretty-printed diagnosis of how any object either satisfies or fails to
satisfy any validator. Unrelatedly, this commit also optimizes the
runtime efficiency of our core
`beartype._util.kind.utilkinddict.die_if_mappings_two_items_collide()`
validation function and thus the `@beartype` decorator at decoration
time. (*Unsung doom-brung looms caromed off the bottom rung!*)
  • Loading branch information
leycec committed Dec 7, 2021
1 parent ed19d18 commit b88445a
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 45 deletions.
10 changes: 5 additions & 5 deletions beartype/_decor/_error/_pep/_errorpep593.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ def get_cause_or_none_annotated(sleuth: CauseSleuth) -> Optional[str]:
Human-readable string describing the failure of the passed arbitrary object
to satisfy the passed :pep:`593`-compliant :mod:`beartype`-specific
**metahint** (i.e., type hint annotating a standard class with one or more
:class:`BeartypeValidator` objects, each produced by subscripting the
:class:`beartype.vale.Is` class or a subclass of that class) if this object
actually fails to satisfy this hint *or* ``None`` otherwise (i.e., if this
object satisfies this hint).
:class:`beartype.vale._valevale.BeartypeValidator` objects, each produced
by subscripting the :class:`beartype.vale.Is` class or a subclass of that
class) if this object actually fails to satisfy this hint *or* ``None``
otherwise (i.e., if this object satisfies this hint).
Parameters
----------
Expand Down Expand Up @@ -74,7 +74,7 @@ def get_cause_or_none_annotated(sleuth: CauseSleuth) -> Optional[str]:
f'{sleuth.exception_prefix}PEP 593 type hint '
f'{repr(sleuth.hint)} argument {repr(hint_metadatum)} '
f'not beartype validator '
f'(i.e., subscripted "beartype.vale.Is*" factory).'
f'(i.e., "beartype.vale.Is*[...]" object).'
)
# Else, this object is beartype-specific.

Expand Down
50 changes: 22 additions & 28 deletions beartype/_util/kind/utilkinddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_9
from beartype._util.text.utiltextrepr import represent_object
from collections.abc import Sequence, Mapping, MutableMapping
from threading import Lock
# from threading import Lock
from typing import Sequence as SequenceHint

# ....................{ VALIDATORS }....................
Expand Down Expand Up @@ -46,7 +46,7 @@ def die_if_mappings_two_items_collide(
assert isinstance(mapping_a, Mapping), f'{repr(mapping_a)} not mapping.'
assert isinstance(mapping_b, Mapping), f'{repr(mapping_b)} not mapping.'

# Set of all keys of the first mapping.
# Set of all keys of the first mapping, localized for efficiency.
mapping_a_keys = mapping_a.keys()

# Set of all key collisions (i.e., keys residing in both mappings). Since
Expand Down Expand Up @@ -74,29 +74,25 @@ def die_if_mappings_two_items_collide(
# throughout this codebase. Ergo, we fallback to a less efficient but
# considerably more robust alternative supporting unhashable values.
mapping_keys_shared_safe = {
# For each key-value pair of the second mapping, this key...
mapping_b_key
for mapping_b_key, mapping_b_value in mapping_b.items()
# If...
if (
# This key also resides in the first mapping *AND*...
mapping_b_key in mapping_a_keys and
# This key also maps to the same value in the first mapping.
mapping_a[mapping_b_key] is mapping_b_value
)
# For each possibly unsafe key collision (i.e., shared key associated
# with possibly different values in both mappings), this key...
mapping_key_shared
for mapping_key_shared in mapping_keys_shared
# If this key maps to the same value in both mappings and is thus safe.
if mapping_a[mapping_key_shared] is mapping_b[mapping_key_shared]
}

# If the number of key and item collisions differ, then one or more
# keys residing in both mappings have differing values. Since merging
# these mappings as is would silently and thus unsafely override the
# values associated with these keys in the former mapping with the
# values associated with these keys in the latter mapping, raise an
# exception to notify the caller.
# If the number of key and item collisions differ, then one or more keys
# residing in both mappings have differing values. Since merging these
# mappings as is would silently and thus unsafely override the values
# associated with these keys in the former mapping with the values
# associated with these keys in the latter mapping, raise an exception to
# notify the caller.
if len(mapping_keys_shared) != len(mapping_keys_shared_safe):
# Dictionary of all unsafe key-value pairs (i.e., pairs such that
# merging these keys would silently override the values associated
# with these keys in either the first or second mappings) from the
# first and second mappings.
# merging these keys would silently override the values associated with
# these keys in either the first or second mappings) from the first and
# second mappings.
mapping_a_unsafe = dict(
(key_shared_unsafe, mapping_a[key_shared_unsafe])
for key_shared_unsafe in mapping_keys_shared
Expand All @@ -116,12 +112,11 @@ def die_if_mappings_two_items_collide(
)
# print(exception_message)
raise _BeartypeUtilMappingException(exception_message)
# Else, the number of key and item collisions are the same, implying
# that all colliding keys are associated with the same values in both
# mappings, implying that both mappings contain the same colliding
# key-value pairs. Since merging these mappings as is will *NOT*
# silently and thus unsafely override any values of either mapping,
# merge these mappings as is.
# Else, the number of key and item collisions are the same, implying that
# all colliding keys are associated with the same values in both mappings,
# implying that both mappings contain the same colliding key-value pairs.
# Since merging these mappings as is will *NOT* silently and thus unsafely
# override any values of either mapping, merge these mappings as is.

# ....................{ MERGERS }....................
def merge_mappings(*mappings: Mapping) -> Mapping:
Expand Down Expand Up @@ -301,7 +296,6 @@ def merge_mappings_two_or_more(mappings: SequenceHint[Mapping]) -> Mapping:
return mapping_merged

# ....................{ UPDATERS }....................
#FIXME: Unit test us up.
def update_mapping(mapping_trg: MutableMapping, mapping_src: Mapping) -> None:
'''
Safely update in-place the first passed mapping with all key-value pairs of
Expand Down
87 changes: 75 additions & 12 deletions beartype/vale/_valevale.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class instantiated by public **beartype validator factories** (i.e., instances
)
from beartype._util.func.utilfuncscope import CallableScope
from beartype._util.text.utiltextrepr import represent_object
# from collections.abc import Tuple
from typing import Any, Callable, Union

# See the "beartype.cave" submodule for further commentary.
Expand Down Expand Up @@ -281,9 +282,9 @@ def __init__(
@property
def is_valid(self) -> BeartypeValidatorTester:
'''
**Validator** (i.e., caller-defined callable accepting a single
arbitrary object and returning either ``True`` if that object satisfies
an arbitrary constraint *or* ``False`` otherwise).
**Validator callable** (i.e., caller-defined callable accepting a
single arbitrary object and returning either ``True`` if that object
satisfies an arbitrary constraint *or* ``False`` otherwise).
'''

return self._is_valid
Expand All @@ -296,8 +297,8 @@ def get_repr(self) -> BeartypeValidatorRepresenter:
'''
**Representer** (i.e., either a string *or* caller-defined callable
accepting no arguments returning a machine-readable representation of
this validator). See the :data:`BeartypeValidatorRepresenter` type hint for
further details.
this validator). See the :data:`BeartypeValidatorRepresenter` type hint
for further details.
'''

return self._get_repr
Expand All @@ -313,8 +314,8 @@ def get_repr(self, get_repr: BeartypeValidatorRepresenter) -> None:
get_repr : BeartypeValidatorRepresenter
**Representer** (i.e., either a string *or* caller-defined callable
accepting no arguments returning a machine-readable representation
of this validator). See the :data:`BeartypeValidatorRepresenter` type
hint for further details.
of this validator). See the :data:`BeartypeValidatorRepresenter`
type hint for further details.
Raises
----------
Expand Down Expand Up @@ -347,11 +348,74 @@ def get_repr(self, get_repr: BeartypeValidatorRepresenter) -> None:
# Set this representer.
self._get_repr = get_repr

# ..................{ GETTERS }..................
#FIXME: Implement us up, please. The output should probably resemble that
#of "repr(self)", except that the "repr(...)" for each subvalidator of this
#validator in this output should be prefixed by a substring denoting the
#truthiness of the passed object against that subvalidator: e.g.,
# # The most Pythonic and thus probably the most readable and best.
# True == Is[lambda foo: foo.x + foo.y >= 0] &
# False == Is[lambda foo: foo.x + foo.y <= 10]
# # Or...
# True --> Is[lambda foo: foo.x + foo.y >= 0] &
# False --> Is[lambda foo: foo.x + foo.y <= 10]
# # Or...
# True <-- Is[lambda foo: foo.x + foo.y >= 0] &
# False <-- Is[lambda foo: foo.x + foo.y <= 10]
# # Or...
# { True} Is[lambda foo: foo.x + foo.y >= 0] &
# {False} Is[lambda foo: foo.x + foo.y <= 10]
#Note this output also implies a pretty printing regimen for readability.
#Implementing pretty printing will probably require extending this method
#to accept an optional nesting level: e.g.,
# def get_diagnosis(
# self,
#
# # Mandatory parameters.
# obj: object,
#
# # Optional parameters.
# indent_level: str = CODE_INDENT_1
# ) -> str:
#FIXME: Should this method be marked @abstract? Contemplate.
#FIXME: Call this method from get_cause_or_none_annotated(), please.
#FIXME: Unit test us up, please -- particularly with respect to non-trivial
#nested subvalidators.
def get_diagnosis(self, obj: object) -> str:
'''
Human-readable **validation failure diagnosis** (i.e., substring
describing how the passed object either satisfies *or* fails to satisfy
this validator).
This method is typically called by high-level error-handling logic to
unambiguously describe the failure of an arbitrary object to satisfy an
arbitrary validator. Since this validator may be synthesized from one
or more lower-level validators (e.g., via the :meth:`__and__`,
:meth:`__or__`, and :meth:`__invert__` dunder methods), the simple
machine-readable representation of this validator does *not* adequately
describe how the passed object satisfies or fails to satisfy this
validator.
Parameters
----------
obj : object
Arbitrary object to be described with respect to this validator.
Returns
----------
str
Truthy representation of this object against this validator.
'''

# For now, do the wrong (but simple) thing.
return repr(self)

# ..................{ DUNDERS ~ operator }..................
# Define a domain-specific language (DSL) enabling callers to dynamically
# combine and Override
def __and__(self, other: 'BeartypeValidator') -> (
'BeartypeValidator'):
# synthesize higher-level validators from lower-level validators via
# overloaded set theoretic operators.

def __and__(self, other: 'BeartypeValidator') -> 'BeartypeValidator':
'''
**Conjunction** (i.e., ``self & other``), synthesizing a new
:class:`BeartypeValidator` object whose validator returns ``True`` only
Expand Down Expand Up @@ -401,8 +465,7 @@ def __and__(self, other: 'BeartypeValidator') -> (
)


def __or__(self, other: 'BeartypeValidator') -> (
'BeartypeValidator'):
def __or__(self, other: 'BeartypeValidator') -> 'BeartypeValidator':
'''
**Disjunction** (i.e., ``self | other``), synthesizing a new
:class:`BeartypeValidator` object whose validator returns ``True`` only
Expand Down

0 comments on commit b88445a

Please sign in to comment.