Skip to content

Commit bc9a028

Browse files
committed
[Schema] Implementing interfaces with covariant return types.
Related GraphQL-js commit: graphql/graphql-js@edbe063
1 parent b1bfadf commit bc9a028

File tree

5 files changed

+208
-33
lines changed

5 files changed

+208
-33
lines changed

graphql/core/type/schema.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
)
1010
from .directives import GraphQLDirective, GraphQLIncludeDirective, GraphQLSkipDirective
1111
from .introspection import IntrospectionSchema
12+
from ..utils.type_comparators import is_equal_type, is_type_sub_type_of
1213

1314

1415
class GraphQLSchema(object):
@@ -144,7 +145,7 @@ def assert_object_implements_interface(object, interface):
144145
interface, field_name, object
145146
)
146147

147-
assert is_equal_type(interface_field.type, object_field.type), (
148+
assert is_type_sub_type_of(object_field.type, interface_field.type), (
148149
'{}.{} expects type "{}" but {}.{} provides type "{}".'
149150
).format(interface, field_name, interface_field.type, object, field_name, object_field.type)
150151

@@ -171,13 +172,3 @@ def assert_object_implements_interface(object, interface):
171172
'"{}" but is not also provided by the '
172173
'interface {}.{}.'
173174
).format(object, field_name, arg_name, object_arg.type, interface, field_name)
174-
175-
176-
def is_equal_type(type_a, type_b):
177-
if isinstance(type_a, GraphQLNonNull) and isinstance(type_b, GraphQLNonNull):
178-
return is_equal_type(type_a.of_type, type_b.of_type)
179-
180-
if isinstance(type_a, GraphQLList) and isinstance(type_b, GraphQLList):
181-
return is_equal_type(type_a.of_type, type_b.of_type)
182-
183-
return type_a is type_b
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from ..type.definition import (
2+
is_abstract_type,
3+
GraphQLObjectType,
4+
GraphQLList,
5+
GraphQLNonNull,
6+
)
7+
8+
9+
def is_equal_type(type_a, type_b):
10+
if type_a is type_b:
11+
return True
12+
13+
if isinstance(type_a, GraphQLNonNull) and isinstance(type_b, GraphQLNonNull):
14+
return is_equal_type(type_a.of_type, type_b.of_type)
15+
16+
if isinstance(type_a, GraphQLList) and isinstance(type_b, GraphQLList):
17+
return is_equal_type(type_a.of_type, type_b.of_type)
18+
19+
return False
20+
21+
22+
def is_type_sub_type_of(maybe_subtype, super_type):
23+
if maybe_subtype is super_type:
24+
return True
25+
26+
if isinstance(super_type, GraphQLNonNull):
27+
if isinstance(maybe_subtype, GraphQLNonNull):
28+
return is_type_sub_type_of(maybe_subtype.of_type, super_type.of_type)
29+
return False
30+
elif isinstance(maybe_subtype, GraphQLNonNull):
31+
return is_type_sub_type_of(maybe_subtype.of_type, super_type)
32+
33+
if isinstance(super_type, GraphQLList):
34+
if isinstance(maybe_subtype, GraphQLList):
35+
return is_type_sub_type_of(maybe_subtype.of_type, super_type.of_type)
36+
return False
37+
elif isinstance(maybe_subtype, GraphQLList):
38+
return False
39+
40+
if is_abstract_type(super_type) and isinstance(maybe_subtype, GraphQLObjectType) and super_type.is_possible_type(maybe_subtype):
41+
return True
42+
43+
return False

graphql/core/validation/rules/overlapping_fields_can_be_merged.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
get_named_type,
1212
)
1313
from ...utils.type_from_ast import type_from_ast
14+
from ...utils.type_comparators import is_equal_type
1415
from .base import ValidationRule
1516

1617

@@ -128,7 +129,8 @@ def leave_SelectionSet(self, node, key, parent, path, ancestors):
128129

129130
@staticmethod
130131
def same_type(type1, type2):
131-
return type1.is_same_type(type2)
132+
return is_equal_type(type1, type2)
133+
# return type1.is_same_type(type2)
132134

133135
@staticmethod
134136
def same_value(value1, value2):

graphql/core/validation/rules/variables_in_allowed_position.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from ...error import GraphQLError
22
from ...type.definition import (
3-
GraphQLList,
43
GraphQLNonNull
54
)
65
from ...utils.type_from_ast import type_from_ast
6+
from ...utils.type_comparators import is_type_sub_type_of
77
from .base import ValidationRule
88

99

@@ -27,7 +27,7 @@ def leave_OperationDefinition(self, operation, key, parent, path, ancestors):
2727
var_def = self.var_def_map.get(var_name)
2828
if var_def and type:
2929
var_type = type_from_ast(self.context.get_schema(), var_def.type)
30-
if var_type and not self.var_type_allowed_for_type(self.effective_type(var_type, var_def), type):
30+
if var_type and not is_type_sub_type_of(self.effective_type(var_type, var_def), type):
3131
self.context.report_error(GraphQLError(
3232
self.bad_var_pos_message(var_name, var_type, type),
3333
[var_def, node]
@@ -43,22 +43,6 @@ def effective_type(var_type, var_def):
4343

4444
return GraphQLNonNull(var_type)
4545

46-
@classmethod
47-
def var_type_allowed_for_type(cls, var_type, expected_type):
48-
if isinstance(expected_type, GraphQLNonNull):
49-
if isinstance(var_type, GraphQLNonNull):
50-
return cls.var_type_allowed_for_type(var_type.of_type, expected_type.of_type)
51-
52-
return False
53-
54-
if isinstance(var_type, GraphQLNonNull):
55-
return cls.var_type_allowed_for_type(var_type.of_type, expected_type)
56-
57-
if isinstance(var_type, GraphQLList) and isinstance(expected_type, GraphQLList):
58-
return cls.var_type_allowed_for_type(var_type.of_type, expected_type.of_type)
59-
60-
return var_type == expected_type
61-
6246
@staticmethod
6347
def bad_var_pos_message(var_name, var_type, expected_type):
6448
return 'Variable "{}" of type "{}" used in position expecting type "{}".'.format(var_name, var_type,

tests/core_type/test_validation.py

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,98 @@ def test_rejects_an_object_with_an_incorrectly_typed_interface_argument(self):
14591459
assert str(excinfo.value) == 'AnotherInterface.field(input:) expects type "String" ' \
14601460
'but AnotherObject.field(input:) provides type "SomeScalar".'
14611461

1462+
def test_rejects_an_object_with_an_incorrectly_typed_interface_field(self):
1463+
AnotherInterface = GraphQLInterfaceType(
1464+
name='AnotherInterface',
1465+
resolve_type=_none,
1466+
fields={
1467+
'field': GraphQLField(GraphQLString)
1468+
}
1469+
)
1470+
AnotherObject = GraphQLObjectType(
1471+
name='AnotherObject',
1472+
interfaces=[AnotherInterface],
1473+
fields={
1474+
'field': GraphQLField(SomeScalarType)
1475+
}
1476+
)
1477+
1478+
with raises(AssertionError) as excinfo:
1479+
schema_with_field_type(AnotherObject)
1480+
1481+
assert str(excinfo.value) == 'AnotherInterface.field expects type "String" ' \
1482+
'but AnotherObject.field provides type "SomeScalar".'
1483+
1484+
def test_rejects_an_object_with_a_differently_typed_Interface_field(self):
1485+
TypeA = GraphQLObjectType(
1486+
name='A',
1487+
fields={
1488+
'foo': GraphQLField(GraphQLString)
1489+
}
1490+
)
1491+
TypeB = GraphQLObjectType(
1492+
name='B',
1493+
fields={
1494+
'foo': GraphQLField(GraphQLString)
1495+
}
1496+
)
1497+
AnotherInterface = GraphQLInterfaceType(
1498+
name='AnotherInterface',
1499+
resolve_type=_none,
1500+
fields={
1501+
'field': GraphQLField(TypeA)
1502+
}
1503+
)
1504+
AnotherObject = GraphQLObjectType(
1505+
name='AnotherObject',
1506+
interfaces=[AnotherInterface],
1507+
fields={
1508+
'field': GraphQLField(TypeB)
1509+
}
1510+
)
1511+
1512+
with raises(AssertionError) as excinfo:
1513+
schema_with_field_type(AnotherObject)
1514+
1515+
assert str(excinfo.value) == 'AnotherInterface.field expects type "A" but ' \
1516+
'AnotherObject.field provides type "B".'
1517+
1518+
def test_accepts_an_object_with_a_subtyped_interface_field_interface(self):
1519+
AnotherInterface = GraphQLInterfaceType(
1520+
name='AnotherInterface',
1521+
resolve_type=_none,
1522+
fields=lambda: {
1523+
'field': GraphQLField(AnotherInterface)
1524+
}
1525+
)
1526+
AnotherObject = GraphQLObjectType(
1527+
name='AnotherObject',
1528+
interfaces=[AnotherInterface],
1529+
fields=lambda: {
1530+
'field': GraphQLField(AnotherObject)
1531+
}
1532+
)
1533+
1534+
assert schema_with_field_type(AnotherObject)
1535+
1536+
def test_accepts_an_object_with_a_subtyped_interface_field_union(self):
1537+
AnotherInterface = GraphQLInterfaceType(
1538+
name='AnotherInterface',
1539+
resolve_type=_none,
1540+
fields=lambda: {
1541+
'field': GraphQLField(SomeUnionType)
1542+
}
1543+
)
1544+
AnotherObject = GraphQLObjectType(
1545+
name='AnotherObject',
1546+
interfaces=[AnotherInterface],
1547+
fields=lambda: {
1548+
'field': GraphQLField(SomeObjectType)
1549+
}
1550+
)
1551+
1552+
assert schema_with_field_type(AnotherObject)
1553+
14621554
def test_accepts_an_object_with_an_equivalently_modified_interface_field_type(self):
14631555
AnotherInterface = GraphQLInterfaceType(
14641556
name='AnotherInterface',
@@ -1478,7 +1570,29 @@ def test_accepts_an_object_with_an_equivalently_modified_interface_field_type(se
14781570

14791571
assert schema_with_field_type(AnotherObject)
14801572

1481-
def test_rejects_an_object_with_an_differently_modified_interface_field_type(self):
1573+
def test_rejects_an_object_with_a_non_list_interface_field_list_type(self):
1574+
AnotherInterface = GraphQLInterfaceType(
1575+
name='AnotherInterface',
1576+
resolve_type=_none,
1577+
fields={
1578+
'field': GraphQLField(GraphQLList(GraphQLString))
1579+
}
1580+
)
1581+
AnotherObject = GraphQLObjectType(
1582+
name='AnotherObject',
1583+
interfaces=[AnotherInterface],
1584+
fields={
1585+
'field': GraphQLField(GraphQLString)
1586+
}
1587+
)
1588+
1589+
with raises(AssertionError) as excinfo:
1590+
schema_with_field_type(AnotherObject)
1591+
1592+
assert str(excinfo.value) == 'AnotherInterface.field expects type "[String]" ' \
1593+
'but AnotherObject.field provides type "String".'
1594+
1595+
def test_rejects_a_object_with_a_list_interface_field_non_list_type(self):
14821596
AnotherInterface = GraphQLInterfaceType(
14831597
name='AnotherInterface',
14841598
resolve_type=_none,
@@ -1490,7 +1604,7 @@ def test_rejects_an_object_with_an_differently_modified_interface_field_type(sel
14901604
name='AnotherObject',
14911605
interfaces=[AnotherInterface],
14921606
fields={
1493-
'field': GraphQLField(GraphQLNonNull(GraphQLString))
1607+
'field': GraphQLField(GraphQLList(GraphQLString))
14941608

14951609
}
14961610
)
@@ -1499,4 +1613,45 @@ def test_rejects_an_object_with_an_differently_modified_interface_field_type(sel
14991613
schema_with_field_type(AnotherObject)
15001614

15011615
assert str(excinfo.value) == 'AnotherInterface.field expects type "String" ' \
1502-
'but AnotherObject.field provides type "String!".'
1616+
'but AnotherObject.field provides type "[String]".'
1617+
1618+
def test_accepts_an_object_with_a_subset_non_null_interface_field_type(self):
1619+
AnotherInterface = GraphQLInterfaceType(
1620+
name='AnotherInterface',
1621+
resolve_type=_none,
1622+
fields={
1623+
'field': GraphQLField(GraphQLString)
1624+
}
1625+
)
1626+
AnotherObject = GraphQLObjectType(
1627+
name='AnotherObject',
1628+
interfaces=[AnotherInterface],
1629+
fields={
1630+
'field': GraphQLField(GraphQLNonNull(GraphQLString))
1631+
}
1632+
)
1633+
1634+
assert schema_with_field_type(AnotherObject)
1635+
1636+
def test_rejects_a_object_with_a_superset_nullable_interface_field_type(self):
1637+
AnotherInterface = GraphQLInterfaceType(
1638+
name='AnotherInterface',
1639+
resolve_type=_none,
1640+
fields={
1641+
'field': GraphQLField(GraphQLNonNull(GraphQLString))
1642+
}
1643+
)
1644+
AnotherObject = GraphQLObjectType(
1645+
name='AnotherObject',
1646+
interfaces=[AnotherInterface],
1647+
fields={
1648+
'field': GraphQLField(GraphQLString)
1649+
1650+
}
1651+
)
1652+
1653+
with raises(AssertionError) as excinfo:
1654+
schema_with_field_type(AnotherObject)
1655+
1656+
assert str(excinfo.value) == 'AnotherInterface.field expects type "String!" but ' \
1657+
'AnotherObject.field provides type "String".'

0 commit comments

Comments
 (0)