Skip to content

Commit 512619b

Browse files
committed
Implementation of input types.
1 parent 6e90774 commit 512619b

File tree

8 files changed

+227
-12
lines changed

8 files changed

+227
-12
lines changed

epoxy/bases/input_type.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
class InputTypeBase(object):
2+
T = None
3+
_field_attr_map = None
4+
5+
def __init__(self, arg_value=None):
6+
if arg_value is None:
7+
return
8+
9+
if self._field_attr_map is None:
10+
raise RuntimeError("You cannot construct type {} until it is used in a created Schema.".format(
11+
self.T
12+
))
13+
14+
for attr_name, (field_name, field) in self._field_attr_map.items():
15+
if field_name in arg_value:
16+
setattr(self, attr_name, arg_value[field_name])
17+
18+
else:
19+
setattr(self, attr_name, field.default_value)
20+
21+
def __repr__(self):
22+
if self._field_attr_map is None:
23+
return '<{}>'.format(self.T)
24+
25+
return '<{} {}>'.format(
26+
self.T,
27+
' '.join('{}={!r}'.format(field_name, getattr(self, field_name))
28+
for field_name in self._field_attr_map.keys())
29+
)

epoxy/field.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from graphql.core.type import GraphQLField
1+
from graphql.core.type import GraphQLField, GraphQLInputObjectField
22
from .utils.gen_id import gen_id
33

44

@@ -15,3 +15,16 @@ def __init__(self, type, description=None, args=None, name=None, resolver=None,
1515

1616
def to_field(self, registry, resolver):
1717
return GraphQLField(registry[self.type](), args=self.args, resolver=resolver)
18+
19+
20+
class InputField(object):
21+
def __init__(self, type, description=None, default_value=None, name=None, _counter=None):
22+
self.name = name
23+
self.type = type
24+
self.description = description
25+
self.default_value = default_value
26+
self._counter = _counter or gen_id()
27+
28+
def to_field(self, registry):
29+
return GraphQLInputObjectField(registry[self.type](), default_value=self.default_value,
30+
description=self.description)

epoxy/metaclasses/input_type.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from collections import OrderedDict
2+
from functools import partial
3+
from graphql.core.type.definition import GraphQLInputObjectType
4+
from ..field import InputField
5+
from ..utils.get_declared_fields import get_declared_fields
6+
from ..utils.weak_ref_holder import WeakRefHolder
7+
from ..utils.yank_potential_fields import yank_potential_fields
8+
9+
10+
class InputTypeMeta(type):
11+
def __new__(mcs, name, bases, attrs):
12+
if attrs.pop('abstract', False):
13+
return super(InputTypeMeta, mcs).__new__(mcs, name, bases, attrs)
14+
15+
name = attrs.pop('_name', name)
16+
class_ref = WeakRefHolder()
17+
declared_fields = get_declared_fields(name, yank_potential_fields(attrs, InputField), InputField)
18+
interface = GraphQLInputObjectType(
19+
name,
20+
fields=partial(mcs._build_field_map, class_ref, declared_fields),
21+
description=attrs.get('__doc__'),
22+
)
23+
24+
mcs._register(interface)
25+
cls = super(InputTypeMeta, mcs).__new__(mcs, name, bases, attrs)
26+
cls.T = interface
27+
cls._registry = mcs._get_registry()
28+
class_ref.set(cls)
29+
30+
return cls
31+
32+
@staticmethod
33+
def _register(object_type):
34+
raise NotImplementedError('_register must be implemented in the sub-metaclass')
35+
36+
@staticmethod
37+
def _get_registry():
38+
raise NotImplementedError('_get_registry must be implemented in the sub-metaclass')
39+
40+
@staticmethod
41+
def _build_field_map(class_ref, fields):
42+
cls = class_ref.get()
43+
if not cls:
44+
return
45+
46+
registry = cls._registry
47+
field_map = OrderedDict()
48+
field_attr_map = OrderedDict()
49+
50+
for field_attr_name, field in fields:
51+
graphql_field = field_map[field.name] = field.to_field(registry)
52+
53+
if field_attr_name in field_attr_map:
54+
del field_attr_map[field_attr_name]
55+
56+
field_attr_map[field_attr_name] = (field.name, graphql_field)
57+
58+
cls._field_attr_map = field_attr_map
59+
return field_map

epoxy/registry.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
import six
2020

2121
from .bases.class_type_creator import ClassTypeCreator
22+
from .bases.input_type import InputTypeBase
2223
from .bases.object_type import ObjectTypeBase
23-
from .field import Field
24+
from .field import Field, InputField
25+
from .metaclasses.input_type import InputTypeMeta
2426
from .metaclasses.interface import InterfaceMeta
2527
from .metaclasses.object_type import ObjectTypeMeta
2628
from .metaclasses.union import UnionMeta
@@ -39,8 +41,17 @@
3941

4042

4143
class TypeRegistry(object):
42-
# Fields
44+
_reserved_names = frozenset([
45+
# Types
46+
'ObjectType', 'InputType', 'Union' 'Interface', 'Implements',
47+
# Functions
48+
'Schema', 'Register', 'Mixin',
49+
# Fields
50+
'Field', 'InputField',
51+
])
52+
4353
Field = Field
54+
InputField = InputField
4455

4556
def __init__(self):
4657
self._registered_types = {}
@@ -50,6 +61,7 @@ def __init__(self):
5061
self._pending_types_can_be = defaultdict(set)
5162
self._proxy = ResolvedRegistryProxy(self)
5263
self.ObjectType = self._create_object_type_class()
64+
self.InputType = self._create_input_type_class()
5365
self.Implements = ClassTypeCreator(self, self._create_object_type_class)
5466
self.Union = ClassTypeCreator(self, self._create_union_type_class)
5567
self.Interface = self._create_interface_type_class()
@@ -75,7 +87,7 @@ def Register(self, t):
7587
def register_(self, t):
7688
assert not t.name.startswith('_'), \
7789
'Registered type name cannot start with an "_".'
78-
assert t.name not in ('ObjectType', 'Implements', 'Interface', 'Schema', 'Register'), \
90+
assert t.name not in self._reserved_names, \
7991
'You cannot register a type named "{}".'.format(t.name)
8092
assert t.name not in self._registered_types, \
8193
'There is already a registered type named "{}".'.format(t.name)
@@ -175,6 +187,24 @@ class Union(six.with_metaclass(RegistryUnionMeta)):
175187

176188
return Union
177189

190+
def _create_input_type_class(self):
191+
registry = self
192+
193+
class RegistryInputTypeMeta(InputTypeMeta):
194+
@staticmethod
195+
def _register(input_type):
196+
registry.Register(input_type)
197+
198+
@staticmethod
199+
def _get_registry():
200+
return registry
201+
202+
@six.add_metaclass(RegistryInputTypeMeta)
203+
class InputType(InputTypeBase):
204+
abstract = True
205+
206+
return InputType
207+
178208
def _create_is_type_of(self, type):
179209
return partial(self._is_type_of, type)
180210

epoxy/utils/get_declared_fields.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,21 @@
77
from ..utils.to_camel_case import to_camel_case
88

99

10-
def get_declared_fields(type_name, attrs):
10+
def get_declared_fields(type_name, attrs, field_class=Field):
1111
fields = []
1212

1313
for field_attr_name, obj in list(attrs.items()):
14-
if isinstance(obj, Field):
14+
if isinstance(obj, field_class):
1515
field = copy.copy(obj)
1616
field.name = first_of(field.name, to_camel_case(field_attr_name))
1717
# Bind field.type to the maybe scope.
1818
field.type = (lambda field_type: lambda: maybe_t(maybe_callable(field_type)))(field.type)
1919
fields.append((field_attr_name, field))
2020

21-
continue
22-
23-
if isinstance(obj, TypeThunk):
21+
elif isinstance(obj, TypeThunk):
2422
counter = obj._counter
2523

26-
field = Field(obj, name=to_camel_case(field_attr_name), _counter=counter, **(obj._kwargs or {}))
24+
field = field_class(obj, name=to_camel_case(field_attr_name), _counter=counter, **(obj._kwargs or {}))
2725
fields.append((field_attr_name, field))
2826

2927
fields.sort(key=lambda f: f[1]._counter)

epoxy/utils/yank_potential_fields.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
from ..thunk import TypeThunk
33

44

5-
def yank_potential_fields(attrs):
5+
def yank_potential_fields(attrs, field_class=Field):
66
field_attrs = {}
7+
potential_types = (field_class, TypeThunk)
78

89
for field_attr_name, obj in list(attrs.items()):
910
if field_attr_name == 'T':
1011
continue
1112

12-
if isinstance(obj, (Field, TypeThunk)):
13+
if isinstance(obj, potential_types):
1314
field_attrs[field_attr_name] = attrs.pop(field_attr_name)
1415

1516
return field_attrs

tests/test_input_type.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from graphql.core import graphql
2+
from graphql.core.type import GraphQLArgument, GraphQLInputObjectType, GraphQLString
3+
from graphql.core.type.scalars import GraphQLInt
4+
from epoxy import TypeRegistry
5+
6+
7+
def test_input_type_creation():
8+
R = TypeRegistry()
9+
10+
class SimpleInput(R.InputType):
11+
a = R.Int
12+
b = R.Int
13+
some_underscore = R.String
14+
some_from_field = R.InputField(R.String, default_value='Hello World')
15+
16+
input_type = SimpleInput.T
17+
assert input_type is R.SimpleInput()
18+
assert isinstance(input_type, GraphQLInputObjectType)
19+
fields = input_type.get_fields()
20+
assert list(fields.keys()) == ['a', 'b', 'someUnderscore', 'someFromField']
21+
assert [field.name for field in fields.values()] == ['a', 'b', 'someUnderscore', 'someFromField']
22+
23+
assert fields['a'].type == GraphQLInt
24+
assert fields['b'].type == GraphQLInt
25+
assert fields['someUnderscore'].type == GraphQLString
26+
assert fields['someFromField'].type == GraphQLString
27+
assert fields['someFromField'].default_value == 'Hello World'
28+
29+
input_value = SimpleInput({
30+
'a': 1,
31+
'someUnderscore': 'hello',
32+
})
33+
34+
assert input_value.a == 1
35+
assert input_value.b is None
36+
assert input_value.some_underscore == 'hello'
37+
assert input_value.some_from_field == 'Hello World'
38+
39+
40+
def test_input_type():
41+
R = TypeRegistry()
42+
43+
class SimpleInput(R.InputType):
44+
a = R.Int
45+
b = R.Int
46+
47+
class Query(R.ObjectType):
48+
f = R.String(args={
49+
'input': GraphQLArgument(R.SimpleInput())
50+
})
51+
52+
def resolve_f(self, obj, args, info):
53+
input = SimpleInput(args['input'])
54+
return "I was given {i.a} and {i.b}".format(i=input)
55+
56+
Schema = R.Schema(R.Query)
57+
query = '''
58+
{
59+
f(input: {a: 1, b: 2})
60+
}
61+
'''
62+
63+
result = graphql(Schema, query)
64+
assert not result.errors
65+
assert result.data == {
66+
'f': "I was given 1 and 2"
67+
}

tests/test_register_reserved_name.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from graphql.core.type import GraphQLField, GraphQLObjectType
2+
from graphql.core.type import GraphQLString
3+
from epoxy import TypeRegistry
4+
from pytest import raises
5+
6+
7+
def test_reserved_names():
8+
R = TypeRegistry()
9+
10+
for name in R._reserved_names:
11+
type = GraphQLObjectType(
12+
name=name,
13+
fields={'a': GraphQLField(GraphQLString)}
14+
)
15+
with raises(AssertionError) as excinfo:
16+
R(type)
17+
18+
assert str(excinfo.value) == 'You cannot register a type named "{}".'.format(name)

0 commit comments

Comments
 (0)