Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Internal error when using beartype in contexts where a callable / class doesn't have __module__ defined #381

Closed
femtomc opened this issue May 20, 2024 · 10 comments

Comments

@femtomc
Copy link

femtomc commented May 20, 2024

Hi!

I'm attempting to show users of my library what a beartype error will look like, if they trip over it.

I have some code:

from my_lib.typing import typecheck, Int

@typecheck # beartype
def f(x: Int):
    return x + 1.0

try:
    f(1.0)
except TypeError as e:
    print(e)

Now, if I execute this inside my documentation (which is using mkdocs, and markdown-exec https://github.com/pawamoy/markdown-exec) -- I get what I think is an internal beartype error rendered into my documentation:

Traceback (most recent call last):
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/markdown_exec/formatters/python.py", line 59, in _run_python
    exec(compiled, exec_globals)  # noqa: S102
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<code block: session core; n551>", line 3, in <module>
    @typecheck
      ^^^^^^^^^
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_decor/decorcache.py", line 130, in beartype_confed
    return beartype_object(obj, conf)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_decor/decorcore.py", line 87, in beartype_object
    _beartype_object_fatal(obj, conf=conf, **kwargs)
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_decor/decorcore.py", line 136, in _beartype_object_fatal
    beartype_nontype(obj, **kwargs)  # type: ignore[return-value]
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_decor/_decornontype.py", line 182, in beartype_nontype
    return beartype_func(obj, **kwargs)  # type: ignore[return-value]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_decor/_decornontype.py", line 247, in beartype_func
    func_wrapper_code = generate_code(bear_call)
                        ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_decor/wrap/wrapmain.py", line 118, in generate_code
    code_check_params = _code_check_args(bear_call)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_decor/wrap/_wrapargs.py", line 334, in code_check_args
    reraise_exception_placeholder(
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_util/error/utilerrraise.py", line 138, in reraise_exception_placeholder
    raise exception.with_traceback(exception.__traceback__)
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_decor/wrap/_wrapargs.py", line 189, in code_check_args
    hint = sanify_hint_root_func(
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_check/convert/convsanify.py", line 130, in sanify_hint_root_func
    coerce_func_hint_root(
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_check/convert/convcoerce.py", line 141, in coerce_func_hint_root
    hint = resolve_hint(
           ^^^^^^^^^^^^^
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_check/forward/fwdmain.py", line 254, in resolve_hint
    func_module_name = get_object_module_name(func)  # type: ignore[operator]
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mccoybecker/research/genjax/.venv/lib/python3.12/site-packages/beartype/_util/module/utilmodget.py", line 210, in get_object_module_name
    raise _BeartypeUtilModuleException(
beartype.roar._BeartypeUtilModuleException: <function f at 0x2a3546520> "__module__" dunder attribute undefined (e.g., due to being neither class nor callable).

I believe this is due to how markdown-exec works -- perhaps the __module__ dunder is not defined for objects which are defined in the context which markdown-exec executes the code.

But simply, I'm wondering if I can turn off this level of reporting -- via customizing the configuration. It also seems reasonable to expect that this could be a hard problem (it makes sense for beartype to expect that this attribute is always defined). I can then go file an issue at markdown-exec.

Environment

I'm using beartype==0.18.5.

@femtomc
Copy link
Author

femtomc commented May 20, 2024

Note that, for the above example:

print(f.__module__) # None

@leycec
Copy link
Member

leycec commented May 21, 2024

So. You bring to me a third-party horror show, huh? During the bountiful peak of Summer in a nation not known for Summer, huh? ...friends don't let friends live in Canada

This is fine. I can do this. I can code instead of garden, hike, or bicycle! I believe in myself!

@beartype Is On a Crazy Train and You're Invited

Actually, I believe I can elucidate a bit of what's happening here. What's happening here is that either my_lib or markdown-exec appears to be crazy. Clearly, you're not crazy. Thus, we assume markdown-exec to be crazy.

Specifically, someone who shall remain nameless appears to be stringifying type hints (i.e., converting normal type hints from normal usable runtime objects into useless strings that @beartype can't do much with). Like all type-checkers, @beartype treats stringified type hints as forward references to undefined classes that are subsequently defined later. @beartype resolves forward references by dynamically importing those previously undefined classes at the last possible moment from the modules defining those classes. So far so good, right?

But let's back up a bit. You might now be ruminating to yourself:

"But how do we know that someone is stringifying type hints, huh? Do we know that? Do we really?"

We do. In the traceback you so graciously copy-pasted above, we clearly see that the private beartype._check.convert.convcoerce submodule is calling the equally private beartype._check.forward.fwdmain.resolve_hint() function to resolve a forward reference as just described. So here's where good things break down.

What forward reference could possibly be resolved here? I'm not quite sure, honestly. I only see a single type hint my_lib.typing.Int annotating a single parameter x of a single function f(). Int doesn't particularly look like a forward reference – but is it? What is Int, exactly? This is what I'm asking while biting my lower lip in consternation:

# Is "Int" a class defined something like this...?
class Int(...): ...

# ...or is "Int" a string referring to a class that has yet to be defined like this...?
Int = 'GehHehHehYouGoAndJustDieBeartype'

I'm assuming the former. The latter would be kinda weird, right? And you're not weird, unlike me. You live in Boston. Everyone cool lives in Boston!

When the Badness Get Going, the Code Get Blowing Up

So. my_lib.typing.Int is a simple class that is defined. Clearly, that's not a forward reference or a string. This can only mean one thing – and that thing is a calamity. markdown-exec (...or some other equally bad actor) is both:

  • Destroying the standard __module__ dunder attribute defined on classes and callables, which is absolutely guaranteed to break lots of stuff across the Python ecosystem (including but not limited to @beartype).
  • Destroying type hints by stringifying usable type hints into unusable strings. How? Presumably, by enabling PEP 563. How? Presumably, by forcefully injecting from __future__ import annotations at the top of your module without your permission.

Somewhere, someone who is not you is forcefully rewriting your module to resemble:

from __future__ import annotations  # <-- NO GODS WHY NOOOOOOOOOOOOOOOOOOOOOOOOOOOO
from my_lib.typing import typecheck, Int

@typecheck # beartype
def f(x: Int):
    return x + 1.0

try:
    f(1.0)
except TypeError as e:
    print(e)

@leycec Bout to Throw Down the Gauntlet on Somebody's Issue Tracker

So. Some Bad Dude™ both destroyed type hints and the standard __module__ dunder attribute, which then prevents @beartype from reconstructing said destroyed type hints. If only of those two bad things were happening, @beartype would be fine. But both of those bad things are happening. So much badness is happening all at once that @beartype can no longer reasonably cope with this dramatic explosion in badness.

Of course, this isn't to say that @beartype isn't at least partially at fault here. @beartype is raising an unreadable exception, which is totally our fault. That said, a dramatic explosion in badness is still happening. Since @beartype was given a function annotated by a forward reference but given no means of resolving that forward reference, the best that @beartype can do here is raise a more human-readable exception message resembling:

BeartypeCallHintForwardReference: Function f() parameter "x"
forward reference "Int" not resolvable, as f() dunder attribute
"__module__" undefined. So much bad stuff is happening here all
at once that @beartype can no longer cope with the explosion in badness.

Rage! Rage at the dying of the codebase, @beartype!


@beartype takes down another innocent codebase as hapless DevOps cry in fear

leycec added a commit that referenced this issue May 21, 2024
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!*)
@leycec
Copy link
Member

leycec commented May 21, 2024

Resolved by b215401... which is to say, I basically did nothing. That commit sucks so much! I fully admit it. Sadly, there's simply not much of value that @beartype can add here. The problem and thus the solution is outside our paws. All @beartype can do is raise more human-readable exceptions here.

Given this minimal-working example that exhibits a similar issue...

from beartype import beartype

def muh_func(muh_arg: 'MuhType'):
    return muh_arg + 1.0

del muh_func.__module__  # <-- *GAH!* wat u doin, markdown-exec!?!? y u do us dirty like that?

muh_func_checked = beartype(muh_func)

...@beartype now raises this slightly more readable exception:

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 0x7fd3f5402160> 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.

I now blame markdown-exec for literally everything wrong in the Universe. Exhaling face, exhale. 😮‍💨

@leycec leycec closed this as completed May 21, 2024
@pawamoy
Copy link

pawamoy commented May 21, 2024

I understand the humorous tone (and I know you have this reputation to keep @leycec), though your answers could definitely use less negative words towards a project you don't know, whether it's why it does what it does, or how it does it 😉

For the explanation: markdown-exec executes code in code blocks, for documentation purposes (literate programming if you like). For Python code blocks, it takes the code and execute it with exec, in the same interpreter as markdown-exec is executed. And it happens that I indeed use from __future__ import annotations in the module that uses exec, so I suppose this is leaking into the user's code 🙂 (even though I pass it a clean global context 🤔 but I think it doesn't matter to __future__.annotations). I'm not sure what I can do about that, but I'll think about it. And I'll try to confirm that my own import of future annotations is indeed leaking into executed code.

@pawamoy
Copy link

pawamoy commented May 21, 2024

and probably indefensible

Yeah I have to say I really don't like this communication style.

@leycec
Copy link
Member

leycec commented May 21, 2024

Yeah I have to say I really don't like this communication style.

It is what it is. markdown-exec definitely shouldn't be doing what it's doing. What markdown-exec is doing is plainly harmful to other third-party packages like @beartype, which then only uselessly wastes everyone else's own scarce open-source volunteerism. They need to play well with others. They're not playing well with others.

I'm happy – nay, delighted even – to call out bad actors for bad behaviour. It's simply honesty. When @beartype behaves badly (...an all too common occurrence, sadly), I expect and hope for an equal dose of honesty.

Then again, I'm literally autistic. I don't necessarily play well with others, because I praise honesty and integrity well above conviviality and sociability. You can have honesty or you can have sociability – but you can't necessarily have both. This probably explains why hierarchical institutions tend to favour sociability over honesty. As a hierarchical institution increases in scale, sociability supplies the continuous stream of ideological "glue" needed to weld disparate individuals with diverse dispositions, intentions, and expectations into a cohesive unity. Honesty's pretty much out the window at that point, huh?

Of course, even honesty can swing too far. Linus Torvalds is the open-source poster child for "honesty" so acerbic that it was really just toxic abusiveness by another word. It's a fine line. Being autistic, I neurologically have no idea where the line even is. I try never to cross the line... but all I can do is try. I try to always inject humour, good cheer, and a penchant for madcap memes into everything I do. In the end, though, I'm just fumbling around in the dark like Daredevil on all all-night circus bender.

But... yeah. markdown-exec's behaviour is indefensible. It's bad, it's wrong, and it's wasted everyone's time. That's not abuse, as I see it. That's just the truth. I see the humour in that truth, because markdown-exec meant well; everyone means well in this space. No one's here for the money, because the intersection of money and volunteerism in the empty set. We're here for the fun, the thrills, the spills, the chills, and ultimately being a part of something much greater than ourselves.

Honesty. What you gonna do, huh? 🤔

@posita
Copy link
Collaborator

posita commented May 22, 2024

@femtomc, as a work-around, have you considered something like what numerary does to allow toggling @beartype decorators at load time via an environment variable [update] @leycec suggests in his next comment?

@pawamoy, this is a bit off topic, but it can be useful to recall that human beings are diverse, with many different communication styles. One of the wonderful things about this modern world is venues like these which afford collaboration across great divides. Understanding that others' styles may differ than our own or stray slightly from our preferences can be very helpful. Admittedly, this difficult sometimes, but I find it useful to start with the assumption that we all come with the best of intentions and we're all doing the best we reasonably can. This affords connection to others without first requiring they conform to our individual ideals.

@leycec, have I ever told you you're my hero? You are the wind beneath my wings. 🪽💨

@leycec
Copy link
Member

leycec commented May 22, 2024

@femtomc: Gah! I forgot to mention the most important and useful thing while delivering an unimportant and useless manologue – which is that @beartype encourages you to manually disable it when you need to. This is one of those cases. There are innumerably many ways to accomplish this – but the best way is probably as follows: e.g.,

import sys  # <-- new imports x 1
from beartype import beartype, BeartypeConf, BeartypeStrategy  # <-- new imports x 2
from my_lib.typing import Int

# If "markdown-exec" is currently running, reduce the @beartype decorator to a noop.
if 'markdown_exec' in sys.modules:
    typecheck = beartype(conf=BeartypeConf(strategy=BeartypeStrategy.O0))
# Else, preserve the @beartype decorator as is.
else:
    typecheck = beartype

@typecheck
def f(x: Int):
    return x + 1.0

try:
    f(1.0)
except TypeError as e:
    print(e)

Does that actually work? No idea, bro. It should – but I should have hair. "Don't worry!", they said. "Hair loss skips a generation!", they said. Sometimes, should just isn't good enough. 😭

@posita: Such a boss! Such a boss reply! Understanding intensifies. Now, let's retroactively time-travel to the past ...wait. isn't time-travel by definition retroactive? 🤔 as I beg you to deliver this empathetic pro-neurodiversity wall of inspirational speech to all of my prior workplaces. We can finish the night at an all-you-can-stuff-into-your-face izakaya in downtown Manhattan. I might still live and work in the United States if we establish this time loop right now. My Canadian wife is wailing in anguish, though! Noooooooooooo. And... on second thought, let's not do this. The space-time continuum is warped enough. 😅


Left: Young Man @posita wearing fashionable shades. Right: Old Man @leycec wearing fashionable wig.

@pawamoy
Copy link

pawamoy commented May 22, 2024

@posita thanks for chiming in. I do recall that human beings are diverse, with many different communication styles. That is exactly why I allowed myself to express that I was uncomfortable with the way @leycec put things. I don't ask, and will never ask of anyone to conform to a particular communication style. You do what you want. Note that I wrote communication and not writing: I actually like @leycec's writing style. I did smile when reading your answers @leycec. But to me they're just full of assumptions that something does what it does for a reason, while we all know that the software world is riddled with bugs and unexpected/accidental behavior. You actually don't need to call out "bad actors", because we're all "bad actors" at some point. I'd prefer we keep this negative wording for actually bad/malicious actors. Or just, you know, don't use it in the entirety of our comments towards someone else's project. There's a way to be funny without being mean and making assumptions. I hope you'll be able to use my answer as a data point to know where my line is (and maybe others') in the future, if you even care, instead of doubling-down on assumptions and negative wording 🤷

uselessly wastes everyone else's own scarce open-source volunteerism
it's wasted everyone's time

These are completely insensitive things to say to other open-source maintainers.

What you gonna do, huh?

...nothing.

@posita
Copy link
Collaborator

posita commented May 22, 2024

There's a way to be funny without being mean and making assumptions.

At the risk of venturing waaay off topic. I concede that humor (perhaps especially written exaggeration or irony) resonates very differently with different people. I accept this isn't your cup of tea. With all love and respect, I also don't see the presence of mal-intent anywhere in this thread. I'm willing to stake my career that @leycec does not take pleasure in the suffering of others, and doubt very much you do either. Perhaps we can all give each other the benefit of the doubt? ☺️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants