Skip to content

Commit dceb5cd

Browse files
committed
Add support for adding description to schema
Replicates graphql/graphql-js@b883320
1 parent 67d28f8 commit dceb5cd

21 files changed

+147
-35
lines changed

src/graphql/language/ast.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,9 @@ class TypeSystemDefinitionNode(DefinitionNode):
477477

478478

479479
class SchemaDefinitionNode(TypeSystemDefinitionNode):
480-
__slots__ = "directives", "operation_types"
480+
__slots__ = "description", "directives", "operation_types"
481481

482+
description: Optional[StringValueNode]
482483
directives: Optional[FrozenList[DirectiveNode]]
483484
operation_types: FrozenList["OperationTypeDefinitionNode"]
484485

src/graphql/language/parser.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,13 +581,17 @@ def parse_description(self) -> Optional[StringValueNode]:
581581
def parse_schema_definition(self) -> SchemaDefinitionNode:
582582
"""SchemaDefinition"""
583583
start = self._lexer.token
584+
description = self.parse_description()
584585
self.expect_keyword("schema")
585586
directives = self.parse_directives(True)
586587
operation_types = self.many(
587588
TokenKind.BRACE_L, self.parse_operation_type_definition, TokenKind.BRACE_R
588589
)
589590
return SchemaDefinitionNode(
590-
directives=directives, operation_types=operation_types, loc=self.loc(start)
591+
description=description,
592+
directives=directives,
593+
operation_types=operation_types,
594+
loc=self.loc(start),
591595
)
592596

593597
def parse_operation_type_definition(self) -> OperationTypeDefinitionNode:

src/graphql/language/printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ def leave_non_null_type(self, node, *_args):
153153

154154
# Type System Definitions
155155

156+
@add_description
156157
def leave_schema_definition(self, node, *_args):
157158
return join(
158159
["schema", join(node.directives, " "), block(node.operation_types)], " "

src/graphql/language/visitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"named_type": ("name",),
7272
"list_type": ("type",),
7373
"non_null_type": ("type",),
74-
"schema_definition": ("directives", "operation_types"),
74+
"schema_definition": ("description", "directives", "operation_types"),
7575
"operation_type_definition": ("type",),
7676
"scalar_type_definition": ("description", "name", "directives"),
7777
"object_type_definition": (

src/graphql/type/introspection.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
" on the server, as well as the entry points for query,"
4141
" mutation, and subscription operations.",
4242
fields=lambda: {
43+
"description": GraphQLField(
44+
GraphQLString, resolve=lambda schema, _info: schema.description
45+
),
4346
"types": GraphQLField(
4447
GraphQLNonNull(GraphQLList(GraphQLNonNull(__Type))),
4548
resolve=lambda schema, _info: schema.type_map.values(),

src/graphql/type/schema.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from ..error import GraphQLError
1414
from ..language import ast
15-
from ..pyutils import inspect, is_collection, FrozenList
15+
from ..pyutils import inspect, is_collection, is_description, FrozenList
1616
from .definition import (
1717
GraphQLAbstractType,
1818
GraphQLInterfaceType,
@@ -94,6 +94,7 @@ class GraphQLSchema:
9494
subscription_type: Optional[GraphQLObjectType]
9595
type_map: TypeMap
9696
directives: FrozenList[GraphQLDirective]
97+
description: Optional[str]
9798
extensions: Optional[Dict[str, Any]]
9899
ast_node: Optional[ast.SchemaDefinitionNode]
99100
extension_ast_nodes: Optional[FrozenList[ast.SchemaExtensionNode]]
@@ -109,6 +110,7 @@ def __init__(
109110
subscription: Optional[GraphQLObjectType] = None,
110111
types: Optional[Collection[GraphQLNamedType]] = None,
111112
directives: Optional[Collection[GraphQLDirective]] = None,
113+
description: Optional[str] = None,
112114
extensions: Optional[Dict[str, Any]] = None,
113115
ast_node: Optional[ast.SchemaDefinitionNode] = None,
114116
extension_ast_nodes: Optional[Collection[ast.SchemaExtensionNode]] = None,
@@ -144,6 +146,8 @@ def __init__(
144146
raise TypeError("Schema directives must be a collection.")
145147
if not isinstance(directives, FrozenList):
146148
directives = FrozenList(directives)
149+
if description is not None and not is_description(description):
150+
raise TypeError("Schema description must be a string.")
147151
if extensions is not None and (
148152
not isinstance(extensions, dict)
149153
or not all(isinstance(key, str) for key in extensions)
@@ -163,6 +167,7 @@ def __init__(
163167
if not isinstance(extension_ast_nodes, FrozenList):
164168
extension_ast_nodes = FrozenList(extension_ast_nodes)
165169

170+
self.description = description
166171
self.extensions = extensions
167172
self.ast_node = ast_node
168173
self.extension_ast_nodes = (
@@ -268,6 +273,7 @@ def to_kwargs(self) -> Dict[str, Any]:
268273
subscription=self.subscription_type,
269274
types=FrozenList(self.type_map.values()) or None,
270275
directives=self.directives[:],
276+
description=self.description,
271277
extensions=self.extensions,
272278
ast_node=self.ast_node,
273279
extension_ast_nodes=self.extension_ast_nodes,

src/graphql/utilities/build_client_schema.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,6 @@ def build_directive(directive_introspection: Dict) -> GraphQLDirective:
337337
type_map[std_type_name] = std_type
338338

339339
# Get the root Query, Mutation, and Subscription types.
340-
341340
query_type_ref = schema_introspection.get("queryType")
342341
query_type = None if query_type_ref is None else get_object_type(query_type_ref)
343342
mutation_type_ref = schema_introspection.get("mutationType")
@@ -363,11 +362,13 @@ def build_directive(directive_introspection: Dict) -> GraphQLDirective:
363362
else []
364363
)
365364

365+
# Then produce and return a Schema with these types.
366366
return GraphQLSchema(
367367
query=query_type,
368368
mutation=mutation_type,
369369
subscription=subscription_type,
370370
types=list(type_map.values()),
371371
directives=directives,
372+
description=schema_introspection.get("description"),
372373
assume_valid=assume_valid,
373374
)

src/graphql/utilities/extend_schema.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,9 @@ def build_type(ast_node: TypeDefinitionNode) -> GraphQLNamedType:
652652
replace_directive(directive) for directive in schema_kwargs["directives"]
653653
]
654654
+ [build_directive(directive) for directive in directive_defs],
655+
"description": schema_def.description.value
656+
if schema_def and schema_def.description
657+
else None,
655658
"extensions": None,
656659
"ast_node": schema_def or schema_kwargs["ast_node"],
657660
"extension_ast_nodes": (

src/graphql/utilities/get_introspection_query.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@
33
__all__ = ["get_introspection_query"]
44

55

6-
def get_introspection_query(descriptions=True, directive_is_repeatable=False) -> str:
6+
def get_introspection_query(
7+
descriptions=True, directive_is_repeatable=False, schema_description=False
8+
) -> str:
79
"""Get a query for introspection.
810
9-
Optionally, you can exclude descriptions and include repeatability of directives.
11+
Optionally, you can exclude descriptions, include repeatability of directives,
12+
and specify whether to include the schema description as well.
1013
"""
1114
maybe_description = "description" if descriptions else ""
1215
maybe_directive_is_repeatable = "isRepeatable" if directive_is_repeatable else ""
16+
maybe_schema_description = maybe_description if schema_description else ""
1317
return dedent(
1418
f"""
1519
query IntrospectionQuery {{
1620
__schema {{
21+
{maybe_schema_description}
1722
queryType {{ name }}
1823
mutationType {{ name }}
1924
subscriptionType {{ name }}

src/graphql/utilities/introspection_from_schema.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def introspection_from_schema(
1515
schema: GraphQLSchema,
1616
descriptions: bool = True,
1717
directive_is_repeatable: bool = True,
18+
schema_description: bool = True,
1819
) -> IntrospectionSchema:
1920
"""Build an IntrospectionQuery from a GraphQLSchema
2021
@@ -24,7 +25,11 @@ def introspection_from_schema(
2425
This is the inverse of build_client_schema. The primary use case is outside of the
2526
server context, for instance when doing schema comparisons.
2627
"""
27-
document = parse(get_introspection_query(descriptions, directive_is_repeatable))
28+
document = parse(
29+
get_introspection_query(
30+
descriptions, directive_is_repeatable, schema_description
31+
)
32+
)
2833

2934
from ..execution.execute import execute, ExecutionResult
3035

src/graphql/utilities/schema_printer.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def print_filtered_schema(
7070

7171

7272
def print_schema_definition(schema: GraphQLSchema) -> Optional[str]:
73-
if is_schema_of_common_names(schema):
73+
if schema.description is None and is_schema_of_common_names(schema):
7474
return None
7575

7676
operation_types = []
@@ -87,7 +87,7 @@ def print_schema_definition(schema: GraphQLSchema) -> Optional[str]:
8787
if subscription_type:
8888
operation_types.append(f" subscription: {subscription_type.name}")
8989

90-
return "schema {\n" + "\n".join(operation_types) + "\n}"
90+
return print_description(schema) + "schema {\n" + "\n".join(operation_types) + "\n}"
9191

9292

9393
def is_schema_of_common_names(schema: GraphQLSchema) -> bool:
@@ -264,7 +264,13 @@ def print_deprecated(field_or_enum_value: Union[GraphQLField, GraphQLEnumValue])
264264

265265

266266
def print_description(
267-
def_: Union[GraphQLArgument, GraphQLDirective, GraphQLEnumValue, GraphQLNamedType],
267+
def_: Union[
268+
GraphQLArgument,
269+
GraphQLDirective,
270+
GraphQLEnumValue,
271+
GraphQLNamedType,
272+
GraphQLSchema,
273+
],
268274
indentation="",
269275
first_in_block=True,
270276
) -> str:

tests/fixtures/kitchen_sink.graphql

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
# Copyright (c) 2015-present, Facebook, Inc.
2-
#
3-
# This source code is licensed under the MIT license found in the
4-
# LICENSE file in the root directory of this source tree.
5-
61
query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
72
whoever123is: node(id: [123, 456]) {
83
id ,

tests/fixtures/schema_kitchen_sink.graphql

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
# Copyright (c) 2015-present, Facebook, Inc.
2-
#
3-
# This source code is licensed under the MIT license found in the
4-
# LICENSE file in the root directory of this source tree.
5-
1+
"""This is a description of the schema as a whole."""
62
schema {
73
query: QueryType
84
mutation: MutationType

tests/language/test_schema_parser.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
OperationType,
2525
OperationTypeDefinitionNode,
2626
ScalarTypeDefinitionNode,
27+
SchemaDefinitionNode,
2728
SchemaExtensionNode,
2829
StringValueNode,
2930
UnionTypeDefinitionNode,
@@ -96,6 +97,10 @@ def boolean_value_node(value, loc):
9697
return BooleanValueNode(value=value, loc=loc)
9798

9899

100+
def string_value_node(value, block, loc):
101+
return StringValueNode(value=value, block=block, loc=loc)
102+
103+
99104
def list_type_node(type_, loc):
100105
return ListTypeNode(type=type_, loc=loc)
101106

@@ -149,10 +154,7 @@ def parses_type_with_description_string():
149154
assert isinstance(definition, ObjectTypeDefinitionNode)
150155
assert definition.name == name_node("Hello", (20, 25))
151156
description = definition.description
152-
assert isinstance(description, StringValueNode)
153-
assert description.value == "Description"
154-
assert description.block is False
155-
assert description.loc == (1, 14)
157+
assert description == string_value_node("Description", False, (1, 14))
156158

157159
def parses_type_with_description_multi_line_string():
158160
body = dedent(
@@ -169,10 +171,21 @@ def parses_type_with_description_multi_line_string():
169171
assert isinstance(definition, ObjectTypeDefinitionNode)
170172
assert definition.name == name_node("Hello", (60, 65))
171173
description = definition.description
172-
assert isinstance(description, StringValueNode)
173-
assert description.value == "Description"
174-
assert description.block is True
175-
assert description.loc == (1, 20)
174+
assert description == string_value_node("Description", True, (1, 20))
175+
176+
def parses_schema_with_description_string():
177+
body = dedent(
178+
"""
179+
"Description"
180+
schema {
181+
query: Foo
182+
}
183+
"""
184+
)
185+
definition = assert_definitions(body, (0, 39))
186+
assert isinstance(definition, SchemaDefinitionNode)
187+
description = definition.description
188+
assert description == string_value_node("Description", False, (1, 14))
176189

177190
def description_followed_by_something_other_than_type_system_definition_throws():
178191
assert_syntax_error('"Description" 1', "Unexpected Int '1'.", (1, 15))

tests/language/test_schema_printer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def prints_kitchen_sink(kitchen_sink_sdl): # noqa: F811
3636

3737
assert printed == dedent(
3838
'''
39+
"""This is a description of the schema as a whole."""
3940
schema {
4041
query: QueryType
4142
mutation: MutationType

tests/type/test_introspection.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
def describe_introspection():
1818
def executes_an_introspection_query():
1919
schema = GraphQLSchema(
20-
GraphQLObjectType("QueryRoot", {"onlyField": GraphQLField(GraphQLString)})
20+
GraphQLObjectType("QueryRoot", {"onlyField": GraphQLField(GraphQLString)}),
21+
description="Sample schema",
2122
)
2223
source = get_introspection_query(
2324
descriptions=False, directive_is_repeatable=True
@@ -74,6 +75,17 @@ def executes_an_introspection_query():
7475
"kind": "OBJECT",
7576
"name": "__Schema",
7677
"fields": [
78+
{
79+
"name": "description",
80+
"args": [],
81+
"type": {
82+
"kind": "SCALAR",
83+
"name": "String",
84+
"ofType": None,
85+
},
86+
"isDeprecated": False,
87+
"deprecationReason": None,
88+
},
7789
{
7890
"name": "types",
7991
"args": [],
@@ -1240,6 +1252,7 @@ def exposes_descriptions_on_types_and_fields():
12401252
" directives on the server, as well as the entry points"
12411253
" for query, mutation, and subscription operations.",
12421254
"fields": [
1255+
{"name": "description", "description": None},
12431256
{
12441257
"name": "types",
12451258
"description": "A list of all types supported"

tests/type/test_schema.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ def define_sample_schema():
9191
},
9292
)
9393

94-
schema = GraphQLSchema(BlogQuery, BlogMutation, BlogSubscription)
94+
schema = GraphQLSchema(
95+
BlogQuery, BlogMutation, BlogSubscription, description="Sample schema",
96+
)
9597

9698
kwargs = schema.to_kwargs()
9799
types = kwargs.pop("types")
@@ -101,14 +103,22 @@ def define_sample_schema():
101103
"mutation": BlogMutation,
102104
"subscription": BlogSubscription,
103105
"directives": specified_directives,
104-
"ast_node": None,
106+
"description": "Sample schema",
105107
"extensions": None,
108+
"ast_node": None,
106109
"extension_ast_nodes": None,
107110
"assume_valid": False,
108111
}
109112

110113
assert print_schema(schema) == dedent(
111-
"""
114+
'''
115+
"""Sample schema"""
116+
schema {
117+
query: Query
118+
mutation: Mutation
119+
subscription: Subscription
120+
}
121+
112122
type Query {
113123
article(id: String): Article
114124
feed: [Article]
@@ -142,7 +152,7 @@ def define_sample_schema():
142152
type Subscription {
143153
articleSubscribe(id: String): Article
144154
}
145-
"""
155+
'''
146156
)
147157

148158
def freezes_the_specified_directives():
@@ -154,6 +164,12 @@ def freezes_the_specified_directives():
154164
schema = GraphQLSchema(directives=directives)
155165
assert schema.directives is directives
156166

167+
def rejects_a_schema_with_incorrectly_typed_description():
168+
with raises(TypeError) as exc_info:
169+
# noinspection PyTypeChecker
170+
GraphQLSchema(description=[]) # type: ignore
171+
assert str(exc_info.value) == "Schema description must be a string."
172+
157173
def describe_type_map():
158174
def includes_interface_possible_types_in_the_type_map():
159175
SomeInterface = GraphQLInterfaceType("SomeInterface", {})

0 commit comments

Comments
 (0)