Skip to content

Commit 9f35a7a

Browse files
committed
Move validation of names into GraphQL object constructors
Replicates graphql/graphql-js@5774173
1 parent 1fc8e7e commit 9f35a7a

16 files changed

+365
-215
lines changed

.coveragerc

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ branch = True
33
source = src
44
omit =
55
*/conftest.py
6+
*/test_*_fuzz.py
67
*/cached_property.py
78
*/is_iterable.py
8-
*/test_*_fuzz.py
9+
*/assert_valid_name.py
910

1011
[report]
1112
exclude_lines =

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ a query language for APIs created by Facebook.
1414

1515
The current version 3.1.6 of GraphQL-core is up-to-date with GraphQL.js version 15.5.1.
1616

17-
An extensive test suite with over 2200 unit tests and 100% coverage comprises a
17+
An extensive test suite with nearly 2300 unit tests and 100% coverage comprises a
1818
replication of the complete test suite of GraphQL.js, making sure this port is
1919
reliable and compatible with GraphQL.js.
2020

docs/modules/type.rst

+12-2
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,22 @@ Definitions
187187
Validate
188188
--------
189189

190-
Functions:
191-
^^^^^^^^^^
190+
Functions
191+
^^^^^^^^^
192192

193193
.. autofunction:: validate_schema
194194

195195
Assertions
196196
^^^^^^^^^^
197197

198198
.. autofunction:: assert_valid_schema
199+
200+
201+
Other
202+
-----
203+
204+
Assertions
205+
^^^^^^^^^^
206+
207+
.. autofunction:: assert_name
208+
.. autofunction:: assert_enum_value_name

src/graphql/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@
132132
# Validate GraphQL schema.
133133
validate_schema,
134134
assert_valid_schema,
135+
# Uphold the spec rules about naming
136+
assert_name,
137+
assert_enum_value_name,
135138
# Types
136139
GraphQLType,
137140
GraphQLInputType,
@@ -508,6 +511,8 @@
508511
"get_named_type",
509512
"validate_schema",
510513
"assert_valid_schema",
514+
"assert_name",
515+
"assert_enum_value_name",
511516
"GraphQLType",
512517
"GraphQLInputType",
513518
"GraphQLOutputType",

src/graphql/type/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
GraphQLSchema,
1515
)
1616

17+
# Uphold the spec rules about naming.
18+
from .assert_name import assert_name, assert_enum_value_name
19+
1720
from .definition import (
1821
# Predicates
1922
is_type,
@@ -149,6 +152,8 @@
149152
__all__ = [
150153
"is_schema",
151154
"assert_schema",
155+
"assert_name",
156+
"assert_enum_value_name",
152157
"GraphQLSchema",
153158
"is_type",
154159
"is_scalar_type",

src/graphql/type/assert_name.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from ..error import GraphQLError
2+
from ..language.character_classes import is_name_start, is_name_continue
3+
4+
__all__ = ["assert_name", "assert_enum_value_name"]
5+
6+
7+
def assert_name(name: str) -> str:
8+
"""Uphold the spec rules about naming."""
9+
if name is None:
10+
raise TypeError("Must provide name.")
11+
if not isinstance(name, str):
12+
raise TypeError("Expected name to be a string.")
13+
if not name:
14+
raise GraphQLError("Expected name to be a non-empty string.")
15+
if not all(is_name_continue(char) for char in name[1:]):
16+
raise GraphQLError(
17+
f"Names must only contain [_a-zA-Z0-9] but {name!r} does not."
18+
)
19+
if not is_name_start(name[0]):
20+
raise GraphQLError(f"Names must start with [_a-zA-Z] but {name!r} does not.")
21+
return name
22+
23+
24+
def assert_enum_value_name(name: str) -> str:
25+
"""Uphold the spec rules about naming enum values."""
26+
assert_name(name)
27+
if name in {"true", "false", "null"}:
28+
raise GraphQLError(f"Enum values cannot be named: {name}.")
29+
return name

src/graphql/type/definition.py

+19-15
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
Undefined,
5757
)
5858
from ..utilities.value_from_ast_untyped import value_from_ast_untyped
59+
from .assert_name import assert_name, assert_enum_value_name
5960

6061
if TYPE_CHECKING:
6162
from .schema import GraphQLSchema # noqa: F401
@@ -208,10 +209,7 @@ def __init__(
208209
ast_node: Optional[TypeDefinitionNode] = None,
209210
extension_ast_nodes: Optional[Collection[TypeExtensionNode]] = None,
210211
) -> None:
211-
if not name:
212-
raise TypeError("Must provide name.")
213-
if not isinstance(name, str):
214-
raise TypeError("The name must be a string.")
212+
assert_name(name)
215213
if description is not None and not is_description(description):
216214
raise TypeError("The description must be a string.")
217215
if extensions is None:
@@ -465,7 +463,7 @@ def __init__(
465463
)
466464
else:
467465
args = {
468-
name: value
466+
assert_name(name): value
469467
if isinstance(value, GraphQLArgument)
470468
else GraphQLArgument(cast(GraphQLInputType, value))
471469
for name, value in args.items()
@@ -737,7 +735,8 @@ def fields(self) -> GraphQLFieldMap:
737735
try:
738736
fields = resolve_thunk(self._fields)
739737
except Exception as error:
740-
raise TypeError(f"{self.name} fields cannot be resolved. {error}")
738+
cls = GraphQLError if isinstance(error, GraphQLError) else TypeError
739+
raise cls(f"{self.name} fields cannot be resolved. {error}") from error
741740
if not isinstance(fields, Mapping) or not all(
742741
isinstance(key, str) for key in fields
743742
):
@@ -753,7 +752,7 @@ def fields(self) -> GraphQLFieldMap:
753752
f"{self.name} fields must be GraphQLField or output type objects."
754753
)
755754
return {
756-
name: value
755+
assert_name(name): value
757756
if isinstance(value, GraphQLField)
758757
else GraphQLField(value) # type: ignore
759758
for name, value in fields.items()
@@ -767,7 +766,8 @@ def interfaces(self) -> List["GraphQLInterfaceType"]:
767766
self._interfaces # type: ignore
768767
)
769768
except Exception as error:
770-
raise TypeError(f"{self.name} interfaces cannot be resolved. {error}")
769+
cls = GraphQLError if isinstance(error, GraphQLError) else TypeError
770+
raise cls(f"{self.name} interfaces cannot be resolved. {error}") from error
771771
if interfaces is None:
772772
interfaces = []
773773
elif not is_collection(interfaces) or not all(
@@ -862,7 +862,8 @@ def fields(self) -> GraphQLFieldMap:
862862
try:
863863
fields = resolve_thunk(self._fields)
864864
except Exception as error:
865-
raise TypeError(f"{self.name} fields cannot be resolved. {error}")
865+
cls = GraphQLError if isinstance(error, GraphQLError) else TypeError
866+
raise cls(f"{self.name} fields cannot be resolved. {error}") from error
866867
if not isinstance(fields, Mapping) or not all(
867868
isinstance(key, str) for key in fields
868869
):
@@ -878,7 +879,7 @@ def fields(self) -> GraphQLFieldMap:
878879
f"{self.name} fields must be GraphQLField or output type objects."
879880
)
880881
return {
881-
name: value
882+
assert_name(name): value
882883
if isinstance(value, GraphQLField)
883884
else GraphQLField(value) # type: ignore
884885
for name, value in fields.items()
@@ -892,7 +893,8 @@ def interfaces(self) -> List["GraphQLInterfaceType"]:
892893
self._interfaces # type: ignore
893894
)
894895
except Exception as error:
895-
raise TypeError(f"{self.name} interfaces cannot be resolved. {error}")
896+
cls = GraphQLError if isinstance(error, GraphQLError) else TypeError
897+
raise cls(f"{self.name} interfaces cannot be resolved. {error}") from error
896898
if interfaces is None:
897899
interfaces = []
898900
elif not is_collection(interfaces) or not all(
@@ -985,7 +987,8 @@ def types(self) -> List[GraphQLObjectType]:
985987
try:
986988
types: Collection[GraphQLObjectType] = resolve_thunk(self._types)
987989
except Exception as error:
988-
raise TypeError(f"{self.name} types cannot be resolved. {error}")
990+
cls = GraphQLError if isinstance(error, GraphQLError) else TypeError
991+
raise cls(f"{self.name} types cannot be resolved. {error}") from error
989992
if types is None:
990993
types = []
991994
elif not is_collection(types) or not all(
@@ -1081,7 +1084,7 @@ def __init__(
10811084
values = cast(Dict, values)
10821085
values = {key: value.value for key, value in values.items()}
10831086
values = {
1084-
key: value
1087+
assert_enum_value_name(key): value
10851088
if isinstance(value, GraphQLEnumValue)
10861089
else GraphQLEnumValue(value)
10871090
for key, value in values.items()
@@ -1338,7 +1341,8 @@ def fields(self) -> GraphQLInputFieldMap:
13381341
try:
13391342
fields = resolve_thunk(self._fields)
13401343
except Exception as error:
1341-
raise TypeError(f"{self.name} fields cannot be resolved. {error}")
1344+
cls = GraphQLError if isinstance(error, GraphQLError) else TypeError
1345+
raise cls(f"{self.name} fields cannot be resolved. {error}") from error
13421346
if not isinstance(fields, Mapping) or not all(
13431347
isinstance(key, str) for key in fields
13441348
):
@@ -1355,7 +1359,7 @@ def fields(self) -> GraphQLInputFieldMap:
13551359
" GraphQLInputField or input type objects."
13561360
)
13571361
return {
1358-
name: value
1362+
assert_name(name): value
13591363
if isinstance(value, GraphQLInputField)
13601364
else GraphQLInputField(value) # type: ignore
13611365
for name, value in fields.items()

src/graphql/type/directives.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ..language import ast, DirectiveLocation
44
from ..pyutils import inspect, is_description, FrozenList
5+
from .assert_name import assert_name
56
from .definition import GraphQLArgument, GraphQLInputType, GraphQLNonNull, is_input_type
67
from .scalars import GraphQLBoolean, GraphQLString
78

@@ -45,10 +46,7 @@ def __init__(
4546
extensions: Optional[Dict[str, Any]] = None,
4647
ast_node: Optional[ast.DirectiveDefinitionNode] = None,
4748
) -> None:
48-
if not name:
49-
raise TypeError("Directive must be named.")
50-
elif not isinstance(name, str):
51-
raise TypeError("The directive name must be a string.")
49+
assert_name(name)
5250
try:
5351
locations = [
5452
value
@@ -76,7 +74,7 @@ def __init__(
7674
)
7775
else:
7876
args = {
79-
name: value
77+
assert_name(name): value
8078
if isinstance(value, GraphQLArgument)
8179
else GraphQLArgument(cast(GraphQLInputType, value))
8280
for name, value in args.items()

src/graphql/type/validate.py

+8-15
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
cast,
1212
)
1313

14-
from ..error import GraphQLError, located_error
14+
from ..error import GraphQLError
1515
from ..pyutils import inspect
1616
from ..language import (
1717
DirectiveNode,
@@ -41,7 +41,6 @@
4141
is_required_argument,
4242
is_required_input_field,
4343
)
44-
from ..utilities.assert_valid_name import is_valid_name_error
4544
from ..utilities.type_comparators import is_equal_type, is_type_sub_type_of
4645
from .directives import is_directive, GraphQLDeprecatedDirective
4746
from .introspection import is_introspection_type
@@ -109,10 +108,7 @@ def report_error(
109108
if nodes and not isinstance(nodes, Node):
110109
nodes = [node for node in nodes if node]
111110
nodes = cast(Optional[Collection[Node]], nodes)
112-
self.add_error(GraphQLError(message, nodes))
113-
114-
def add_error(self, error: GraphQLError) -> None:
115-
self.errors.append(error)
111+
self.errors.append(GraphQLError(message, nodes))
116112

117113
def validate_root_types(self) -> None:
118114
schema = self.schema
@@ -191,9 +187,12 @@ def validate_name(self, node: Any, name: Optional[str] = None) -> None:
191187
except AttributeError: # pragma: no cover
192188
pass
193189
else:
194-
error = is_valid_name_error(name)
195-
if error:
196-
self.add_error(located_error(error, ast_node))
190+
if name.startswith("__"):
191+
self.report_error(
192+
f"Name {name!r} must not begin with '__',"
193+
" which is reserved by GraphQL introspection.",
194+
ast_node,
195+
)
197196

198197
def validate_types(self) -> None:
199198
validate_input_object_circular_refs = InputObjectCircularRefsValidator(self)
@@ -457,12 +456,6 @@ def validate_enum_values(self, enum_type: GraphQLEnumType) -> None:
457456
for value_name, enum_value in enum_values.items():
458457
# Ensure valid name.
459458
self.validate_name(enum_value, value_name)
460-
if value_name in ("true", "false", "null"):
461-
self.report_error(
462-
f"Enum type {enum_type.name} cannot include value:"
463-
f" {value_name}.",
464-
enum_value.ast_node,
465-
)
466459

467460
def validate_input_fields(self, input_obj: GraphQLInputObjectType) -> None:
468461
fields = input_obj.fields
+15-20
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,38 @@
1-
import re
21
from typing import Optional
32

4-
from ..language.character_classes import is_name_start, is_name_continue
3+
from ..type.assert_name import assert_name
54
from ..error import GraphQLError
65

76
__all__ = ["assert_valid_name", "is_valid_name_error"]
87

98

10-
re_name = re.compile("^[_a-zA-Z][_a-zA-Z0-9]*$")
11-
12-
139
def assert_valid_name(name: str) -> str:
14-
"""Uphold the spec rules about naming."""
10+
"""Uphold the spec rules about naming.
11+
12+
.. deprecated:: 3.2
13+
Please use ``assert_name`` instead. Will be removed in v3.3.
14+
"""
1515
error = is_valid_name_error(name)
1616
if error:
1717
raise error
1818
return name
1919

2020

2121
def is_valid_name_error(name: str) -> Optional[GraphQLError]:
22-
"""Return an Error if a name is invalid."""
22+
"""Return an Error if a name is invalid.
23+
24+
.. deprecated:: 3.2
25+
Please use ``assert_name`` instead. Will be removed in v3.3.
26+
"""
2327
if not isinstance(name, str):
2428
raise TypeError("Expected name to be a string.")
25-
2629
if name.startswith("__"):
2730
return GraphQLError(
2831
f"Name {name!r} must not begin with '__',"
2932
" which is reserved by GraphQL introspection."
3033
)
31-
32-
if not name:
33-
return GraphQLError("Expected name to be a non-empty string.")
34-
35-
if not all(is_name_continue(char) for char in name[1:]):
36-
return GraphQLError(
37-
f"Names must only contain [_a-zA-Z0-9] but {name!r} does not."
38-
)
39-
40-
if not is_name_start(name[0]):
41-
return GraphQLError(f"Names must start with [_a-zA-Z] but {name!r} does not.")
42-
34+
try:
35+
assert_name(name)
36+
except GraphQLError as error:
37+
return error
4338
return None

0 commit comments

Comments
 (0)