A simple, concise, type-safe way to write GraphQL backends in Python.
Any Boy or Girl Can Operate It!
Define your GraphQL schema in Python3.6+ code using classes and type annotations.
Graphotype is intended as a replacement for Graphene. However, graphotype (like graphene) depends on graphql-core.
We highly recommend use of mypy alongside this library. Using the type annotations you write for your schema, mypy can check that your implementation satisfies the schema that you define, which rules out large classes of programming errors when implementing your schema.
Example:
import main
import graphql
class Query(main.GQLObject):
def c(self, d: bool, e: float) -> 'Foo':
return Foo(7 if d else int(e), str(d))
class Foo(main.GQLObject):
def __init__(self, a: int, b: str) -> None:
self.a = a
self.b = b
a: int
b = 'foo'
schema = main.make_schema(query=Query, mutation=None)
print(graphql.print_schema(schema))
prints:
schema {
query: Query
}
type Foo {
b: String
a: Int
}
type Query {
c(d: Boolean, e: Float): Foo
}
- The usual Python types (int, str, bool, float) are mapped to the corresponding usual GraphQL types.
- To get the
ID
GraphQL type, import it from this package. ID is defined as atyping.NewType
of str. - In fact, any
typing.NewType
of a known scalar type can be used: we'll automatically derive serialize and deserialize functions for your NewType according to the underlying type, and define a distinct scalar type in your schema. - Enums must extend from the Python standard library enum.Enum. The Python names are exposed as the elements of the schema enum. The values are not exposed in the schema.
- Custom scalar types are serialized/deserialized using supplemental classes provided at schema creation time. To support scalar type T in your Python schema, supply a class which implements the Scalar[T] protocol (i.e., expose
parse
andserialize
as classmethods). The GraphQL schema will be created with a custom scalar type whose name isT.__name__
.
- Lists are defined via
typing.List
. - Optional values are defined via
typing.Optional
. All values not marked optional are marked as required in the schema. We recommend using Optional types liberally, as that's how GraphQL recommends you do it. - Interfaces are defined as Python classes which derive from
graphotype.Interface
, either directly or indirectly via other interfaces. - Object types are defined as Python classes which derive from
graphotype.Object
, plus zero or more interfaces. - Input objects are defined as Python dataclasses (must be annotated with @dataclass).
- Unions are defined using
EitherAB = typing.Union[A, B]
.- Unions must be referenced by name, which means using strings ("forward references") in your type annotations when referencing a union.
For example, if
EitherAB
is a Union, you must useMaybeAB = Optional['EitherAB']
instead ofMaybeAB = Optional[EitherAB]
. - Note: If you use Python 3.7+ with
from __future__ import annotations
at the top of your file, this restriction is lifted (because all annotations are interpreted as strings anyway). Seetests/test_unions.py
for examples.
- Unions must be referenced by name, which means using strings ("forward references") in your type annotations when referencing a union.
For example, if
Under the hood, the options for interfaces and unions are discriminated at runtime where necessary using isinstance
checks.
In order to determine the set of types which are part of the schema, we recursively traverse all types referenced by the root Query or Mutation types provided to make_schema
and add all thus-discovered types to the schema.
Additionally, we recursively search for subclasses of all Interface
s thus discovered and add them to the schema as well. (Why? You might create a class hierarchy of, say, an Animal interface, Dog(Animal) and Cat(Animal); we assume that if you created Dog and Cat, you might want to return them someday wherever Animal is currently part of the interface, even if Dog/Cat are not separately referenced.)
Graphene | Graphotype |
---|---|
class Query(graphene.ObjectType):
hello = graphene.String(
description='A typical hello world'
)
def resolve_hello(self, info):
return 'World' |
class Query(graphotype.Object):
def hello(self) -> str:
"""A typical hello world"""
return 'World' |
class Query(graphene.ObjectType):
hello = graphene.String(
argument=graphene.String(
default_value="stranger"
)
)
def resolve_hello(self, info, argument):
return 'Hello ' + argument Here, Graphene requires you both specify |
class Query(graphotype.Object):
def hello(
self,
argument: str = "stranger"
) -> str:
return 'Hello ' + argument With Graphotype, the definitions are one and the same. |
class Person(graphene.ObjectType):
first_name = graphene.String()
last_name = graphene.String()
full_name = graphene.String()
def resolve_full_name(self, info):
return '{} {}'.format(
self.first_name, self.last_name
) |
@dataclass
class Person(graphotype.Object):
first_name: str
last_name: str
def full_name(self) -> str:
return '{} {}'.format(
self.first_name, self.last_name
) |
If you already have a GraphQL schema you want to work with, it's easy to get started with Graphotype.
After installing graphotype, you also need to pip install jinja2 black
, then do
python -m graphotype import [schema_file]
This works with either .graphql schema format or a JSON introspection-query result, and outputs a Python file for you.
See the --help
of that command for more info.
To run the unit tests:
pip3 install tox
tox
Tox will run the tests against whichever Python versions (>=3.6) you have installed. To test against multiple versions, we recommend using pyenv. After installing pyenv, run pyenv global system 3.7 3.6
to ensure that python3.7
and python3.6
are in your PATH
.
To pass command line args to pytest, tox -- <pytest_args>
will work.
To track test coverage: rm .coverage && tox -- --cov=graphotype --cov-append
.