2
2
import collections .abc
3
3
import datetime
4
4
import decimal
5
- import inspect
6
5
import enum
6
+ import inspect
7
7
import sys
8
8
import typing as T
9
9
import uuid
10
+ from typing import Type , get_origin
10
11
12
+ import graphene
11
13
from graphene import (
12
- UUID ,
13
14
Boolean ,
14
15
Enum ,
15
16
Field ,
16
17
Float ,
18
+ ID ,
17
19
InputField ,
18
20
Int ,
21
+ JSONString ,
19
22
List ,
20
23
String ,
24
+ UUID ,
21
25
Union ,
22
26
)
23
- import graphene
24
27
from graphene .types .base import BaseType
25
28
from graphene .types .datetime import Date , DateTime , Time
26
29
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
29
32
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
32
35
33
- from pydantic import fields
36
+ PYTHON10 = sys .version_info >= (3 , 10 )
37
+ if PYTHON10 :
38
+ from types import UnionType
34
39
35
40
GRAPHENE2 = graphene .VERSION [0 ] < 3
36
41
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
52
44
45
+ BSON_OBJECT_ID_SUPPORTED = True
46
+ except ImportError :
47
+ BSON_OBJECT_ID_SUPPORTED = False
53
48
54
49
try :
55
50
from graphene .types .decimal import Decimal as GrapheneDecimal
59
54
# graphene 2.1.5+ is required for Decimals
60
55
DECIMAL_SUPPORTED = False
61
56
62
-
63
57
NONE_TYPE = None .__class__ # need to do this because mypy complains about type(None)
64
58
65
59
@@ -80,7 +74,7 @@ def _get_field(root, _info):
80
74
81
75
82
76
def convert_pydantic_input_field (
83
- field : ModelField ,
77
+ field : FieldInfo ,
84
78
registry : Registry ,
85
79
parent_type : T .Type = None ,
86
80
model : T .Type [BaseModel ] = None ,
@@ -90,26 +84,29 @@ def convert_pydantic_input_field(
90
84
Convert a Pydantic model field into a Graphene type field that we can add
91
85
to the generated Graphene data model type.
92
86
"""
93
- declared_type = getattr (field , "type_ " , None )
87
+ declared_type = getattr (field , "annotation " , None )
94
88
field_kwargs .setdefault (
95
89
"type" if GRAPHENE2 else "type_" ,
96
90
convert_pydantic_type (
97
91
declared_type , field , registry , parent_type = parent_type , model = model
98
92
),
99
93
)
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
+ )
102
98
# TODO: find a better way to get a field's description. Some ideas include:
103
99
# - hunt down the description from the field's schema, or the schema
104
100
# from the field's base model
105
101
# - maybe even (Sphinx-style) parse attribute documentation
106
- field_kwargs .setdefault ("description" , field .field_info . description )
102
+ field_kwargs .setdefault ("description" , field .description )
107
103
108
104
return InputField (** field_kwargs )
109
105
110
106
111
107
def convert_pydantic_field (
112
- field : ModelField ,
108
+ name : str ,
109
+ field : FieldInfo ,
113
110
registry : Registry ,
114
111
parent_type : T .Type = None ,
115
112
model : T .Type [BaseModel ] = None ,
@@ -119,44 +116,67 @@ def convert_pydantic_field(
119
116
Convert a Pydantic model field into a Graphene type field that we can add
120
117
to the generated Graphene data model type.
121
118
"""
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
+
123
133
field_kwargs .setdefault (
124
134
"type" if GRAPHENE2 else "type_" ,
125
135
convert_pydantic_type (
126
136
declared_type , field , registry , parent_type = parent_type , model = model
127
137
),
128
138
)
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 :
132
152
field_kwargs .setdefault ("name" , field .alias )
133
153
# TODO: find a better way to get a field's description. Some ideas include:
134
154
# - hunt down the description from the field's schema, or the schema
135
155
# from the field's base model
136
156
# - maybe even (Sphinx-style) parse attribute documentation
137
- field_kwargs .setdefault ("description" , field .field_info . description )
157
+ field_kwargs .setdefault ("description" , field .description )
138
158
139
159
# Handle Graphene 2 and 3
140
160
field_type = field_kwargs .pop ("type" , field_kwargs .pop ("type_" , None ))
141
161
if field_type is None :
142
162
raise ValueError ("No field type could be determined." )
143
163
144
- resolver_function = getattr (parent_type , "resolve_" + field . name , None )
164
+ resolver_function = getattr (parent_type , "resolve_" + name , None )
145
165
if resolver_function and callable (resolver_function ):
146
166
field_resolver = resolver_function
147
167
else :
148
- field_resolver = get_attr_resolver (field . name )
168
+ field_resolver = get_attr_resolver (name )
149
169
150
170
return Field (field_type , resolver = field_resolver , ** field_kwargs )
151
171
152
172
153
173
def convert_pydantic_type (
154
174
type_ : T .Type ,
155
- field : ModelField ,
175
+ field : FieldInfo ,
156
176
registry : Registry ,
157
177
parent_type : T .Type = None ,
158
178
model : T .Type [BaseModel ] = None ,
159
- ) -> BaseType : # noqa: C901
179
+ ) -> T . Union [ Type [ T . Union [ BaseType , List ]], Placeholder ] : # noqa: C901
160
180
"""
161
181
Convert a Pydantic type to a Graphene Field type, including not just the
162
182
native Python type but any additional metadata (e.g. shape) that Pydantic
@@ -165,26 +185,30 @@ def convert_pydantic_type(
165
185
graphene_type = find_graphene_type (
166
186
type_ , field , registry , parent_type = parent_type , model = model
167
187
)
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
174
190
raise ConversionError ("Don't know how to handle mappings in Graphene." )
175
191
192
+ return graphene_type
193
+
176
194
177
195
def find_graphene_type (
178
196
type_ : T .Type ,
179
- field : ModelField ,
197
+ field : FieldInfo ,
180
198
registry : Registry ,
181
199
parent_type : T .Type = None ,
182
200
model : T .Type [BaseModel ] = None ,
183
- ) -> BaseType : # noqa: C901
201
+ ) -> T . Union [ Type [ T . Union [ BaseType , List ]], Placeholder ] : # noqa: C901
184
202
"""
185
203
Map a native Python type to a Graphene-supported Field type, where possible,
186
204
throwing an error if we don't know what to map it to.
187
205
"""
206
+
207
+ # Convert Python 10 UnionType to T.Union
208
+ if PYTHON10 :
209
+ if isinstance (type_ , UnionType ):
210
+ type_ = T .Union [type_ .__args__ ]
211
+
188
212
if type_ == uuid .UUID :
189
213
return UUID
190
214
elif type_ in (str , bytes ):
@@ -199,6 +223,10 @@ def find_graphene_type(
199
223
return Boolean
200
224
elif type_ == float :
201
225
return Float
226
+ elif BSON_OBJECT_ID_SUPPORTED and type_ == ObjectId :
227
+ return ID
228
+ elif type_ == dict :
229
+ return JSONString
202
230
elif type_ == decimal .Decimal :
203
231
return GrapheneDecimal if DECIMAL_SUPPORTED else Float
204
232
elif type_ == int :
@@ -231,12 +259,13 @@ def find_graphene_type(
231
259
if not sibling :
232
260
raise ConversionError (
233
261
"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 "
235
263
"the forward reference. Did you call `resolve_placeholders()`? "
236
264
"See the README for more on forward references."
237
265
)
266
+
238
267
module_ns = sys .modules [sibling .__module__ ].__dict__
239
- resolved = evaluate_forwardref (type_ , module_ns , None )
268
+ resolved = evaluate_forward_ref (type_ , module_ns , None )
240
269
# TODO: make this behavior optional. maybe this is a place for the TypeOptions to play a role?
241
270
if registry :
242
271
registry .add_placeholder_for_model (resolved )
@@ -265,20 +294,20 @@ def find_graphene_type(
265
294
return List
266
295
else :
267
296
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 } )"
269
298
)
270
299
271
300
272
301
def convert_generic_python_type (
273
302
type_ : T .Type ,
274
- field : ModelField ,
303
+ field : FieldInfo ,
275
304
registry : Registry ,
276
305
parent_type : T .Type = None ,
277
306
model : T .Type [BaseModel ] = None ,
278
- ) -> BaseType : # noqa: C901
307
+ ) -> T . Union [ Type [ T . Union [ BaseType , List ]], Placeholder ] : # noqa: C901
279
308
"""
280
309
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.
282
311
"""
283
312
origin = type_ .__origin__
284
313
if not origin : # pragma: no cover # this really should be impossible
@@ -321,14 +350,14 @@ def convert_generic_python_type(
321
350
elif origin in (T .Dict , T .Mapping , collections .OrderedDict , dict ) or issubclass (
322
351
origin , collections .abc .Mapping
323
352
):
324
- raise ConversionError ("Don't know how to handle mappings in Graphene" )
353
+ raise ConversionError ("Don't know how to handle mappings in Graphene. " )
325
354
else :
326
355
raise ConversionError (f"Don't know how to handle { type_ } (generic: { origin } )" )
327
356
328
357
329
358
def convert_union_type (
330
359
type_ : T .Type ,
331
- field : ModelField ,
360
+ field : FieldInfo ,
332
361
registry : Registry ,
333
362
parent_type : T .Type = None ,
334
363
model : T .Type [BaseModel ] = None ,
@@ -361,11 +390,11 @@ def convert_union_type(
361
390
362
391
def convert_literal_type (
363
392
type_ : T .Type ,
364
- field : ModelField ,
393
+ field : FieldInfo ,
365
394
registry : Registry ,
366
395
parent_type : T .Type = None ,
367
396
model : T .Type [BaseModel ] = None ,
368
- ):
397
+ ) -> T . Union [ Type [ T . Union [ BaseType , List ]], Placeholder ] :
369
398
"""
370
399
Convert an annotated Python Literal type into a Graphene Scalar or Union of Scalars.
371
400
"""
0 commit comments