beartype
beartype copied to clipboard
[Feature request] Automagical Import Hook
@ZeevoX of SentiPy and Sentiment Investor fame kindly requested automation to automagically decorate all callables in a given package with @beartype
without needing to explicitly do so:
Is it necessary to annotate each function with @beartype? Is there a way of more concisely adding beartype everywhere automagically?
This feature request tracks our eventual (hopefully not too eventual) solution: a beartype import hook. Specifically:
- We need to implement a new AST-based
beartype.hook.beartype_everything()
import hook inspired by typeguard's comparabletypeguard.importhook.install_import_hook()
function. That's only 162 lines of pure Python, which means this is mostly trivial, but it's a dense 162 lines, which means this might take an undefinable quantum of space-time. - Interested third parties will then call our import hook at the top of their top-level
__init__
submodules to globally and transparently apply@beartype
across their entire codebases.
Grab that popcorn! @leycec's Wild Ride is now boarding for an improved @beartype
user experience (UX).
Having this usable as a PyTest extension to decorate all PyTest testsuite calls like TypeGuard does would be automagical. One of our projects used to use Typeguard's PyTest setup for the CI, but we had to drop it because it was too slow with our PyTorch tests.
Oh. Oh, yes. Thanks for the instructive heads up on typeguard's --typeguard-packages
pytest option, flying lion with a secretive British license to kill. Allow me to publicly applaud your GitHub avatar as well, which may very well be the cutest thing I have ever seen – and I watch anime. Voluntarily.
I did notice typeguard
surprisingly installed a pytest extension when I packaged typeguard
for Gentoo Linux a year ago, but never actually found the scarce time to dig into what exactly that extension was doing. Now I know and feel modestly smarter for it.
A beartype
pytest extension is an even higher-level abstraction and more automagical than an import hook. Great! This will automate the import hook that automates the decoration that automates the type-checking. The automation is gettin' crazy.
I've added yet another feature request tracking a pytest extension so I feel slightly less overwhelmed. It probably won't help. :sweat_smile:
@langfield, @justinchuby, @BowenBao, @k4ml:
Hi @leycec I am currently running install_import_hook from typeguard in our unit tests to type check a large codebase. We would like to switch to beartype. If you need any help to test this new hook you are making, and hopefully it's a direct equivalent, that would be a good opportunity to flush out some bugs before release.
The sad thing is that I couldn't find any the doc explaining how to do that.
To make things clear, we call install_import_hook()
at the top of our conftest.py. I found that using the pytest plugin of typeguard didn't have the same effect at all (it seems to only typecheck the main python module? And not the modules below it?)
We would like to switch to
beartype
.
:partying_face:
...hopefully it's a direct equivalent...
It is! Everybody's in luck. The upcoming beartype.claw
API ...get it, claw? hook? i'll stop now. in @beartype 0.15.0 (to be released shortly for certain definitions of "shortly") will include directly compatible typeguard
import hooks. I think.
Actually, I can't quite recall what the scope of the typeguard.install_import_hook()
function is anymore – but some combination of these beartype.claw
functions should get you there:
-
beartype.claw.beartype_this_package()
, runtime type-check only the current package. Typically called from{your_package}.__init__
as the first lines of that submodule. -
beartype.claw.beartype_package()
, runtime type-check any single package or module by name (e.g.,beartype_package('your_package')
). -
beartype.claw.beartype_packages()
, runtime type-check multiple packages or modules by name (e.g.,beartype_packages(('your_package.your_submodule1', 'your_package.your_submodule2')
). -
beartype.claw.beartype_all()
, runtime type-check literally everything in your app stack – including your package, other people's packages, and the entire standard library. This is likely to explode catastrophically. But there's a chance it won't. We live for that chance! :exploding_head: -
beartype.claw.beartyping()
, a context manager runtime type-checking only the imports isolated to the body of this context manager: e.g.,
# Import the requisite machinery.
from beartype.claw import beartyping
# @beartype all classes and callables (transitively) declared by any
# package or module imported in the body of this context manager.
with beartyping():
# @beartype this and everything this submodule imports, too!
from your_package import your_submodule1
# Do it again. Do it for justice.
from your_package import your_submodule2
If you need any help to test this new hook you are making...
Super high-five! That'd be swell, actually. I just finalized the draft implementation of beartype.claw
last night. This is my timetable for making this happen in a public release:
- Locally test, test, and more test. I'll spend the rest of this and next week implementing unit and integration tests exhaustively exercising this as hard I can against various edge cases. I expect fireworks and fires in a dumpster.
- Remotely test, test, and more test. I'll then throw our GitHub Actions-based continuous integration (CI) at this and see if anything vomits on another non-Linux platform or Python environment.
-
Beg kind community members such as yourself, @gabrieldemarmiesse. Once I've finished local and remote testing, it'd be wondrous if you and your team could have a brief go at the
beartype.claw
API as well. Team Bear: Assemble! :muscle: :bear: :muscle: - Release this as @beartype 0.15.0.
- Document this after releasing this on our ReadTheDocs (RTD)-hosted Sphinx doco site. Ideally, I'd author docos before releasing @beartype 0.15.0. But... this has already taken two years. The perfect has become the enemy of the good. Sometimes, you just gotta ship it. :sweat:
The sad thing is that I couldn't find any the doc explaining how to do that.
...heh. That is sad, indeed. Thankfully, there is a reasonable explanation: the docos don't exist, because beartype.claw
itself really doesn't exist. But I optimistically expect while crossing my fingers that:
-
beartype.claw
should exist by July 1st. -
beartype.claw
docos should exist by August 1st.
Thanks so much for the generous feedback and volunteer awesomeness, @gabrieldemarmiesse. We're nearly there, everybody! So cloooooose...
Ho, ho, ho. At long last, the promised day of salvation-by-API has come. The beartype.claw
subpackage is now feature complete and set to be published this Friday as @beartype 0.15.0.
Bug catchers like @gabrieldemarmiesse and his formidable band of accomplices, you know what to do. For anyone who would like to preview @beartype import hooks against your real-world codebase that is full of bugs before the fateful hype finally drops this weekend, consider:
- Locally installing our most recent commit. All tests pass. I'm certain! Probably. :crossed_fingers:
pip install git+https://github.com/beartype/beartype.git@0b880d9dff484acfc0259cd10efe169688c49c55
-
Installing a @beartype import hook. The simplest approach is to just call the
beartype.claw.beartype_this_package()
function at the head of your top-level{your_package}.__init__
submodule. As the name suggests, that function installs an import hook type-checking literally everything in{your_package}
with @beartype: e.g.,
# As the top of "{your_package}.__init__" submodule:
from beartype.claw import beartype_this_package
beartype_this_package() # <-- that's it. you're done.
Everything means everything, including PEP 526-compliant annotated variable assignments: e.g.,
# @beartype type-checks this too, now. Regardless of how deeply you nest
# annotated variable assignments in the bowels of closures wrapped in
# methods defined in classes hidden in functions, @beartype will find them
# and in the darkness of your codebase bind them.
some_number_probably: int = (
"Pretty sure this isn't a number... OR IS IT!?!? I don't even know anymore."
'Only @beartype can uncover the sordid truth. Okay. Other type-checkers'
"probably can, too. But they're slow and less complicated, which is all"
'that matters.'
if 0.01010101 > -0xFEEDFACE else
42
)
This means that @beartype is now the first usable hybrid runtime-static type-checker.
That is to say, @beartype now performs both the standard runtime type-checking you've all come to know and dolefully shake your head at and a fundamentally new mutant breed of type-checking best described as static type-checking (ala mypy or pyright
) except checked at runtime. When you're playing with @beartype import hooks, you're playing with both power and catastrophe. I promise nothing and tested even less. Release the bugs of war!
Party like it's @beartype 0.1.0 all over again. :partying_face:
I'm not sure where you want this, but I tried this quickly and had import issues with both PIL and psycopg2.
Narrowing it down, if I have a package fred
with an empty __init__.py
and only from PIL import Image
in bob.py
then I can do from fred import bob
but if I add the lines above to the __init__.py
then I get:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/gordon/fred/bob.py", line 3, in <module>
from PIL import Image
File "/home/gordon/venv/lib/python3.10/site-packages/PIL/Image.py", line 103, in <module>
from . import _imaging as core
ImportError: cannot import name '_imaging' from 'PIL' (/home/gordon/venv/lib/python3.10/site-packages/PIL/__init__.py)
ohgods. So much for the @beartype Summer Christmas Extravaganza. :face_exhaling:
Thanks so much for that detailed heads up, @tolomea. I never would have thought to test beartype.claw
against C extensions. Even after seeing the horrifying exception myself, my spongy brain refuses to believe. This must be what it feels like when your latest Dragon's Den proposal (codename: "BaconMaster 5000") belches green flames into the audience stands.
You Suck, @leycec!
Put down those automated pitchforks. Please, beloved audience! Allow me to tediously explain what's probably going on here.
All existing import hooks implemented by other third-party packages are incompatible with pytest
, because pytest
(...wait for it) installs its own import hook that tends to override everything else. Clearly, that's unacceptable. Import hooks that are incompatible with pytest
are useless import hooks, which means that all existing import hooks are kinda useless. It is sad. To circumvent this, I invented a novel and hitherto unknown kind of import hook compatible with pytest
. That's great... except we see where my blatant braggadocio is going. What I invented currently conflicts with C extension loading. That's awful!
Give me a hot minute to tinker around in the engine like a greasy code monkey. We'll get this rickety Model T driven by a growling black bear back on the road. Vrrrrrrrrroooooooooom.
C extension issue resolved... probably. Are you ~~foolish~~ brave enough to drown your weekend in retesting beartype.claw
against your formidable use case, @tolomea? If so, please flex those shiny GitHub biceps at our newest commit:
pip install git+https://github.com/beartype/beartype.git@26f936b6989766f7c4adf43c293bd95b93704515
And thanks so much! We're almost there, everybody. The Summer of @beartype continues.
Hi @leycec , i tested against my work codebase using beartype.claw.beartype_packages(["common_business"]) and the commit 26f936b6989766f7c4adf43c293bd95b93704515
. After using this, importing a pydantic class doesn't work:
from typing import Any
from fastapi import HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import asc, desc
import sqlalchemy.orm.query
from sqlalchemy.sql import Select
class SortingParameter(BaseModel):
"""This can be used in any endpoint."""
name: str
asc: bool = True
I get this stacktrace:
ImportError while loading conftest '/projects/work/scube/services/parent-package/tests/conftest.py'.
tests/conftest.py:76: in <module>
from xxxxxx.main import app
xxxxxx/main.py:6: in <module>
from xxxxxx.api.api_v1.api import api_router
xxxxxx/api/api_v1/api.py:3: in <module>
from xxxxxx.api.api_v1.endpoints import (
xxxxxx/api/api_v1/endpoints/areas_endpoint.py:19: in <module>
from common_business.utils.sorting import SortingParameter, get_order_by_parser
common_business/utils/sorting.py:10: in <module>
class SortingParameter(BaseModel):
common_business/utils/sorting.py:13: in SortingParameter
name: str
E NameError: name 'name' is not defined
Blargh! Thanks so much for the detailed heads-up, @gabrieldemarmiesse. I can confirm that @beartype has flown the cuckoo's nest in your minimal-length example. Yet again, I failed to adequately test the obvious edge case of PEP 526-compliant unassigned variable annotations.
Interestingly, this doesn't actually have anything to do with Pydantic, FastAPI, or SQLAlchemy. We can breathe a collective sigh of relief. In fact, @beartype raises the exact same exception in this even more minimal example:
asc: bool = True # <-- this is fine
name: str # <-- *THIS BLOWS UP THE ENTIRE WORLD*
That's it. That's the example. Annotated variable assignments are fine. Annotated variables without assignments are not fine. Clearly, both should be fine. This is what happens when a test suite fails.
There goes Monday night. But tonight, I studiously pretend this didn't happen and play Persona 5 Strikers for hours yet again. Since this is still busted, let's reopen for business with a smile. :smile:
Blargh-argh! Rather than brainlessly play video games, I instead brainfully resolved this. @gabrieldemarmiesse, I proudly summon you to yet again throw the ponderous bulk of your work codebase common_business
against the beartype.claw
API.
Wonderful people who @beartype is eternally indebted to like a parasitic vampire that feasts on human suffering and bugs, if you would be so kind...
pip install git+https://github.com/beartype/beartype.git@34931c1a58157daaa0518f9094e702209af86661
I humbly apologize for prior breakage and promise that a beautiful new world of usable hybrid runtime-static type-checking is almost within everyone's grasp. Your gracious volunteerism has been invaluable, @gabrieldemarmiesse and @tolomea. :hugs: :people_hugging:
@beartype 0.15.0 is now officially codenamed "Gabby Tolo" in honour of your lives.
Thanks @leycec for the quick patch. I ran the test again and it worked!
I tried something else and I got an interesting warning:
dada.py:
from pydantic import BaseModel
class Something(BaseModel):
name: str
dodo.py
import beartype.claw
beartype.claw.beartype_packages(["dada"])
from dada import Something
Something(name="some-name")
I get the following warning, which is likely not desirable with the claw api:
python dodo.py
/opt/conda/lib/python3.10/site-packages/beartype/_decor/decorcore.py:381: BeartypeClawDecorWarning: Object pydantic.json.pydantic_encoder() :
<cyfunction pydantic_encoder at 0x7f79eb7b0110> not pure-Python function.
warn(warning_message, warning_category)
I played with it a bit more and it seems it doesn't like pydantic much. Here is an example taken from the pydantic documentation: https://docs.pydantic.dev/latest/usage/models/#fields-with-dynamic-default-values
# dada.py
from uuid import UUID, uuid4
from pydantic import BaseModel, Field
class Something(BaseModel):
uid: UUID = Field(default_factory=uuid4)
# dodo.py
import beartype.claw
beartype.claw.beartype_packages(["dada"])
from dada import Something
Something()
I get
Traceback (most recent call last):
File "/opt/conda/lib/python3.10/site-packages/beartype/door/_doorcheck.py", line 147, in die_if_unbearable
_check_object(obj)
File "<@beartype(beartype.door._doorcheck._get_type_checker._die_if_unbearable) at 0x7f54db2263b0>", line 19, in _die_if_unbearable
beartype.roar.BeartypeCallHintReturnViolation: Function beartype.door._doorcheck._get_type_checker._die_if_unbearable() return "FieldInfo(annotation=NoneType, required=False, default_factory=uuid4)" violates type hint <class 'uuid.UUID'> under non-default configuration BeartypeConf(claw_is_pep526=True, is_color=None, is_debug=False, is_pep484_tower=False, reduce_decorator_exception_to_warning_category=<class 'beartype.roar._roarwarn.BeartypeClawDecorWarning'>, strategy=<BeartypeStrategy.O1: 2>), as <class "pydantic.fields.FieldInfo"> "FieldInfo(annotation=NoneType, required=False, default_factory=uuid4)" not instance of <protocol "uuid.UUID">.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/projects/open_source/test_beartype/dodo.py", line 4, in <module>
from dada import Something
File "/projects/open_source/test_beartype/dada.py", line 6, in <module>
class Something(BaseModel):
File "/projects/open_source/test_beartype/dada.py", line 7, in Something
uid: UUID = Field(default_factory=uuid4)
File "/opt/conda/lib/python3.10/site-packages/beartype/door/_doorcheck.py", line 177, in die_if_unbearable
raise BeartypeDoorHintViolation(
beartype.roar.BeartypeDoorHintViolation: Object "FieldInfo(annotation=NoneType, required=False, default_factory=uuid4)" violates type hint <class 'uuid.UUID'> under non-default configuration BeartypeConf(claw_is_pep526=True, is_color=None, is_debug=False, is_pep484_tower=False, reduce_decorator_exception_to_warning_category=<class 'beartype.roar._roarwarn.BeartypeClawDecorWarning'>, strategy=<BeartypeStrategy.O1: 2>), as <class "pydantic.fields.FieldInfo"> "FieldInfo(annotation=NoneType, required=False, default_factory=uuid4)" not instance of <protocol "uuid.UUID">.
Here obviously pydantic is guilty since a FieldInfo is not a UUID but I wonder if there is a way to make it work a bit more nicely.
I'll try to find out if there is some related issue in the pydantic repo since it seems a general issue about typing.
EDIT: Doing this fixes the issue:
from uuid import UUID, uuid4
from typing import Annotated
from pydantic import BaseModel, Field
class Something(BaseModel):
uid: Annotated[UUID, Field(default_factory=uuid4)]
But I don't know if it's technically allowed to use any arbitrary python expression in python types For more info see https://github.com/pydantic/pydantic/issues/837
I actually like this annotated syntax. When I first started with pydantic, I was upset that there were type inconsistencies everywhere.
EDIT 2:
I read the PEP and it allows any python value, so we're safe. I would advise creating a section in the bearpedia about pydantic since claw will typecheck those classes too. Explaining to users that they should be using typing.Annotated
every time they use pydantic or fastapi should help a lot
EDIT 3: Fastapi is pushing for the Annotated syntax: https://fastapi.tiangolo.com/tutorial/dependencies/#create-a-dependency-or-dependable
New report from trying the typebear the entire world:
# dada.py
from contextlib import contextmanager
from collections.abc import Generator
@contextmanager
def my_iterator() -> Generator[int, None, None]:
yield 4
# dodo.py
import beartype.claw
beartype.claw.beartype_packages(["dada"])
from dada import my_iterator
value: int
with my_iterator() as value:
print(value)
Can some smart bear tell if I'm typing the thing wrong or if the issue comes from somewhere else?
A question about the semantics, is it logical to have a function called beartype_packages
that also works with modules (which are not packages when they are single .py files)? In my example above beartype.claw.beartype_packages(["dada"])
, dada is a module name and is not a package.
Bears are running everywhere and attacking fastapi!
# dada.py
from typing import Annotated, TypeVar
from collections.abc import Iterator
from fastapi import Depends, FastAPI
Self = TypeVar("Self")
class SomeClass:
@classmethod
def my_iterator(cls: type[Self]) -> Iterator[Self]:
yield cls()
app = FastAPI()
@app.get("/items/")
async def read_items(my_value: Annotated[SomeClass, Depends(SomeClass.my_iterator)]):
assert isinstance(my_value, SomeClass)
# dodo.py
import beartype.claw
beartype.claw.beartype_packages(["dada"])
from dada import app
from fastapi.testclient import TestClient
test_client = TestClient(app)
test_client.get("/items")
That is a edgy case.
As is, I get this error:
Traceback (most recent call last):
File "/projects/open_source/test_beartype/dodo.py", line 9, in <module>
test_client.get("/items")
File "/opt/conda/lib/python3.10/site-packages/starlette/testclient.py", line 499, in get
return super().get(
File "/opt/conda/lib/python3.10/site-packages/httpx/_client.py", line 1041, in get
return self.request(
File "/opt/conda/lib/python3.10/site-packages/starlette/testclient.py", line 465, in request
return super().request(
File "/opt/conda/lib/python3.10/site-packages/httpx/_client.py", line 814, in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
File "/opt/conda/lib/python3.10/site-packages/httpx/_client.py", line 901, in send
response = self._send_handling_auth(
File "/opt/conda/lib/python3.10/site-packages/httpx/_client.py", line 929, in _send_handling_auth
response = self._send_handling_redirects(
File "/opt/conda/lib/python3.10/site-packages/httpx/_client.py", line 966, in _send_handling_redirects
response = self._send_single_request(request)
File "/opt/conda/lib/python3.10/site-packages/httpx/_client.py", line 1002, in _send_single_request
response = transport.handle_request(request)
File "/opt/conda/lib/python3.10/site-packages/starlette/testclient.py", line 342, in handle_request
raise exc
File "/opt/conda/lib/python3.10/site-packages/starlette/testclient.py", line 339, in handle_request
portal.call(self.app, scope, receive, send)
File "/opt/conda/lib/python3.10/site-packages/anyio/from_thread.py", line 283, in call
return cast(T_Retval, self.start_task_soon(func, *args).result())
File "/opt/conda/lib/python3.10/concurrent/futures/_base.py", line 458, in result
return self.__get_result()
File "/opt/conda/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
raise self._exception
File "/opt/conda/lib/python3.10/site-packages/anyio/from_thread.py", line 219, in _call_func
retval = await retval
File "/opt/conda/lib/python3.10/site-packages/fastapi/applications.py", line 282, in __call__
await super().__call__(scope, receive, send)
File "/opt/conda/lib/python3.10/site-packages/starlette/applications.py", line 122, in __call__
await self.middleware_stack(scope, receive, send)
File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
raise exc
File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
raise exc
File "/opt/conda/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
await self.app(scope, receive, sender)
File "/opt/conda/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__
raise e
File "/opt/conda/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__
await self.app(scope, receive, send)
File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 718, in __call__
await route.handle(scope, receive, send)
File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle
await self.app(scope, receive, send)
File "/opt/conda/lib/python3.10/site-packages/starlette/routing.py", line 66, in app
response = await func(request)
File "/opt/conda/lib/python3.10/site-packages/fastapi/routing.py", line 241, in app
raw_response = await run_endpoint_function(
File "/opt/conda/lib/python3.10/site-packages/fastapi/routing.py", line 167, in run_endpoint_function
return await dependant.call(**values)
File "/projects/open_source/test_beartype/dada.py", line 19, in read_items
assert isinstance(my_value, SomeClass)
AssertionError
Without beartype.claw.beartype_packages(["dada"])
, fastapi behaves normally and the script ends without any output (as expected).
So, I'm not sure if this is a bug, or if I'm just doing something wrong - I haven't used beartype before (or I did, and forgot?), and thought I'd give it a try on qutebrowser.
For a full reproducer:
$ git clone https://github.com/qutebrowser/qutebrowser
$ git checkout 8da62bcbf4e90cc3952decf72b6798540f4b9d10 # current master branch at the time of writing
$ cd qutebrowser
$ python3 -m venv .venv
$ .venv/bin/pip install -r requirements.txt PyQt5 PyQtWebEngine git+https://github.com/beartype/beartype.git@34931c1a58157daaa0518f9094e702209af86661
$ echo "import beartype.claw; beartype.claw.beartype_this_package()" >> qutebrowser/__init__.py
$ .venv/bin/python -m qutebrowser --temp-basedir
results in:
/home/florian/tmp/qutebrowser/.venv/lib/python3.11/site-packages/beartype/_util/hint/pep/utilpeptest.py:311: BeartypeDecorHintPep585DeprecationWarning: PEP 484 type hint typing.Dict[str, str] deprecated by PEP 585. This hint is scheduled for removal in the first Python version released after October 5th, 2025. To resolve this, import this hint from "beartype.typing" rather than "typing". For further commentary and alternatives, see also:
https://beartype.readthedocs.io/en/latest/api_roar/#pep-585-deprecations
warn(
Traceback (most recent call last):
File "/home/florian/tmp/qutebrowser/.venv/lib/python3.11/site-packages/beartype/door/_doorcheck.py", line 147, in die_if_unbearable
_check_object(obj)
File "<@beartype(beartype.door._doorcheck._get_type_checker._die_if_unbearable) at 0x7efc2e9ad940>", line 18, in _die_if_unbearable
beartype.roar.BeartypeCallHintReturnViolation: Function beartype.door._doorcheck._get_type_checker._die_if_unbearable() return "Field(name=None,type=None,default=<dataclasses._MISSING_TYPE object at 0x7efc2ea7f390>,defa..._ violates type hint typing.Dict[str, str] under non-default configuration BeartypeConf(claw_is_pep526=True, is_color=None, is_debug=False, is_pep484_tower=False, reduce_decorator_exception_to_warning_category=<class 'beartype.roar._roarwarn.BeartypeClawDecorWarning'>, strategy=<BeartypeStrategy.O1: 2>), as <class "dataclasses.Field"> "Field(name=None,type=None,default=<dataclasses._MISSING_TYPE object at 0x7efc2ea7f390>,defa..._ not instance of dict.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "/home/florian/tmp/qutebrowser/qutebrowser/__main__.py", line 23, in <module>
import qutebrowser.qutebrowser
File "/home/florian/tmp/qutebrowser/qutebrowser/qutebrowser.py", line 54, in <module>
from qutebrowser.misc import earlyinit
File "/home/florian/tmp/qutebrowser/qutebrowser/misc/earlyinit.py", line 47, in <module>
from qutebrowser.qt import machinery
File "/home/florian/tmp/qutebrowser/qutebrowser/qt/machinery.py", line 86, in <module>
class SelectionInfo:
File "/home/florian/tmp/qutebrowser/qutebrowser/qt/machinery.py", line 90, in SelectionInfo
outcomes: Dict[str, str] = dataclasses.field(default_factory=dict)
File "/home/florian/tmp/qutebrowser/.venv/lib/python3.11/site-packages/beartype/door/_doorcheck.py", line 177, in die_if_unbearable
raise BeartypeDoorHintViolation(
beartype.roar.BeartypeDoorHintViolation: Object "Field(name=None,type=None,default=<dataclasses._MISSING_TYPE object at 0x7efc2ea7f390>,defa..._ violates type hint typing.Dict[str, str] under non-default configuration BeartypeConf(claw_is_pep526=True, is_color=None, is_debug=False, is_pep484_tower=False, reduce_decorator_exception_to_warning_category=<class 'beartype.roar._roarwarn.BeartypeClawDecorWarning'>, strategy=<BeartypeStrategy.O1: 2>), as <class "dataclasses.Field"> "Field(name=None,type=None,default=<dataclasses._MISSING_TYPE object at 0x7efc2ea7f390>,defa..._ not instance of dict.
which got me stumped. For one, File "<@beartype(beartype.door._doorcheck._get_type_checker._die_if_unbearable) at 0x7efc2e9ad940>"
in the first selection sounds like a weird filename! :upside_down_face:.
But also, it points here:
https://github.com/qutebrowser/qutebrowser/blob/8da62bcbf4e90cc3952decf72b6798540f4b9d10/qutebrowser/qt/machinery.py#L90
which seems to be correct...
I tried extracting things into a more minimal example, by creating a pkg
folder, with an __init__.py
:
import beartype.claw
beartype.claw.beartype_all()
and an x.py
:
import dataclasses
import enum
from typing import Dict, Optional
class SelectionReason(enum.Enum):
"""Reasons for selecting a Qt wrapper."""
#: The wrapper was selected via --qt-wrapper.
cli = "--qt-wrapper"
#: The wrapper was selected via the QUTE_QT_WRAPPER environment variable.
env = "QUTE_QT_WRAPPER"
#: The wrapper was selected via autoselection.
auto = "autoselect"
#: The default wrapper was selected.
default = "default"
#: The wrapper was faked/patched out (e.g. in tests).
fake = "fake"
#: The reason was not set.
unknown = "unknown"
@dataclasses.dataclass
class SelectionInfo:
"""Information about outcomes of importing Qt wrappers."""
wrapper: Optional[str] = None
outcomes: Dict[str, str] = dataclasses.field(default_factory=dict)
reason: SelectionReason = SelectionReason.unknown
s = SelectionInfo()
and then running python -m pkg.x
, but that greets me with:
Traceback (most recent call last):
File "/home/florian/tmp/.venv/lib/python3.11/site-packages/beartype/claw/_importlib/clawimpcache.py", line 101, in __getitem__
return super().__getitem__(module_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: '__main__'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "/home/florian/tmp/pkg/x.py", line 5, in <module>
class SelectionReason(enum.Enum):
File "/home/florian/tmp/.venv/lib/python3.11/site-packages/beartype/claw/_importlib/clawimpcache.py", line 105, in __getitem__
raise BeartypeClawImportConfException(
beartype.roar.BeartypeClawImportConfException: Beartype configuration associated with module "__main__" hooked by "beartype.claw" import hooks not found. Existing beartype configurations associated with such modules include:
{'pkg.x': BeartypeConf(claw_is_pep526=True, is_color=None, is_debug=False, is_pep484_tower=False, reduce_decorator_exception_to_warning_category=<class 'beartype.roar._roarwarn.BeartypeClawDecorWarning'>, strategy=<BeartypeStrategy.O1: 2>)}
You are getting the same issue I had with pydantic, but pydantic uses Annotated
to fix it, while dataclass does not.
Reading the source code of field
in dataclasses, I can see the comment:
# This function is used instead of exposing Field creation directly,
# so that a type checker can be told (via overloads) that this is a
# function whose type depends on its parameters.
It's all fine and dandy but it's just misleading the static type checker. The type returned is always, always dataclasses.Field
. As such, typebear is seeing through the trick of dataclasses.field
since it sees the true types at runtime.
Since dataclasses doesn't provide the annotated option, I think the only way to fix the dataclass type mistake in the stdlib and declare the real types is this:
from dataclasses import dataclass, field, Field
@dataclass
class MyClass:
my_list: list[str] | Field = field(default_factory=list)
@leycec this could also go in the bearpedia I believe
OMG. The beartype.claw
tech preview erupts in chaos. There's a medley of intersecting issues here – most of which are @beartype's fault but only a few of which are beartype.claw
's fault:
-
Pydantic integration. I confess I've never tried integrating @beartype + Pydantic. Unsurprisingly, they hate each other. Pydantic ~~shamefully lies~~ gently massages type hints into Pydantic-specific objects, which is understandable from its perspective but less understandable from our perspective. Although the
typing.Annotated
workaround is certainly admirable, @beartype should really just silently look the other way when presented with Pydantic types. I've opened a new feature request tracking this at #248. -
Dataclass integration. I thought we'd nailed to the floor our @beartype +
@dataclass
integration. I thought wrong. This is probably abeartype.claw
-specific issue. Let's see what I can jury-rig together in my copious lack of free time. Gah!
And so much more unexpected madness. I'm eternally grateful, everyone! I can confidently say that @beartype 0.15.0 is now due by September 2057 at the latest. My forehead is crinkling. :frowning:
@The-Compiler: Brilliantness! Forcefully ramming beartype.claw
down the throat of your Vim-adjacent magnum opus is an inspired decision. I applaud. Seriously, I'm both humbled and honoured that you'd even consider @beartype for eventual inclusion in qutebrowser.
This also gives me a reasonable release milestone for @beartype 0.15.0. Specifically, @beartype 0.15.0 will be ready when beartype.claw
at least superficially supports qutebrowser without raising exceptions. Warnings – even many, many warnings – are only to be expected. Exceptions, however, are right out. Which brings me to...
...the fascinating traceback you submitted for your minimal-length pkg.x
example. Unreadable errors like KeyError: '__main__'
are obvious bad news. Resolving that is now my immediate priority. Let's do this, bear bois! :keyboard: :bear: :keyboard:
@The-Compiler: Commit 15291ba resolves the unseemly pkg.x
exception you kindly exposed above. But there are still soooooooo many outstanding issues here that it's not really worth anyone's precious lifeforce to test this commit.
Up Next, @leycec Self-flagellates Himself
...is what I'd say if I had an itchy burlap sack with which to self-flagellate myself. Thankfully, I don't. Therefore, my next action item is to instead hack on dataclasses.field()
integration.
As @gabrieldemarmiesse wisely observed, the standard dataclasses
module probably doesn't yet support typing-friendly syntax like muh_field = typing.Annotated[{muh_type}, field(...)]
. I'm unclear on this point, because I am lazy and tired and failed to actually test that. Interestingly, the third-party dataclass-wizard
package does already support that syntax:
from dataclasses import dataclass, field
from typing import Union
from typing_extensions import Annotated
from dataclass_wizard import property_wizard
@dataclass
class Vehicle(metaclass=property_wizard):
wheels: Annotated[Union[int, str], field(default=4)] # <-- woah
Still, nobody should have to require yet another third-party dependency just to get dataclasses.field()
to type-check as expected. Instead, @beartype 0.15.0 will support dataclasses.field()
type hints out-of-the-box. Sharpen those claws, @beartype! :feet: :bear: :feet:
@leycec I haven't tested yet, but it's very likely that you will get similar errors with sqlalchemy annotations, they use a similar syntax: https://docs.sqlalchemy.org/en/20/orm/extensions/mypy.html
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user: User = relationship(User)
ohgodsohgods. Head-pounding pain continues and @leycec continues clutching his head. :sneezing_face:
Thanks so much for that rapid heads-up, though. As always, @gabrieldemarmiesse delivers! And what is it with all these popular APIs (including the standard @dataclasses.dataclass
decorator) that violate PEP standards, anyway? Are typing-driven IDEs like VSCode and PyCharm really okay with PEP-noncompliant type hints or did they just hard-code special cases for each of these APIs?
I suspect the latter. If ignorance is bliss, I now wish to become re-ignorant.
Fascinating. At least in the case of SQLAlchemy, it would thankfully seem that SQLAlchemy's older PEP-noncompliant syntax is officially deprecated and to be removed shortly:
Deprecated since version 2.0: The SQLAlchemy Mypy Plugin is DEPRECATED, and will be removed possibly as early as the SQLAlchemy 2.1 release. We would urge users to please migrate away from it ASAP.
Instead, SQLAlchemy now mandates use of supposedly PEP-compliant type hints ala:
from typing import List
from typing import Optional
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(30), nullable=False)
fullname: Mapped[Optional[str]] = mapped_column(String)
addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = "address"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
email_address: Mapped[str] = mapped_column(String, nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"), nullable=False)
user: Mapped["User"] = relationship("User", back_populates="addresses")
Greatness! The only obvious question now is:
"What is the
sqlalchemy.orm.Mapped[...]
type hint factory?"
Is Mapped[...]
something only static type-checkers are expected to understand or are those SQLAlchemy-specific type hints actually amenable to runtime type-checking – hopefully via a metaclass Mapped.__class__.__instancecheck__()
dunder method like in jaxtyping
and phantomtypes
? In theory, it's the responsibility of SQLAlchemy to get this right. In practice, even popular frameworks like NumPy and Pandera never got this right and instead expected @beartype to do all the heavy lifting, which we then did but which exhausted us.
My face continues sighing dolefully. :face_exhaling:
@The-Compiler: Brilliantness! Forcefully ramming
beartype.claw
down the throat of your Vim-adjacent magnum opus is an inspired decision. I applaud. Seriously, I'm both humbled and honoured that you'd even consider @beartype for eventual inclusion in qutebrowser.This also gives me a reasonable release milestone for @beartype 0.15.0. Specifically, @beartype 0.15.0 will be ready when
beartype.claw
at least superficially supports qutebrowser without raising exceptions. Warnings – even many, many warnings – are only to be expected. Exceptions, however, are right out.
You're humbled? I'm... uuuh... humbleder!
Note I don't have any plans to ship beartype to qutebrowser users - but I'd perhaps consider enabling it when running the testsuite. For now, it all remains a giant experiment!
As for...
Thanks so much for that rapid heads-up, though. As always, @gabrieldemarmiesse delivers! And what is it with all these popular APIs (including the standard
@dataclasses.dataclass
decorator) that violate PEP standards, anyway? Are typing-driven IDEs like VSCode and PyCharm really okay with PEP-noncompliant type hints or did they just hard-code special cases for each of these APIs?
I have no idea what sqlalchemy's Mapped
is exactly (apparently, it's a SQLORMExpression[_T]
, an ORMDescriptor[_T]
, a _MappedAnnotationBase[_T]
and a roles.DDLConstraintColumnRole
- that explains things!).
However, as for dataclass.field
, I believe this used to be hardcoded in a mypy plugin indeed. However, that changed with PEP 681 – Data Class Transforms | peps.python.org:
There is no existing, standard way for libraries with dataclass-like semantics to declare their behavior to type checkers. To work around this limitation, Mypy custom plugins have been developed for many of these libraries, but these plugins don’t work with other type checkers, linters or language servers. They are also costly to maintain for library authors, and they require that Python developers know about the existence of these plugins and download and configure them within their environment.
and apparently a @dataclass_transform
decorator has the ability to specify Field specifiers, with some standardized arguments which are probably meant for type checkers to inspect accordingly.
Welp. Maybe you had a "this shouldn't be too hard!" moment when starting beartype, as I had with qutebrowser when I started it. I, for one, commend your efforts on competing with the big type checkers in this space!
My opinion is my own on this, but I would totally understand if the first version of beartype.claw didn't support those weird default argument for dataclasses. A workaround is available for users in every library and raising awareness about this very common bad typing pattern seems like a good thing to me.
If no one complains about this issue about dataclasses, it will never change and it will continue to confuse newcomers and type checker maintainers forever.
I have very high hopes for beartype.claw as a CI tool. And if it gets the success it deserves, I believe people will change their code so that the type hints are actually correct.
In our work codebase I already fixed 50+ type errors thanks to beartype.claw and 3 real bugs. This has a lot of potential. Mypy has too many false positive, beartype.claw has none.
Fastapi is actively pushing for this, I believe pydantic will too, we can also do our part to raise awareness by raising an exception and provide a very helpful error message with examples and documentation links.
If this goes well, the standard library dataclasses can even deprecate thefield
function in favor of Field
thus simplifying their api and their documentation (and being less confusing for newcomers).
Wowza! Thank you both so much, @The-Compiler and @gabrieldemarmiesse, for being so awesome and accepting of @beartype's rapidly accumulating pile of festering bugs that suspiciously resemble alien facehuggers when you squint at them shortly before running.
I spent the better part of a dangerous bicycle trip through the uncharted Canadian wilderness contemplating this topic rather than watching for man-eating fishers and wolverines. My conclusion is... beartype.claw
needs to temporarily become dumber before it eventually becomes smarter (in several decades after the inevitable development of TypingGPT, of course).
beartype.claw
is currently trying to be "smart" by transparently treating annotated class field declarations as both syntactically and semantically equivalent to annotated variable assignments. That is to say, @beartype currently treats muh_field
and muh_local
in the following example as equivalent:
muh_local: int = 0xBABECAFE # <-- is this...
from dataclasses import dataclass
@dataclass
class MuhDataclass(object):
muh_field: int = field(default=0xCAFEBABE) # <-- ...*really* the same as this?
From the low-level perspective of abstract syntax tree (AST) transformations fuelled by the secretive and deadly art of importlib.machinery
, those two things are equivalent; the annotated class field declaration for muh_field
is at least syntactically equivalent to the annotated variable assignment for muh_local
. In particular, they're both governed by the ast.AnnAssign
node and corresponding ast.NodeTransformer.visit_AnnAssign()
method.
From the higher-level perspective of sanity, semantics, typing
, and "What does this actually mean?", however, we can agree that those two things are actually not at all alike and probably shouldn't be treated as such. What I am trying to say here is that the next commit will explicitly ignore annotated class field declarations when applying beartype.claw
import hooks. This should get us much closer to where we want to be, which is a working beartype.claw
API.
B-b-but... What About Type-checking Dataclass Fields?
Yup. Type-checking dataclass fields is absolutely something that a future version of @beartype should do. Indeed, @beartype 0.14.1 already type-checks dataclass fields at __init__()
time; all that's left is to type-check dataclass fields at assignment time for non-frozen dataclasses.
But that's sorta outside the scope of beartype.claw
. Let's leave this as an exercise to the reader with too much time and too little sanity. So, none of us. Prepare for incoming commits! :expressionless: :exploding_head: :boom:
Ah-ha! Rejoice, bear fellows, for it is Summer. The Sun is sweltering, the water is warm, and GitHub is banging. @leycec's back with a brand new commit promising to resolve some – possibly even many, but doubtfully all – of your pressing issues with the oppressive beartype.claw
API.
Let's type this:
pip install git+https://github.com/beartype/beartype.git@7bfa5c61598a21b6024ba1ab43a0a0bc76818b88
Specifically, this commit relaxes @beartype's prior death-grip on dataclasses. Whether standard @dataclasses.dataclass
classes or third-party dataclasses from Pydantic or SQLAlchemy, beartype.claw
should now transparently support your data without barfing over itself.
Back to the fun stuff! @The-Compiler, you said this fun stuff...
Note I don't have any plans to ship beartype to qutebrowser users
ohthankgods
I was sweating bullets there, thinking that I'd accidentally destroyed my favourite browser by my own ill-gotten hands.
Dear beloved users: Please do not embed beartype.claw
in production code just yet. @beartype 0.15.0 will be strenuously tested against all possible edge cases you are currently discovering for me. But... the real world is a nastier, grimier, and filthier place than even we can conceive. There are chromatic dragons only seen by mantis shrimp that will raze and burn your codebases to the ground as management justifiably roars in inchoate rage.
Of course, I myself will violate my own sound advice by embedding beartype.claw
in production code shortly after releasing @beartype 0.15.0. Honestly, I'm just kinda done with manually decorating everything by @beartype
. Gets old after awhile, doesn't it? That's why we're all here. Laziness triumphs over carpal tunnel syndrome.
...but I'd perhaps consider enabling it when running the testsuite. For now, it all remains a giant experiment!
This is the way. beartype.claw
is explicitly compatible with pytest
. Indeed, beartype.claw
itself is tested entirely with native pytest
tests. Indeed, I'm pretty sure beartype.claw
is the only import hook API that is compatible with pytest
. Let's pretend I know what I'm talking about.
Welp. Maybe you had a "this shouldn't be too hard!" moment when starting beartype, as I had with qutebrowser when I started it.
I am still having that moment. :rofl: :hand_over_mouth: :sob:
It all seemed so simple, once. "Just a hundred lines of code, innit?" I said. "What could possibly go wrong?" they said. I am here to tell you that some dreams are better left under the pillow.
Qutebrowser also probably seemed so simple, once. By harnessing the ferocious browser power of QtWebEngine
+ PyQt, your destiny to make the ultimate browser was already in reach. Or so it seemed... Cue orchestral score in the opening credits to "Qutebrowser: The True Story of One Man's Titanic Struggle."
Honestly, I was incredibly impressed by qutebrowser a decade ago. I'm even more impressed now. Against all economic odds, against the mainstream grain, you've hand-built a part-time – and possibly someday full-time – career around your life's passion. You win GitHub. </big_applause>
I, for one, commend your efforts on competing with the big type checkers in this space!
They will all submit to the Bear! Okay. They probably won't. We'll probably be left biting the dust of their exhaust trails as the proudly jet ahead under profitable corporate and non-profit funding models while I toil quietly in the dark man cave that has yet to be cleaned in over a decade.
But... seriously. I can't stand those big type-checkers. They may be big, but they either lie profusely about everything (mypy, pyright
) or tell the truth but are so enfeebled with worst-case inefficiency (typeguard
) that your continuous integration workflow dissolves into a froth of aborted processes as the wall-clock minutes churn into hours. This is why I @beartype.
I'm imagining you trying to use vanilla Firefox or Chromium on an unfamiliar laptop at the family snow chalet in the Swiss Alps, @The-Compiler – only to curse the sky as muscle memory and cherished keyboard shortcuts all fail to do anything. This is why you qutebrowser.
More fun stuff! @gabrieldemarmiesse, you said this fun stuff...
My opinion is my own on this, but I would totally understand if the first version of
beartype.claw
didn't support those weird default argument for dataclasses.
I share your correct opinion. @beartype users are usually right about everything. This is why beartype.claw
now ignores field declarations in all classes, ...including dataclasses because it's basically infeasible to differentiate between standard classes and dataclasses from the low-level perspective of AST transformations in the beartype.claw
API.
You are right about everything is what I'm saying.
I have very high hopes for
beartype.claw
as a CI tool.
Awwwww! You so nice, Gabriel. These sweet words are like a torrent of optimism jacked directly into my brainstem. :hugs:
And if it gets the success it deserves, I believe people will change their code so that the type hints are actually correct.
Yes! Change your incorrectly typed code, people. @beartype ~~demands~~ politely requests this.
In our work codebase I already fixed 50+ type errors thanks to
beartype.claw
and 3 real bugs.
:open_mouth:
...wow. That many? That's amazing. Well, horrifying. But also amazing! I'm delighted that beartype.claw
has already yielded something positive for someone – and it's not even officially out or even ~~working~~ fully debugged yet.
This has a lot of potential. Mypy has too many false positive,
beartype.claw
has none.
Right? Riiiiiiight!? My wife refuses to even touch mypy or pyright
, because "They waste my time." But she does religiously decorate everything with @beartype
. "But isn't that a waste of time, too?", I mutter to myself. When I suggested she no longer needs to do that, she exclaimed: "Over my dead and twitching body!"
I feel like my wife has an unhealthy attachment to the @beartype
decorator.
Thanks for all the laughs, everybody. We'll all summit the mountain of a better, bolder, safer Python world... together. muhahahahah— gurgling sounds as @leycec passes out
Welp, I believe I ran into another interesting corner case - here is a minimal example:
from collections.abc import Iterator
from contextlib import contextmanager
from beartype import beartype
@beartype # this breaks, and is what beartype.claw does
@contextmanager
# @beartype # this works
def ctx() -> Iterator[None]:
yield
with ctx():
pass
which results in (newlines inserted for readability):
Traceback (most recent call last):
File ".../test.py", line 11, in <module>
with ctx():
File "<@beartype(__main__.ctx) at 0x7f6e76793be0>", line 19, in ctx
beartype.roar.BeartypeCallHintReturnViolation: Function __main__.ctx()
return <contextlib._GeneratorContextManager object at 0x7f6e767a1570>
violates type hint collections.abc.Iterator[None],
as <protocol ABC "contextlib._GeneratorContextManager"> <contextlib._GeneratorContextManager object at 0x7f6e767a1570>
not instance of <protocol ABC "collections.abc.Iterator">.
Which is true when looking at the decorated function. However, we're annotating the undecorated one. Moving the @beartype
to ~~after (lines-wise)~~ before (time-wise, as Python runs decorators from innermost to outermost) @contextmanager
fixes this, but obviously that isn't possible with beartype.claw
.