Skip to content

Commit

Permalink
Granular PEP 526 messages x 3.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain improving the granularity of
both exception and warning messages emitted for **PEP 526-compliant
annotated variable assignments** (e.g., `muh_var: int | str = True`).
Previously, @beartype provided *no* contextual metadata describing these
assignments in these messages. Once this commit chain is complete,
@beartype will prefix these messages with a substring describing the
fully-qualified class, callable, and/or module directly performing these
assignments. Specifically, this commit improves the code generation
internally performed by beartype to reliably pass this prefix to our
exception handler. This feature is now nearly complete. Rejoice, elves!
(*Binary stars in a unary bar!*)
  • Loading branch information
leycec committed Mar 5, 2024
1 parent a9c05e2 commit e2ff974
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 49 deletions.
4 changes: 3 additions & 1 deletion beartype/_check/_checksnip.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ARG_NAME_CONF,
ARG_NAME_CLS_STACK,
ARG_NAME_FUNC,
ARG_NAME_EXCEPTION_PREFIX,
ARG_NAME_GET_VIOLATION,
ARG_NAME_HINT,
ARG_NAME_WARN,
Expand Down Expand Up @@ -97,7 +98,8 @@
{VAR_NAME_VIOLATION} = {ARG_NAME_GET_VIOLATION}(
obj={VAR_NAME_PITH_ROOT},
hint={ARG_NAME_HINT},
conf={ARG_NAME_CONF},{{arg_random_int}}
conf={ARG_NAME_CONF},
exception_prefix={ARG_NAME_EXCEPTION_PREFIX},{{arg_random_int}}
)
'''
'''
Expand Down
12 changes: 12 additions & 0 deletions beartype/_check/checkmagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@
'''


ARG_NAME_EXCEPTION_PREFIX = f'{NAME_PREFIX}exception_prefix'
'''
Name of the **private exception prefix parameter** (i.e.,
:mod:`beartype`-specific parameter whose default value is the human-readable
label prefixing the representation of the currently type-checked object in
exception messages raised when this object violates its type hint, conditionally
passed to wrappers generated by the :func:`beartype.door.die_if_unbearable`
type-checker injected for :pep:`526`-compliant annotated variable assignments by
:mod:`beartype.claw`-published import hooks).
'''


ARG_NAME_FUNC = f'{NAME_PREFIX}func'
'''
Name of the **private decorated callable parameter** (i.e.,
Expand Down
12 changes: 3 additions & 9 deletions beartype/_check/checkmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from beartype._cave._cavemap import NoneTypeOr
from beartype._check.checkmagic import (
ARG_NAME_CONF,
ARG_NAME_EXCEPTION_PREFIX,
ARG_NAME_GETRANDBITS,
ARG_NAME_GET_VIOLATION,
ARG_NAME_HINT,
Expand Down Expand Up @@ -475,19 +476,12 @@ def make_code_raiser_hint_object_check(
''
)

#FIXME: Refactor as follows, please:
#* Define a new "ARG_NAME_EXCEPTION_PREFIX" global in "checkmagic".
#* Import that above.
#* Assign here:
# func_scope[ARG_NAME_EXCEPTION_PREFIX] = exception_prefix
#* Refactor the "CODE_GET_HINT_OBJECT_VIOLATION" global to additionally pass
# the following keyword parameter:
# exception_prefix={ARG_NAME_EXCEPTION_PREFIX},

# Pass hidden parameters to this raiser function exposing:
# * The passed exception prefix accessed by this snippet.
# * The get_hint_object_violation() getter called by the
# "CODE_GET_HINT_OBJECT_VIOLATION" snippet.
# * The passed type hint accessed by this snippet.
func_scope[ARG_NAME_EXCEPTION_PREFIX] = exception_prefix
func_scope[ARG_NAME_GET_VIOLATION] = get_hint_object_violation
func_scope[ARG_NAME_HINT] = hint

Expand Down
97 changes: 64 additions & 33 deletions beartype/_check/error/errorget.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ def get_func_pith_violation(
Raises
------
All exceptions raised by the lower-level :func:`.get_hint_object_violation`
getter.
getter as well as:
_BeartypeCallHintPepRaiseException
If the parameter or return with the passed name is unannotated.
See Also
--------
Expand Down Expand Up @@ -195,8 +198,9 @@ def get_hint_object_violation(

# Optional parameters.
func: Optional[CallableABC] = None,
pith_name: Optional[str] = None,
cls_stack: TypeStack = None,
exception_prefix: Optional[str] = None,
pith_name: Optional[str] = None,
random_int: Optional[int] = None,
) -> Exception:
'''
Expand Down Expand Up @@ -256,6 +260,24 @@ def get_hint_object_violation(
* Else, :data:`None`.
Defaults to :data:`None`.
cls_stack : TypeStack, optional
**Type stack** (i.e., either a tuple of the one or more
:func:`beartype.beartype`-decorated classes lexically containing the
class variable or method annotated by this hint *or* :data:`None`).
Defaults to :data:`None`.
exception_prefix : Optional[str]
Either:
* If the caller prefers specifying an explicit human-readable label
prefixing the representation of this object in the exception message,
that labal.
* Else, :data:`None`. In this case, this getter automatically
synthesizes this label from the other passed parameters that are
required to be non-:data:`None`. If any such parameter is
:data:`None`, an exception is raised. These parameters include:
* The passed ``func`` parameter, required to be non-:data:`None`.
* The passed ``pith_name`` parameter, required to be non-:data:`None`.
pith_name : Optional[str]
Either:
Expand All @@ -264,11 +286,6 @@ def get_hint_object_violation(
* If this hint annotates the return of some callable, ``"return"``.
* Else, :data:`None`.
Defaults to :data:`None`.
cls_stack : TypeStack, optional
**Type stack** (i.e., either a tuple of the one or more
:func:`beartype.beartype`-decorated classes lexically containing the
class variable or method annotated by this hint *or* :data:`None`).
Defaults to :data:`None`.
random_int: Optional[int], optional
**Pseudo-random integer** (i.e., unsigned 32-bit integer
Expand Down Expand Up @@ -307,7 +324,8 @@ class variable or method annotated by this hint *or* :data:`None`).
BeartypeDecorHintPepException
If the type hint annotating this object is *not* PEP-compliant.
_BeartypeCallHintPepRaiseException
If the parameter or return value with the passed name is unannotated.
If all three of the ``exception_prefix``,``func``, and ``pith_name``
parameters are :data:`None`.
_BeartypeCallHintPepRaiseDesynchronizationException
If this pith actually satisfies this hint, implying either:
Expand All @@ -330,34 +348,47 @@ class variable or method annotated by this hint *or* :data:`None`).
# Type of violation to be raised.
exception_cls: TypeException = None # type: ignore[assignment]

# Substring prefixing the message of the violation to be raised below.
exception_prefix: str = None # type: ignore[assignment]

#FIXME: Refactor this function to accept a new "exception_prefix" parameter.
#FIXME: Refactor the first "if" branch below to instead resemble:
#else:
# exception_cls = conf.violation_door_type
# exception_prefix = f'{exception_prefix}value '

# If the passed object is neither a parameter or return of a decorated
# callable, this object was directly passed to either the
# beartype.door.is_bearable() or beartype.door.die_if_unbearable()
# functions. In either case, set the above local variables appropriately.
# If the caller passed *NO* parameter name, the passed object is neither a
# parameter nor return of a decorated callable. By elimination, this object
# *MUST* have been directly passed to the beartype.door.die_if_unbearable()
# type-checker. In this case...
if pith_name is None:
# If the caller also passed *NO* exception prefix, raise an exception.
if exception_prefix is None:
raise _BeartypeCallHintPepRaiseException(
'get_hint_object_violation() passed neither '
'"exception_prefix" nor "pith_name" parameters.'
)
# Else, the caller passed an exception prefix.

# Default the exception class appropriately.
exception_cls = conf.violation_door_type
exception_prefix = 'Object '
# If the name of this parameter is the magic string implying the passed
# object to be a return value, set the above local variables appropriately.
elif pith_name == ARG_NAME_RETURN:
exception_cls = conf.violation_return_type
exception_prefix = prefix_callable_return_value(
func=func, return_value=obj) # type: ignore[arg-type]
# Else, the passed object is a parameter. In this case, set the above local
# variables appropriately.

# Suffix this exception prefix with an additional noun for disambiguity.
exception_prefix = f'{exception_prefix}value '
# Else, the caller passed a parameter name. In this case...
else:
exception_cls = conf.violation_param_type
exception_prefix = prefix_callable_arg_value(
func=func, arg_name=pith_name, arg_value=obj) # type: ignore[arg-type]
# If the caller also passed an exception prefix, raise an exception.
if exception_prefix is not None:
raise _BeartypeCallHintPepRaiseException(
'get_hint_object_violation() passed both '
'"exception_prefix" and "pith_name" parameters.'
)
# Else, the caller passed *NO* exception prefix.

# If the name of this parameter is the magic string implying the passed
# object to be a return value...
if pith_name == ARG_NAME_RETURN:
# Default these exception locals appropriately
exception_cls = conf.violation_return_type
exception_prefix = prefix_callable_return_value(
func=func, return_value=obj) # type: ignore[arg-type]
# Else, the passed object is a parameter. In this case...
else:
# Default these exception locals appropriately
exception_cls = conf.violation_param_type
exception_prefix = prefix_callable_arg_value(
func=func, arg_name=pith_name, arg_value=obj) # type: ignore[arg-type]

# Uppercase the first character of this violation prefix for readability.
exception_prefix = uppercase_str_char_first(exception_prefix)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def test_door_die_if_unbearable(iter_hints_piths_meta) -> None:

# Assert this raiser successfully replaced the irrelevant substring
# previously prefixing this message.
assert exception_str.startswith('Object ')
assert exception_str.startswith('Die_if_unbearable() value ')
assert ' violates type hint ' in exception_str
# Else, this raiser satisfies this hint. In this case...
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# ....................{ TESTS }....................
# ....................{ TESTS ~ pith }....................
def test_get_func_pith_violation() -> None:
'''
Test the
Expand Down Expand Up @@ -54,8 +54,7 @@ def forest_unknown(

return achromatic_voice

# Keyword arguments to be unconditionally passed to *ALL* calls of the
# get_func_pith_violation() getter below.
# Keyword arguments to be unconditionally passed to all getter calls below.
kwargs = dict(
func=forest_unknown,
conf=BeartypeConf(),
Expand Down Expand Up @@ -145,7 +144,7 @@ def forest_unknown(
**kwargs
)

# ....................{ TESTS ~ conf }....................
# ....................{ TESTS ~ pith : conf }....................
def test_get_func_pith_violation_conf_is_color() -> None:
'''
Test the
Expand Down Expand Up @@ -209,7 +208,7 @@ def she_drew_back(
# standard output is attached to an interactive terminal.
assert is_str_ansi(str(violation)) is is_stdout_terminal()

# ....................{ TESTS ~ conf : violation_* }....................
# ....................{ TESTS ~ pith : conf : violation_* }....................
def test_get_func_pith_violation_conf_violation_types() -> None:
'''
Test the
Expand Down Expand Up @@ -346,3 +345,41 @@ def like_a_dark_flood(

# Store the previously iterated violation for subsequent reference.
violation_prev = violation

# ....................{ TESTS ~ pith }....................
def test_get_hint_object_violation() -> None:
'''
Test the
:func:`beartype._check.error.errorget.get_hint_object_violation` getter.
'''

# ..................{ IMPORTS }..................
# Defer test-specific imports.
from beartype.roar._roarexc import _BeartypeCallHintPepRaiseException
from beartype._data.func.datafuncarg import ARG_NAME_RETURN
from beartype._check.error.errorget import get_hint_object_violation
from beartype._conf.confcls import BEARTYPE_CONF_DEFAULT
from pytest import raises

# ..................{ LOCALS }..................
# Keyword arguments to be unconditionally passed to all getter calls below.
kwargs = dict(
obj='Frantic with dizzying anguish, her blind flight',
hint=str,
conf=BEARTYPE_CONF_DEFAULT,
)

# ..................{ FAIL }..................
# Assert that this getter raises the expected exception when passed neither
# an exception prefix *NOR* parameter name.
with raises(_BeartypeCallHintPepRaiseException):
get_hint_object_violation(**kwargs)

# Assert that this getter raises the expected exception when passed both an
# exception prefix *AND* parameter name.
with raises(_BeartypeCallHintPepRaiseException):
get_hint_object_violation(
exception_prefix="O'er the wide aëry wilderness: thus driven",
pith_name=ARG_NAME_RETURN,
**kwargs
)

0 comments on commit e2ff974

Please sign in to comment.