Skip to content

Commit e42b8ae

Browse files
authored
Merge pull request #2 from upsidetravel/update-public
Bring public version up to scratch
2 parents 31d9306 + c24e054 commit e42b8ae

File tree

12 files changed

+396
-160
lines changed

12 files changed

+396
-160
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repos:
1414
exclude: README.md
1515
- id: flake8
1616
- repo: https://github.com/pre-commit/mirrors-mypy
17-
rev: v0.701
17+
rev: v0.720
1818
hooks:
1919
- id: mypy
2020
args: [--ignore-missing-imports, --no-strict-optional]

examples/departments.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def resolve_list_departments(self, info):
113113
id
114114
name
115115
hiredOn
116+
salary { rating }
116117
}
117118
...on Manager {
118119
name

graphene_pydantic/converters.py

Lines changed: 81 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,30 @@
1-
from collections import OrderedDict
2-
from collections.abc import Mapping, Sequence
1+
import collections
2+
from collections import abc
33
import typing as T
44
import uuid
55
import datetime
66
import decimal
77
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
2210
from graphene.types.base import BaseType
2311

2412
try:
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

3220
from graphene.types.datetime import Date, Time, DateTime
3321
from pydantic import fields
3422

3523
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)
3628

3729

3830
class 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,85 @@ 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 (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")
167173
else:
168174
raise ConversionError(f"Don't know how to handle {type_} (generic: {origin})")
169175

170176

171-
def convert_union_type(type_, field, registry=None):
177+
def convert_union_type(type_: T.Type, field: fields.Field, registry: Registry = None):
172178
"""
173179
Convert an annotated Python Union type into a Graphene Union.
174180
"""
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)
181188
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})
189189

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

graphene_pydantic/registry.py

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,67 @@
1+
import typing as T
12
from collections import defaultdict
2-
from enum import Enum as PyEnum
33

4-
from graphene import Enum
4+
from pydantic import BaseModel
5+
from pydantic.fields import Field
6+
7+
if T.TYPE_CHECKING: # pragma: no cover
8+
from .types import PydanticObjectType
59

610

7-
def assert_is_pydantic_object_type(obj_type):
11+
def assert_is_pydantic_object_type(obj_type: T.Type["PydanticObjectType"]):
12+
"""An object in this registry must be a PydanticObjectType."""
813
from .types import PydanticObjectType
914

1015
if not isinstance(obj_type, type) or not issubclass(obj_type, PydanticObjectType):
1116
raise TypeError(f"Expected PydanticObjectType, but got: {obj_type!r}")
1217

1318

14-
class Registry(object):
19+
class Registry:
20+
"""Hold information about Pydantic models and how they (and their fields) map to Graphene types."""
21+
1522
def __init__(self):
1623
self._registry = {}
1724
self._registry_models = {}
18-
self._registry_orm_fields = defaultdict(dict)
19-
self._registry_composites = {}
20-
self._registry_enums = {}
21-
self._registry_sort_enums = {}
25+
self._registry_object_fields = defaultdict(dict)
2226

23-
def register(self, obj_type):
27+
def register(self, obj_type: T.Type["PydanticObjectType"]):
2428
assert_is_pydantic_object_type(obj_type)
2529

2630
assert (
2731
obj_type._meta.registry == self
2832
), "Can't register models linked to another Registry"
2933
self._registry[obj_type._meta.model] = obj_type
3034

31-
def get_type_for_model(self, model):
35+
def get_type_for_model(self, model: T.Type[BaseModel]) -> "PydanticObjectType":
3236
return self._registry.get(model)
3337

34-
def register_orm_field(self, obj_type, field_name, orm_field):
38+
def register_object_field(
39+
self, obj_type: T.Type["PydanticObjectType"], field_name: str, obj_field: Field
40+
):
3541
assert_is_pydantic_object_type(obj_type)
3642

37-
if not field_name or not isinstance(field_name, str):
43+
if not field_name or not isinstance(field_name, str): # pragma: no cover
3844
raise TypeError(f"Expected a field name, but got: {field_name!r}")
39-
self._registry_orm_fields[obj_type][field_name] = orm_field
40-
41-
def get_orm_field_for_graphene_field(self, obj_type, field_name):
42-
return self._registry_orm_fields.get(obj_type, {}).get(field_name)
43-
44-
def register_composite_converter(self, composite, converter):
45-
self._registry_composites[composite] = converter
46-
47-
def get_converter_for_composite(self, composite):
48-
return self._registry_composites.get(composite)
49-
50-
def register_enum(self, py_enum, graphene_enum):
51-
if not isinstance(py_enum, PyEnum):
52-
raise TypeError(f"Expected Python Enum, but got: {py_enum!r}")
53-
if not isinstance(graphene_enum, type(Enum)):
54-
raise TypeError(f"Expected Graphene Enum, but got: {graphene_enum!r}")
55-
56-
self._registry_enums[py_enum] = graphene_enum
57-
58-
def register_sort_enum(self, obj_type, sort_enum):
59-
assert_is_pydantic_object_type(obj_type)
60-
61-
if not isinstance(sort_enum, type(Enum)):
62-
raise TypeError(f"Expected Graphene Enum, but got: {sort_enum!r}")
63-
self._registry_sort_enums[obj_type] = sort_enum
45+
self._registry_object_fields[obj_type][field_name] = obj_field
6446

65-
def get_sort_enum_for_object_type(self, obj_type):
66-
return self._registry_sort_enums.get(obj_type)
47+
def get_object_field_for_graphene_field(
48+
self, obj_type: "PydanticObjectType", field_name: str
49+
) -> Field:
50+
return self._registry_object_fields.get(obj_type, {}).get(field_name)
6751

6852

69-
registry = None
53+
registry: T.Optional[Registry] = None
7054

7155

72-
def get_global_registry():
56+
def get_global_registry() -> Registry:
57+
"""Return a global instance of Registry for common use."""
7358
global registry
7459
if not registry:
7560
registry = Registry()
7661
return registry
7762

7863

7964
def reset_global_registry():
65+
"""Clear the global instance of the registry."""
8066
global registry
8167
registry = None

0 commit comments

Comments
 (0)