Skip to content

Commit 4b0ddb3

Browse files
authored
Merge pull request #29 from excessdenied/master
Added support for InputObjectTypes.
2 parents 1b168c3 + d3877cc commit 4b0ddb3

13 files changed

+530
-145
lines changed

README.md

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ pip install "graphene-pydantic"
1515
Here is a simple Pydantic model:
1616

1717
```python
18+
import uuid
1819
import pydantic
1920

2021
class PersonModel(pydantic.BaseModel):
2122
id: uuid.UUID
2223
first_name: str
2324
last_name: str
24-
2525
```
2626

2727
To create a GraphQL schema for it you simply have to write the following:
@@ -33,32 +33,81 @@ from graphene_pydantic import PydanticObjectType
3333
class Person(PydanticObjectType):
3434
class Meta:
3535
model = PersonModel
36-
# only return specified fields
37-
only_fields = ("name",)
3836
# exclude specified fields
3937
exclude_fields = ("id",)
4038

4139
class Query(graphene.ObjectType):
4240
people = graphene.List(Person)
4341

44-
def resolve_people(self, info):
45-
return get_people() # function returning `PersonModel`s
42+
@staticmethod
43+
def resolve_people(parent, info):
44+
# fetch actual PersonModels here
45+
return [PersonModel(id=uuid.uuid4(), first_name="Beth", last_name="Smith")]
4646

4747
schema = graphene.Schema(query=Query)
4848
```
4949

5050
Then you can simply query the schema:
5151

5252
```python
53-
query = '''
53+
query = """
5454
query {
5555
people {
5656
firstName,
5757
lastName
5858
}
5959
}
60-
'''
60+
"""
6161
result = schema.execute(query)
62+
print(result.data['people'][0])
63+
```
64+
65+
### Input Object Types
66+
67+
You can also create input object types from Pydantic models for mutations and queries:
68+
69+
```python
70+
from graphene_pydantic import PydanticInputObjectType
71+
72+
class PersonInput(PydanticInputObjectType):
73+
class Meta:
74+
model = PersonModel
75+
# exclude specified fields
76+
exclude_fields = ("id",)
77+
78+
class CreatePerson(graphene.Mutation):
79+
class Arguments:
80+
person = PersonInput()
81+
82+
Output = Person
83+
84+
@staticmethod
85+
def mutate(parent, info, person):
86+
personModel = PersonModel(id=uuid.uuid4(), first_name=person.first_name, last_name=person.last_name)
87+
# save PersonModel here
88+
return person
89+
90+
class Mutation(graphene.ObjectType):
91+
createPerson = CreatePerson.Field()
92+
93+
schema = graphene.Schema(mutation=Mutation)
94+
```
95+
96+
Then execute with the input:
97+
98+
```python
99+
mutation = '''
100+
mutation {
101+
createPerson(person: {
102+
firstName: "Jerry",
103+
lastName: "Smith"
104+
}) {
105+
firstName
106+
}
107+
}
108+
'''
109+
result = schema.execute(mutation)
110+
print(result.data['createPerson']['firstName'])
62111
```
63112

64113
### Forward declarations and circular references
@@ -210,3 +259,7 @@ If a field on a model is a Union between a class and a subclass (as in our examp
210259
Python 3.6's typing will not preserve the Union and throws away the annotation for the subclass.
211260
See [this issue](https://github.com/upsidetravel/graphene-pydantic/issues/11) for more details.
212261
The solution at present is to use Python 3.7.
262+
263+
##### Input Object Types don't support unions as fields
264+
265+
This is a GraphQL limitation. See [this RFC](https://github.com/graphql/graphql-spec/blob/master/rfcs/InputUnion.md) for the progress on supporting input unions. If you see an error like '{union-type} may only contain Object types', you are most likely encountering this limitation.

graphene_pydantic/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
from .types import PydanticObjectType
1+
from .inputobjecttype import PydanticInputObjectType
2+
from .objecttype import PydanticObjectType
23

3-
__all__ = ["PydanticObjectType"]
4+
__all__ = ["PydanticObjectType", "PydanticInputObjectType"]

graphene_pydantic/converters.py

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
1-
import sys
21
import collections
32
import collections.abc
4-
import typing as T
5-
import uuid
63
import datetime
74
import decimal
85
import enum
6+
import sys
7+
import typing as T
8+
import uuid
99

10+
from graphene import (
11+
UUID,
12+
Boolean,
13+
Enum,
14+
Field,
15+
Float,
16+
InputField,
17+
Int,
18+
List,
19+
String,
20+
Union,
21+
)
22+
from graphene.types.base import BaseType
23+
from graphene.types.datetime import Date, DateTime, Time
1024
from pydantic import BaseModel
11-
from pydantic.fields import Field as PydanticField
25+
from pydantic.fields import ModelField
26+
27+
from .registry import Registry
28+
from .util import construct_union_class_name
1229

1330
try:
1431
# Pydantic pre-1.0
@@ -38,10 +55,6 @@
3855
SHAPE_MAPPING = (fields.SHAPE_MAPPING,)
3956

4057

41-
from graphene import Field, Boolean, Enum, Float, Int, List, String, UUID, Union
42-
from graphene.types.base import BaseType
43-
from graphene.types.datetime import Date, Time, DateTime
44-
4558
try:
4659
from graphene.types.decimal import Decimal as GrapheneDecimal
4760

@@ -50,9 +63,6 @@
5063
# graphene 2.1.5+ is required for Decimals
5164
DECIMAL_SUPPORTED = False
5265

53-
from .registry import Registry
54-
from .util import construct_union_class_name
55-
5666

5767
NONE_TYPE = None.__class__ # need to do this because mypy complains about type(None)
5868

@@ -73,8 +83,37 @@ def _get_field(root, _info):
7383
return _get_field
7484

7585

86+
def convert_pydantic_input_field(
87+
field: ModelField,
88+
registry: Registry,
89+
parent_type: T.Type = None,
90+
model: T.Type[BaseModel] = None,
91+
**field_kwargs,
92+
) -> InputField:
93+
"""
94+
Convert a Pydantic model field into a Graphene type field that we can add
95+
to the generated Graphene data model type.
96+
"""
97+
declared_type = getattr(field, "type_", None)
98+
field_kwargs.setdefault(
99+
"type",
100+
convert_pydantic_type(
101+
declared_type, field, registry, parent_type=parent_type, model=model
102+
),
103+
)
104+
field_kwargs.setdefault("required", field.required)
105+
field_kwargs.setdefault("default_value", field.default)
106+
# TODO: find a better way to get a field's description. Some ideas include:
107+
# - hunt down the description from the field's schema, or the schema
108+
# from the field's base model
109+
# - maybe even (Sphinx-style) parse attribute documentation
110+
field_kwargs.setdefault("description", field.__doc__)
111+
112+
return InputField(**field_kwargs)
113+
114+
76115
def convert_pydantic_field(
77-
field: PydanticField,
116+
field: ModelField,
78117
registry: Registry,
79118
parent_type: T.Type = None,
80119
model: T.Type[BaseModel] = None,
@@ -104,8 +143,8 @@ def convert_pydantic_field(
104143

105144
def convert_pydantic_type(
106145
type_: T.Type,
107-
field: PydanticField,
108-
registry: Registry = None,
146+
field: ModelField,
147+
registry: Registry,
109148
parent_type: T.Type = None,
110149
model: T.Type[BaseModel] = None,
111150
) -> BaseType: # noqa: C901
@@ -128,8 +167,8 @@ def convert_pydantic_type(
128167

129168
def find_graphene_type(
130169
type_: T.Type,
131-
field: PydanticField,
132-
registry: Registry = None,
170+
field: ModelField,
171+
registry: Registry,
133172
parent_type: T.Type = None,
134173
model: T.Type[BaseModel] = None,
135174
) -> BaseType: # noqa: C901
@@ -203,8 +242,8 @@ def find_graphene_type(
203242

204243
def convert_generic_python_type(
205244
type_: T.Type,
206-
field: PydanticField,
207-
registry: Registry = None,
245+
field: ModelField,
246+
registry: Registry,
208247
parent_type: T.Type = None,
209248
model: T.Type[BaseModel] = None,
210249
) -> BaseType: # noqa: C901
@@ -256,8 +295,8 @@ def convert_generic_python_type(
256295

257296
def convert_union_type(
258297
type_: T.Type,
259-
field: PydanticField,
260-
registry: Registry = None,
298+
field: ModelField,
299+
registry: Registry,
261300
parent_type: T.Type = None,
262301
model: T.Type[BaseModel] = None,
263302
):

0 commit comments

Comments
 (0)