Skip to content

Commit 801b42d

Browse files
authored
Merge pull request #1 from PotomacInnovation/update-public
Bring public version up to scratch
2 parents 161ba0e + c24e054 commit 801b42d

14 files changed

+397
-169
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]

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ this project to learn more.
3333

3434
To sign the CLA, open a pull request as usual. If you haven't signed the CLA
3535
yet, we cannot merge any pull request until the CLA is signed. You only need to
36-
sign the CLA once. Please submit a signed CLA via email to [[email protected]](mailto:[email protected]).
36+
sign the CLA once.

CONTRIBUTOR_LICENSE_AGREEMENT.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,3 @@ You accept and agree to the following terms and conditions for Your present and
2727
7. Should You wish to submit work that is not Your original creation, You may submit it to Upside separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as “Submitted on behalf of a third-party: [named here]”.
2828

2929
8. You agree to notify Upside of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
30-
31-
32-
33-
Please sign: __________________________________ Date: ________________
34-
35-
36-
37-
**Please submit a signed copy via email to [email protected].**

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)