Skip to content

Commit f62eddc

Browse files
committed
[RFC] Proposed change to directive location introspection
Related GraphQL-js commit: graphql/graphql-js@e89c19d
1 parent f26bb2e commit f62eddc

16 files changed

+1178
-570
lines changed

graphql/pyutils/contain_subset.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
obj = (dict, list, tuple)
2+
3+
def contain_subset(expected, actual):
4+
t_actual = type(actual)
5+
t_expected = type(expected)
6+
if not(issubclass(t_actual, t_expected) or issubclass(t_expected, t_actual)):
7+
return False
8+
if not isinstance(expected, obj) or expected is None:
9+
return expected == actual
10+
if expected and not actual:
11+
return False
12+
if isinstance(expected, list):
13+
aa = actual[:]
14+
return all([any([contain_subset(exp, act) for act in aa]) for exp in expected])
15+
for key in expected.keys():
16+
eo = expected[key]
17+
ao = actual.get(key)
18+
if isinstance(eo, obj) and eo is not None and ao is not None:
19+
if not contain_subset(eo, ao):
20+
return False
21+
continue
22+
if ao != eo:
23+
return False
24+
return True
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from ..contain_subset import contain_subset
2+
3+
plain_object = {
4+
'a':'b',
5+
'c':'d'
6+
}
7+
8+
complex_object = {
9+
'a': 'b',
10+
'c': 'd',
11+
'e': {
12+
'foo': 'bar',
13+
'baz': {
14+
'qux': 'quux'
15+
}
16+
}
17+
}
18+
19+
def test_plain_object_should_pass_for_smaller_object():
20+
assert contain_subset({'a': 'b'}, plain_object)
21+
22+
23+
def test_plain_object_should_pass_for_same_object():
24+
assert contain_subset({
25+
'a':'b',
26+
'c':'d'
27+
}, plain_object)
28+
29+
30+
def test_plain_object_should_reject_for_similar_object():
31+
assert not contain_subset({
32+
'a':'notB',
33+
'c':'d'
34+
}, plain_object)
35+
36+
37+
def test_complex_object_should_pass_for_smaller_object():
38+
assert contain_subset({
39+
'a':'b',
40+
'e': {
41+
'foo':'bar'
42+
}
43+
}, complex_object)
44+
45+
46+
def test_complex_object_should_pass_for_smaller_object_other():
47+
assert contain_subset({
48+
'e': {
49+
'foo':'bar',
50+
'baz':{
51+
'qux': 'quux'
52+
}
53+
}
54+
}, complex_object)
55+
56+
57+
def test_complex_object_should_pass_for_same_object():
58+
assert contain_subset({
59+
'a': 'b',
60+
'c': 'd',
61+
'e': {
62+
'foo': 'bar',
63+
'baz': {
64+
'qux': 'quux'
65+
}
66+
}
67+
}, complex_object)
68+
69+
70+
def test_complex_object_should_reject_for_similar_object():
71+
assert not contain_subset({
72+
'e': {
73+
'foo':'bar',
74+
'baz':{
75+
'qux': 'notAQuux'
76+
}
77+
}
78+
}, complex_object)
79+
80+
81+
def test_circular_objects_should_contain_subdocument():
82+
obj = {}
83+
obj['arr'] = [obj,obj]
84+
obj['arr'].append(obj['arr'])
85+
obj['obj'] = obj
86+
87+
assert contain_subset({
88+
'arr': [
89+
{'arr': []},
90+
{'arr': []},
91+
[
92+
{'arr': []},
93+
{'arr': []}
94+
]
95+
]
96+
}, obj)
97+
98+
99+
def test_circular_objects_should_not_contain_similardocument():
100+
obj = {}
101+
obj['arr'] = [obj,obj]
102+
obj['arr'].append(obj['arr'])
103+
obj['obj'] = obj
104+
105+
assert not contain_subset({
106+
'arr': [
107+
{'arr': ['just random field']},
108+
{'arr': []},
109+
[
110+
{'arr': []},
111+
{'arr': []}
112+
]
113+
]
114+
}, obj)
115+
116+
117+
def test_should_contain_others():
118+
obj = {
119+
'elems': [{'a':'b', 'c':'d', 'e':'f'}, {'g':'h'}]
120+
}
121+
assert contain_subset({
122+
'elems': [{
123+
'g':'h'
124+
},{'a':'b','e':'f'}
125+
]
126+
}, obj)

graphql/type/definition.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import collections
22
import copy
3-
import re
43

54
from ..language import ast
5+
from ..utils.assert_valid_name import assert_valid_name
66

77

88
def is_type(type):
@@ -751,11 +751,3 @@ def __str__(self):
751751

752752
def is_same_type(self, other):
753753
return isinstance(other, GraphQLNonNull) and self.of_type.is_same_type(other.of_type)
754-
755-
756-
NAME_PATTERN = r'^[_a-zA-Z][_a-zA-Z0-9]*$'
757-
COMPILED_NAME_PATTERN = re.compile(NAME_PATTERN)
758-
759-
760-
def assert_valid_name(name):
761-
assert COMPILED_NAME_PATTERN.match(name), 'Names must match /{}/ but "{}" does not.'.format(NAME_PATTERN, name)

graphql/type/directives.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,48 @@
1+
import collections
2+
13
from .definition import GraphQLArgument, GraphQLNonNull
24
from .scalars import GraphQLBoolean
5+
from ..utils.assert_valid_name import assert_valid_name
6+
7+
8+
class DirectiveLocation(object):
9+
QUERY = 'QUERY'
10+
MUTATION = 'MUTATION'
11+
SUBSCRIPTION = 'SUBSCRIPTION'
12+
FIELD = 'FIELD'
13+
FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION'
14+
FRAGMENT_SPREAD = 'FRAGMENT_SPREAD'
15+
INLINE_FRAGMENT = 'INLINE_FRAGMENT'
16+
17+
OPERATION_LOCATIONS = [
18+
QUERY,
19+
MUTATION,
20+
SUBSCRIPTION
21+
]
22+
23+
FRAGMENT_LOCATIONS = [
24+
FRAGMENT_DEFINITION,
25+
FRAGMENT_SPREAD,
26+
INLINE_FRAGMENT
27+
]
28+
29+
FIELD_LOCATIONS = [
30+
FIELD
31+
]
332

433

534
class GraphQLDirective(object):
6-
__slots__ = 'name', 'args', 'description', 'on_operation', 'on_fragment', 'on_field'
35+
__slots__ = 'name', 'args', 'description', 'locations'
36+
37+
def __init__(self, name, description=None, args=None, locations=None):
38+
assert name, 'Directive must be named.'
39+
assert_valid_name(name)
40+
assert isinstance(locations, collections.Iterable), 'Must provide locations for directive.'
741

8-
def __init__(self, name, description=None, args=None, on_operation=False, on_fragment=False, on_field=False):
942
self.name = name
1043
self.description = description
1144
self.args = args or []
12-
self.on_operation = on_operation
13-
self.on_fragment = on_fragment
14-
self.on_field = on_field
45+
self.locations = locations
1546

1647

1748
def arg(name, *args, **kwargs):
@@ -27,9 +58,11 @@ def arg(name, *args, **kwargs):
2758
type=GraphQLNonNull(GraphQLBoolean),
2859
description='Directs the executor to include this field or fragment only when the `if` argument is true.',
2960
)],
30-
on_operation=False,
31-
on_fragment=True,
32-
on_field=True
61+
locations=[
62+
DirectiveLocation.FIELD,
63+
DirectiveLocation.FRAGMENT_SPREAD,
64+
DirectiveLocation.INLINE_FRAGMENT,
65+
]
3366
)
3467

3568
GraphQLSkipDirective = GraphQLDirective(
@@ -39,7 +72,9 @@ def arg(name, *args, **kwargs):
3972
type=GraphQLNonNull(GraphQLBoolean),
4073
description='Directs the executor to skip this field or fragment only when the `if` argument is true.',
4174
)],
42-
on_operation=False,
43-
on_fragment=True,
44-
on_field=True
75+
locations=[
76+
DirectiveLocation.FIELD,
77+
DirectiveLocation.FRAGMENT_SPREAD,
78+
DirectiveLocation.INLINE_FRAGMENT,
79+
]
4580
)

graphql/type/introspection.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
GraphQLObjectType, GraphQLScalarType,
99
GraphQLUnionType)
1010
from .scalars import GraphQLBoolean, GraphQLString
11+
from .directives import DirectiveLocation
12+
1113

1214
__Schema = GraphQLObjectType(
1315
'__Schema',
@@ -44,6 +46,10 @@
4446
)),
4547
]))
4648

49+
_on_operation_locations = set(DirectiveLocation.OPERATION_LOCATIONS)
50+
_on_fragment_locations = set(DirectiveLocation.FRAGMENT_LOCATIONS)
51+
_on_field_locations = set(DirectiveLocation.FIELD_LOCATIONS)
52+
4753
__Directive = GraphQLObjectType(
4854
'__Directive',
4955
description='A Directive provides a way to describe alternate runtime execution and '
@@ -55,24 +61,67 @@
5561
fields=lambda: OrderedDict([
5662
('name', GraphQLField(GraphQLNonNull(GraphQLString))),
5763
('description', GraphQLField(GraphQLString)),
64+
('locations', GraphQLField(
65+
type=GraphQLNonNull(GraphQLList(GraphQLNonNull(__DirectiveLocation))),
66+
)),
5867
('args', GraphQLField(
5968
type=GraphQLNonNull(GraphQLList(GraphQLNonNull(__InputValue))),
6069
resolver=lambda directive, *args: directive.args or [],
6170
)),
6271
('onOperation', GraphQLField(
6372
type=GraphQLNonNull(GraphQLBoolean),
64-
resolver=lambda directive, *args: directive.on_operation,
73+
deprecation_reason='Use `locations`.',
74+
resolver=lambda directive, *args: set(directive.locations) & _on_operation_locations,
6575
)),
6676
('onFragment', GraphQLField(
6777
type=GraphQLNonNull(GraphQLBoolean),
68-
resolver=lambda directive, *args: directive.on_fragment,
78+
deprecation_reason='Use `locations`.',
79+
resolver=lambda directive, *args: set(directive.locations) & _on_fragment_locations,
6980
)),
7081
('onField', GraphQLField(
7182
type=GraphQLNonNull(GraphQLBoolean),
72-
resolver=lambda directive, *args: directive.on_field,
83+
deprecation_reason='Use `locations`.',
84+
resolver=lambda directive, *args: set(directive.locations) & _on_field_locations,
7385
))
7486
]))
7587

88+
__DirectiveLocation = GraphQLEnumType(
89+
'__DirectiveLocation',
90+
description=(
91+
'A Directive can be adjacent to many parts of the GraphQL language, a ' +
92+
'__DirectiveLocation describes one such possible adjacencies.'
93+
),
94+
values=OrderedDict([
95+
('QUERY', GraphQLEnumValue(
96+
DirectiveLocation.QUERY,
97+
description='Location adjacent to a query operation.'
98+
)),
99+
('MUTATION', GraphQLEnumValue(
100+
DirectiveLocation.MUTATION,
101+
description='Location adjacent to a mutation operation.'
102+
)),
103+
('SUBSCRIPTION', GraphQLEnumValue(
104+
DirectiveLocation.SUBSCRIPTION,
105+
description='Location adjacent to a subscription operation.'
106+
)),
107+
('FIELD', GraphQLEnumValue(
108+
DirectiveLocation.FIELD,
109+
description='Location adjacent to a field.'
110+
)),
111+
('FRAGMENT_DEFINITION', GraphQLEnumValue(
112+
DirectiveLocation.FRAGMENT_DEFINITION,
113+
description='Location adjacent to a fragment definition.'
114+
)),
115+
('FRAGMENT_SPREAD', GraphQLEnumValue(
116+
DirectiveLocation.FRAGMENT_SPREAD,
117+
description='Location adjacent to a fragment spread.'
118+
)),
119+
('INLINE_FRAGMENT', GraphQLEnumValue(
120+
DirectiveLocation.INLINE_FRAGMENT,
121+
description='Location adjacent to an inline fragment.'
122+
)),
123+
]))
124+
76125

77126
class TypeKind(object):
78127
SCALAR = 'SCALAR'

graphql/type/schema.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,8 @@ def get_directive(self, name):
8383
return None
8484

8585
def _build_type_map(self):
86-
type_map = OrderedDict()
87-
types = (self.get_query_type(), self.get_mutation_type(), self.get_subscription_type(), IntrospectionSchema)
88-
for type in types:
89-
type_map = type_map_reducer(type_map, type)
90-
86+
types = [self.get_query_type(), self.get_mutation_type(), self.get_subscription_type(), IntrospectionSchema]
87+
type_map = reduce(type_map_reducer, types, OrderedDict())
9188
return type_map
9289

9390

0 commit comments

Comments
 (0)