Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Annotate decorators that wrap Document methods (#679) #886

Merged

Conversation

bedlamzd
Copy link
Contributor

Type checkers were complaining about missing self argument in decorated Document methods. This was caused by incomplete annotations of used decorators.

@bedlamzd bedlamzd force-pushed the issue-679_properly-annotate-decorators branch from 9372fd4 to d3a5959 Compare February 26, 2024 02:44
Copy link
Contributor Author

@bedlamzd bedlamzd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen tests/typing folder, but not sure what's the process for it. It seems the idea that mypy will error if there is anything wrong with typing in the test, right? Are they run as regular tests as well?

Let me know what kind of test you'd like to see and if simple usage of insert, save and replace is enough

@@ -605,12 +612,12 @@ async def save(
@wrap_with_actions(EventTypes.SAVE_CHANGES)
@validate_self_before
async def save_changes(
self,
self: DocType,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI typing_extensions has Self which is intended for this purpose. I can replace usages of DocType in Document class, if it isn't too out of scope. Or submit a new PR.

There is no difference between using two, though.

result = await f(
self,
*args,
skip_actions=skip_actions, # type: ignore[arg-type]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type annotation spec prohibits using named arguments along with typed **kwargs arguments.

See typing docs and PEP 612

One way to avoid using type: ignore is to write something like:

@wraps(f)
async def wrapper(
    self: "Document",
    *args: P.args,
    **kwargs: P.kwargs,
) -> R:
    skip_actions = cast(Optional[List[Union[ActionDirections, str]]], kwargs.get("skip_actions"))

    if skip_actions is None:
        skip_actions = []
    if skip_actions in kwargs:
        kwargs["skip_actions"] = skip_actions

    await ActionRegistry.run_actions(
        self,
        event_type=event_type,
        action_direction=ActionDirections.BEFORE,
        exclude=skip_actions,
    )

    result = await f(
        self,
        *args,
        **kwargs,
    )

    await ActionRegistry.run_actions(
        self,
        event_type=event_type,
        action_direction=ActionDirections.AFTER,
        exclude=skip_actions,
    )

    return result

But that doesn't look better in any way.

@roman-right
Copy link
Member

Hi @bedlamzd,

Thank you very much for your work! Should this case be added to the mypy test suite, or was it a Pylance catch?

I use this test module for testing typing in usage: https://github.com/roman-right/beanie/tree/main/tests/typing.
This is where it runs: https://github.com/roman-right/beanie/blob/main/.pre-commit-config.yaml#L11-L18.

@bedlamzd
Copy link
Contributor Author

bedlamzd commented Feb 27, 2024

@roman-right That's what I was wondering in the first comment

I don't remember how I've noticed it, likely an IDE check or pyright (I use it mostly). Mypy doesn't show errors because it infers Any type for Document.insert but I can write something like

from typing import (
    Awaitable,
    Protocol,
    assert_type,
)

from pymongo.client_session import ClientSession

from beanie import Document, WriteRules
from beanie.odm.actions import ActionDirections
from tests.typing.models import Test


class DocumentInsertSignature(Protocol):
    def __call__(
        self,
        *,
        link_rule: WriteRules = ...,
        session: ClientSession | None = ...,
        skip_actions: list[ActionDirections | str] | None = ...,
    ) -> Awaitable[Document]:
        ...


async def test_insert_signature() -> None:
    test = Test(
        foo="foo",
        bar="bar",
        baz="baz",
    )
    assert_type(test.insert, DocumentInsertSignature)

and mypy will do the check

You might've noticed that in the above signature returns Document and not Test as it should in reality. That is due to mypy weirdness, for some reason it can't infer subclass and fallbacks to Document. pyright doesn't have this issue and will raise an error here.

Also, protocol's __call__ is not async because in that case the return type is Coroutine[Any, Any, Document] which is incompatible with Awaitable in this context. But returning awaitable is basically the same thing as being async

@bedlamzd
Copy link
Contributor Author

Turns out it's a lot simpler than I thought. This works as well and I think is better because works both for mypy and pyright

class DocumentClsInsertSignature(Protocol):
    def __call__(
        self,
        self_: DocType,
        /,
        *,
        link_rule: WriteRules = ...,
        session: ClientSession | None = ...,
        skip_actions: list[ActionDirections | str] | None = ...,
    ) -> Awaitable[DocType]:
        ...


def test_insert_signature() -> None:
    test_insert: DocumentClsInsertSignature = Test.insert  # noqa: F841

Suggested here

I will add tests a bit later

Type checkers were complaining about missing `self`
argument in decorated `Document` methods. This was
caused by incomplete annotations of used decorators.
@bedlamzd bedlamzd force-pushed the issue-679_properly-annotate-decorators branch from 6d82ae3 to 6e231eb Compare February 29, 2024 10:41
@bedlamzd
Copy link
Contributor Author

@roman-right so while writing tests I found out that overloads don't work well with mypy, it always chooses async case. So I rewrote annotations and just ignore type errors in async wrappers.

You can see the changes in the fixup commit. Let me know when you've reviewed everything and I will squash it.

Overall I consider this ready, let me know if you want me to change anything.

@bedlamzd bedlamzd force-pushed the issue-679_properly-annotate-decorators branch 2 times, most recently from 0ac26c9 to b18dd1a Compare February 29, 2024 10:55
Removed sync/async overload in favour of ignoring errors in wrappers
because mypy confused them and always expected async function.
@bedlamzd bedlamzd force-pushed the issue-679_properly-annotate-decorators branch from b18dd1a to 64b7790 Compare February 29, 2024 11:07
@tristandeborde
Copy link

@bedlamzd Thanks a lot for the PR !
@roman-right do we have an estimate for when this PR could be reviewed and merged ?
Not trying to be pushy but this would be super helpful for my team - we're using Pylance :)
Since Pylance is VSCode's default Python linting option, surely it would greatly help with Beanie adoption !
Thanks in advance

@roman-right
Copy link
Member

Hi @bedlamzd and @tristandeborde ,
Thank you very much for your work and following up. I plan to have this merged at this weekend

@rawnly
Copy link

rawnly commented Apr 28, 2024

hey any plan on releasing this?

@roman-right roman-right merged commit 7de0303 into BeanieODM:main May 1, 2024
@roman-right
Copy link
Member

Hi everyone!
Merged. It will be released today

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

Successfully merging this pull request may close these issues.

4 participants