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
[Feature Request] Generalize beartype.claw.beartype_this_package()
to transparently support unpackaged scripts as an internal fallback
#320
Comments
...heh. Painting outside the lines, huh? At this point, most Python projects would probably start screeching that "you're doing it wrong," your team is morally bankrupt, and everyone should feel bad. Thankfully, we disagree. This is @beartype – the Fightin' Man's Type-checker. We strongly support your rebelliousness here. I myself live in a claptrap shed in the Canadian wilderness. I voluntarily run Gentoo Linux. It don't get more fightin' man. Of course, it's not simply fightin' man ethics; you're also grappling with crippling technical debt and exploding maintenance burden. You can't refactor, because PyCharm hates you, your coworkers grimace every time the new guy commits well-intended but ultimately broken improvements, and like not being homeless. Thankfully, I've also tested @beartype against your real-world use case. Congratulations to all of us! @beartype behaves as expected and fully supports what you want to do. We now breath a feels-good sigh of relief. 😌 You are now thinking:
Let @beartype Massage Those Worries AwaySo, I replicated your ad-hoc-yet-awesome project structure with random gibberish of my own devising, like so: # In "src/script.py":
def _hack_sys_path() -> None:
'''
Explicitly register all files and subdirectories of the root directory
containing this top-level ``setup.py`` script to be importable modules and
packages (respectively) for the remainder of this Python process if this
directory has yet to be registered.
'''
# Avert thy eyes, purist Pythonistas!
import os, sys
# Absolute dirname of this directory inspired by this StackOverflow answer:
# https://stackoverflow.com/a/8663557/2809027
setup_dirname = os.path.join(
os.path.dirname(os.path.realpath(__file__)), '..')
# If the current PYTHONPATH does *NOT* already contain this directory...
if setup_dirname not in sys.path:
# Print this registration.
print(f'Registering project "{setup_dirname}" for importation...')
# Append this directory to the current PYTHONPATH.
sys.path.append(setup_dirname)
# Kludge us up the bomb.
_hack_sys_path()
# Subject this package to beartype-based runtime-static type-checking.
from beartype.claw import beartype_package
beartype_package('src')
# Import type-checked nonsense. Then call type-checked nonsense.
from src.lib import broke_af
print(broke_af()) And then: # In "src/lib.py":
def broke_af() -> int:
return "Broke af func ain't so dope af." That's it. Our $ python src/script.py # <-- do it, @beartype. do it for johnny.
Registering project "/home/leycec/tmp/pogo/src/.." for importation...
Traceback (most recent call last):
File "/home/leycec/tmp/pogo/src/script.py", line 45, in <module>
print(broke_af())
^^^^^^^^^^
File "<@beartype(src.lib.broke_af) at 0x7fd27c6589a0>", line 24, in broke_af
beartype.roar.BeartypeCallHintReturnViolation: Function src.lib.broke_af() return
"Broke af func ain't so dope af." violates type hint <class 'int'>, as str
"Broke af func ain't so dope af." not instance of int. We see that our No One Asked for This MonologueThe core takeaways here are:
Painting outside the lines is, indeed, fraught with pain. Personally, I'd:
That's probably the optimal long-term solution, honestly. Why? It preserves backward compatibility in the short term (good) while also preserving standard Python package semantics in the long term (gooder) and avoiding bothersome issues like this for your team forevermore (goodest). Thanks for All the Fishy CodeThis isn't so much a feature request or outstanding issue as it is an AMA on the intersection of Python packaging, scripting, and import hooks. That's not necessarily a bad thing, of course. This is a fascinating conundrum. But it does kinda lie outside the purview of community standards and norms. Most Pythonistas would regard this sort of thing as a code smell, code debt, and devtool antipattern. Thanks for the intriguing puzzle, @komodovaran. Let me know if you've got any other questions, concerns, or compulsions to break @beartype over your knee. Until then, let's quietly close this to prevent anyone else from getting any similarly bright ideas. 😉 |
You were totally right, @komodovaran! @beartype was communicating poorly with everybody about this. I hang my head and sigh publicly. 😮💨 More importantly, I did something about this. Exception messages raised by the $ python3.12 src/script.py
beartype.roar.BeartypeClawHookUnpackagedException: Top-level script
"/home/leycec/tmp/pogo/src/script.py" resides outside package structure.
Consider calling another "beartype.claw" import hook. However, note that only
other modules will be type-checked. "/home/leycec/tmp/pogo/src/script.py" itself
will remain unchecked. All business logic should reside in submodules
subsequently imported by "/home/leycec/tmp/pogo/src/script.py": e.g.,
# Instead of this at the top of "/home/leycec/tmp/pogo/src/script.py"...
from beartype.claw import beartype_this_package # <-- you are here
beartype_this_package() # <-- feels bad
# ...pass the basename of the "src/" subdirectory explicitly.
from beartype.claw import beartype_package # <-- you want to be here
beartype_package("src") # <-- feels good
from src.main_submodule import main_func # <-- still feels good
main_func() # <-- *GOOD*! "beartype.claw" type-checks this
some_global: str = 0xFEEDFACE # <-- *BAD*! "beartype.claw" ignores this
This has been a message from your friendly neighbourhood bear. You've probably already resolved this on your end – or just abandoned @beartype altogether. Still, @beartype now tries a bit harder to help. Thanks so much for getting us started on this. 😅 |
This commit significantly improves exceptions raised by the `beartype.claw.beartype_this_package()` import hook for common use cases, resolving both issue #320 kindly submitted by Copenhagen superstar @komodovaran (Johannes Thomsen) and forum thread #330 kindly submitted by long-time `typing` fiend @JWCS (Jude). Specifically, `beartype_this_package()` now raises exception messages resembling the following when called directly from: * Top-level scripts: ```python beartype.roar.BeartypeClawHookUnpackagedException: Top-level script "/home/leycec/tmp/src/script.py" resides outside package structure. Consider calling another "beartype.claw" import hook. However, note that only other modules will be type-checked. "/home/leycec/tmp/src/script.py" itself will remain unchecked. All business logic should reside in submodules subsequently imported by "/home/leycec/tmp/src/script.py": e.g., # Instead of this at the top of "/home/leycec/tmp/src/script.py"... from beartype.claw import beartype_this_package # <-- you are here beartype_this_package() # <-- feels bad # ...pass the basename of the "src/" subdirectory explicitly. from beartype.claw import beartype_package # <-- you want to be here beartype_package("src") # <-- feels good from src.main_submodule import main_func # <-- still feels good main_func() # <-- *GOOD*! "beartype.claw" type-checks this some_global: str = 0xFEEDFACE # <-- *BAD*! "beartype.claw" ignores this This has been a message from your friendly neighbourhood bear. ``` * Top-level modules: ```python Top-level module "main.py" resides outside package structure but was *NOT* directly run as a script. "beartype.claw" import hooks require that modules either reside inside a package structure or be directly run as scripts. Since neither applies here, you are now off the deep end. @beartype no longer has any idea what is going on, sadly. Consider directly decorating classes and functions by the @beartype.beartype decorator instead: e.g., # Instead of this at the top of "main"... from beartype.claw import beartype_this_package # <-- you are here beartype_this_package() # <-- feels bad \n' # ...go old-school like it's 2017 and you just don't care. from beartype import beartype # <-- you want to be here @beartype # <-- feels good, yet kinda icky at same time def spicy_func() -> str: ... # <-- *GOOD*! @beartype type-checks this some_global: str = 0xFEEDFACE # <-- *BAD*! @beartype ignores this, but what can you do For your safety, @beartype will now crash and burn. ``` Unrelatedly, this commit also revises our ReadTheDocs (RTD)-hosted FAQ entry on `pytest-beartype` to document configuration via standard `pyproject.toml` files, resolving issue #327 kindly submitted by spaghetti-loving Bay Area pasta guru @jamesbraza (James Braza). (*Watery warts on a tart rotisserie!*)
@leycec @komodovaran
Convert yonder given executable #!/usr/bin/env python
from lib1 import lib1_fn
from scripts import helper
some_global: str = 0xFEEDFACE
def main():
print("Where was the beartype!?")
if __name__ == '__main__':
import sys
main(*sys.argv)
# Note, this is implicit: sys.exit(0) To the incredibly scandalous ordering of: #!/usr/bin/env python
if __name__ == '__main__':
# This self-import is req to trigger ../__init__.py, and beartype!
from scripts import ex as this_module
import sys
this_module.main(*sys.argv)
sys.exit(0) # Implicit in the other case
from lib1 import lib1_fn
from scripts import helper
some_global: str = 0xFEEDFACE
def main():
print("Note, this shouldn't run, bc of the beartype exception on some_global") Oh, did I mention the #!/usr/bin/env python
# scripts/__init__.py
from beartype.claw import beartype_this_package
beartype_this_package() Because I saw the above solution... and nah fam |
WOAH. You have just entered the event horizon of Seriously, though. How did you even think of that "special" darkness? If self-importing the script you're currently running isn't the Python Highway to Hell™, I don't know what is. And I know what is. This is like the @beartype codebase all over again.
😆 😄 🥲 😢 😭 I'm tempted to officially document this somewhere. Then I remember that I would be personally responsible for destroying all Python scripting. I slowly back away from the edge. Gibbering whispers can be heard from the yawning abyss below. ...hurk gurgle... |
It was pretty easy:
For my projects, the ratio of scripts to helper files is 50/50, so I'll probably be rolling this out everywhere 😬 But, you speak of event horizons? Truly eldritch implementations? DID HE ASK FOR MORE # But, we could further do away with the args by invoking `inspect`
def beartype_this_executable(file_path, module_name):
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
module.__name__ = '__main__'
spec.loader.exec_module(module)
sys.exit(0) But yeah, a "For now here are the 'ok-ish' options" bit of documentation might be helpful; this is a bit buried (yet existing). |
proceeds to list five hard bullet points
"Drop a solution, run for the lakes" The next time my wife begs me to do something about the rotting food and dish piles, my one-liner is preprepared.
I know not what I speak of! I recant! I dissemble! No, not that! Anything but... the eldritch abominaeeeeeeeeeeeeeee— 💀
Actually, that's ingenious. Let's reopen this as a new feature request to improve the fetid intelligence of
I see no fault with black magic. Let thy minions of darkness arise, Overlord @JWCS! Geh heh heh heh. |
beartype.claw.beartype_this_package()
to transparently support unpackaged scripts as an internal fallback
My setup is as follows:
And I run my script (using PyCharm, which does not really like
-m
it seems) the script-y way asAdditionally, my
sys.path
contains the full path to/
and/src
so I can go wild with imports. Whether this is kosher or not is beside the point. The thing is that it works. I can importlib
intoscript
, and I can import bothscript
andlib
intotest_script
without breaking any qualified names anywhere.I tried to add the
beartype
magical hooks everywhere to see if it would make an aging codebase compatible out of the box. In the init. In the file. I also tried with bothsrc.script
,src
. Yet none of it works....Except if I run the script like
python -m script
from a separate terminal. This is certainly doable but it's enough friction that a bunch of developers with already-configured hotkeys in their editor would just... not usebeartype
. We're talking decades of accumulated muscle memory here.So what can I do? Is this a feature request that hasn't been raised before or am I trying to do the impossible?
The text was updated successfully, but these errors were encountered: