Skip to content

Commit 6576993

Browse files
committed
Implementation of automatic runtime types.
Code refactor and clean-up. More tests!
1 parent 45becbf commit 6576993

14 files changed

+207
-54
lines changed

epoxy/bases/class_type_creator.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from ..thunk import ResolveThunk, ThunkList
2+
3+
4+
class ClassTypeCreator(object):
5+
def __init__(self, registry, class_type_creator):
6+
self._registry = registry
7+
self._class_type_creator = class_type_creator
8+
9+
def __getattr__(self, item):
10+
return self[item]
11+
12+
def __getitem__(self, item):
13+
if isinstance(item, tuple):
14+
type_thunk = ThunkList([ResolveThunk(self._registry._resolve_type, i) for i in item])
15+
16+
else:
17+
type_thunk = ThunkList([ResolveThunk(self._registry._resolve_type, item)])
18+
19+
return self._class_type_creator(type_thunk)

epoxy/metaclasses/interface.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,21 @@
33
from graphql.core.type.definition import GraphQLInterfaceType
44
from ..utils.get_declared_fields import get_declared_fields
55
from ..utils.make_default_resolver import make_default_resolver
6-
from ..utils.ref_holder import RefHolder
6+
from ..utils.weak_ref_holder import WeakRefHolder
77
from ..utils.yank_potential_fields import yank_potential_fields
88

99

1010
class InterfaceMeta(type):
1111
def __new__(mcs, name, bases, attrs):
12-
if attrs.get('abstract'):
12+
if attrs.pop('abstract', False):
1313
return super(InterfaceMeta, mcs).__new__(mcs, name, bases, attrs)
1414

15-
class_ref = RefHolder()
15+
class_ref = WeakRefHolder()
1616
declared_fields = get_declared_fields(name, yank_potential_fields(attrs))
1717
interface = GraphQLInterfaceType(
1818
name,
1919
fields=partial(mcs._build_field_map, class_ref, declared_fields),
2020
description=attrs.get('__doc__'),
21-
resolve_type=lambda: None
2221
)
2322

2423
mcs._register(interface, declared_fields)

epoxy/metaclasses/object_type.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from ..utils.get_declared_fields import get_declared_fields
55
from ..utils.make_default_resolver import make_default_resolver
66
from ..utils.no_implementation_registration import no_implementation_registration
7-
from ..utils.ref_holder import RefHolder
7+
from ..utils.weak_ref_holder import WeakRefHolder
88
from ..utils.yank_potential_fields import yank_potential_fields
99

1010

@@ -13,8 +13,10 @@ def __new__(mcs, name, bases, attrs):
1313
if attrs.pop('abstract', False):
1414
return super(ObjectTypeMeta, mcs).__new__(mcs, name, bases, attrs)
1515

16-
class_ref = RefHolder()
16+
class_ref = WeakRefHolder()
17+
registry = mcs._get_registry()
1718
declared_fields = get_declared_fields(name, yank_potential_fields(attrs))
19+
1820
with no_implementation_registration():
1921
object_type = GraphQLObjectType(
2022
name,
@@ -23,10 +25,12 @@ def __new__(mcs, name, bases, attrs):
2325
interfaces=mcs._get_interfaces()
2426
)
2527

26-
mcs._register(object_type)
28+
object_type.is_type_of = registry._create_is_type_of(object_type)
29+
2730
cls = super(ObjectTypeMeta, mcs).__new__(mcs, name, bases, attrs)
31+
mcs._register(object_type, cls)
32+
cls._registry = registry
2833
cls.T = object_type
29-
cls._registry = mcs._get_registry()
3034
class_ref.set(cls)
3135

3236
return cls

epoxy/metaclasses/union.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from collections import OrderedDict
2+
from functools import partial
3+
from graphql.core.type import GraphQLObjectType
4+
from graphql.core.type.definition import GraphQLInterfaceType, GraphQLUnionType
5+
from ..utils.get_declared_fields import get_declared_fields
6+
from ..utils.make_default_resolver import make_default_resolver
7+
from ..utils.no_implementation_registration import no_implementation_registration
8+
from ..utils.weak_ref_holder import WeakRefHolder
9+
from ..utils.yank_potential_fields import yank_potential_fields
10+
11+
12+
class ObjectTypeMeta(type):
13+
def __new__(mcs, name, bases, attrs):
14+
if attrs.pop('abstract', False):
15+
return super(ObjectTypeMeta, mcs).__new__(mcs, name, bases, attrs)
16+
17+
with no_implementation_registration():
18+
union_type = GraphQLUnionType(
19+
name,
20+
types=mcs._get_types()
21+
description=attrs.get('__doc__'),
22+
)
23+
24+
mcs._register(union_type)
25+
cls = super(ObjectTypeMeta, mcs).__new__(mcs, name, bases, attrs)
26+
cls.T = union_type
27+
cls._registry = mcs._get_registry()
28+
29+
return cls
30+
31+
@staticmethod
32+
def _register(union_type):
33+
raise NotImplementedError('_register must be implemented in the sub-metaclass')
34+
35+
@staticmethod
36+
def _get_registry():
37+
raise NotImplementedError('_get_registry must be implemented in the sub-metaclass')
38+
39+
@staticmethod
40+
def _get_types():
41+
return None

epoxy/registry.py

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from collections import defaultdict
12
from enum import Enum
2-
3+
from functools import partial
34
from graphql.core.type import (
45
GraphQLBoolean,
56
GraphQLEnumType,
@@ -19,6 +20,7 @@
1920

2021
import six
2122
from .bases.object_type import ObjectTypeBase
23+
from .bases.class_type_creator import ClassTypeCreator
2224
from .field import Field
2325
from .metaclasses.interface import InterfaceMeta
2426
from .metaclasses.object_type import ObjectTypeMeta
@@ -44,8 +46,10 @@ def __init__(self):
4446
self._registered_types = {}
4547
self._added_impl_types = set()
4648
self._interface_declared_fields = {}
49+
self._registered_types_can_be = defaultdict(set)
4750
self.ObjectType = self._create_object_type_class()
48-
self.Implements = self._create_implement_type_class()
51+
self.Implements = ClassTypeCreator(self, self._create_object_type_class)
52+
self.Union = ClassTypeCreator(self, self._create_union_type_class)
4953
self.Interface = self._create_interface_type_class()
5054

5155
for type in builtin_scalars:
@@ -66,8 +70,13 @@ def register(self, t):
6670
@register.register(GraphQLInputObjectType)
6771
@register.register(GraphQLScalarType)
6872
def register_(self, t):
69-
assert t.name not in ('ObjectType', 'Implements', 'Interface')
70-
assert t.name not in self._registered_types
73+
assert not t.name.startswith('_'), \
74+
'Registered type name cannot start with an "_".'
75+
assert t.name not in ('ObjectType', 'Implements', 'Interface', 'Schema'), \
76+
'You cannot register a type named "{}".'.format(type.name)
77+
assert t.name not in self._registered_types, \
78+
'There is already a registered type named "{}".'.format(type.name)
79+
7180
self._registered_types[t.name] = t
7281
return t
7382

@@ -103,8 +112,9 @@ def _create_object_type_class(self, interface_thunk=None):
103112

104113
class RegistryObjectTypeMeta(ObjectTypeMeta):
105114
@staticmethod
106-
def _register(object_type):
115+
def _register(object_type, type_class):
107116
registry.register(object_type)
117+
registry._registered_types_can_be[object_type].add(type_class)
108118

109119
@staticmethod
110120
def _get_registry():
@@ -123,25 +133,6 @@ class ObjectType(ObjectTypeBase):
123133

124134
return ObjectType
125135

126-
def _create_implement_type_class(self):
127-
registry = self
128-
129-
class Implements(object):
130-
def __getattr__(self, item):
131-
return self[item]
132-
133-
def __getitem__(self, item):
134-
if isinstance(item, tuple):
135-
type_thunk = ThunkList([ResolveThunk(registry._resolve_type, i) for i in item])
136-
137-
else:
138-
type_thunk = ThunkList([ResolveThunk(registry._resolve_type, item)])
139-
140-
return registry._create_object_type_class(type_thunk)
141-
142-
implements = Implements()
143-
return implements
144-
145136
def _create_interface_type_class(self):
146137
registry = self
147138

@@ -160,18 +151,41 @@ class Interface(six.with_metaclass(RegistryInterfaceMeta)):
160151

161152
return Interface
162153

154+
def _create_union_type_class(self, types_thunk):
155+
registry = self
156+
157+
class RegistryUnionMeta(UnionMeta):
158+
@staticmethod
159+
def _register(union):
160+
registry.register(union)
161+
162+
@staticmethod
163+
def _get_registry():
164+
return registry
165+
166+
class Union(six.with_metaclass(RegistryUnionMeta)):
167+
abstract = True
168+
169+
@staticmethod
170+
def _get_types():
171+
return TransformThunkList(types_thunk, get_named_type)
172+
173+
return Union
174+
175+
def _create_is_type_of(self, type):
176+
return partial(self._is_type_of, type)
177+
178+
def _is_type_of(self, type, obj, info):
179+
return obj.__class__ in self._registered_types_can_be[type]
180+
163181
def _add_interface_declared_fields(self, interface, attrs):
164182
self._interface_declared_fields[interface] = attrs
165183

166184
def _get_interface_declared_fields(self, interface):
167185
return self._interface_declared_fields.get(interface, {})
168186

169-
def _add_impl_to_interfaces(self, *types):
170-
type_map = {}
171-
for type in types:
172-
type_map = type_map_reducer(type_map, type)
173-
174-
for type in type_map:
187+
def _add_impl_to_interfaces(self):
188+
for type in self._registered_types.values():
175189
if not isinstance(type, GraphQLObjectType):
176190
continue
177191

@@ -185,10 +199,10 @@ def _add_impl_to_interfaces(self, *types):
185199

186200
interface._impls.append(type)
187201

188-
def schema(self, query, mutation=None):
202+
def Schema(self, query, mutation=None):
189203
query = self[query]()
190204
mutation = self[mutation]()
191-
self._add_impl_to_interfaces(query, mutation)
205+
self._add_impl_to_interfaces()
192206
return GraphQLSchema(query=query, mutation=mutation)
193207

194208
def type(self, name):

epoxy/thunk.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __init__(self, getter, item):
3636
self.item = item
3737

3838
def _resolve(self, item):
39-
if callable(item):
39+
if callable(item) and not hasattr(item, 'T'):
4040
return self._resolve(item())
4141

4242
return maybe_callable(self.getter(item))

epoxy/utils/maybe_callable.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
def maybe_callable(obj):
2-
if callable(obj):
2+
if callable(obj) and not hasattr(obj, 'T'):
33
return obj()
44

55
return obj

epoxy/utils/ref_holder.py renamed to epoxy/utils/weak_ref_holder.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from weakref import ReferenceType, ref
22

33

4-
class RefHolder(object):
5-
ref = None
4+
class WeakRefHolder(object):
5+
__slots__ = 'ref',
66

77
def __init__(self, ref=None):
88
if ref is not None:
99
self.set(ref)
10+
else:
11+
self.ref = None
1012

1113
def _delete_ref(self, ref):
1214
if ref is self.ref:
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from collections import namedtuple
2+
from graphql.core import graphql
3+
from graphql.core.type import GraphQLObjectType, GraphQLField, GraphQLString
4+
from epoxy.registry import TypeRegistry
5+
6+
7+
def test_resolves_regular_graphql_type():
8+
BuiltInType = GraphQLObjectType(
9+
name='BuiltInType',
10+
fields={
11+
'someString': GraphQLField(GraphQLString)
12+
}
13+
)
14+
15+
BuiltInTypeTuple = namedtuple('BuiltInTypeData', 'someString')
16+
17+
R = TypeRegistry()
18+
19+
class Query(R.ObjectType):
20+
built_in_type = R.Field(BuiltInType)
21+
22+
def resolve_built_in_type(self, obj, args, info):
23+
return BuiltInTypeTuple('Hello World. I am a string.')
24+
25+
schema = R.Schema(R.Query)
26+
result = graphql(schema, '{ builtInType { someString } }')
27+
assert not result.errors
28+
assert result.data == {'builtInType': {'someString': 'Hello World. I am a string.'}}

tests/test_interfaces.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from graphql.core import graphql
12
from epoxy.registry import TypeRegistry
23

34

@@ -132,3 +133,48 @@ class Human(R.Implements[R.Character, Bean]):
132133
human = Human.T
133134
fields = human.get_fields()
134135
assert list(fields.keys()) == ['id', 'name', 'friendsWith', 'livesRemaining', 'real', 'hero', 'homePlanet']
136+
137+
138+
def test_runtime_type_resolution():
139+
R = TypeRegistry()
140+
141+
class Pet(R.Interface):
142+
name = R.String
143+
144+
class Dog(R.Implements.Pet):
145+
bark = R.String
146+
147+
class Cat(R.Implements.Pet):
148+
meow = R.String
149+
150+
class Query(R.ObjectType):
151+
pets = R.Pet.List
152+
153+
schema = R.Schema(Query)
154+
155+
data = Query(pets=[
156+
Dog(name='Clifford', bark='Really big bark, because it\'s a really big dog.'),
157+
Cat(name='Garfield', meow='Lasagna')
158+
])
159+
160+
result = graphql(schema, '''
161+
{
162+
pets {
163+
name
164+
__typename
165+
... on Dog {
166+
bark
167+
}
168+
169+
... on Cat {
170+
meow
171+
}
172+
}
173+
}
174+
175+
''', data)
176+
assert not result.errors
177+
assert result.data == {
178+
'pets': [{'__typename': 'Dog', 'bark': "Really big bark, because it's a really big dog.", 'name': 'Clifford'},
179+
{'__typename': 'Cat', 'meow': 'Lasagna', 'name': 'Garfield'}]
180+
}

tests/test_object_type_as_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Human(R.ObjectType):
1010
favorite_color = R.String
1111

1212

13-
Schema = R.schema(R.Human)
13+
Schema = R.Schema(R.Human)
1414

1515

1616
def test_object_type_as_data():

0 commit comments

Comments
 (0)