diff --git a/poetry.lock b/poetry.lock index 62c2d70e..1c331716 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1195,6 +1195,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1406,13 +1407,13 @@ doc = ["sphinx"] [[package]] name = "strawberry-graphql" -version = "0.227.4" +version = "0.234.2" description = "A library for creating GraphQL APIs" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "strawberry_graphql-0.227.4-py3-none-any.whl", hash = "sha256:73e501d9f37ec66dd61942629b23e2d928417ba0b1f40b2fa8d01b540576a79c"}, - {file = "strawberry_graphql-0.227.4.tar.gz", hash = "sha256:3bad964ea1e46bddef2d1e20a4fea4619918826191d5442311e0246e1e228299"}, + {file = "strawberry_graphql-0.234.2-py3-none-any.whl", hash = "sha256:970d3c8f0ac35618c3f6545af95e3baa9774cf5bb617f32e6d9856807b7aae74"}, + {file = "strawberry_graphql-0.234.2.tar.gz", hash = "sha256:34eb9f2f1e41ed375674d2dd7bcdaa9f03de4f181f14301ed68081dad3233681"}, ] [package.dependencies] @@ -1578,4 +1579,4 @@ enum = ["django-choices-field"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "b8da678b34783efdb6fee362b17e0d3c48c7bf1d581ed17e24392597e9fe21c0" +content-hash = "572932285726563d31fe77f88a0ce9799811a97bc097eb58e93ba8622d9e0d6a" diff --git a/pyproject.toml b/pyproject.toml index ba2803c9..91f87232 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ django = ">=3.2" asgiref = ">=3.8" django-choices-field = { version = ">=2.2.2", optional = true } django-debug-toolbar = { version = ">=3.4", optional = true } -strawberry-graphql = ">=0.227.1" +strawberry-graphql = ">=0.234.2" [tool.poetry.group.dev.dependencies] Markdown = "^3.3.7" diff --git a/strawberry_django/fields/base.py b/strawberry_django/fields/base.py index 480a0e1a..b60c70d5 100644 --- a/strawberry_django/fields/base.py +++ b/strawberry_django/fields/base.py @@ -1,7 +1,7 @@ from __future__ import annotations import functools -from typing import TYPE_CHECKING, Any, Optional, TypeVar +from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast from django.db.models import ForeignKey from strawberry import LazyType, relay @@ -17,6 +17,7 @@ get_object_definition, ) from strawberry.union import StrawberryUnion +from strawberry.utils.inspect import get_specialized_type_var_map from strawberry_django.resolvers import django_resolver from strawberry_django.utils.typing import ( @@ -80,16 +81,24 @@ def is_async(self) -> bool: def django_type(self) -> type[WithStrawberryDjangoObjectDefinition] | None: origin = self.type + if isinstance(origin, LazyType): + origin = origin.resolve_type() + object_definition = get_object_definition(origin) if object_definition and issubclass(object_definition.origin, relay.Connection): - origin = object_definition.type_var_map.get("NodeType") + origin_specialized_type_var_map = ( + get_specialized_type_var_map(cast(type, origin)) or {} + ) + origin = origin_specialized_type_var_map.get("NodeType") + + if origin is None: + origin = object_definition.type_var_map.get("NodeType") if origin is None: specialized_type_var_map = ( object_definition.specialized_type_var_map or {} ) - origin = specialized_type_var_map["NodeType"] if isinstance(origin, LazyType): diff --git a/tests/relay/lazy/__init__.py b/tests/relay/lazy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/relay/lazy/a.py b/tests/relay/lazy/a.py new file mode 100644 index 00000000..05297ea6 --- /dev/null +++ b/tests/relay/lazy/a.py @@ -0,0 +1,24 @@ +from typing import TYPE_CHECKING + +import strawberry +from strawberry import relay +from typing_extensions import Annotated, TypeAlias + +import strawberry_django +from strawberry_django.relay import ListConnectionWithTotalCount + +from .models import RelayAuthor + +if TYPE_CHECKING: + from .b import BookConnection + + +@strawberry_django.type(RelayAuthor) +class AuthorType(relay.Node): + name: str + books: Annotated["BookConnection", strawberry.lazy("tests.relay.lazy.b")] = ( + strawberry_django.connection() + ) + + +AuthorConnection: TypeAlias = ListConnectionWithTotalCount[AuthorType] diff --git a/tests/relay/lazy/b.py b/tests/relay/lazy/b.py new file mode 100644 index 00000000..c94d0a20 --- /dev/null +++ b/tests/relay/lazy/b.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING + +import strawberry +from strawberry import relay +from typing_extensions import Annotated, TypeAlias + +import strawberry_django +from strawberry_django.relay import ListConnectionWithTotalCount + +from .models import RelayBook + +if TYPE_CHECKING: + from .a import AuthorType + + +@strawberry_django.filter(RelayBook) +class BookFilter: + name: str + + +@strawberry_django.order(RelayBook) +class BookOrder: + name: str + + +@strawberry_django.type(RelayBook, filters=BookFilter, order=BookOrder) +class BookType(relay.Node): + name: str + author: Annotated["AuthorType", strawberry.lazy("tests.relay.lazy.a")] + + +BookConnection: TypeAlias = ListConnectionWithTotalCount[BookType] diff --git a/tests/relay/lazy/models.py b/tests/relay/lazy/models.py new file mode 100644 index 00000000..9f5ab08e --- /dev/null +++ b/tests/relay/lazy/models.py @@ -0,0 +1,14 @@ +from django.db import models + + +class RelayAuthor(models.Model): + name = models.CharField(max_length=100) + + +class RelayBook(models.Model): + title = models.CharField(max_length=100) + author = models.ForeignKey( + RelayAuthor, + on_delete=models.CASCADE, + related_name="books", + ) diff --git a/tests/relay/lazy/snapshots/test_lazy_annotations/test_lazy_type_annotations_in_schema/authors_and_books_schema.gql b/tests/relay/lazy/snapshots/test_lazy_annotations/test_lazy_type_annotations_in_schema/authors_and_books_schema.gql new file mode 100644 index 00000000..22e40a48 --- /dev/null +++ b/tests/relay/lazy/snapshots/test_lazy_annotations/test_lazy_type_annotations_in_schema/authors_and_books_schema.gql @@ -0,0 +1,169 @@ +type AuthorType implements Node { + """The Globally Unique ID of this object""" + id: GlobalID! + name: String! + books( + filters: BookFilter + order: BookOrder + + """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 + ): BookTypeConnection! +} + +"""A connection to a list of items.""" +type AuthorTypeConnection { + """Pagination data for this connection""" + pageInfo: PageInfo! + + """Contains the nodes in this connection""" + edges: [AuthorTypeEdge!]! + + """Total quantity of existing nodes.""" + totalCount: Int +} + +"""An edge in a connection.""" +type AuthorTypeEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: AuthorType! +} + +input BookFilter { + name: String! + AND: BookFilter + OR: BookFilter + NOT: BookFilter + DISTINCT: Boolean +} + +input BookOrder { + name: String +} + +type BookType implements Node { + """The Globally Unique ID of this object""" + id: GlobalID! + name: String! + author: AuthorType! +} + +"""A connection to a list of items.""" +type BookTypeConnection { + """Pagination data for this connection""" + pageInfo: PageInfo! + + """Contains the nodes in this connection""" + edges: [BookTypeEdge!]! + + """Total quantity of existing nodes.""" + totalCount: Int +} + +"""An edge in a connection.""" +type BookTypeEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: BookType! +} + +""" +The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. +""" +scalar GlobalID @specifiedBy(url: "https://relay.dev/graphql/objectidentification.htm") + +"""An object with a Globally Unique ID""" +interface Node { + """The Globally Unique ID of this object""" + id: GlobalID! +} + +"""Information to aid in pagination.""" +type PageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String + + """When paginating forwards, the cursor to continue.""" + endCursor: String +} + +type Query { + booksConn( + filters: BookFilter + order: BookOrder + + """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 + ): BookTypeConnection! + booksConn2( + filters: BookFilter + order: BookOrder + + """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 + ): BookTypeConnection! + authorsConn( + """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 + ): AuthorTypeConnection! + authorsConn2( + """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 + ): AuthorTypeConnection! +} \ No newline at end of file diff --git a/tests/relay/lazy/test_lazy_annotations.py b/tests/relay/lazy/test_lazy_annotations.py new file mode 100644 index 00000000..7d69bd04 --- /dev/null +++ b/tests/relay/lazy/test_lazy_annotations.py @@ -0,0 +1,28 @@ +import pathlib + +import strawberry +from pytest_snapshot.plugin import Snapshot + +import strawberry_django +from strawberry_django.relay import ListConnectionWithTotalCount + +from .a import AuthorConnection, AuthorType +from .b import BookConnection, BookType + +SNAPSHOTS_DIR = pathlib.Path(__file__).parent / "snapshots" + + +def test_lazy_type_annotations_in_schema(snapshot: Snapshot): + @strawberry.type + class Query: + books_conn: BookConnection = strawberry_django.connection() + books_conn2: ListConnectionWithTotalCount[BookType] = ( + strawberry_django.connection() + ) + authors_conn: AuthorConnection = strawberry_django.connection() + authors_conn2: ListConnectionWithTotalCount[AuthorType] = ( + strawberry_django.connection() + ) + + schema = strawberry.Schema(query=Query) + snapshot.assert_match(str(schema), "authors_and_books_schema.gql")