Skip to content

Commit

Permalink
Granular warning messages 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 of
warning messages, en-route to resolving feature request #73 kindly
submitted several lifetimes ago by Stuttgart `typing` master @amogorkon
(Anselm Kiefner). Specifically, this commit:

* Prefixes warning messages with contextual metadata describing the
  origin of those warnings, typically including fully-qualified module,
  class, and/or callable names as relevant.
* Associates warnings emitted by @beartype with the external
  user-defined modules to which those warnings apply under Python ≥
  3.12, whose standard `warnings.warn()` function accepts a new
  `skip_file_prefixes` parameter for exactly this purpose.

Thanks so much to @amogorkon for gently prodding us to do this several
lifetimes ago. This miracle on Earth is happening because of you.
(*Considerable sidereal dirigible!*)
  • Loading branch information
leycec committed Feb 24, 2024
1 parent 9b33afd commit c5a435d
Show file tree
Hide file tree
Showing 28 changed files with 891 additions and 696 deletions.
2 changes: 1 addition & 1 deletion beartype/_check/_checksnip.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@


CODE_WARN_VIOLATION = f'''
{ARG_NAME_WARN}(str({VAR_NAME_VIOLATION}), type({VAR_NAME_VIOLATION}))'''
{ARG_NAME_WARN}(cls=type({VAR_NAME_VIOLATION}), message=str({VAR_NAME_VIOLATION}))'''
'''
Code snippet emitting the type-checking violation previously generated by the
:data:`.CODE_HINT_ROOT_SUFFIX` or
Expand Down
2 changes: 1 addition & 1 deletion beartype/_check/checkmagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,6 @@
See Also
--------
:attr:`beartype._util.error.utilerrorraise.EXCEPTION_PLACEHOLDER`
:attr:`beartype._data.error.dataerrmagic.EXCEPTION_PLACEHOLDER`
Related commentary.
'''
248 changes: 127 additions & 121 deletions beartype/_check/checkmake.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion beartype/_check/code/codemagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
'''

# ....................{ IMPORTS }....................
from beartype._util.error.utilerrorraise import EXCEPTION_PLACEHOLDER
from beartype._data.error.dataerrmagic import EXCEPTION_PLACEHOLDER
from itertools import count

# ....................{ EXCEPTION }....................
Expand Down
18 changes: 11 additions & 7 deletions beartype/_check/code/codemake.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
LINE_RSTRIP_INDEX_AND,
LINE_RSTRIP_INDEX_OR,
)
from beartype._data.error.dataerrmagic import EXCEPTION_PLACEHOLDER
from beartype._data.hint.datahinttyping import (
CodeGenerated,
LexicalScope,
Expand Down Expand Up @@ -113,8 +114,9 @@
acquire_object_typed,
release_object_typed,
)
from beartype._util.error.utilerrorraise import EXCEPTION_PLACEHOLDER
from beartype._util.func.utilfuncscope import add_func_scope_attr
from beartype._util.hint.pep.proposal.pep484.utilpep484 import (
warn_if_hint_pep484_deprecated)
from beartype._util.hint.pep.proposal.pep484585.utilpep484585 import (
is_hint_pep484585_tuple_empty)
from beartype._util.hint.pep.proposal.pep484585.utilpep484585arg import (
Expand Down Expand Up @@ -143,7 +145,6 @@
die_if_hint_pep_unsupported,
is_hint_pep,
is_hint_pep_args,
warn_if_hint_pep_deprecated,
)
from beartype._util.hint.utilhinttest import is_hint_ignorable
from beartype._util.kind.map.utilmapset import update_mapping
Expand Down Expand Up @@ -251,10 +252,10 @@ def make_check_expr(
:meth:`str.replace` method) to generate the desired non-generic working
code type-checking that parameter or return value.
* Raises generic non-human-readable exceptions containing the placeholder
:attr:`beartype._util.error.utilerrorraise.EXCEPTION_PLACEHOLDER` substring
:attr:`beartype._util.error.utilerrraise.EXCEPTION_PLACEHOLDER` substring
that the caller is required to explicitly catch and raise non-generic
human-readable exceptions from by calling the
:func:`beartype._util.error.utilerrorraise.reraise_exception_placeholder`
:func:`beartype._util.error.utilerrraise.reraise_exception_placeholder`
function.
Parameters
Expand Down Expand Up @@ -718,10 +719,13 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
hint_curr_sign = get_hint_pep_sign(hint_curr)
# print(f'Visiting PEP type hint {repr(hint_curr)} sign {repr(hint_curr_sign)}...')

# If this hint is deprecated, emit a non-fatal warning.
#FIXME: Non-ideal. Shift elsewhere, please. See this function for
#"FIXME:" comments pertaining to this.
# If this is a PEP 484-compliant type hint deprecated by an
# equivalent PEP 585-compliant type hint, emit a non-fatal warning.
# print(f'Testing {hint_curr_exception_prefix} hint {repr(hint_curr)} for deprecation...')
warn_if_hint_pep_deprecated(
hint=hint_curr, warning_prefix=_EXCEPTION_PREFIX)
warn_if_hint_pep484_deprecated(
hint=hint_curr, exception_prefix=_EXCEPTION_PREFIX)

#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# NOTE: Whenever adding support for (i.e., when generating code
Expand Down
2 changes: 1 addition & 1 deletion beartype/_check/convert/convsanify.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
)
from beartype._check.convert.convreduce import reduce_hint
from beartype._conf.confcls import BeartypeConf
from beartype._data.error.dataerrmagic import EXCEPTION_PLACEHOLDER
from beartype._data.func.datafuncarg import ARG_NAME_RETURN
from beartype._data.hint.datahinttyping import TypeStack
from beartype._util.cache.map.utilmapbig import CacheUnboundedStrong
from beartype._util.error.utilerrorraise import EXCEPTION_PLACEHOLDER
from beartype._util.hint.pep.proposal.pep484585.utilpep484585func import (
reduce_hint_pep484585_func_return)

Expand Down
8 changes: 4 additions & 4 deletions beartype/_conf/_confget.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
SHELL_VAR_CONF_IS_COLOR_NAME,
SHELL_VAR_CONF_IS_COLOR_VALUE_TO_OBJ,
)
from beartype._util.error.utilerrwarn import issue_warning
from beartype._util.os.utilosshell import get_shell_var_value_or_none
from beartype._util.text.utiltextjoin import join_delimited_disjunction
from warnings import warn

# ....................{ GETTERS }....................
def get_is_color(is_color: BoolTristateUnpassable) -> BoolTristate:
Expand Down Expand Up @@ -115,15 +115,15 @@ def get_is_color(is_color: BoolTristateUnpassable) -> BoolTristate:
):
# Warn the caller that @beartype non-fatally resolved this conflict
# by ignoring this parameter in favour of this environment variable.
warn(
(
issue_warning(
cls=BeartypeConfShellVarWarning,
message=(
f'Beartype configuration parameter "is_color" '
f'value {repr(is_color)} ignored in favour of '
f'environment variable '
f'"${{{SHELL_VAR_CONF_IS_COLOR_NAME}}}" '
f'value {repr(is_color_override)}.'
),
BeartypeConfShellVarWarning,
)

# Override the value of the passed "is_color" parameter with
Expand Down
Empty file.
81 changes: 81 additions & 0 deletions beartype/_data/error/dataerrmagic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **magic error substrings** (i.e., string constants intended to be
embedded in exception and warning messages or otherwise pertaining to exceptions
and warnings).
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ CODE ~ operator }....................
EXCEPTION_PLACEHOLDER = '$%ROOT_PITH_LABEL/~'
'''
Non-human-readable source substring to be globally replaced by a human-readable
target substring in the messages of memoized exceptions passed to the
:func:`reraise_exception` function.
This substring prefixes most exception messages raised by memoized callables,
including code generation factories memoized on passed PEP-compliant type hints
(e.g., the :mod:`beartype._check` and :mod:`beartype._decor` submodules). The
:func:`beartype._util.error.utilerrraise.reraise_exception_placeholder` function
then dynamically replaces this prefix of the message of the passed exception
with a human-readable synopsis of the current unmemoized exception context,
including the name of both the currently decorated callable *and* the currently
iterated parameter or return of that callable for aforementioned code generation
factories.
Usage
-----
This substring is typically hard-coded into non-human-readable exception
messages raised by low-level callables memoized with the
:func:`beartype._util.cache.utilcachecall.callable_cached` decorator. Why?
Memoization prohibits those callables from raising human-readable exception
messages. Why? Doing so would require those callables to accept fine-grained
parameters unique to each call to those callables, which those callables would
then dynamically format into human-readable exception messages raised by those
callables. The standard example would be a ``exception_prefix`` parameter
labelling the human-readable category of type hint being inspected by the
current call (e.g., ``@beartyped muh_func() parameter "muh_param" PEP type hint
"List[int]"`` for a ``List[int]`` type hint on the `muh_param` parameter of a
``muh_func()`` function decorated by the :func:`beartype.beartype` decorator).
Since the whole point of memoization is to cache callable results between calls,
any callable accepting any fine-grained parameter unique to each call to that
callable is effectively *not* memoizable in any meaningful sense of the
adjective "memoizable." Ergo, memoized callables *cannot* raise human-readable
exception messages unique to each call to those callables.
This substring indirectly solves this issue by inverting the onus of human
readability. Rather than requiring memoized callables to raise human-readable
exception messages unique to each call to those callables (which we've shown
above to be pragmatically infeasible), memoized callables instead raise
non-human-readable exception messages containing this substring where they
instead would have contained the human-readable portions of their messages
unique to each call to those callables. This indirection renders exceptions
raised by memoized callables generic between calls and thus safely memoizable.
This indirection has the direct consequence, however, of shifting the onus of
human readability from those lower-level memoized callables onto higher-level
non-memoized callables -- which are then required to explicitly (in order):
#. Catch exceptions raised by those lower-level memoized callables.
#. Call the :func:`reraise_exception_placeholder` function with those
exceptions and desired human-readable substrings. That function then:
#. Replaces this magic substring hard-coded into those exception messages
with those human-readable substring fragments.
#. Reraises the original exceptions in a manner preserving their original
tracebacks.
Unsurprisingly, as with most inversion of control schemes, this approach is
non-intuitive. Surprisingly, however, the resulting code is actually *more*
elegant than the standard approach of raising human-readable exceptions from
low-level callables. Why? Because the standard approach percolates
human-readable substring fragments from the higher-level callables defining
those fragments to the lower-level callables raising exception messages
containing those fragments. The indirect approach avoids percolation, thus
streamlining the implementations of all callables involved. Phew!
'''
10 changes: 7 additions & 3 deletions beartype/_decor/decorcore.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeException
from beartype._conf.confcls import BeartypeConf
from beartype._data.hint.datahinttyping import BeartypeableT
from beartype._data.hint.datahinttyping import (
BeartypeableT,
TypeWarning,
)
from beartype._decor._decornontype import beartype_nontype
from beartype._decor._decortype import beartype_type
from beartype._util.cls.utilclstest import is_type_subclass
from beartype._util.error.utilerrwarn import issue_warning
from beartype._util.text.utiltextlabel import (
label_exception,
label_object_context,
Expand Down Expand Up @@ -212,7 +216,7 @@ def _beartype_object_nonfatal(
# into a non-fatal warning for nebulous safety.
except Exception as exception:
# Category of warning to be emitted.
warning_category = conf.warning_cls_on_decorator_exception
warning_category: TypeWarning = conf.warning_cls_on_decorator_exception # type: ignore[assignment]
assert is_type_subclass(warning_category, Warning), (
f'{repr(warning_category)} not warning category.')

Expand Down Expand Up @@ -251,7 +255,7 @@ def _beartype_object_nonfatal(
)

# Emit this message under this category.
warn(warning_message, warning_category)
issue_warning(cls=warning_category, message=warning_message)

# Return this object unmodified, as @beartype failed to successfully wrap
# this object with a type-checking class or callable. So it goes, fam.
Expand Down

0 comments on commit c5a435d

Please sign in to comment.