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

RecursionError hit when defining nested generics #3636

Open
libor-saq opened this issue Sep 20, 2024 · 2 comments
Open

RecursionError hit when defining nested generics #3636

libor-saq opened this issue Sep 20, 2024 · 2 comments
Labels
bug Something isn't working

Comments

@libor-saq
Copy link

libor-saq commented Sep 20, 2024

Describe the Bug

Hello! First of all, thank you for this library!

I have a bug probably related to #3466 - consider creating a boolean algebra of AND, OR and NOT and trying to encode it within GraphQL:

import dataclasses
import typing as t

import strawberry

_T = t.TypeVar("_T")


@strawberry.input(one_of=True)
class BoolOp(t.Generic[_T]):
    and_: t.Optional[t.List["BoolOp[_T]"]] = strawberry.UNSET
    or_: t.Optional[t.List["BoolOp[_T]"]] = strawberry.UNSET
    not_: t.Optional["BoolOp[_T]"] = strawberry.UNSET
    val: t.Optional[_T] = strawberry.UNSET


@strawberry.type
class Obj:
    a: t.Optional[int] = strawberry.UNSET
    b: t.Optional[str] = strawberry.UNSET


@strawberry.type
class Query:
    @strawberry.field
    def objs(self, where: BoolOp[Obj]) -> list:
        return []


schema = strawberry.Schema(query=Query)

I would like to create a query akin to:

query Q {
    objs(where: {or_: [{val: {a: 1, b: "abc"}, {and_: [{val: {a: 2, b: ""}}]}] {
        ...
    }
}

however, RecursionError is raised - here is a snippet of the traceback:

$ ipython dev/strawberry_nested_generics.py
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
File ~/Desktop/alembic/root/projects/aerosani/dev/strawberry_nested_generics.py:24
     19     a: t.Optional[int] = strawberry.UNSET
     20     b: t.Optional[str] = strawberry.UNSET
     23 @strawberry.type
---> 24 class Query:
     25     @strawberry.field
     26     def objs(self, where: BoolOp[Obj]) -> list:
     27         return []

File ~/Desktop/alembic/root/projects/aerosani/dev/strawberry_nested_generics.py:26, in Query()
     23 @strawberry.type
     24 class Query:
     25     @strawberry.field
---> 26     def objs(self, where: BoolOp[Obj]) -> list:
     27         return []

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/field.py:596, in field(resolver, name, is_subscription, description, permission_classes, deprecation_reason, default, default_factory, metadata, directives, extensions, graphql_type, init)
    594 if resolver:
    595     assert init is not True, "Can't set init as True when passing a resolver."
--> 596     return field_(resolver)
    597 return field_

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/field.py:199, in StrawberryField.__call__(self, resolver)
    197 if isinstance(argument.type_annotation.annotation, str):
    198     continue
--> 199 elif isinstance(argument.type, StrawberryUnion):
    200     raise InvalidArgumentTypeError(
    201         resolver,
    202         argument,
    203     )
    204 elif has_object_definition(argument.type):

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/arguments.py:131, in StrawberryArgument.type(self)
    129 @property
    130 def type(self) -> Union[StrawberryType, type]:
--> 131     return self.type_annotation.resolve()

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:133, in StrawberryAnnotation.resolve(self)
    131 """Return resolved (transformed) annotation."""
    132 if self.__resolve_cache__ is None:
--> 133     self.__resolve_cache__ = self._resolve()
    135 return self.__resolve_cache__

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:152, in StrawberryAnnotation._resolve(self)
    149 if self._is_list(evaled_type):
    150     return self.create_list(evaled_type)
--> 152 if self._is_graphql_generic(evaled_type):
    153     if any(is_type_var(type_) for type_ in get_args(evaled_type)):
    154         return evaled_type

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:281, in StrawberryAnnotation._is_graphql_generic(cls, annotation)
    279 if hasattr(annotation, "__origin__"):
    280     if definition := get_object_definition(annotation.__origin__):
--> 281         return definition.is_graphql_generic
    283     return is_generic(annotation.__origin__)
    285 return False

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/base.py:347, in StrawberryObjectDefinition.is_graphql_generic(self)
    342     return False
    344 # here we are checking if any exposed field is generic
    345 # a Strawberry class can be "generic", but not expose any
    346 # generic field to GraphQL
--> 347 return any(field.is_graphql_generic for field in self.fields)

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/base.py:347, in <genexpr>(.0)
    342     return False
    344 # here we are checking if any exposed field is generic
    345 # a Strawberry class can be "generic", but not expose any
    346 # generic field to GraphQL
--> 347 return any(field.is_graphql_generic for field in self.fields)

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/field.py:256, in StrawberryField.is_graphql_generic(self)
    251 @property
    252 def is_graphql_generic(self) -> bool:
    253     return (
    254         self.base_resolver.is_graphql_generic
    255         if self.base_resolver
--> 256         else _is_generic(self.type)
    257     )

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/field.py:305, in StrawberryField.type(self)
    297 @property  # type: ignore
    298 def type(
    299     self,
   (...)
    303     Literal[UNRESOLVED],
    304 ]:
--> 305     return self.resolve_type()

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/field.py:352, in StrawberryField.resolve_type(self, type_definition)
    349 with contextlib.suppress(NameError):
    350     # Prioritise the field type over the resolver return type
    351     if self.type_annotation is not None:
--> 352         resolved = self.type_annotation.resolve()
    353     elif self.base_resolver is not None and self.base_resolver.type is not None:
    354         # Handle unannotated functions (such as lambdas)
    355         # Generics will raise MissingTypesForGenericError later
    356         # on if we let it be returned. So use `type_annotation` instead
    357         # which is the same behaviour as having no type information.
    358         resolved = self.base_resolver.type

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:133, in StrawberryAnnotation.resolve(self)
    131 """Return resolved (transformed) annotation."""
    132 if self.__resolve_cache__ is None:
--> 133     self.__resolve_cache__ = self._resolve()
    135 return self.__resolve_cache__

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:162, in StrawberryAnnotation._resolve(self)
    160     return self.create_enum(evaled_type)
    161 elif self._is_optional(evaled_type, args):
--> 162     return self.create_optional(evaled_type)
    163 elif self._is_union(evaled_type, args):
    164     return self.create_union(evaled_type, args)

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:220, in StrawberryAnnotation.create_optional(self, evaled_type)
    210 # Note that passing a single type to `Union` is equivalent to not using `Union`
    211 # at all. This allows us to not di any checks for how many types have been
    212 # passed as we can safely use `Union` for both optional types
    213 # (e.g. `Optional[str]`) and optional unions (e.g.
    214 # `Optional[Union[TypeA, TypeB]]`)
    215 child_type = Union[non_optional_types]  # type: ignore
    217 of_type = StrawberryAnnotation(
    218     annotation=child_type,
    219     namespace=self.namespace,
--> 220 ).resolve()
    222 return StrawberryOptional(of_type)

    [... skipping similar frames: StrawberryAnnotation.resolve at line 133 (1 times)]

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:150, in StrawberryAnnotation._resolve(self)
    148     return evaled_type
    149 if self._is_list(evaled_type):
--> 150     return self.create_list(evaled_type)
    152 if self._is_graphql_generic(evaled_type):
    153     if any(is_type_var(type_) for type_ in get_args(evaled_type)):

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:197, in StrawberryAnnotation.create_list(self, evaled_type)
    192 def create_list(self, evaled_type: Any) -> StrawberryList:
    193     item_type, *_ = get_args(evaled_type)
    194     of_type = StrawberryAnnotation(
    195         annotation=item_type,
    196         namespace=self.namespace,
--> 197     ).resolve()
    199     return StrawberryList(of_type)

    [... skipping similar frames: StrawberryAnnotation.resolve at line 133 (1 times)]

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:152, in StrawberryAnnotation._resolve(self)
    149 if self._is_list(evaled_type):
    150     return self.create_list(evaled_type)
--> 152 if self._is_graphql_generic(evaled_type):
    153     if any(is_type_var(type_) for type_ in get_args(evaled_type)):
    154         return evaled_type

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:281, in StrawberryAnnotation._is_graphql_generic(cls, annotation)
    279 if hasattr(annotation, "__origin__"):
    280     if definition := get_object_definition(annotation.__origin__):
--> 281         return definition.is_graphql_generic
    283     return is_generic(annotation.__origin__)
    285 return False

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/base.py:347, in StrawberryObjectDefinition.is_graphql_generic(self)
    342     return False
    344 # here we are checking if any exposed field is generic
    345 # a Strawberry class can be "generic", but not expose any
    346 # generic field to GraphQL
--> 347 return any(field.is_graphql_generic for field in self.fields)

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/base.py:347, in <genexpr>(.0)
    342     return False
    344 # here we are checking if any exposed field is generic
    345 # a Strawberry class can be "generic", but not expose any
    346 # generic field to GraphQL
--> 347 return any(field.is_graphql_generic for field in self.fields)

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/field.py:256, in StrawberryField.is_graphql_generic(self)
    251 @property
    252 def is_graphql_generic(self) -> bool:
    253     return (
    254         self.base_resolver.is_graphql_generic
    255         if self.base_resolver
--> 256         else _is_generic(self.type)
    257     )

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/field.py:305, in StrawberryField.type(self)
    297 @property  # type: ignore
    298 def type(
    299     self,
   (...)
    303     Literal[UNRESOLVED],
    304 ]:
--> 305     return self.resolve_type()

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/types/field.py:352, in StrawberryField.resolve_type(self, type_definition)
    349 with contextlib.suppress(NameError):
    350     # Prioritise the field type over the resolver return type
    351     if self.type_annotation is not None:
--> 352         resolved = self.type_annotation.resolve()
    353     elif self.base_resolver is not None and self.base_resolver.type is not None:
    354         # Handle unannotated functions (such as lambdas)
    355         # Generics will raise MissingTypesForGenericError later
    356         # on if we let it be returned. So use `type_annotation` instead
    357         # which is the same behaviour as having no type information.
    358         resolved = self.base_resolver.type

    [... skipping similar frames: StrawberryAnnotation.resolve at line 133 (1 times)]

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:162, in StrawberryAnnotation._resolve(self)
    160     return self.create_enum(evaled_type)
    161 elif self._is_optional(evaled_type, args):
--> 162     return self.create_optional(evaled_type)
    163 elif self._is_union(evaled_type, args):
    164     return self.create_union(evaled_type, args)

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:220, in StrawberryAnnotation.create_optional(self, evaled_type)
    210 # Note that passing a single type to `Union` is equivalent to not using `Union`
    211 # at all. This allows us to not di any checks for how many types have been
    212 # passed as we can safely use `Union` for both optional types
    213 # (e.g. `Optional[str]`) and optional unions (e.g.
    214 # `Optional[Union[TypeA, TypeB]]`)
    215 child_type = Union[non_optional_types]  # type: ignore
    217 of_type = StrawberryAnnotation(
    218     annotation=child_type,
    219     namespace=self.namespace,
--> 220 ).resolve()
    222 return StrawberryOptional(of_type)

    [... skipping similar frames: StrawberryAnnotation.resolve at line 133 (1 times)]

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:150, in StrawberryAnnotation._resolve(self)
    148     return evaled_type
    149 if self._is_list(evaled_type):
--> 150     return self.create_list(evaled_type)
    152 if self._is_graphql_generic(evaled_type):
    153     if any(is_type_var(type_) for type_ in get_args(evaled_type)):

File ~/.virtualenvs/alembic/lib/python3.10/site-packages/strawberry/annotation.py:197, in StrawberryAnnotation.create_list(self, evaled_type)
    192 def create_list(self, evaled_type: Any) -> StrawberryList:
    193     item_type, *_ = get_args(evaled_type)
    194     of_type = StrawberryAnnotation(
    195         annotation=item_type,
    196         namespace=self.namespace,
--> 197     ).resolve()
    199     return StrawberryList(of_type)

    [... skipping similar frames: StrawberryAnnotation.resolve at line 133 (584 times), <genexpr> at line 347 (195 times), StrawberryAnnotation._is_graphql_generic at line 281 (195 times), StrawberryAnnotation._resolve at line 152 (195 times), StrawberryObjectDefinition.is_graphql_generic at line 347 (195 times), StrawberryField.is_graphql_generic at line 256 (195 times), StrawberryField.resolve_type at line 352 (195 times), StrawberryField.type at line 305 (195 times), StrawberryAnnotation._resolve at line 162 (194 times), StrawberryAnnotation._resolve at line 150 (194 times), StrawberryAnnotation.create_list at line 197 (194 times), StrawberryAnnotation.create_optional at line 220 (194 times)]

... SNIPPED ...

System Information

  • Operating system: macOS
  • Strawberry version: 0.237.3 and 0.242

If I wanted to contribute this fix (seems like a major feature though 😄 ), where should I start?

Thank you for your time!
Libor

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar
@libor-saq libor-saq added the bug Something isn't working label Sep 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants
@libor-saq and others