1
- from collections import OrderedDict
2
- from collections . abc import Mapping , Sequence
1
+ import collections
2
+ from collections import abc
3
3
import typing as T
4
4
import uuid
5
5
import datetime
6
6
import decimal
7
7
import enum
8
- import inspect
9
-
10
- from graphene import (
11
- Field ,
12
- Boolean ,
13
- Dynamic ,
14
- Enum ,
15
- Float ,
16
- Int ,
17
- List ,
18
- String ,
19
- UUID ,
20
- Union ,
21
- )
8
+
9
+ from graphene import Field , Boolean , Enum , Float , Int , List , String , UUID , Union
22
10
from graphene .types .base import BaseType
23
11
24
12
try :
25
13
from graphene .types .decimal import Decimal as GrapheneDecimal
26
14
27
15
DECIMAL_SUPPORTED = True
28
- except ImportError :
16
+ except ImportError : # pragma: no cover
29
17
# graphene 2.1.5+ is required for Decimals
30
18
DECIMAL_SUPPORTED = False
31
19
32
20
from graphene .types .datetime import Date , Time , DateTime
33
21
from pydantic import fields
34
22
35
23
from .registry import Registry
24
+ from .util import construct_union_class_name
25
+
26
+
27
+ NONE_TYPE = None .__class__ # need to do this because mypy complains about type(None)
36
28
37
29
38
30
class ConversionError (TypeError ):
@@ -73,11 +65,36 @@ def convert_pydantic_field(
73
65
return Field (resolver = get_attr_resolver (field .name ), ** field_kwargs )
74
66
75
67
76
- def to_graphene_type (
68
+ def convert_pydantic_type (
77
69
type_ : T .Type , field : fields .Field , registry : Registry = None
78
70
) -> BaseType : # noqa: C901
79
71
"""
80
- Map a native Python type to a Graphene-supported Field type, where possible.
72
+ Convert a Pydantic type to a Graphene Field type, including not just the
73
+ native Python type but any additional metadata (e.g. shape) that Pydantic
74
+ knows about.
75
+ """
76
+ graphene_type = find_graphene_type (type_ , field , registry )
77
+ if field .shape == fields .Shape .SINGLETON :
78
+ return graphene_type
79
+ elif field .shape in (
80
+ fields .Shape .LIST ,
81
+ fields .Shape .TUPLE ,
82
+ fields .Shape .TUPLE_ELLIPS ,
83
+ fields .Shape .SEQUENCE ,
84
+ fields .Shape .SET ,
85
+ ):
86
+ # TODO: _should_ Sets remain here?
87
+ return List (graphene_type )
88
+ elif field .shape == fields .Shape .MAPPING :
89
+ raise ConversionError (f"Don't know how to handle mappings in Graphene." )
90
+
91
+
92
+ def find_graphene_type (
93
+ type_ : T .Type , field : fields .Field , registry : Registry = None
94
+ ) -> BaseType : # noqa: C901
95
+ """
96
+ Map a native Python type to a Graphene-supported Field type, where possible,
97
+ throwing an error if we don't know what to map it to.
81
98
"""
82
99
if type_ == uuid .UUID :
83
100
return UUID
@@ -97,98 +114,85 @@ def to_graphene_type(
97
114
return GrapheneDecimal if DECIMAL_SUPPORTED else Float
98
115
elif type_ == int :
99
116
return Int
117
+ # NOTE: this has to come before any `issubclass()` checks, because annotated
118
+ # generic types aren't valid arguments to `issubclass`
119
+ elif hasattr (type_ , "__origin__" ):
120
+ return convert_generic_python_type (type_ , field , registry )
100
121
elif type_ in (tuple , list , set ):
101
122
# TODO: do Sets really belong here?
102
123
return List
103
- elif hasattr (type_ , "__origin__" ):
104
- return convert_generic_type (type_ , field , registry )
105
124
elif issubclass (type_ , enum .Enum ):
106
125
return Enum .from_enum (type_ )
107
126
elif registry and registry .get_type_for_model (type_ ):
108
127
return registry .get_type_for_model (type_ )
109
- elif inspect .isfunction (type_ ):
110
- # TODO: this may result in false positives?
111
- return Dynamic (type_ )
112
128
else :
113
- raise Exception (
129
+ raise ConversionError (
114
130
f"Don't know how to convert the Pydantic field { field !r} ({ field .type_ } )"
115
131
)
116
132
117
133
118
- def convert_pydantic_type (
134
+ def convert_generic_python_type (
119
135
type_ : T .Type , field : fields .Field , registry : Registry = None
120
136
) -> BaseType : # noqa: C901
121
- """
122
- Convert a Pydantic type to a Graphene Field type, including not just the
123
- native Python type but any additional metadata (e.g. shape) that Pydantic
124
- knows about.
125
- """
126
- graphene_type = to_graphene_type (type_ , field , registry )
127
- if field .shape == fields .Shape .SINGLETON :
128
- return graphene_type
129
- elif field .shape in (
130
- fields .Shape .LIST ,
131
- fields .Shape .TUPLE ,
132
- fields .Shape .SEQUENCE ,
133
- fields .Shape .SET ,
134
- ):
135
- # TODO: _should_ Sets remain here?
136
- return List (graphene_type )
137
- elif field .shape == fields .Shape .MAPPING :
138
- raise ConversionError (f"Don't know how to handle mappings in Graphene." )
139
-
140
-
141
- def convert_generic_type (type_ , field , registry = None ):
142
137
"""
143
138
Convert annotated Python generic types into the most appropriate Graphene
144
139
Field type -- e.g. turn `typing.Union` into a Graphene Union.
145
140
"""
146
141
origin = type_ .__origin__
147
- if not origin :
142
+ if not origin : # pragma: no cover # this really should be impossible
148
143
raise ConversionError (f"Don't know how to convert type { type_ !r} ({ field } )" )
144
+
149
145
# NOTE: This is a little clumsy, but working with generic types is; it's hard to
150
146
# decide whether the origin type is a subtype of, say, T.Iterable since typical
151
147
# Python functions like `isinstance()` don't work
152
148
if origin == T .Union :
153
149
return convert_union_type (type_ , field , registry )
154
- elif origin in (T .Dict , T .OrderedDict , T .Mapping , dict , OrderedDict ) or issubclass (
155
- origin , Mapping
156
- ):
157
- raise ConversionError ("Don't know how to handle mappings in Graphene" )
158
- elif origin in (T .List , T .Set , T .Collection , T .Iterable , list , set ) or issubclass (
159
- origin , Sequence
160
- ):
161
- wrapped_types = getattr (type_ , "__args__" , [])
162
- if not wrapped_types :
150
+ elif origin in (
151
+ T .Tuple ,
152
+ T .List ,
153
+ T .Set ,
154
+ T .Collection ,
155
+ T .Iterable ,
156
+ list ,
157
+ set ,
158
+ ) or issubclass (origin , abc .Sequence ):
159
+ # TODO: find a better way of divining that the origin is sequence-like
160
+ inner_types = getattr (type_ , "__args__" , [])
161
+ if not inner_types : # pragma: no cover # this really should be impossible
163
162
raise ConversionError (
164
163
f"Don't know how to handle { type_ } (generic: { origin } )"
165
164
)
166
- return List (to_graphene_type (wrapped_types [0 ], field , registry ))
165
+ # Of course, we can only return a homogeneous type here, so we pick the
166
+ # first of the wrapped types
167
+ inner_type = inner_types [0 ]
168
+ return List (find_graphene_type (inner_type , field , registry ))
169
+ elif origin in (T .Dict , T .Mapping , collections .OrderedDict , dict ) or issubclass (
170
+ origin , abc .Mapping
171
+ ):
172
+ raise ConversionError ("Don't know how to handle mappings in Graphene" )
167
173
else :
168
174
raise ConversionError (f"Don't know how to handle { type_ } (generic: { origin } )" )
169
175
170
176
171
- def convert_union_type (type_ , field , registry = None ):
177
+ def convert_union_type (type_ : T . Type , field : fields . Field , registry : Registry = None ):
172
178
"""
173
179
Convert an annotated Python Union type into a Graphene Union.
174
180
"""
175
- wrapped_types = type_ .__args__
176
- # NOTE: a typing.Optional decomposes to a Union[None, T], so we can return
177
- # the Graphene type for T; Pydantic will have already parsed it as optional
178
- if len (wrapped_types ) == 2 and type (None ) in wrapped_types :
179
- native_type = next (x for x in wrapped_types if x != type (None )) # noqa: E721
180
- graphene_type = to_graphene_type (native_type , field , registry )
181
+ inner_types = type_ .__args__
182
+ if len (inner_types ) == 2 and NONE_TYPE in inner_types :
183
+ # This is effectively a typing.Optional[T], which decomposes into a
184
+ # typing.Union[None, T] -- we can return the Graphene type for T directly
185
+ # since Pydantic will have already parsed it as optional
186
+ native_type = next (x for x in inner_types if x != NONE_TYPE ) # noqa: E721
187
+ graphene_type = find_graphene_type (native_type , field , registry )
181
188
return graphene_type
182
- else :
183
- # Otherwise, we use a little metaprogramming -- create our own unique
184
- # subclass of graphene.Union that knows its constituent Graphene types
185
- graphene_types = tuple (
186
- to_graphene_type (x , field , registry ) for x in wrapped_types
187
- )
188
- internal_meta = type ("Meta" , (), {"types" : graphene_types })
189
189
190
- union_class_name = "" .join (x .__name__ for x in wrapped_types )
191
- union_class = type (
192
- f"Union_{ union_class_name } " , (Union ,), {"Meta" : internal_meta }
193
- )
194
- return union_class
190
+ # Otherwise, we use a little metaprogramming -- create our own unique
191
+ # subclass of graphene.Union that knows its constituent Graphene types
192
+ parent_types = tuple (find_graphene_type (x , field , registry ) for x in inner_types )
193
+ internal_meta_cls = type ("Meta" , (), {"types" : parent_types })
194
+
195
+ union_cls = type (
196
+ construct_union_class_name (inner_types ), (Union ,), {"Meta" : internal_meta_cls }
197
+ )
198
+ return union_cls
0 commit comments