beartype icon indicating copy to clipboard operation
beartype copied to clipboard

[Feature request] Automagical Import Hook

Open leycec opened this issue 2 years ago • 2 comments

@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 comparable typeguard.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).

leycec avatar Jul 29 '21 07:07 leycec

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.

Skylion007 avatar Jul 31 '21 15:07 Skylion007

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:

leycec avatar Aug 01 '21 03:08 leycec

@langfield, @justinchuby, @BowenBao, @k4ml:

so it begins

leycec avatar Apr 13 '23 07:04 leycec

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?)

gabrieldemarmiesse avatar Jun 15 '23 16:06 gabrieldemarmiesse

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:

  1. 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.
  2. 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.
  3. 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:
  4. Release this as @beartype 0.15.0.
  5. 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...

leycec avatar Jun 15 '23 19:06 leycec

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.

hohoho

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:

  1. 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
  1. 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:

leycec avatar Jul 06 '23 06:07 leycec

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)

tolomea avatar Jul 06 '23 08:07 tolomea

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.

leycec avatar Jul 07 '23 05:07 leycec

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.

leycec avatar Jul 08 '23 06:07 leycec

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

gabrieldemarmiesse avatar Jul 08 '23 11:07 gabrieldemarmiesse

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:

leycec avatar Jul 09 '23 02:07 leycec

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.

leycec avatar Jul 09 '23 03:07 leycec

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)

gabrieldemarmiesse avatar Jul 09 '23 08:07 gabrieldemarmiesse

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

gabrieldemarmiesse avatar Jul 09 '23 08:07 gabrieldemarmiesse

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?

gabrieldemarmiesse avatar Jul 09 '23 11:07 gabrieldemarmiesse

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.

gabrieldemarmiesse avatar Jul 09 '23 11:07 gabrieldemarmiesse

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).

gabrieldemarmiesse avatar Jul 09 '23 12:07 gabrieldemarmiesse

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>)}

The-Compiler avatar Jul 09 '23 13:07 The-Compiler

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

gabrieldemarmiesse avatar Jul 09 '23 13:07 gabrieldemarmiesse

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 a beartype.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:

leycec avatar Jul 09 '23 16:07 leycec

@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:

leycec avatar Jul 10 '23 01:07 leycec

@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 avatar Jul 10 '23 05:07 leycec

@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)

gabrieldemarmiesse avatar Jul 10 '23 09:07 gabrieldemarmiesse

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.

leycec avatar Jul 10 '23 17:07 leycec

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:

leycec avatar Jul 10 '23 17:07 leycec

@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!

The-Compiler avatar Jul 10 '23 17:07 The-Compiler

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).

gabrieldemarmiesse avatar Jul 10 '23 19:07 gabrieldemarmiesse

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:

leycec avatar Jul 10 '23 22:07 leycec

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

leycec avatar Jul 11 '23 06:07 leycec

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.

The-Compiler avatar Jul 11 '23 12:07 The-Compiler