1- from collections import OrderedDict
2- from collections . abc import Mapping , Sequence
1+ import collections
2+ from collections import abc
33import typing as T
44import uuid
55import datetime
66import decimal
77import 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
2210from graphene .types .base import BaseType
2311
2412try :
2513 from graphene .types .decimal import Decimal as GrapheneDecimal
2614
2715 DECIMAL_SUPPORTED = True
28- except ImportError :
16+ except ImportError : # pragma: no cover
2917 # graphene 2.1.5+ is required for Decimals
3018 DECIMAL_SUPPORTED = False
3119
3220from graphene .types .datetime import Date , Time , DateTime
3321from pydantic import fields
3422
3523from .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)
3628
3729
3830class ConversionError (TypeError ):
@@ -73,11 +65,36 @@ def convert_pydantic_field(
7365 return Field (resolver = get_attr_resolver (field .name ), ** field_kwargs )
7466
7567
76- def to_graphene_type (
68+ def convert_pydantic_type (
7769 type_ : T .Type , field : fields .Field , registry : Registry = None
7870) -> BaseType : # noqa: C901
7971 """
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.
8198 """
8299 if type_ == uuid .UUID :
83100 return UUID
@@ -97,98 +114,89 @@ def to_graphene_type(
97114 return GrapheneDecimal if DECIMAL_SUPPORTED else Float
98115 elif type_ == int :
99116 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 )
100121 elif type_ in (tuple , list , set ):
101122 # TODO: do Sets really belong here?
102123 return List
103- elif hasattr (type_ , "__origin__" ):
104- return convert_generic_type (type_ , field , registry )
105124 elif issubclass (type_ , enum .Enum ):
106125 return Enum .from_enum (type_ )
107126 elif registry and registry .get_type_for_model (type_ ):
108127 return registry .get_type_for_model (type_ )
109- elif inspect .isfunction (type_ ):
110- # TODO: this may result in false positives?
111- return Dynamic (type_ )
112128 else :
113- raise Exception (
129+ raise ConversionError (
114130 f"Don't know how to convert the Pydantic field { field !r} ({ field .type_ } )"
115131 )
116132
117133
118- def convert_pydantic_type (
134+ def convert_generic_python_type (
119135 type_ : T .Type , field : fields .Field , registry : Registry = None
120136) -> 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 ):
142137 """
143138 Convert annotated Python generic types into the most appropriate Graphene
144139 Field type -- e.g. turn `typing.Union` into a Graphene Union.
145140 """
146141 origin = type_ .__origin__
147- if not origin :
142+ if not origin : # pragma: no cover # this really should be impossible
148143 raise ConversionError (f"Don't know how to convert type { type_ !r} ({ field } )" )
144+
149145 # NOTE: This is a little clumsy, but working with generic types is; it's hard to
150146 # decide whether the origin type is a subtype of, say, T.Iterable since typical
151147 # Python functions like `isinstance()` don't work
152148 if origin == T .Union :
153149 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
163162 raise ConversionError (
164163 f"Don't know how to handle { type_ } (generic: { origin } )"
165164 )
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 (
170+ T .Dict ,
171+ T .OrderedDict ,
172+ T .Mapping ,
173+ collections .OrderedDict ,
174+ dict ,
175+ ) or issubclass (origin , abc .Mapping ):
176+ raise ConversionError ("Don't know how to handle mappings in Graphene" )
167177 else :
168178 raise ConversionError (f"Don't know how to handle { type_ } (generic: { origin } )" )
169179
170180
171- def convert_union_type (type_ , field , registry = None ):
181+ def convert_union_type (type_ : T . Type , field : fields . Field , registry : Registry = None ):
172182 """
173183 Convert an annotated Python Union type into a Graphene Union.
174184 """
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 )
185+ inner_types = type_ .__args__
186+ if len (inner_types ) == 2 and NONE_TYPE in inner_types :
187+ # This is effectively a typing.Optional[T], which decomposes into a
188+ # typing.Union[None, T] -- we can return the Graphene type for T directly
189+ # since Pydantic will have already parsed it as optional
190+ native_type = next (x for x in inner_types if x != NONE_TYPE ) # noqa: E721
191+ graphene_type = find_graphene_type (native_type , field , registry )
181192 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 })
189193
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
194+ # Otherwise, we use a little metaprogramming -- create our own unique
195+ # subclass of graphene.Union that knows its constituent Graphene types
196+ parent_types = tuple (find_graphene_type (x , field , registry ) for x in inner_types )
197+ internal_meta_cls = type ("Meta" , (), {"types" : parent_types })
198+
199+ union_cls = type (
200+ construct_union_class_name (inner_types ), (Union ,), {"Meta" : internal_meta_cls }
201+ )
202+ return union_cls
0 commit comments