From 3a1e0d59cd435569c38abb817895e539474cb7ea Mon Sep 17 00:00:00 2001 From: Thiago Bellini Ribeiro Date: Tue, 28 May 2024 22:04:47 +0100 Subject: [PATCH 1/5] fix: Fix lazy aliased connections type resolution --- RELEASE.md | 23 +++++++++++++++++++++++ strawberry/relay/fields.py | 13 ++++++++++--- strawberry/utils/typing.py | 15 ++++++++++----- tests/relay/schema.py | 10 +++++++++- tests/relay/snapshots/schema.gql | 26 ++++++++++++++++++++++++++ tests/relay/test_fields.py | 2 ++ tests/types/test_lazy_types.py | 28 +++++++++++++++++++++++++++- 7 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..888dfc7bf8 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,23 @@ +Release type: patch + +This release fixes an issue that would prevent using lazy aliased connections to +annotate a connection field. + +For example, this should now work correctly: + +```python +# types.py + +@strawberry.type +class Fruit: + ... + +FruitConnection: TypeAlias = ListConnection[Fruit] + + +# schema.py + +@strawberry.type +class Query: + fruits: Annotated["FruitConnection", strawberry.lazy("types")] = strawberry.connection() +``` diff --git a/strawberry/relay/fields.py b/strawberry/relay/fields.py index fa58852748..a1d0f86091 100644 --- a/strawberry/relay/fields.py +++ b/strawberry/relay/fields.py @@ -36,6 +36,7 @@ SyncExtensionResolver, ) from strawberry.field import _RESOLVER_TYPE, StrawberryField, field +from strawberry.lazy_type import LazyType from strawberry.relay.exceptions import ( RelayWrongAnnotationError, RelayWrongResolverAnnotationError, @@ -43,7 +44,7 @@ from strawberry.type import StrawberryList, StrawberryOptional from strawberry.types.fields.resolver import StrawberryResolver from strawberry.utils.aio import asyncgen_to_list -from strawberry.utils.typing import eval_type +from strawberry.utils.typing import eval_type, is_generic_alias from .types import Connection, GlobalID, Node, NodeIterableType, NodeType @@ -223,7 +224,13 @@ def apply(self, field: StrawberryField) -> None: ] f_type = field.type - if not isinstance(f_type, type) or not issubclass(f_type, Connection): + + if isinstance(f_type, LazyType): + f_type = f_type.resolve_type() + field.type = f_type + + type_origin = get_origin(f_type) if is_generic_alias(f_type) else f_type + if not isinstance(type_origin, type) or not issubclass(type_origin, Connection): raise RelayWrongAnnotationError(field.name, cast(type, field.origin)) assert field.base_resolver @@ -248,7 +255,7 @@ def apply(self, field: StrawberryField) -> None: ): raise RelayWrongResolverAnnotationError(field.name, field.base_resolver) - self.connection_type = cast(Type[Connection[Node]], field.type) + self.connection_type = cast(Type[Connection[Node]], f_type) def resolve( self, diff --git a/strawberry/utils/typing.py b/strawberry/utils/typing.py index 0d10eb3a86..83ef1eff68 100644 --- a/strawberry/utils/typing.py +++ b/strawberry/utils/typing.py @@ -23,7 +23,7 @@ cast, overload, ) -from typing_extensions import Annotated, get_args, get_origin +from typing_extensions import Annotated, TypeGuard, get_args, get_origin ast_unparse = getattr(ast, "unparse", None) # ast.unparse is only available on python 3.9+. For older versions we will @@ -63,15 +63,20 @@ def get_generic_alias(type_: Type) -> Type: continue attr = getattr(typing, attr_name) - # _GenericAlias overrides all the methods that we can use to know if - # this is a subclass of it. But if it has an "_inst" attribute - # then it for sure is a _GenericAlias - if hasattr(attr, "_inst") and attr.__origin__ is type_: + if is_generic_alias(attr) and attr.__origin__ is type_: return attr raise AssertionError(f"No GenericAlias available for {type_}") # pragma: no cover +def is_generic_alias(type_: Any) -> TypeGuard[_GenericAlias]: + """Returns True if the type is a generic alias.""" + # _GenericAlias overrides all the methods that we can use to know if + # this is a subclass of it. But if it has an "_inst" attribute + # then it for sure is a _GenericAlias + return hasattr(type_, "_inst") + + def is_list(annotation: object) -> bool: """Returns True if annotation is a List""" diff --git a/tests/relay/schema.py b/tests/relay/schema.py index 10324c6d76..5bd8cea2be 100644 --- a/tests/relay/schema.py +++ b/tests/relay/schema.py @@ -12,7 +12,7 @@ Optional, cast, ) -from typing_extensions import Annotated, Self +from typing_extensions import Annotated, Self, TypeAlias import strawberry from strawberry import relay @@ -191,6 +191,9 @@ async def has_permission( return True +FruitsListConnectionAlias: TypeAlias = relay.ListConnection[Fruit] + + @strawberry.type class Query: node: relay.Node = relay.node() @@ -204,6 +207,11 @@ class Query: fruits_lazy: relay.ListConnection[ Annotated["Fruit", strawberry.lazy("tests.relay.schema")] ] = relay.connection(resolver=fruits_resolver) + fruits_alias: FruitsListConnectionAlias = relay.connection(resolver=fruits_resolver) + fruits_alias_lazy: Annotated[ + "FruitsListConnectionAlias", + strawberry.lazy("tests.relay.schema"), + ] = relay.connection(resolver=fruits_resolver) fruits_async: relay.ListConnection[FruitAsync] = relay.connection( resolver=fruits_async_resolver ) diff --git a/tests/relay/snapshots/schema.gql b/tests/relay/snapshots/schema.gql index 45eb0a8640..93c0fc7337 100644 --- a/tests/relay/snapshots/schema.gql +++ b/tests/relay/snapshots/schema.gql @@ -146,6 +146,32 @@ type Query { """Returns the items in the list that come after the specified cursor.""" last: Int = null ): FruitConnection! + fruitsAlias( + """Returns the items in the list that come before the specified cursor.""" + before: String = null + + """Returns the items in the list that come after the specified cursor.""" + after: String = null + + """Returns the first n items from the list.""" + first: Int = null + + """Returns the items in the list that come after the specified cursor.""" + last: Int = null + ): FruitConnection! + fruitsAliasLazy( + """Returns the items in the list that come before the specified cursor.""" + before: String = null + + """Returns the items in the list that come after the specified cursor.""" + after: String = null + + """Returns the first n items from the list.""" + first: Int = null + + """Returns the items in the list that come after the specified cursor.""" + last: Int = null + ): FruitConnection! fruitsAsync( """Returns the items in the list that come before the specified cursor.""" before: String = null diff --git a/tests/relay/test_fields.py b/tests/relay/test_fields.py index 33c211a27f..9b13fe9c65 100644 --- a/tests/relay/test_fields.py +++ b/tests/relay/test_fields.py @@ -363,6 +363,8 @@ async def test_query_nodes_optional_async(): attrs = [ "fruits", "fruitsLazy", + "fruitsAlias", + "fruitsAliasLazy", "fruitsConcreteResolver", "fruitsCustomResolver", "fruitsCustomResolverLazy", diff --git a/tests/types/test_lazy_types.py b/tests/types/test_lazy_types.py index 4a2f65a7a3..65e0afff5e 100644 --- a/tests/types/test_lazy_types.py +++ b/tests/types/test_lazy_types.py @@ -1,7 +1,7 @@ # type: ignore import enum from typing import Generic, TypeVar -from typing_extensions import Annotated +from typing_extensions import Annotated, TypeAlias import strawberry from strawberry.annotation import StrawberryAnnotation @@ -11,6 +11,8 @@ from strawberry.types.fields.resolver import StrawberryResolver from strawberry.union import StrawberryUnion, union +T = TypeVar("T") + # This type is in the same file but should adequately test the logic. @strawberry.type @@ -18,6 +20,14 @@ class LaziestType: something: bool +@strawberry.type +class LazyGenericType(Generic[T]): + something: T + + +LazyTypeAlias: TypeAlias = LazyGenericType[int] + + @strawberry.enum class LazyEnum(enum.Enum): BREAD = "BREAD" @@ -38,6 +48,22 @@ def test_lazy_type(): assert resolved.resolve_type() is LaziestType +def test_lazy_type_alias(): + # Module path is short and relative because of the way pytest runs the file + LazierType = LazyType("LazyTypeAlias", "test_lazy_types") + + annotation = StrawberryAnnotation(LazierType) + resolved = annotation.resolve() + + # Currently StrawberryAnnotation(LazyType).resolve() returns the unresolved + # LazyType. We may want to find a way to directly return the referenced object + # without a second resolving step. + assert isinstance(resolved, LazyType) + resolved_type = resolved.resolve_type() + assert resolved_type.__origin__ is LazyGenericType + assert resolved_type.__args__ == (int,) + + def test_lazy_type_function(): LethargicType = Annotated["LaziestType", strawberry.lazy("test_lazy_types")] From 96d3a008fae57f7ff28e610fcfe3e5d17b7a9fb6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 21:12:25 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- RELEASE.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 888dfc7bf8..7442f730aa 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,16 +8,20 @@ For example, this should now work correctly: ```python # types.py + @strawberry.type -class Fruit: - ... +class Fruit: ... + FruitConnection: TypeAlias = ListConnection[Fruit] # schema.py + @strawberry.type class Query: - fruits: Annotated["FruitConnection", strawberry.lazy("types")] = strawberry.connection() + fruits: Annotated["FruitConnection", strawberry.lazy("types")] = ( + strawberry.connection() + ) ``` From aceeee0d08f7341eba804c1704d5863e18bc5dac Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 28 May 2024 22:22:52 +0100 Subject: [PATCH 3/5] Apply suggestions from code review --- RELEASE.md | 5 ++--- strawberry/relay/fields.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 7442f730aa..5c9e6500f4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,17 +8,16 @@ For example, this should now work correctly: ```python # types.py - @strawberry.type class Fruit: ... FruitConnection: TypeAlias = ListConnection[Fruit] +``` - +```python # schema.py - @strawberry.type class Query: fruits: Annotated["FruitConnection", strawberry.lazy("types")] = ( diff --git a/strawberry/relay/fields.py b/strawberry/relay/fields.py index a1d0f86091..72cf500db0 100644 --- a/strawberry/relay/fields.py +++ b/strawberry/relay/fields.py @@ -255,7 +255,7 @@ def apply(self, field: StrawberryField) -> None: ): raise RelayWrongResolverAnnotationError(field.name, field.base_resolver) - self.connection_type = cast(Type[Connection[Node]], f_type) + self.connection_type = cast(Type[Connection[Node]], field.type) def resolve( self, From 1217bc0263c7c4372146838097de33ab2815dbe5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 21:23:05 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- RELEASE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 5c9e6500f4..5c318e66b9 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,6 +8,7 @@ For example, this should now work correctly: ```python # types.py + @strawberry.type class Fruit: ... @@ -18,6 +19,7 @@ FruitConnection: TypeAlias = ListConnection[Fruit] ```python # schema.py + @strawberry.type class Query: fruits: Annotated["FruitConnection", strawberry.lazy("types")] = ( From 1a0de54d101dbb76890d53d51f6e2888a1d98474 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 28 May 2024 22:23:55 +0100 Subject: [PATCH 5/5] Add tweet file --- TWEET.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 TWEET.md diff --git a/TWEET.md b/TWEET.md new file mode 100644 index 0000000000..580aa64b93 --- /dev/null +++ b/TWEET.md @@ -0,0 +1,6 @@ +🆕 Release $version is out! Thanks to $contributor for the PR 👏 + +This release fixes a bug where you couldn't use relay connection with lazy +types. + +Get it here 👉 $release_url