From b215401bc932bf9dc09884bbceb9b7142dc5c9c0 Mon Sep 17 00:00:00 2001 From: leycec Date: Tue, 21 May 2024 02:23:09 -0400 Subject: [PATCH] Throw shade at bad boi callables. 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 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!*) --- beartype/_check/forward/fwdmain.py | 42 +++++++++++++++---- .../a40_code/a90_pep/pep484/test_pep484ref.py | 22 ++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/beartype/_check/forward/fwdmain.py b/beartype/_check/forward/fwdmain.py index 235303f6..6ff72e8f 100644 --- a/beartype/_check/forward/fwdmain.py +++ b/beartype/_check/forward/fwdmain.py @@ -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 }.................... @@ -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: @@ -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 diff --git a/beartype_test/a00_unit/a70_decor/a40_code/a90_pep/pep484/test_pep484ref.py b/beartype_test/a00_unit/a70_decor/a40_code/a90_pep/pep484/test_pep484ref.py index e3ca691b..1b4f91dc 100644 --- a/beartype_test/a00_unit/a70_decor/a40_code/a90_pep/pep484/test_pep484ref.py +++ b/beartype_test/a00_unit/a70_decor/a40_code/a90_pep/pep484/test_pep484ref.py @@ -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. @@ -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: '''