Skip to content

Commit 5d66766

Browse files
authored
Merge pull request #94 from strollby/feat/pydantic-v2
Pydantic V2 Support
2 parents 54a159b + 58135e7 commit 5d66766

13 files changed

+379
-167
lines changed

.github/workflows/tests.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
strategy:
1818
fail-fast: false
1919
matrix:
20-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
20+
python-version: ["3.8", "3.9", "3.10", "3.11"]
2121
os: [ubuntu-latest, macos-latest, windows-latest]
2222
steps:
2323
- uses: actions/checkout@v3

graphene_pydantic/converters.py

+86-57
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,49 @@
22
import collections.abc
33
import datetime
44
import decimal
5-
import inspect
65
import enum
6+
import inspect
77
import sys
88
import typing as T
99
import uuid
10+
from typing import Type, get_origin
1011

12+
import graphene
1113
from graphene import (
12-
UUID,
1314
Boolean,
1415
Enum,
1516
Field,
1617
Float,
18+
ID,
1719
InputField,
1820
Int,
21+
JSONString,
1922
List,
2023
String,
24+
UUID,
2125
Union,
2226
)
23-
import graphene
2427
from graphene.types.base import BaseType
2528
from graphene.types.datetime import Date, DateTime, Time
2629
from pydantic import BaseModel
27-
from pydantic.fields import ModelField
28-
from pydantic.typing import evaluate_forwardref
30+
from pydantic.fields import FieldInfo
31+
from pydantic_core import PydanticUndefined
2932

30-
from .registry import Registry
31-
from .util import construct_union_class_name
33+
from .registry import Placeholder, Registry
34+
from .util import construct_union_class_name, evaluate_forward_ref
3235

33-
from pydantic import fields
36+
PYTHON10 = sys.version_info >= (3, 10)
37+
if PYTHON10:
38+
from types import UnionType
3439

3540
GRAPHENE2 = graphene.VERSION[0] < 3
3641

37-
SHAPE_SINGLETON = (fields.SHAPE_SINGLETON,)
38-
SHAPE_SEQUENTIAL = (
39-
fields.SHAPE_LIST,
40-
fields.SHAPE_TUPLE,
41-
fields.SHAPE_TUPLE_ELLIPSIS,
42-
fields.SHAPE_SEQUENCE,
43-
fields.SHAPE_SET,
44-
)
45-
46-
if hasattr(fields, "SHAPE_DICT"):
47-
SHAPE_MAPPING = T.cast(
48-
T.Tuple, (fields.SHAPE_MAPPING, fields.SHAPE_DICT, fields.SHAPE_DEFAULTDICT)
49-
)
50-
else:
51-
SHAPE_MAPPING = T.cast(T.Tuple, (fields.SHAPE_MAPPING,))
42+
try:
43+
from bson import ObjectId
5244

45+
BSON_OBJECT_ID_SUPPORTED = True
46+
except ImportError:
47+
BSON_OBJECT_ID_SUPPORTED = False
5348

5449
try:
5550
from graphene.types.decimal import Decimal as GrapheneDecimal
@@ -59,7 +54,6 @@
5954
# graphene 2.1.5+ is required for Decimals
6055
DECIMAL_SUPPORTED = False
6156

62-
6357
NONE_TYPE = None.__class__ # need to do this because mypy complains about type(None)
6458

6559

@@ -80,7 +74,7 @@ def _get_field(root, _info):
8074

8175

8276
def convert_pydantic_input_field(
83-
field: ModelField,
77+
field: FieldInfo,
8478
registry: Registry,
8579
parent_type: T.Type = None,
8680
model: T.Type[BaseModel] = None,
@@ -90,26 +84,29 @@ def convert_pydantic_input_field(
9084
Convert a Pydantic model field into a Graphene type field that we can add
9185
to the generated Graphene data model type.
9286
"""
93-
declared_type = getattr(field, "type_", None)
87+
declared_type = getattr(field, "annotation", None)
9488
field_kwargs.setdefault(
9589
"type" if GRAPHENE2 else "type_",
9690
convert_pydantic_type(
9791
declared_type, field, registry, parent_type=parent_type, model=model
9892
),
9993
)
100-
field_kwargs.setdefault("required", field.required)
101-
field_kwargs.setdefault("default_value", field.default)
94+
field_kwargs.setdefault("required", field.is_required())
95+
field_kwargs.setdefault(
96+
"default_value", None if field.default is PydanticUndefined else field.default
97+
)
10298
# TODO: find a better way to get a field's description. Some ideas include:
10399
# - hunt down the description from the field's schema, or the schema
104100
# from the field's base model
105101
# - maybe even (Sphinx-style) parse attribute documentation
106-
field_kwargs.setdefault("description", field.field_info.description)
102+
field_kwargs.setdefault("description", field.description)
107103

108104
return InputField(**field_kwargs)
109105

110106

111107
def convert_pydantic_field(
112-
field: ModelField,
108+
name: str,
109+
field: FieldInfo,
113110
registry: Registry,
114111
parent_type: T.Type = None,
115112
model: T.Type[BaseModel] = None,
@@ -119,44 +116,67 @@ def convert_pydantic_field(
119116
Convert a Pydantic model field into a Graphene type field that we can add
120117
to the generated Graphene data model type.
121118
"""
122-
declared_type = getattr(field, "type_", None)
119+
declared_type = getattr(field, "annotation", None)
120+
121+
# Convert Python 10 UnionType to T.Union
122+
if PYTHON10:
123+
is_union_type = (
124+
get_origin(declared_type) is T.Union
125+
or get_origin(declared_type) is UnionType
126+
)
127+
else:
128+
is_union_type = get_origin(declared_type) is T.Union
129+
130+
if is_union_type:
131+
declared_type = T.Union[declared_type.__args__]
132+
123133
field_kwargs.setdefault(
124134
"type" if GRAPHENE2 else "type_",
125135
convert_pydantic_type(
126136
declared_type, field, registry, parent_type=parent_type, model=model
127137
),
128138
)
129-
field_kwargs.setdefault("required", not field.allow_none)
130-
field_kwargs.setdefault("default_value", field.default)
131-
if field.has_alias:
139+
field_kwargs.setdefault(
140+
"required",
141+
field.is_required()
142+
or (
143+
type(field.default) is not PydanticUndefined
144+
and getattr(declared_type, "_name", "") != "Optional"
145+
and not is_union_type
146+
),
147+
)
148+
field_kwargs.setdefault(
149+
"default_value", None if field.default is PydanticUndefined else field.default
150+
)
151+
if field.alias:
132152
field_kwargs.setdefault("name", field.alias)
133153
# TODO: find a better way to get a field's description. Some ideas include:
134154
# - hunt down the description from the field's schema, or the schema
135155
# from the field's base model
136156
# - maybe even (Sphinx-style) parse attribute documentation
137-
field_kwargs.setdefault("description", field.field_info.description)
157+
field_kwargs.setdefault("description", field.description)
138158

139159
# Handle Graphene 2 and 3
140160
field_type = field_kwargs.pop("type", field_kwargs.pop("type_", None))
141161
if field_type is None:
142162
raise ValueError("No field type could be determined.")
143163

144-
resolver_function = getattr(parent_type, "resolve_" + field.name, None)
164+
resolver_function = getattr(parent_type, "resolve_" + name, None)
145165
if resolver_function and callable(resolver_function):
146166
field_resolver = resolver_function
147167
else:
148-
field_resolver = get_attr_resolver(field.name)
168+
field_resolver = get_attr_resolver(name)
149169

150170
return Field(field_type, resolver=field_resolver, **field_kwargs)
151171

152172

153173
def convert_pydantic_type(
154174
type_: T.Type,
155-
field: ModelField,
175+
field: FieldInfo,
156176
registry: Registry,
157177
parent_type: T.Type = None,
158178
model: T.Type[BaseModel] = None,
159-
) -> BaseType: # noqa: C901
179+
) -> T.Union[Type[T.Union[BaseType, List]], Placeholder]: # noqa: C901
160180
"""
161181
Convert a Pydantic type to a Graphene Field type, including not just the
162182
native Python type but any additional metadata (e.g. shape) that Pydantic
@@ -165,26 +185,30 @@ def convert_pydantic_type(
165185
graphene_type = find_graphene_type(
166186
type_, field, registry, parent_type=parent_type, model=model
167187
)
168-
if field.shape in SHAPE_SINGLETON:
169-
return graphene_type
170-
elif field.shape in SHAPE_SEQUENTIAL:
171-
# TODO: _should_ Sets remain here?
172-
return List(graphene_type)
173-
elif field.shape in SHAPE_MAPPING:
188+
field_type = getattr(field.annotation, "__origin__", None)
189+
if field_type == map: # SHAPE_MAPPING
174190
raise ConversionError("Don't know how to handle mappings in Graphene.")
175191

192+
return graphene_type
193+
176194

177195
def find_graphene_type(
178196
type_: T.Type,
179-
field: ModelField,
197+
field: FieldInfo,
180198
registry: Registry,
181199
parent_type: T.Type = None,
182200
model: T.Type[BaseModel] = None,
183-
) -> BaseType: # noqa: C901
201+
) -> T.Union[Type[T.Union[BaseType, List]], Placeholder]: # noqa: C901
184202
"""
185203
Map a native Python type to a Graphene-supported Field type, where possible,
186204
throwing an error if we don't know what to map it to.
187205
"""
206+
207+
# Convert Python 10 UnionType to T.Union
208+
if PYTHON10:
209+
if isinstance(type_, UnionType):
210+
type_ = T.Union[type_.__args__]
211+
188212
if type_ == uuid.UUID:
189213
return UUID
190214
elif type_ in (str, bytes):
@@ -199,6 +223,10 @@ def find_graphene_type(
199223
return Boolean
200224
elif type_ == float:
201225
return Float
226+
elif BSON_OBJECT_ID_SUPPORTED and type_ == ObjectId:
227+
return ID
228+
elif type_ == dict:
229+
return JSONString
202230
elif type_ == decimal.Decimal:
203231
return GrapheneDecimal if DECIMAL_SUPPORTED else Float
204232
elif type_ == int:
@@ -231,12 +259,13 @@ def find_graphene_type(
231259
if not sibling:
232260
raise ConversionError(
233261
"Don't know how to convert the Pydantic field "
234-
f"{field!r} ({field.type_}), could not resolve "
262+
f"{field!r} ({field.annotation}), could not resolve "
235263
"the forward reference. Did you call `resolve_placeholders()`? "
236264
"See the README for more on forward references."
237265
)
266+
238267
module_ns = sys.modules[sibling.__module__].__dict__
239-
resolved = evaluate_forwardref(type_, module_ns, None)
268+
resolved = evaluate_forward_ref(type_, module_ns, None)
240269
# TODO: make this behavior optional. maybe this is a place for the TypeOptions to play a role?
241270
if registry:
242271
registry.add_placeholder_for_model(resolved)
@@ -265,20 +294,20 @@ def find_graphene_type(
265294
return List
266295
else:
267296
raise ConversionError(
268-
f"Don't know how to convert the Pydantic field {field!r} ({field.type_})"
297+
f"Don't know how to convert the Pydantic field {field!r} ({field.annotation})"
269298
)
270299

271300

272301
def convert_generic_python_type(
273302
type_: T.Type,
274-
field: ModelField,
303+
field: FieldInfo,
275304
registry: Registry,
276305
parent_type: T.Type = None,
277306
model: T.Type[BaseModel] = None,
278-
) -> BaseType: # noqa: C901
307+
) -> T.Union[Type[T.Union[BaseType, List]], Placeholder]: # noqa: C901
279308
"""
280309
Convert annotated Python generic types into the most appropriate Graphene
281-
Field type -- e.g. turn `typing.Union` into a Graphene Union.
310+
Field type -- e.g., turn `typing.Union` into a Graphene Union.
282311
"""
283312
origin = type_.__origin__
284313
if not origin: # pragma: no cover # this really should be impossible
@@ -321,14 +350,14 @@ def convert_generic_python_type(
321350
elif origin in (T.Dict, T.Mapping, collections.OrderedDict, dict) or issubclass(
322351
origin, collections.abc.Mapping
323352
):
324-
raise ConversionError("Don't know how to handle mappings in Graphene")
353+
raise ConversionError("Don't know how to handle mappings in Graphene.")
325354
else:
326355
raise ConversionError(f"Don't know how to handle {type_} (generic: {origin})")
327356

328357

329358
def convert_union_type(
330359
type_: T.Type,
331-
field: ModelField,
360+
field: FieldInfo,
332361
registry: Registry,
333362
parent_type: T.Type = None,
334363
model: T.Type[BaseModel] = None,
@@ -361,11 +390,11 @@ def convert_union_type(
361390

362391
def convert_literal_type(
363392
type_: T.Type,
364-
field: ModelField,
393+
field: FieldInfo,
365394
registry: Registry,
366395
parent_type: T.Type = None,
367396
model: T.Type[BaseModel] = None,
368-
):
397+
) -> T.Union[Type[T.Union[BaseType, List]], Placeholder]:
369398
"""
370399
Convert an annotated Python Literal type into a Graphene Scalar or Union of Scalars.
371400
"""

graphene_pydantic/inputobjecttype.py

+16-6
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,24 @@ def construct_fields(
3636
if exclude_fields:
3737
excluded = exclude_fields
3838
elif only_fields:
39-
excluded = tuple(k for k in model.__fields__ if k not in only_fields)
39+
excluded = tuple(k for k in model.model_fields if k not in only_fields)
4040

4141
fields_to_convert = (
42-
(k, v) for k, v in model.__fields__.items() if k not in excluded
42+
(k, v) for k, v in model.model_fields.items() if k not in excluded
4343
)
4444

4545
fields = {}
4646
for name, field in fields_to_convert:
47+
# Graphql does not accept union as input. Refer https://github.com/graphql/graphql-spec/issues/488
48+
annotation = getattr(field, "annotation", None)
49+
if isinstance(annotation, str) or isinstance(annotation, int):
50+
union_types = field.annotation.__args__
51+
if type(None) not in union_types or len(union_types) > 2:
52+
continue
53+
# But str|None or Union[str, None] is valid input equivalent to Optional[str]
54+
base_type = list(filter(lambda x: x is not type(None), union_types)).pop()
55+
field.annotation = T.Optional[base_type]
56+
4757
converted = convert_pydantic_input_field(
4858
field, registry, parent_type=obj_type, model=model
4959
)
@@ -127,11 +137,11 @@ def resolve_placeholders(cls):
127137
meta = cls._meta
128138
fields_to_update = {}
129139
for name, field in meta.fields.items():
130-
target_type = field._type
131-
if hasattr(target_type, "_of_type"):
132-
target_type = target_type._of_type
140+
target_type = field.type
141+
while hasattr(target_type, "of_type"):
142+
target_type = target_type.of_type
133143
if isinstance(target_type, Placeholder):
134-
pydantic_field = meta.model.__fields__[name]
144+
pydantic_field = meta.model.model_fields[name]
135145
graphene_field = convert_pydantic_input_field(
136146
pydantic_field,
137147
meta.registry,

0 commit comments

Comments
 (0)