Skip to content

Commit

Permalink
Throw shade at bad boi callables.
Browse files Browse the repository at this point in the history
This commit improves the readability of exceptions raised by edge-case
`@beartype`-decorated callables annotated by one or more forward
references while also failing to define the `__module__` dunder
attribute, resolving issue #381 kindly submitted by dynamical Bostonian
and bonafide real-life main character @femtomc (McCoy R. Becker). For
unknown (and probably indefensible) reasons, the third-party
`markdown-exec` package appears to define such horrifying callables.
When confronted with such horrifying callables, the `@beartype`
decorator now raises human-readable exceptions resembling:

```python
beartype.roar.BeartypeDecorHintForwardRefException: Function muh_func()
parameter "muh_arg" forward reference type hint "MuhType" unresolvable,
as "muh_func.__module__" dunder attribute undefined (e.g., due to
<function muh_func at 0x7f030d402160> being defined only dynamically
in-memory). So much bad stuff is happening here all at once that
@beartype can no longer cope with the explosion in badness.
```

Flex those burly exception messages, @beartype! Flex 'em! (*A flexible lexicon on iconic nicks!*)
  • Loading branch information
leycec committed May 21, 2024
1 parent d1d6f55 commit b215401
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 8 deletions.
42 changes: 34 additions & 8 deletions beartype/_check/forward/fwdmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
get_func_globals,
get_func_locals,
)
from beartype._util.module.utilmodget import get_object_module_name
from beartype._util.module.utilmodget import get_object_module_name_or_none
from beartype._util.py.utilpyversion import IS_PYTHON_AT_MOST_3_9
from beartype._util.utilobject import get_object_name
from builtins import __dict__ as func_builtins # type: ignore[attr-defined]

# ....................{ RESOLVERS }....................
Expand Down Expand Up @@ -249,13 +250,6 @@ def resolve_hint(
# Localize metadata for readability and efficiency. Look. Just do it.
cls_stack = decor_meta.cls_stack

# Fully-qualified name of the module declaring the decorated callable,
# which also serves as the name of this module and thus global scope.
func_module_name = get_object_module_name(func) # type: ignore[operator]

# Global scope of the decorated callable.
func_globals = get_func_globals(func=func, exception_cls=exception_cls)

# If the decorated callable is nested (rather than global) and thus
# *MAY* have a non-empty local nested scope...
if decor_meta.func_wrappee_is_nested:
Expand Down Expand Up @@ -438,6 +432,38 @@ def resolve_hint(
else:
func_locals = DICT_EMPTY

# Fully-qualified name of the module declaring the decorated callable if
# that callable defines the "__module__" dunder attribute *OR* "None"
# (i.e., if that callable fails to define that attribute).
func_module_name = get_object_module_name_or_none(func) # type: ignore[operator]

# If the decorated callable fails to define the "__module__" dunder
# attribute, there exists *NO* known module against which to resolve
# this stringified type hint. Since this implies that this hint *CANNOT*
# be reliably resolved, raise an exception.
#
# Note that this is an uncommon edge case that nonetheless occurs
# frequently enough to warrant explicit handling by raising a more
# human-readable exception than would otherwise be raised (e.g., if the
# lower-level get_object_module_name() getter were called instead
# above). Notably, the third-party "markdown-exec" package behaved like
# this -- and possibly still does. See also:
# https://github.com/beartype/beartype/issues/381
if not func_module_name:
raise exception_cls(
f'{exception_prefix}forward reference type hint "{hint}" '
f'unresolvable, as '
f'"{get_object_name(func)}.__module__" dunder attribute '
f'undefined (e.g., due to {repr(func)} being defined only '
f'dynamically in-memory). '
f'So much bad stuff is happening here all at once that '
f'@beartype can no longer cope with the explosion in badness.'
)
# Else, the decorated callable defines that attribute.

# Global scope of the decorated callable.
func_globals = get_func_globals(func=func, exception_cls=exception_cls)

# Forward scope compositing this global and local scope of the decorated
# callable as well as dynamically replacing each unresolved attribute of
# this stringified type hint with a forward reference proxy resolving
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ def test_pep484_ref_decor_fail() -> None:
from beartype.roar import BeartypeDecorHintForwardRefException
from beartype_test._util.pytroar import raises_uncached

# ..................{ CALLABLES }..................
def of_oceans(mountainous_waste: 'ToMutualWar'):
'''
Arbitrary callable annotated by an **unresolvable relative forward
reference** (i.e., unqualified name of a user-defined type that is
*never* defined in either local or global scope).
'''

return mountainous_waste

# Maliciously delete the "__module__" dunder attribute of that callable to
# exercise an edge case below.
del of_oceans.__module__

# ..................{ FAIL }..................
#FIXME: Uncomment if and when a future Python release unconditionally
#enables some variant of PEP 563... yet again.
Expand Down Expand Up @@ -182,6 +196,14 @@ def deep_hearts_core(i_hear_it: (
'While.I.stand.on.the.roadway.or.on.the.pavements.0grey')):
return i_hear_it

# Assert @beartype raises the expected exception when decorating a callable
# annotated by a syntactically valid forward reference type hint when the
# caller maliciously deletes the "__module__" dunder attribute of that
# callable *AFTER* defining that callable but before decorating that
# callable by @beartype.
with raises_uncached(exception_cls):
beartype(of_oceans)


def test_pep484_ref_call_fail() -> None:
'''
Expand Down

0 comments on commit b215401

Please sign in to comment.