Skip to content

Commit

Permalink
Added is_simplified_exception configuration option. (#301)
Browse files Browse the repository at this point in the history
`beartype.BeartypeConf.violation_*` options x 1.

This commit by Montreal API wizard @felixchenier (Félix Chénier) is the
first in a commit chain defining a trio of new public
`beartype.BeartypeConf.violation_*` options, en-route to resolving issue
#216 also kindly submitted by that same wizard. Specifically, this
commit refactors the `beartype.BeartypeConf` dataclass to accept these
new keyword-only parameters – enabling users to configure both the types
and messages of type-checking violation exceptions raised by @beartype:

* `violation_param_type: type[Exception] =
  beartype.roar.BeartypeCallHintParamViolation`, the type of exception
  raised by @beartype when a parameter violates a type-check.
* `violation_return_type: type[Exception] =
  beartype.roar.BeartypeCallHintReturnViolation`, the type of exception
  raised by @beartype when a return violates a type-check.
* `violation_verbosity: int = 3`, a non-negative integer in the range
  `[0, 5]` controlling the verbosity of exceptions raised by @beartype
  when *anything* violates a type-check. This integer varies as follows:
  1. **Minimal verbosity.** Equivalently, maximal terseness. This level
     is intended for library users whose end users prefer concise,
     simple, and sparse type-checking violations.
  2. **[Reserved].** Currently simply an alias for
     ``violation_verbosity=1`.
  3. **Standard verbosity.** This level is intended for software
     developers by adding context on the underlying causes of
     type-checking violations.
  4. **[Reserved].** Currently simply an alias for
     ``violation_verbosity=3`.
  5. **Maximal verbosity.** Equivalently, minimal terseness. This level
     is intended for software developers debugging type-checking
     violations by adding even more context on the underlying causes of
     type-checking violations -- including metadata on the @beartype
     configurations under which those violations occurred.

Thanks so much to @felixchenier for his brilliant volunteerism that
transforms @beartype into something actually usable by living humans
that breathe oxygen. (*Exulted stories of excitatory orations!*)
  • Loading branch information
felixchenier committed Nov 10, 2023
1 parent 6e64bdd commit 3a835d5
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 20 deletions.
125 changes: 124 additions & 1 deletion beartype/_conf/confcls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@

# ....................{ IMPORTS }....................
from beartype.roar import (
BeartypeConfParamException,
BeartypeConfParamException, BeartypeCallHintParamViolation, BeartypeCallHintReturnViolation
)
from beartype.roar._roarwarn import (
_BeartypeConfReduceDecoratorExceptionToWarningDefault)
from beartype.typing import (
TYPE_CHECKING,
Dict,
Optional,
Type,
)
from beartype._cave._cavemap import NoneTypeOr
from beartype._conf.confenum import BeartypeStrategy
Expand Down Expand Up @@ -99,6 +100,16 @@ class BeartypeConf(object):
decorator reduces otherwise fatal exceptions raised at decoration time
to equivalent non-fatal warnings of this warning category. See also the
:meth:`__new__` method docstring.
_violation_param_type : Type[Exception]
:data:The exception that beartype raises in case of parameter type hint violation.
:meth:`__new__` method docstring.
_violation_return_type : Type[Exception]
:data:The exception that beartype raises in case of return type hint violation.
:meth:`__new__` method docstring.
_violation_verbosity : int
:data:An integer between 1 and 5 that defines the level of verbosity in the
violation exception message. 1 is the least verbose, 5 is the most verbose.
:meth:`__new__` method docstring.
'''

# ..................{ CLASS VARIABLES }..................
Expand Down Expand Up @@ -127,6 +138,9 @@ class BeartypeConf(object):
'_is_warning_cls_on_decorator_exception_set',
'_strategy',
'_warning_cls_on_decorator_exception',
'_violation_param_type',
'_violation_return_type',
'_violation_verbosity',
)

# Squelch false negatives from mypy. This is absurd. This is mypy. See:
Expand All @@ -141,6 +155,9 @@ class BeartypeConf(object):
_is_warning_cls_on_decorator_exception_set: bool
_strategy: BeartypeStrategy
_warning_cls_on_decorator_exception: Optional[TypeWarning]
_violation_param_type: Type[Exception]
_violation_return_type: Type[Exception]
_violation_verbosity: int

# ..................{ INSTANTIATORS }..................
# Note that this __new__() dunder method implements the superset of the
Expand Down Expand Up @@ -168,6 +185,9 @@ def __new__(
strategy: BeartypeStrategy = BeartypeStrategy.O1,
warning_cls_on_decorator_exception: Optional[TypeWarning] = (
_BeartypeConfReduceDecoratorExceptionToWarningDefault),
violation_param_type: Type[Exception] = BeartypeCallHintParamViolation,
violation_return_type: Type[Exception] = BeartypeCallHintReturnViolation,
violation_verbosity: int = 5,
) -> 'BeartypeConf':
'''
Instantiate this configuration if needed (i.e., if *no* prior
Expand Down Expand Up @@ -373,6 +393,21 @@ def __new__(
by :mod:`beartype.claw` will successfully decorate the entirety of
their target packages rather than prematurely halt with a single
fatal exception at the first decoration issue.
violation_param_type : Type[Exception], optional
The exception that beartype raises in case of parameter type hint violation.
Defaults to :class:BeartypeCallHintParamViolation.
violation_return_type : Type[Exception], optional
The exception that beartype raises in case of return type hint violation.
Defaults to :class:BeartypeCallHintReturnViolation.
violation_verbosity : int, optional
An integer between 1 and 5 that defines the level of verbosity in the
violation exception message::
1. Minimal verbosity
2. Reserved for later, currently same as 1
3. Provides more details on the exact cause of type hint violation.
4. Reserved for later, currently same as 3
5. Full verbosity. Like 3, but with additional information on current
configuration settings. Defaults to :data:5.
Returns
----------
Expand Down Expand Up @@ -425,6 +460,9 @@ def __new__(
is_pep484_tower,
strategy,
warning_cls_on_decorator_exception,
violation_param_type,
violation_return_type,
violation_verbosity,
)

# If this method has already instantiated a configuration with these
Expand Down Expand Up @@ -496,6 +534,35 @@ def __new__(
# Else, "warning_cls_on_decorator_exception" is either
# "None" *OR* a warning category.

# If "violation_param_type" is *NOT* an exception, raise an exception.
elif not is_type_subclass(violation_param_type, Exception):
raise BeartypeConfParamException(
f'Beartype configuration parameter "violation_param_type" '
f'value {repr(violation_param_type)} not exception.'
)
# Else, "violation_param_type" is an exception.
#
# If "violation_return_type" is *NOT* an exception, raise an exception.
elif not is_type_subclass(violation_return_type, Exception):
raise BeartypeConfParamException(
f'Beartype configuration parameter "violation_return_type" '
f'value {repr(violation_return_type)} not exception.'
)
# Else, "violation_return_type" is an exception.
#
# If "violation_verbosity" is *NOT* an int between 1 and 5, raise an exception.
elif (
not isinstance(violation_verbosity, int)
or violation_verbosity < 1
or violation_verbosity > 5
):
raise BeartypeConfParamException(
f'Beartype configuration parameter "violation_verbosity" '
f'value {repr(violation_verbosity)} not int between 1 and 5.'
)
# Else, "violation_verbosity" is an int
#

# Instantiate a new configuration of this type.
self = super().__new__(cls)

Expand Down Expand Up @@ -531,6 +598,9 @@ def __new__(
self._warning_cls_on_decorator_exception = (
warning_cls_on_decorator_exception)
self._strategy = strategy
self._violation_param_type = violation_param_type
self._violation_return_type = violation_return_type
self._violation_verbosity = violation_verbosity

# Cache this configuration with all relevant dictionary singletons.
_beartype_conf_args_to_conf[conf_args] = self
Expand All @@ -546,6 +616,9 @@ def __new__(
strategy=strategy,
warning_cls_on_decorator_exception=(
warning_cls_on_decorator_exception),
violation_param_type=violation_param_type,
violation_return_type=violation_return_type,
violation_verbosity=violation_verbosity,
)

# Assert that these two data structures encapsulate the same number
Expand Down Expand Up @@ -691,6 +764,53 @@ def warning_cls_on_decorator_exception(self) -> (

return self._warning_cls_on_decorator_exception

@property
def violation_param_type(self) -> Type[Exception]:
'''
The exception that beartype raises in case of parameter type hint violation.
See Also
----------
:meth:`__new__`
Further details.
'''

return self._violation_param_type

@property
def violation_return_type(self) -> Type[Exception]:
'''
The exception that beartype raises in case of return type hint violation.
See Also
----------
:meth:`__new__`
Further details.
'''

return self._violation_return_type

@property
def violation_verbosity(self) -> int:
'''
An integer that defines the level of verbosity in the violation exception message.
1. Minimal verbosity
2. Reserved for later, currently same as 1
3. Provides more details on the exact cause of type hint violation.
4. Reserved for later, currently same as 3
5. Full verbosity. Like 3, but with additional information on current
configuration settings.
See Also
----------
:meth:`__new__`
Further details.
'''

return self._violation_verbosity


# ..................{ DUNDERS }..................
def __eq__(self, other: object) -> bool:
'''
Expand Down Expand Up @@ -780,6 +900,9 @@ def __repr__(self) -> str:
f', is_pep484_tower={repr(self._is_pep484_tower)}'
f', strategy={repr(self._strategy)}'
f', warning_cls_on_decorator_exception={repr(self._warning_cls_on_decorator_exception)}'
f', violation_param_type={repr(self._violation_param_type)}'
f', violation_return_type={repr(self._violation_return_type)}'
f', violation_verbosity={repr(self._violation_verbosity)}'
f')'
)

Expand Down
58 changes: 39 additions & 19 deletions beartype/_decor/error/errormain.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
Dict,
# NoReturn,
Optional,
Type,
)
# from beartype._cave._cavemap import NoneTypeOr
from beartype._conf.confcls import (
Expand Down Expand Up @@ -143,7 +144,7 @@ def get_beartype_violation(
# Optional parameters.
cls_stack: TypeStack = None,
random_int: Optional[int] = None,
) -> BeartypeCallHintViolation:
) -> Type[Exception]:
'''
Human-readable exception detailing the failure of the parameter with the
passed name *or* return if this name is the magic string ``return`` of the
Expand Down Expand Up @@ -231,10 +232,11 @@ class variable or method annotated by this hint *or* :data:`None`).
Returns
----------
BeartypeCallHintViolation
Exception
Human-readable exception detailing the failure of this parameter or
return to satisfy the PEP-compliant type hint annotating this parameter
or return value, guaranteed to be an instance of either:
or return value. Under default configuration, it is guaranteed to be an instance
of either:
* :class:`.BeartypeCallHintParamViolation`, if the object failing to
satisfy this hint is a parameter.
Expand Down Expand Up @@ -279,13 +281,13 @@ class variable or method annotated by this hint *or* :data:`None`).
# 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.
if pith_name == ARG_NAME_RETURN:
exception_cls = BeartypeCallHintReturnViolation
exception_cls = conf.violation_return_type
exception_prefix = prefix_beartypeable_return_value(
func=func, return_value=pith_value)
# Else, the passed object is a parameter. In this case, set the above local
# variables appropriately.
else:
exception_cls = BeartypeCallHintParamViolation
exception_cls = conf.violation_param_type
exception_prefix = prefix_beartypeable_arg_value(
func=func, arg_name=pith_name, arg_value=pith_value)

Expand Down Expand Up @@ -362,22 +364,37 @@ class variable or method annotated by this hint *or* :data:`None`).

# ....................{ EXCEPTION }....................
# Substring prefixing this exception message.
exception_message_prefix = (
f'{exception_prefix}violates type hint {color_hint(repr(hint))}')
exception_message_prefixes = {
1: f'{exception_prefix}was expected to be of type {color_hint(repr(hint))}',
2: f'{exception_prefix}was expected to be of type {color_hint(repr(hint))}',
3: f'{exception_prefix}violates type hint {color_hint(repr(hint))}',
4: f'{exception_prefix}violates type hint {color_hint(repr(hint))}',
5: f'{exception_prefix}violates type hint {color_hint(repr(hint))}',
}

# If this configuration is *NOT* the default configuration, append the
# machine-readable representation of this non-default configuration to this
# exception message for disambiguity and clarity.
exception_message_conf = (
''
if conf == BEARTYPE_CONF_DEFAULT else
f' under non-default configuration {repr(conf)}'
)
exception_message_confs = {
1: '',
2: '',
3: '',
4: '',
5: f' under non-default configuration {repr(conf)}',
}

exception_message_suffixes = {
1: '.',
2: '.',
3: f', as {violation_cause_suffixed}',
4: f', as {violation_cause_suffixed}',
5: f', as {violation_cause_suffixed}',
}

# Exception message embedding this cause.
exception_message = (
f'{exception_message_prefix}{exception_message_conf}, as '
f'{violation_cause_suffixed}'
exception_message_prefixes[conf.violation_verbosity]
+ exception_message_confs[conf.violation_verbosity]
+ exception_message_suffixes[conf.violation_verbosity]
)

#FIXME: Unit test us up, please.
Expand All @@ -388,10 +405,13 @@ class variable or method annotated by this hint *or* :data:`None`).

#FIXME: Unit test that the caller receives the expected culprit, please.
# Exception of the desired class embedding this cause.
exception = exception_cls( # type: ignore[misc]
message=exception_message,
culprits=tuple(violation_culprits),
)
try:
exception = exception_cls( # type: ignore[misc]
message=exception_message,

Check failure on line 410 in beartype/_decor/error/errormain.py

View workflow job for this annotation

GitHub Actions / [ubuntu-latest] Python 3.8 CI

No parameter named "message" (reportGeneralTypeIssues)
culprits=tuple(violation_culprits),

Check failure on line 411 in beartype/_decor/error/errormain.py

View workflow job for this annotation

GitHub Actions / [ubuntu-latest] Python 3.8 CI

No parameter named "culprits" (reportGeneralTypeIssues)
)
except TypeError: # Standard exceptions do not have arguments. Oooh, this is ugly.
exception = exception_cls(exception_message)

# Return this exception to the @beartype-generated type-checking wrapper
# (which directly calls this function), which will then squelch the
Expand Down

0 comments on commit 3a835d5

Please sign in to comment.