Skip to content

Commit 8f1f653

Browse files
committed
Implementation of ObjectTypes as Data.
1 parent 330f1c4 commit 8f1f653

10 files changed

Lines changed: 161 additions & 20 deletions

File tree

epoxy/bases/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__author__ = 'jake'

epoxy/bases/object_type.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
class ObjectTypeBase(object):
2+
T = None
3+
_field_attr_map = None
4+
5+
def __init__(self, **kwargs):
6+
field_map_init = kwargs.pop('__field_map_init', False)
7+
if field_map_init:
8+
return
9+
10+
if self._field_attr_map is None:
11+
raise RuntimeError("You cannot construct type {} until it is used in a created Schema.".format(
12+
self.T
13+
))
14+
15+
# Todo: Maybe some type checking? Probably not tho.
16+
for field_name in self._field_attr_map.keys():
17+
if field_name in kwargs:
18+
setattr(self, field_name, kwargs.pop(field_name))
19+
20+
else:
21+
setattr(self, field_name, None)
22+
23+
if kwargs:
24+
raise TypeError('Type {} received unexpected keyword argument(s): {}.'.format(
25+
self.T,
26+
', '.join(kwargs.keys())
27+
))
28+
29+
def __repr__(self):
30+
return '<{} {}>'.format(
31+
self.T,
32+
' '.join('{}={!r}'.format(field_name, getattr(self, field_name))
33+
for field_name in self._field_attr_map.keys())
34+
)

epoxy/metaclasses/interface.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from collections import OrderedDict
2+
from functools import partial
23
from graphql.core.type.definition import GraphQLInterfaceType
34
from ..utils.get_declared_fields import get_declared_fields
45
from ..utils.make_default_resolver import make_default_resolver
6+
from ..utils.ref_holder import RefHolder
57
from ..utils.yank_potential_fields import yank_potential_fields
68

79

@@ -10,18 +12,21 @@ def __new__(mcs, name, bases, attrs):
1012
if attrs.get('abstract'):
1113
return super(InterfaceMeta, mcs).__new__(mcs, name, bases, attrs)
1214

15+
class_ref = RefHolder()
1316
declared_fields = get_declared_fields(name, yank_potential_fields(attrs))
1417
interface = GraphQLInterfaceType(
1518
name,
16-
fields=lambda: mcs._build_field_map(attrs, declared_fields),
19+
fields=partial(mcs._build_field_map, class_ref, declared_fields),
1720
description=attrs.get('__doc__'),
1821
resolve_type=lambda: None
1922
)
23+
2024
mcs._register(interface, declared_fields)
21-
attrs['T'] = interface
22-
attrs['_registry'] = mcs._get_registry()
2325
cls = super(InterfaceMeta, mcs).__new__(mcs, name, bases, attrs)
24-
attrs['_cls'] = cls
26+
cls.T = interface
27+
cls._registry = mcs._get_registry()
28+
class_ref.set(cls)
29+
2530
return cls
2631

2732
@staticmethod
@@ -33,9 +38,13 @@ def _get_registry():
3338
raise NotImplementedError('_get_registry must be implemented in the sub-metaclass')
3439

3540
@staticmethod
36-
def _build_field_map(attrs, fields):
37-
instance = attrs['_cls']()
38-
registry = attrs['_registry']
41+
def _build_field_map(class_ref, fields):
42+
cls = class_ref.get()
43+
if not cls:
44+
return
45+
46+
instance = cls()
47+
registry = cls._registry
3948

4049
field_map = OrderedDict()
4150

epoxy/metaclasses/object_type.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
11
from collections import OrderedDict
2+
from functools import partial
23
from graphql.core.type import GraphQLObjectType
34
from ..utils.get_declared_fields import get_declared_fields
45
from ..utils.make_default_resolver import make_default_resolver
56
from ..utils.no_implementation_registration import no_implementation_registration
7+
from ..utils.ref_holder import RefHolder
68
from ..utils.yank_potential_fields import yank_potential_fields
79

810

911
class ObjectTypeMeta(type):
1012
def __new__(mcs, name, bases, attrs):
11-
if attrs.get('abstract'):
13+
if attrs.pop('abstract', False):
1214
return super(ObjectTypeMeta, mcs).__new__(mcs, name, bases, attrs)
1315

16+
class_ref = RefHolder()
1417
declared_fields = get_declared_fields(name, yank_potential_fields(attrs))
1518
with no_implementation_registration():
1619
object_type = GraphQLObjectType(
1720
name,
18-
fields=lambda: mcs._build_field_map(attrs, declared_fields),
21+
fields=partial(mcs._build_field_map, class_ref, declared_fields),
1922
description=attrs.get('__doc__'),
2023
interfaces=mcs._get_interfaces()
2124
)
2225

2326
mcs._register(object_type)
24-
attrs['_registry'] = mcs._get_registry()
25-
attrs['T'] = object_type
2627
cls = super(ObjectTypeMeta, mcs).__new__(mcs, name, bases, attrs)
27-
attrs['_cls'] = cls
28+
cls.T = object_type
29+
cls._registry = mcs._get_registry()
30+
class_ref.set(cls)
31+
2832
return cls
2933

3034
@staticmethod
@@ -36,10 +40,14 @@ def _get_registry():
3640
raise NotImplementedError('_get_registry must be implemented in the sub-metaclass')
3741

3842
@staticmethod
39-
def _build_field_map(attrs, declared_fields):
40-
instance = attrs['_cls']()
41-
type = attrs['T']
42-
registry = attrs['_registry']
43+
def _build_field_map(class_ref, declared_fields):
44+
cls = class_ref.get()
45+
if not cls:
46+
return
47+
48+
instance = cls(__field_map_init=True)
49+
type = cls.T
50+
registry = cls._registry
4351
interfaces = type.get_interfaces()
4452
fields = []
4553

@@ -54,6 +62,7 @@ def _build_field_map(attrs, declared_fields):
5462

5563
fields += declared_fields
5664
field_map = OrderedDict()
65+
field_attr_map = OrderedDict()
5766

5867
for field_attr_name, field in fields:
5968
resolve_fn = (
@@ -70,8 +79,15 @@ def _build_field_map(attrs, declared_fields):
7079
if field.name in field_map:
7180
del field_map[field.name]
7281

73-
field_map[field.name] = field.to_field(registry, resolve_fn)
82+
graphql_field = field.to_field(registry, resolve_fn)
83+
field_map[field.name] = graphql_field
84+
85+
if field_attr_name in field_attr_map:
86+
del field_attr_map[field_attr_name]
87+
88+
field_attr_map[field_attr_name] = graphql_field
7489

90+
cls._field_attr_map = field_attr_map
7591
return field_map
7692

7793
@staticmethod

epoxy/registry.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from graphql.core.type.schema import type_map_reducer
1919

2020
import six
21+
from .bases.object_type import ObjectTypeBase
2122
from .field import Field
2223
from .metaclasses.interface import InterfaceMeta
2324
from .metaclasses.object_type import ObjectTypeMeta
@@ -116,7 +117,8 @@ def _get_interfaces():
116117

117118
return None
118119

119-
class ObjectType(six.with_metaclass(RegistryObjectTypeMeta)):
120+
@six.add_metaclass(RegistryObjectTypeMeta)
121+
class ObjectType(ObjectTypeBase):
120122
abstract = True
121123

122124
return ObjectType

epoxy/utils/gen_id.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
def gen_id():
55
global _prev_id
66

7-
next_id = _prev_id
87
_prev_id += 1
8+
next_id = _prev_id
99

1010
return next_id

epoxy/utils/ref_holder.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from weakref import ReferenceType, ref
2+
3+
4+
class RefHolder(object):
5+
ref = None
6+
7+
def __init__(self, ref=None):
8+
if ref is not None:
9+
self.set(ref)
10+
11+
def _delete_ref(self, ref):
12+
if ref is self.ref:
13+
self.ref = None
14+
15+
def get(self):
16+
if isinstance(self.ref, ReferenceType):
17+
return self.ref()
18+
19+
def set(self, item):
20+
self.ref = ref(item, self._delete_ref)

tests/test_declarative_definition.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,20 @@ class Dog(R.Interface):
161161
friend_aliased = R.Field(R.Dog, name='friend')
162162

163163
assert str(excinfo.value) == 'Duplicate field definition for name "friend" in type "Dog.friend_aliased".'
164+
165+
166+
def test_orders_fields_in_order_declared():
167+
R = TypeRegistry()
168+
169+
class Dog(R.ObjectType):
170+
id = R.ID
171+
name = R.Field('String')
172+
dog = R.Dog
173+
some_other_field = R.Field(R.Int)
174+
some_other_dog = R.Field('Dog')
175+
foo = R.String
176+
bar = R.String
177+
aaa = R.String
178+
179+
field_order = list(Dog.T.get_fields().keys())
180+
assert field_order == ['id', 'name', 'dog', 'someOtherField', 'someOtherDog', 'foo', 'bar', 'aaa']

tests/test_object_type_as_data.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from graphql.core import graphql
2+
from epoxy.registry import TypeRegistry
3+
from pytest import raises
4+
5+
R = TypeRegistry()
6+
7+
8+
class Human(R.ObjectType):
9+
name = R.String
10+
favorite_color = R.String
11+
12+
13+
Schema = R.schema(R.Human)
14+
15+
16+
def test_object_type_as_data():
17+
jake = Human(name='Jake', favorite_color='Red')
18+
assert jake.name == 'Jake'
19+
assert jake.favorite_color == 'Red'
20+
assert repr(jake) == '<Human name={!r} favorite_color={!r}>'.format(jake.name, jake.favorite_color)
21+
22+
result = graphql(Schema, '{ name favoriteColor }', jake)
23+
assert not result.errors
24+
assert result.data == {'name': 'Jake', 'favoriteColor': 'Red'}
25+
26+
27+
def test_object_type_as_data_with_partial_fields_provided():
28+
jake = Human(name='Jake')
29+
assert jake.name == 'Jake'
30+
assert jake.favorite_color is None
31+
assert repr(jake) == '<Human name={!r} favorite_color={!r}>'.format(jake.name, jake.favorite_color)
32+
33+
result = graphql(Schema, '{ name favoriteColor }', jake)
34+
assert not result.errors
35+
assert result.data == {'name': 'Jake', 'favoriteColor': None}
36+
37+
38+
def test_object_type_giving_unexpected_key():
39+
with raises(TypeError) as excinfo:
40+
Human(after_all=True)
41+
42+
assert str(excinfo.value) == 'Type Human received unexpected keyword argument(s): after_all.'

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ deps =
1010
py{py,27,33}: enum34
1111
py{py,27,33,34}: singledispatch
1212
commands =
13-
py{py,27,33,34,35}: py.test --cov {envsitepackagesdir}/epoxy tests
13+
py{py,27,33,34,35}: py.test --cov {envsitepackagesdir}/epoxy tests {posargs}
1414

1515

1616
[testenv:flake8]

0 commit comments

Comments
 (0)