Graphene: Schema syntax inherently conflicts with types annotations

Created on 3 Jan 2019  路  4Comments  路  Source: graphql-python/graphene

_This is probably more for graphene-next or something, but the way Graphene defines schema objects conflicts with python static typing standards._

GraphQL is intended to be statically typed; So a GraphQL server implementation would likely like to have static typing in its own code.

Eg, In Graphene, we'll write this class:

class MyGrapheneObject(ObjectType):
    name = String()
    age = Int()

while in PEP 484, we'll write something like this:

class MyTypedObject(ObjectType):
    name: str
    age: int

Under PEP 484, MyGrapheneObject is interpreted so that name and age are both class members and instance member, with the types String and Int, respectively. That makes code like this fail type checks:

class MyGrapheneObject:
    ....
    def title_case(self):
        return self.name.title()  #  ERROR: `name` of type `String` doesn't have member `title()`

Even though in reality, the value of self.name will generally always be of type str.

I don't have a solution for this yet, but I suspect a slightly different syntax for defining the schema would be in order.

wontfix

Most helpful comment

FYI there was some interesting discussion on using type annotations to actually define the schema in a this issue: https://github.com/graphql-python/graphene/issues/729

All 4 comments

Couple of suggestions on syntax to resolve this issue:

class MyFirstAttempt(ObjectType):
    __SCHEMA = dict(
        name=String(description='Name of person'),
        age=Int(required=True),
    )
    name: str
    age: int


class MySecondAttempt(ObjectType):
    class __SCHEMA:
        name = String()
        age = Int(required=True)

    age: int

(Actual type-hints on name and age instance field is completely optional in these options; __SCHEMA just moves the gql fields into a single class member, so it doesn't interfere with the instance field types.

Another option, which I don't really like, looks like this:

class MyLeastFavoriteOption(ObjectType):
    name = MagicTypingHelper[str](String(description='foo'))
    age = MagicTypingHelper[int](Int)

Where MagicTypingHelper[A](B) is something that statically resolves to have type A, but dynamically resolves to have value B (which is of a completely different type).
I don't like this one because of all the magic, and because we're working against the type checker (eg, MyLeastFavoriteOption.name here have value String() of type String, but the checker thinks it's type is str).

I think this is an issue with all libraries using the same to define "schema"-like entities, e.g.

  • sqlalchemy
  • marshmallow
  • django (orm)

Since this pattern(of using class-level attributes for one purpose, and instance attributes of the same name for another) is used so often, I think there should actually be support for it in type checkers(and the typing module). Some thing like that:

from typing import Hybrid
class Example:
  a: Hybrid[String, str] = String()
  b: Hybrid[Integer, int] = Integer()

x: Example = ... 
print(x.a.encode("utf-8")) # typechecks
print(x.b + 2) # typechecks
print(Example.a.required) # typechecks, assuming String objects have 'required' attribute
print(Example.b.required) # typechecks, assuming Integer objects have 'required' attribute

@avivey Until that happens, I guess your first suggestion is the way to go.

I also prefer having a separate namespace in the class for field definitions, just like there's a separate Meta namespace for other configuration options. This leaves the rest of the class namespace as free real estate for custom class attributes and methods.

And actually, I just thought of a good way to define resolvers in any namespace without relying on the method name:

class MySecondAttempt(ObjectType):
    class __SCHEMA:
        name = String()
        age = Int(required=True)

        @name.resolver
        def resolve_name(self, ...): ...

        @age.resolver
        def resolve_age(self, ...): ...

    age: int

The resolvers could have any arbitrary name, and could also be defined before or after the class definition. Explicit is better than implicit. Resolvers could still be search from method names by default if the explicit decorator isn't used. Guess I could create a separate issue for that.

FYI there was some interesting discussion on using type annotations to actually define the schema in a this issue: https://github.com/graphql-python/graphene/issues/729

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Was this page helpful?
0 / 5 - 0 ratings