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

Fix mypy issue with async resolvers #3516

Merged
merged 17 commits into from
May 27, 2024
7 changes: 7 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Release type: patch

This release fixes an issue where mypy would complain when using a typed async
resolver with `strawberry.field(resolver=...)`.

Now the code will type check correctly. We also updated our test suite to make
we catch similar issues in the future.
6 changes: 6 additions & 0 deletions TWEET.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
🆕 Release $version is out! This release fixes an issue that caused type
patrick91 marked this conversation as resolved.
Show resolved Hide resolved
checking errors when using Mypy and async resolvers.

Thanks to $contributor for the PR 👏

Get it here 👉 $release_url
41 changes: 15 additions & 26 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
"auto",
"--showlocals",
"-vv",
"--ignore=tests/mypy",
"--ignore=tests/pyright",
"--ignore=tests/typecheckers",
"--ignore=tests/cli",
"--ignore=tests/benchmarks",
"--ignore=tests/experimental/pydantic",
Expand Down Expand Up @@ -125,42 +124,24 @@ def test_pydantic(session: Session, pydantic: str) -> None:
)


@session(python=PYTHON_VERSIONS, name="Mypy tests")
def tests_mypy(session: Session) -> None:
session.run_always("poetry", "install", "--with", "integrations", external=True)

session.run(
"pytest",
"--cov=.",
"--cov-append",
"--cov-report=xml",
"tests/mypy",
"-vv",
)


@session(python=PYTHON_VERSIONS, name="Pyright tests", tags=["tests"])
def tests_pyright(session: Session) -> None:
@session(python=PYTHON_VERSIONS, name="Type checkers tests", tags=["tests"])
def tests_typecheckers(session: Session) -> None:
session.run_always("poetry", "install", external=True)

session.install("pyright")
session.install("pydantic")
session.install("git+https://github.com/python/mypy.git#master")

session.run(
"pytest",
"--cov=.",
"--cov-append",
"--cov-report=xml",
"tests/pyright",
"tests/typecheckers",
"-vv",
)


@session(name="Mypy", tags=["lint"])
def mypy(session: Session) -> None:
session.run_always("poetry", "install", "--with", "integrations", external=True)

session.run("mypy", "--config-file", "mypy.ini")


@session(python=PYTHON_VERSIONS, name="CLI tests", tags=["tests"])
def tests_cli(session: Session) -> None:
session.run_always("poetry", "install", external=True)
Expand All @@ -176,3 +157,11 @@ def tests_cli(session: Session) -> None:
"tests/cli",
"-vv",
)


@session(name="Mypy", tags=["lint"])
def mypy(session: Session) -> None:
session.run_always("poetry", "install", "--with", "integrations", external=True)
session.install("mypy")

session.run("mypy", "--config-file", "mypy.ini")
1,635 changes: 886 additions & 749 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,11 @@ types-toml = "^0.10.8"
types-typed-ast = "^1.5.8"
types-ujson = "^5.6.0"
types-protobuf = "^4.23.0.1"
mypy = "1.9.0"
pytest-mypy-plugins = "3.0.0"
poetry-plugin-export = "^1.6.0"
# another bug in poetry
urllib3 = "<2"
graphlib_backport = {version = "*", python = "<3.9", optional = false}
inline-snapshot = "^0.10.1"

[tool.poetry.group.integrations]
optional = true
Expand Down Expand Up @@ -149,7 +148,7 @@ pyinstrument = ["pyinstrument"]
strawberry = "strawberry.cli:run"

[tool.pytest.ini_options]
addopts = "--emoji --mypy-ini-file=mypy.ini"
addopts = "--emoji"
DJANGO_SETTINGS_MODULE = "tests.django.django_settings"
testpaths = ["tests/"]
markers = [
Expand Down
16 changes: 8 additions & 8 deletions strawberry/extensions/base_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,27 @@ class SchemaExtension:
def __init__(self, *, execution_context: ExecutionContext) -> None:
self.execution_context = execution_context

def on_operation(
def on_operation( # type: ignore
self,
) -> AsyncIteratorOrIterator[None]: # pragma: no cover # pyright: ignore
) -> AsyncIteratorOrIterator[None]: # pragma: no cover
"""Called before and after a GraphQL operation (query / mutation) starts"""
yield None

def on_validate(
def on_validate( # type: ignore
self,
) -> AsyncIteratorOrIterator[None]: # pragma: no cover # pyright: ignore
) -> AsyncIteratorOrIterator[None]: # pragma: no cover
"""Called before and after the validation step"""
yield None

def on_parse(
def on_parse( # type: ignore
self,
) -> AsyncIteratorOrIterator[None]: # pragma: no cover # pyright: ignore
) -> AsyncIteratorOrIterator[None]: # pragma: no cover
"""Called before and after the parsing step"""
yield None

def on_execute(
def on_execute( # type: ignore
self,
) -> AsyncIteratorOrIterator[None]: # pragma: no cover # pyright: ignore
) -> AsyncIteratorOrIterator[None]: # pragma: no cover
"""Called before and after the execution step"""
yield None

Expand Down
56 changes: 51 additions & 5 deletions strawberry/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,20 @@

T = TypeVar("T")

_RESOLVER_TYPE = Union[
_RESOLVER_TYPE_SYNC = Union[
patrick91 marked this conversation as resolved.
Show resolved Hide resolved
StrawberryResolver[T],
Callable[..., T],
Callable[..., Coroutine[Any, Any, T]],
Callable[..., Awaitable[T]],
"staticmethod[Any, T]",
"classmethod[Any, Any, T]",
]

_RESOLVER_TYPE_ASYNC = Union[
patrick91 marked this conversation as resolved.
Show resolved Hide resolved
Callable[..., Coroutine[Any, Any, T]],
Callable[..., Awaitable[T]],
]

_RESOLVER_TYPE = Union[_RESOLVER_TYPE_SYNC[T], _RESOLVER_TYPE_ASYNC[T]]
patrick91 marked this conversation as resolved.
Show resolved Hide resolved

UNRESOLVED = object()


Expand Down Expand Up @@ -416,10 +421,33 @@ def is_async(self) -> bool:
return self._has_async_base_resolver


# NOTE: we are separating the sync and async resolvers because using both
# in the same function will cause mypy to raise an error. Not sure if it is a bug
patrick91 marked this conversation as resolved.
Show resolved Hide resolved


@overload
def field(
*,
resolver: _RESOLVER_TYPE_ASYNC[T],
name: Optional[str] = None,
is_subscription: bool = False,
description: Optional[str] = None,
init: Literal[False] = False,
permission_classes: Optional[List[Type[BasePermission]]] = None,
deprecation_reason: Optional[str] = None,
default: Any = dataclasses.MISSING,
default_factory: Union[Callable[..., object], object] = dataclasses.MISSING,
metadata: Optional[Mapping[Any, Any]] = None,
directives: Optional[Sequence[object]] = (),
extensions: Optional[List[FieldExtension]] = None,
graphql_type: Optional[Any] = None,
) -> T: ...


@overload
def field(
*,
resolver: _RESOLVER_TYPE[T],
resolver: _RESOLVER_TYPE_SYNC[T],
name: Optional[str] = None,
is_subscription: bool = False,
description: Optional[str] = None,
Expand Down Expand Up @@ -455,7 +483,25 @@ def field(

@overload
def field(
resolver: _RESOLVER_TYPE[T],
resolver: _RESOLVER_TYPE_ASYNC[T],
*,
name: Optional[str] = None,
is_subscription: bool = False,
description: Optional[str] = None,
permission_classes: Optional[List[Type[BasePermission]]] = None,
deprecation_reason: Optional[str] = None,
default: Any = dataclasses.MISSING,
default_factory: Union[Callable[..., object], object] = dataclasses.MISSING,
metadata: Optional[Mapping[Any, Any]] = None,
directives: Optional[Sequence[object]] = (),
extensions: Optional[List[FieldExtension]] = None,
graphql_type: Optional[Any] = None,
) -> StrawberryField: ...


@overload
def field(
resolver: _RESOLVER_TYPE_SYNC[T],
*,
name: Optional[str] = None,
is_subscription: bool = False,
Expand Down
7 changes: 0 additions & 7 deletions tests/mypy/enums.py

This file was deleted.

38 changes: 0 additions & 38 deletions tests/mypy/federation/test_decorators.yml

This file was deleted.

114 changes: 0 additions & 114 deletions tests/mypy/federation/test_fields.yml

This file was deleted.

Loading
Loading