beartype / pytest-beartype

A pytest plugin to automatically run `beartype` on your test cases.
MIT License
9 stars 1 forks source link

How to fix `Previously imported packages not checkable by beartype` #3

Closed sunildkumar closed 9 months ago

sunildkumar commented 9 months ago

Thank you for building this!!

I'm trying to use this tool to add beartype to one specific module in my project. However, I get this warning: .venv/lib/python3.10/site-packages/pytest_beartype/__init__.py:69: BeartypePytestWarning: Previously imported packages <module name> not checkable by beartype.

Is there a standard way of fixing this?

tusharsadhwani commented 9 months ago

Hey, this usually happens when the module gets imported by the Python runtime, before beartype was initialized.

Can you share some more details, eg. the exact import package/subpackage present in the error message?

sunildkumar commented 9 months ago

Right - that makes sense. For more context, I'm trying to use your tool to add beartype during testing of a django project (unfortunately this repo is private 😞 ). A django project is broken into "django apps" (see docs) which are like submodules within the django project. I tried to add testing to a single "django app" inside my project. The project has a single pyproject toml, where I specified the submodule to add beartype to, and each application within the django project has an __init__.py. I tried to solve this problem by writing my own pytest plugin that would import beartype before django had a chance to init, but that didn't work unfortunately. I understand this is very niche, but any insights or suggestions on how to better integrate pytest-beartype with django would be very appreciated!

tusharsadhwani commented 9 months ago

Sorry for the radio silence, I'll see if I can reproduce this case this week.

leycec commented 9 months ago

Likewise, apologies for the delay. I forgot to properly watch this repo. Oopsie. :woozy_face:

Probably nobody wants to hear this, but... the unfortunate reality is that pytest-beartype won't be able to automate this issue away for you, @sunildkumar. Django or somebody else has (for whatever reason) already imported your package before pytest-beartype gets its chance. There's basically nothing pytest-beartype can do about that. Since Python doesn't support module reloading, ...officially, anyway pytest-beartype can't just "unimport" your package and then quietly re-import your package with @beartype support.

That said, there is still something you can do about this – but you'll have to get your hands a little dirty. I like dirt under my fingernails, but not everybody does. Your own tolerance for special dark magic may vary. If you're still interested, you can:

  1. Manually detect in {your_package}.__init__ whether you are currently running tests (i.e., whether your test package has been imported or not).
  2. If so, call beartype_this_package().

Special Dark Magic!

The code is actually super trivial. So, I'd at least consider trying this on your end:

# In your top-level "{your_package}.__init__" submodule:
import sys

# If this package's test suite is currently being run, runtime type-check this package.
if 'test' in sys.modules:  # <-- replace 'test' with your top-level test directory name
    from beartype.claw import beartype_this_package
    beartype_this_package()

That approach also has the added benefit of configuration. That is, you can explicitly configure @beartype with app-specific logic by passing a custom conf=BeartypeConf(...) parameter to the above beartype_this_package() call. On the other hand, pytest-beartype supports no configuration.

And... I'm spent. Let's sleep, everybody. :sleepy:

sunildkumar commented 9 months ago

Lovely! I'll try it out soon and update this issue appropriately. Thanks!

sunildkumar commented 9 months ago

Unfortunately the dark magic failed, as django practices a darker insidious magic 😞. I was able to verify this, as manually annotating functions with @beartype worked. So something with beartype_this_package() doesn't play nice with how django initializes stuff.

Thank you for trying! You're welcome to close this issue.

leycec commented 9 months ago

Wait! Hold the Django presses, @sunildkumar. An even darker magic avails us. Beartype actually supports multiple types of import hooks. beartype_this_package() is just the most syntactically convenient one, which is why our documentation pushes it everywhere. As you've noted, however, it tends to not play nicely with big dynamic frameworks like Django.

If you'll bear ...heh with me for a moment, would you mind trying one final act of desperation? Please replace beartype_this_package() with beartype_package('{your_package}'): e.g.,

# In your top-level "{your_package}.__init__" submodule:
import sys

# If this package's test suite is currently being run, runtime type-check this package.
if 'test' in sys.modules:  # <-- replace 'test' with your top-level test directory name
    print('Let's make sure something is actually happening, people.')  # <-- just to verify that "if" statement is doing something

    from beartype.claw import beartype_package
    beartype_package('{your_package}')  # <-- replace '{your_package}' with your top-level main directory name

Did the print() statement happen? Did beartype_package() still do absolutely nothing? Tune in next time to find out the stunning answer to these questions and more – including, "Why did @leycec eat that five day-old baloney sandwich in the fridge, anyway?"

sunildkumar commented 9 months ago

Still no luck 😞. I actually added a print to your original suggestion to verify the code was running, and it was. Even unconditionally using either of the import hooks fails, e.g.

# in __init__.py
from beartype.claw import beartype_package
beartype_package('my_package')

and

# in __init__.py
from beartype.claw import beartype_this_package
beartype_this_package()

fail to catch a poorly annotated function. However, beartype easily saves the day when I decorate it directly.

I also tried some other dark magic, like automatically adding the decorator to every function in the module via some code in init, but that didn't play nice with django either.

leycec commented 9 months ago

...ugh. Thanks so much for going the extra mile – and then going beyond that mile to a new plateau of exhaustion. Indeed, Django appears to be doing something perfidious with respect to imports. Clearly, this is a low-level @beartype + Django issue of some sort. My kidneys are hurting.

Let's open a new feature request over at the @beartype issue tracker helpfully tracking this Unidentified Import Phenomena (UIP). Even if it takes us several decades, we will resolve this issue shortly before collapsing in a tragic cross-country skiing accident in British Columbia.