Creating custom types with metadata (similar to Annotated)
Hello again, I'm working on a library that uses the return type of methods at runtime to generate GraphQL types and I'm trying to deal with the case of circular imports and classes referencing each others. I was thinking of create a LazyType where you can pass the type name and the module, so that we are able to resolve the actual type at runtime without having to guess where it came from, like this: ``` import typing import strawberry if typing.TYPE_CHECKING: from .type_a import TypeA @strawberry.type class TypeB: @strawberry.field() def type_a(self, info) -> strawberry.LazyType["TypeA", ".type_a"]: from .type_a import TypeA return TypeA() ``` As previous post unfortunately I can't relay on sys.module, since the type is only imported when TYPE_CHECKING is True or inside the method. Now, I managed to make it work with a custom LazyType class, so the annotation above works; ``` strawberry.LazyType["TypeA", ".type_a"] ``` I don't dislike this to be honest, but looks like mypy isn't really happy, as you can see from the errors here: ``` tests/test_cyclic/type_b.py:13: error: "LazyType" expects no type arguments, but 2 given tests/test_cyclic/type_b.py:13: error: Invalid type comment or annotation tests/test_cyclic/type_b.py:16: error: Incompatible return value type (got "TypeA", expected "LazyType") ``` The first one could be resolve by using generics, but I'm not sure that's a good idea. The last one could be solved by creating a plugin, hopefully it won't be too difficult. But I have no clue about `Invalid type comment or annotation`. Why would that be the case? I noticed that the error disappears when removing the dot from the module name, but I'd like to be able to pass a dot there, so users of the library don't have to type the full module, since we can infer that. Any ideas? Or do I need to change the approach for this? Maybe doing a less typehint-y way: ``` def x() -> strawberry.LazyType("TypeA", ".type_a"): ``` Let me know! :)
You forgot to show us your definition for LazyType. And, honestly, a lot more context explaining what you're trying to do and why. But passing ".type_a" to any generic type isn't going to work in mypy, it's always going to be treated as a syntax error. Maybe you can play some game with `if TYPE_CHECKING` inside strawberry so that mypy thinks that LazyType is an alias for Annotated, but at runtime it's something else? On Thu, Jul 23, 2020 at 3:14 PM Patrick Arminio <patrick.arminio@gmail.com> wrote:
Hello again, I'm working on a library that uses the return type of methods at runtime to generate GraphQL types and I'm trying to deal with the case of circular imports and classes referencing each others.
I was thinking of create a LazyType where you can pass the type name and the module, so that we are able to resolve the actual type at runtime without having to guess where it came from, like this:
``` import typing
import strawberry
if typing.TYPE_CHECKING: from .type_a import TypeA
@strawberry.type class TypeB: @strawberry.field() def type_a(self, info) -> strawberry.LazyType["TypeA", ".type_a"]: from .type_a import TypeA
return TypeA() ```
As previous post unfortunately I can't relay on sys.module, since the type is only imported when TYPE_CHECKING is True or inside the method. Now, I managed to make it work with a custom LazyType class, so the annotation above works;
``` strawberry.LazyType["TypeA", ".type_a"] ```
I don't dislike this to be honest, but looks like mypy isn't really happy, as you can see from the errors here:
``` tests/test_cyclic/type_b.py:13: error: "LazyType" expects no type arguments, but 2 given tests/test_cyclic/type_b.py:13: error: Invalid type comment or annotation tests/test_cyclic/type_b.py:16: error: Incompatible return value type (got "TypeA", expected "LazyType") ```
The first one could be resolve by using generics, but I'm not sure that's a good idea. The last one could be solved by creating a plugin, hopefully it won't be too difficult. But I have no clue about `Invalid type comment or annotation`. Why would that be the case?
I noticed that the error disappears when removing the dot from the module name, but I'd like to be able to pass a dot there, so users of the library don't have to type the full module, since we can infer that.
Any ideas? Or do I need to change the approach for this? Maybe doing a less typehint-y way:
``` def x() -> strawberry.LazyType("TypeA", ".type_a"): ```
Let me know! :) _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: guido@python.org
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
On Fri, 24 Jul 2020 at 00:57, Guido van Rossum <guido@python.org> wrote:
You forgot to show us your definition for LazyType. And, honestly, a lot more context explaining what you're trying to do and why.
Sure, my bad, I thought the implementation of LazyType was quite simple and not needed. Here it is: @dataclass(frozen=True) class LazyType: type_name: str module: str package: Optional[str] def __class_getitem__(cls, params): type_name, module = params package = None if module.startswith("."): current_frame = inspect.currentframe() package = current_frame.f_back.f_globals["__package__"] return cls(type_name, module, package) def resolve_type(self) -> Type: module = importlib.import_module(self.module, self.package) return module.__dict__[self.type_name] The reason why I need to have this class is so that when I create the GraphQL type I can get access to the information of the type referenced by each field. And since types can reference each other I need to have some way of importing the types "lazily", here's an example: @strawberry.type class TypeA: type_b: TypeB @strawberry.type class TypeB: type_a: TypeA The example above works when the types are defined in the same file and using string annotations (I need to work on supporting from __future__ import annotations). But this breaks when the types are defined in two different files, like here: https://github.com/strawberry-graphql/strawberry/blob/master/tests/test_cycl... This could be solved by having a global type registry, but I don't want to have that since I want users to be able to define multiple types with the same name in the same python application. Hope this helps, happy to share more context if anything is unclear.
But passing ".type_a" to any generic type isn't going to work in mypy, it's always going to be treated as a syntax error.
Oh, that's interesting. I guess I can remove the functionality of finding the package name then, it's not a huge deal.
Maybe you can play some game with `if TYPE_CHECKING` inside strawberry so that mypy thinks that LazyType is an alias for Annotated, but at runtime it's something else?
I'll try this tomorrow :) Thank you
On Thu, Jul 23, 2020 at 3:14 PM Patrick Arminio <patrick.arminio@gmail.com> wrote:
Hello again, I'm working on a library that uses the return type of methods at runtime to generate GraphQL types and I'm trying to deal with the case of circular imports and classes referencing each others.
I was thinking of create a LazyType where you can pass the type name and the module, so that we are able to resolve the actual type at runtime without having to guess where it came from, like this:
``` import typing
import strawberry
if typing.TYPE_CHECKING: from .type_a import TypeA
@strawberry.type class TypeB: @strawberry.field() def type_a(self, info) -> strawberry.LazyType["TypeA", ".type_a"]: from .type_a import TypeA
return TypeA() ```
As previous post unfortunately I can't relay on sys.module, since the type is only imported when TYPE_CHECKING is True or inside the method. Now, I managed to make it work with a custom LazyType class, so the annotation above works;
``` strawberry.LazyType["TypeA", ".type_a"] ```
I don't dislike this to be honest, but looks like mypy isn't really happy, as you can see from the errors here:
``` tests/test_cyclic/type_b.py:13: error: "LazyType" expects no type arguments, but 2 given tests/test_cyclic/type_b.py:13: error: Invalid type comment or annotation tests/test_cyclic/type_b.py:16: error: Incompatible return value type (got "TypeA", expected "LazyType") ```
The first one could be resolve by using generics, but I'm not sure that's a good idea. The last one could be solved by creating a plugin, hopefully it won't be too difficult. But I have no clue about `Invalid type comment or annotation`. Why would that be the case?
I noticed that the error disappears when removing the dot from the module name, but I'd like to be able to pass a dot there, so users of the library don't have to type the full module, since we can infer that.
Any ideas? Or do I need to change the approach for this? Maybe doing a less typehint-y way:
``` def x() -> strawberry.LazyType("TypeA", ".type_a"): ```
Let me know! :) _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: guido@python.org
-- --Guido van Rossum (python.org/~guido) *Pronouns: he/him **(why is my pronoun here?)* <http://feministing.com/2015/02/03/how-using-they-as-a-singular-pronoun-can-change-the-world/>
-- Patrick Arminio
On Fri, 24 Jul 2020 at 00:57, Guido van Rossum <guido@python.org> wrote:
You forgot to show us your definition for LazyType. And, honestly, a lot more context explaining what you're trying to do and why.
But passing ".type_a" to any generic type isn't going to work in mypy, it's always going to be treated as a syntax error.
I just released that even using Annotated in the way I want is not going to work unfortunately, as mypy complains about "type_b" not being defined when doing this: Annotated["TypeB", "type_b"] which is unfortunate
Maybe you can play some game with `if TYPE_CHECKING` inside strawberry so that mypy thinks that LazyType is an alias for Annotated, but at runtime it's something else?
I also tried to work with `if TYPE_CHECKING`, but that didn't work as I was getting these errors when reassigning the type with `LazyType = Annotated` tests/test_cyclic/type_a.py:8: error: Cannot assign to a type tests/test_cyclic/type_a.py:8: error: Incompatible types in assignment (expression has type "_SpecialForm", variable has type "Type[LazyType]") tests/test_cyclic/type_a.py:19: error: "LazyType" expects no type arguments, but 2 given tests/test_cyclic/type_a.py:19: error: Name 'type_b' is not defined The only thing that seems to get closer to 0 errors is by doing this for LazyType: TypeName = TypeVar("TypeName") Module = TypeVar("Module") @dataclass(frozen=True) class LazyType(Generic[TypeName, Module]): type_name: str module: str package: Optional[str] def __class_getitem__(cls, params): type_name, module = params package = None if module.startswith("."): current_frame = inspect.currentframe() package = current_frame.f_back.f_globals["__package__"] return cls(type_name, module, package) So adding two generics params to it, even if they are not used :) But I still get this: tests/test_cyclic/type_a.py:13: error: Name 'type_b' is not defined And I wonder if I can use a plugin to tell mypy to ignore this error, I might try to experiment with this later! -- Patrick Arminio
participants (2)
-
Guido van Rossum
-
Patrick Arminio