diff --git a/graphql/language/ast.py b/graphql/language/ast.py index f7f407ea..cd616313 100644 --- a/graphql/language/ast.py +++ b/graphql/language/ast.py @@ -543,13 +543,14 @@ def __hash__(self): class StringValue(Value): - __slots__ = ("loc", "value") + __slots__ = ("loc", "value", "is_block_string") _fields = ("value",) - def __init__(self, value, loc=None): + def __init__(self, value, loc=None, is_block_string=False): # type: (str, Optional[Loc]) -> None self.loc = loc self.value = value + self.is_block_string = is_block_string def __eq__(self, other): # type: (Any) -> bool @@ -989,7 +990,7 @@ def __hash__(self): class ObjectTypeDefinition(TypeDefinition): - __slots__ = ("loc", "name", "interfaces", "directives", "fields") + __slots__ = ("loc", "name", "interfaces", "directives", "fields", "description") _fields = ("name", "interfaces", "fields") def __init__( @@ -999,6 +1000,7 @@ def __init__( interfaces=None, # type: Optional[List[NamedType]] loc=None, # type: Optional[Loc] directives=None, # type: Optional[List[Directive]] + description=None, # type: Optional[String] ): # type: (...) -> None self.loc = loc @@ -1006,12 +1008,12 @@ def __init__( self.interfaces = interfaces self.fields = fields self.directives = directives + self.description = description def __eq__(self, other): # type: (Any) -> bool return self is other or ( - isinstance(other, ObjectTypeDefinition) - and + isinstance(other, ObjectTypeDefinition) and # self.loc == other.loc and self.name == other.name and self.interfaces == other.interfaces @@ -1042,7 +1044,7 @@ def __hash__(self): class FieldDefinition(Node): - __slots__ = ("loc", "name", "arguments", "type", "directives") + __slots__ = ("loc", "name", "arguments", "type", "directives", "description") _fields = ("name", "arguments", "type") def __init__( @@ -1052,6 +1054,7 @@ def __init__( type, # type: Union[NamedType, NonNullType, ListType] loc=None, # type: Optional[Loc] directives=None, # type: Optional[List] + description=None, # type: Optional[String] ): # type: (...) -> None self.loc = loc @@ -1059,6 +1062,7 @@ def __init__( self.arguments = arguments self.type = type self.directives = directives + self.description = description def __eq__(self, other): # type: (Any) -> bool @@ -1094,7 +1098,7 @@ def __hash__(self): class InputValueDefinition(Node): - __slots__ = ("loc", "name", "type", "default_value", "directives") + __slots__ = ("loc", "name", "type", "default_value", "directives", "description") _fields = ("name", "type", "default_value") def __init__( @@ -1104,6 +1108,7 @@ def __init__( default_value=None, # type: Any loc=None, # type: Optional[Loc] directives=None, # type: Optional[List] + description=None, # type: Optional[String] ): # type: (...) -> None self.loc = loc @@ -1111,6 +1116,7 @@ def __init__( self.type = type self.default_value = default_value self.directives = directives + self.description = description def __eq__(self, other): # type: (Any) -> bool @@ -1147,7 +1153,7 @@ def __hash__(self): class InterfaceTypeDefinition(TypeDefinition): - __slots__ = ("loc", "name", "fields", "directives") + __slots__ = ("loc", "name", "fields", "directives", "description") _fields = ("name", "fields") def __init__( @@ -1156,12 +1162,14 @@ def __init__( fields, # type: List[FieldDefinition] loc=None, # type: Optional[Loc] directives=None, # type: Optional[List[Directive]] + description=None, # type: Optional[String] ): # type: (...) -> None self.loc = loc self.name = name self.fields = fields self.directives = directives + self.description = description def __eq__(self, other): # type: (Any) -> bool @@ -1194,7 +1202,7 @@ def __hash__(self): class UnionTypeDefinition(TypeDefinition): - __slots__ = ("loc", "name", "types", "directives") + __slots__ = ("loc", "name", "types", "directives", "description") _fields = ("name", "types") def __init__( @@ -1203,12 +1211,14 @@ def __init__( types, # type: List[NamedType] loc=None, # type: Optional[Loc] directives=None, # type: Optional[List[Directive]] + description=None, # type: Optional[String] ): # type: (...) -> None self.loc = loc self.name = name self.types = types self.directives = directives + self.description = description def __eq__(self, other): # type: (Any) -> bool @@ -1241,7 +1251,7 @@ def __hash__(self): class ScalarTypeDefinition(TypeDefinition): - __slots__ = ("loc", "name", "directives") + __slots__ = ("loc", "name", "directives", "description") _fields = ("name",) def __init__( @@ -1249,11 +1259,13 @@ def __init__( name, # type: Name loc=None, # type: Optional[Loc] directives=None, # type: Optional[List[Directive]] + description=None, # type: Optional[String] ): # type: (...) -> None self.loc = loc self.name = name self.directives = directives + self.description = description def __eq__(self, other): # type: (Any) -> bool @@ -1284,7 +1296,7 @@ def __hash__(self): class EnumTypeDefinition(TypeDefinition): - __slots__ = ("loc", "name", "values", "directives") + __slots__ = ("loc", "name", "values", "directives", "description") _fields = ("name", "values") def __init__( @@ -1293,12 +1305,14 @@ def __init__( values, # type: List[EnumValueDefinition] loc=None, # type: Optional[Loc] directives=None, # type: Optional[List[Directive]] + description=None, # type: Optional[String] ): # type: (...) -> None self.loc = loc self.name = name self.values = values self.directives = directives + self.description = description def __eq__(self, other): # type: (Any) -> bool @@ -1331,7 +1345,7 @@ def __hash__(self): class EnumValueDefinition(Node): - __slots__ = ("loc", "name", "directives") + __slots__ = ("loc", "name", "directives", "description") _fields = ("name",) def __init__( @@ -1339,11 +1353,13 @@ def __init__( name, # type: Name loc=None, # type: Optional[Loc] directives=None, # type: Optional[List[Directive]] + description=None, # type: Optional[String] ): # type: (...) -> None self.loc = loc self.name = name self.directives = directives + self.description = description def __eq__(self, other): # type: (Any) -> bool @@ -1374,7 +1390,7 @@ def __hash__(self): class InputObjectTypeDefinition(TypeDefinition): - __slots__ = ("loc", "name", "fields", "directives") + __slots__ = ("loc", "name", "fields", "directives", "description") _fields = ("name", "fields") def __init__( @@ -1383,12 +1399,14 @@ def __init__( fields, # type: List[InputValueDefinition] loc=None, # type: Optional[Loc] directives=None, # type: Optional[List[Directive]] + description=None, # type: Optional[String] ): # type: (...) -> None self.loc = loc self.name = name self.fields = fields self.directives = directives + self.description = description def __eq__(self, other): # type: (Any) -> bool @@ -1454,7 +1472,7 @@ def __hash__(self): class DirectiveDefinition(TypeSystemDefinition): - __slots__ = ("loc", "name", "arguments", "locations") + __slots__ = ("loc", "name", "arguments", "locations", "description") _fields = ("name", "locations") def __init__( @@ -1463,12 +1481,14 @@ def __init__( locations, # type: List[Name] arguments=None, # type: Optional[List[InputValueDefinition]] loc=None, # type: Optional[Loc] + description=None, # type: Optional[String] ): # type: (...) -> None self.name = name self.locations = locations self.loc = loc self.arguments = arguments + self.description = description def __eq__(self, other): # type: (Any) -> bool diff --git a/graphql/language/lexer.py b/graphql/language/lexer.py index a60bc6e2..b1451dbc 100644 --- a/graphql/language/lexer.py +++ b/graphql/language/lexer.py @@ -1,3 +1,4 @@ +import re import json from six import unichr @@ -55,6 +56,10 @@ def next_token(self, reset_position=None): self.prev_position = token.end return token + def look_ahead(self): + skip_token = read_token(self.source, self.prev_position) + return read_token(self.source, skip_token.start) + class TokenKind(object): EOF = 1 @@ -76,6 +81,7 @@ class TokenKind(object): INT = 17 FLOAT = 18 STRING = 19 + BLOCK_STRING = 20 def get_token_desc(token): @@ -111,6 +117,7 @@ def get_token_kind_desc(kind): TokenKind.INT: "Int", TokenKind.FLOAT: "Float", TokenKind.STRING: "String", + TokenKind.BLOCK_STRING: "Block string", } @@ -155,7 +162,7 @@ def read_token(source, from_position): This skips over whitespace and comments until it finds the next lexable token, then lexes punctuators immediately or calls the appropriate - helper fucntion for more complicated tokens.""" + helper function for more complicated tokens.""" body = source.body body_length = len(body) @@ -191,6 +198,11 @@ def read_token(source, from_position): return read_number(source, position, code) elif code == 34: # " + if ( + char_code_at(body, position + 1) == 34 and + char_code_at(body, position + 2) == 34 + ): + return read_block_string(source, position) return read_string(source, position) raise GraphQLSyntaxError( @@ -417,6 +429,55 @@ def read_string(source, start): return Token(TokenKind.STRING, start, position + 1, u"".join(value)) +def read_block_string(source, from_position): + body = source.body + position = from_position + 3 + + chunk_start = position + code = 0 # type: Optional[int] + value = [] # type: List[str] + + while position < len(body) and code is not None: + code = char_code_at(body, position) + + # Closing triple quote + if ( + code == 34 and + char_code_at(body, position + 1) == 34 and + char_code_at(body, position + 2) == 34 + ): + value.append(body[chunk_start:position]) + return Token( + TokenKind.BLOCK_STRING, + from_position, + position + 3, + block_string_value(u"".join(value)), + ) + + if code < 0x0020 and code not in (0x0009, 0x000a, 0x000d): + raise GraphQLSyntaxError( + source, + position, + "Invalid character within str: %s." % print_char_code(code), + ) + + # Escaped triple quote (\""") + if ( + code == 92 and + char_code_at(body, position + 1) == 34 and + char_code_at(body, position + 2) == 34 and + char_code_at(body, position + 3) == 34 + ): + value.append(body[chunk_start, position] + '"""') + position += 4 + chunk_start = position + else: + position += 1 + + raise GraphQLSyntaxError(source, position, "Unterminated string") + + + def uni_char_code(a, b, c, d): # type: (int, int, int, int) -> int """Converts four hexidecimal chars to the integer that the @@ -473,3 +534,36 @@ def read_name(source, position): end += 1 return Token(TokenKind.NAME, position, end, body[position:end]) + + + +SPLIT_RE = re.compile("\r\n|[\n\r]") +WHITESPACE_RE = re.compile("(^[ |\t]*)") +EMPTY_LINE_RE = re.compile("^\s*$") + +def block_string_value(value): + lines = SPLIT_RE.split(value) + + common_indent = None + for line in lines[1:]: + match = WHITESPACE_RE.match(line) + indent = len(match.groups()[0]) + + if indent < len(line) and (common_indent is None or indent < common_indent): + common_indent = indent + if common_indent == 0: + break + + if common_indent: + new_lines = [lines[0]] + for line in lines[1:]: + new_lines.append(line[common_indent:]) + lines = new_lines + + while len(lines) and EMPTY_LINE_RE.match(lines[0]): + lines = lines[1:] + + while len(lines) and EMPTY_LINE_RE.match(lines[-1]): + lines = lines[:-1] + + return '\n'.join(lines) diff --git a/graphql/language/parser.py b/graphql/language/parser.py index 8b658e50..406abef5 100644 --- a/graphql/language/parser.py +++ b/graphql/language/parser.py @@ -140,6 +140,10 @@ def peek(parser, kind): return parser.token.kind == kind +def peek_description(parser): + return peek(parser, TokenKind.STRING) or peek(parser, TokenKind.BLOCK_STRING) + + def skip(parser, kind): # type: (Parser, int) -> bool """If the next token is of the given kind, return true after advancing @@ -254,6 +258,9 @@ def parse_definition(parser): if peek(parser, TokenKind.BRACE_L): return parse_operation_definition(parser) + if peek_description(parser): + return parse_type_system_definition(parser) + if peek(parser, TokenKind.NAME): name = parser.token.value @@ -277,6 +284,11 @@ def parse_definition(parser): raise unexpected(parser) +def parse_description(parser): + if peek_description(parser): + return parse_value_literal(parser, False) + + # Implements the parsing rules in the Operations section. def parse_operation_definition(parser): # type: (Parser) -> OperationDefinition @@ -494,6 +506,14 @@ def parse_value_literal(parser, is_const): value=token.value, loc=loc(parser, token.start) ) + elif token.kind == TokenKind.BLOCK_STRING: + advance(parser) + return ast.StringValue( # type: ignore + value=token.value, + loc=loc(parser, token.start), + is_block_string=True, + ) + elif token.kind == TokenKind.NAME: if token.value in ("true", "false"): advance(parser) @@ -624,10 +644,10 @@ def parse_type_system_definition(parser): - EnumTypeDefinition - InputObjectTypeDefinition """ - if not peek(parser, TokenKind.NAME): - raise unexpected(parser) - - name = parser.token.value + if peek_description(parser): + name = parser.lexer.look_ahead().value + else: + name = parser.token.value if name == "schema": return parse_schema_definition(parser) @@ -688,9 +708,11 @@ def parse_scalar_type_definition(parser): # type: (Parser) -> ScalarTypeDefinition start = parser.token.start expect_keyword(parser, "scalar") + description = parse_description(parser) return ast.ScalarTypeDefinition( name=parse_name(parser), + description=description, directives=parse_directives(parser), loc=loc(parser, start), ) @@ -699,9 +721,11 @@ def parse_scalar_type_definition(parser): def parse_object_type_definition(parser): # type: (Parser) -> ObjectTypeDefinition start = parser.token.start + description = parse_description(parser) expect_keyword(parser, "type") return ast.ObjectTypeDefinition( name=parse_name(parser), + description=description, interfaces=parse_implements_interfaces(parser), directives=parse_directives(parser), fields=any( @@ -729,9 +753,11 @@ def parse_implements_interfaces(parser): def parse_field_definition(parser): # type: (Parser) -> FieldDefinition start = parser.token.start + description = parse_description(parser) return ast.FieldDefinition( # type: ignore name=parse_name(parser), + description=description, arguments=parse_argument_defs(parser), type=expect(parser, TokenKind.COLON) and parse_type(parser), directives=parse_directives(parser), @@ -750,9 +776,11 @@ def parse_argument_defs(parser): def parse_input_value_def(parser): # type: (Parser) -> InputValueDefinition start = parser.token.start + description = parse_description(parser) return ast.InputValueDefinition( # type: ignore name=parse_name(parser), + description=description, type=expect(parser, TokenKind.COLON) and parse_type(parser), default_value=parse_const_value(parser) if skip(parser, TokenKind.EQUALS) @@ -765,10 +793,12 @@ def parse_input_value_def(parser): def parse_interface_type_definition(parser): # type: (Parser) -> InterfaceTypeDefinition start = parser.token.start + description = parse_description(parser) expect_keyword(parser, "interface") return ast.InterfaceTypeDefinition( name=parse_name(parser), + description=description, directives=parse_directives(parser), fields=any( parser, TokenKind.BRACE_L, parse_field_definition, TokenKind.BRACE_R @@ -780,10 +810,12 @@ def parse_interface_type_definition(parser): def parse_union_type_definition(parser): # type: (Parser) -> UnionTypeDefinition start = parser.token.start + description = parse_description(parser) expect_keyword(parser, "union") return ast.UnionTypeDefinition( # type: ignore name=parse_name(parser), + description=description, directives=parse_directives(parser), types=expect(parser, TokenKind.EQUALS) and parse_union_members(parser), loc=loc(parser, start), @@ -806,10 +838,12 @@ def parse_union_members(parser): def parse_enum_type_definition(parser): # type: (Parser) -> EnumTypeDefinition start = parser.token.start + description = parse_description(parser) expect_keyword(parser, "enum") return ast.EnumTypeDefinition( name=parse_name(parser), + description=description, directives=parse_directives(parser), values=many( parser, TokenKind.BRACE_L, parse_enum_value_definition, TokenKind.BRACE_R @@ -821,9 +855,11 @@ def parse_enum_type_definition(parser): def parse_enum_value_definition(parser): # type: (Parser) -> EnumValueDefinition start = parser.token.start + description = parse_description(parser) return ast.EnumValueDefinition( name=parse_name(parser), + description=description, directives=parse_directives(parser), loc=loc(parser, start), ) @@ -832,10 +868,12 @@ def parse_enum_value_definition(parser): def parse_input_object_type_definition(parser): # type: (Parser) -> InputObjectTypeDefinition start = parser.token.start + description = parse_description(parser) expect_keyword(parser, "input") return ast.InputObjectTypeDefinition( name=parse_name(parser), + description=description, directives=parse_directives(parser), fields=any(parser, TokenKind.BRACE_L, parse_input_value_def, TokenKind.BRACE_R), loc=loc(parser, start), @@ -855,6 +893,7 @@ def parse_type_extension_definition(parser): def parse_directive_definition(parser): # type: (Parser) -> DirectiveDefinition start = parser.token.start + description = parse_description(parser) expect_keyword(parser, "directive") expect(parser, TokenKind.AT) @@ -864,7 +903,11 @@ def parse_directive_definition(parser): locations = parse_directive_locations(parser) return ast.DirectiveDefinition( - name=name, locations=locations, arguments=args, loc=loc(parser, start) + name=name, + description=description, + locations=locations, + arguments=args, + loc=loc(parser, start), ) diff --git a/graphql/language/printer.py b/graphql/language/printer.py index 676af0a8..67cde409 100644 --- a/graphql/language/printer.py +++ b/graphql/language/printer.py @@ -140,8 +140,10 @@ def leave_IntValue(self, node, *args): def leave_FloatValue(self, node, *args): return node.value - def leave_StringValue(self, node, *args): + def leave_StringValue(self, node, key, *args): # type: (Any, *Any) -> str + if node.is_block_string: + return print_block_string(node.value, key == 'description') return json.dumps(node.value) def leave_BooleanValue(self, node, *args): @@ -198,84 +200,102 @@ def leave_OperationTypeDefinition(self, node, *args): def leave_ScalarTypeDefinition(self, node, *args): # type: (Any, *Any) -> str - return "scalar " + node.name + wrap(" ", join(node.directives, " ")) + return join([ + node.description, + join(["scalar", node.name, join(node.directives, " ")], " "), + ], "\n") def leave_ObjectTypeDefinition(self, node, *args): # type: (Any, *Any) -> str - return join( - [ + return join([ + node.description, + join([ "type", node.name, wrap("implements ", join(node.interfaces, ", ")), join(node.directives, " "), block(node.fields), - ], - " ", - ) + ], " "), + ], "\n") def leave_FieldDefinition(self, node, *args): # type: (Any, *Any) -> str - return ( + has_multiline_item = any("\n" in arg for arg in node.arguments) + if has_multiline_item: + arguments_str = wrap("(\n", indent(join(node.arguments, "\n")), "\n)") + else: + arguments_str = wrap("(", join(node.arguments, ", "), ")") + + definition_str = ( node.name - + wrap("(", join(node.arguments, ", "), ")") + + arguments_str + ": " + node.type + wrap(" ", join(node.directives, " ")) ) + return join([node.description, definition_str], "\n") def leave_InputValueDefinition(self, node, *args): # type: (Any, *Any) -> str - return ( + definition_str = ( node.name + ": " + node.type + wrap(" = ", node.default_value) + wrap(" ", join(node.directives, " ")) ) + return join([node.description, definition_str], "\n") def leave_InterfaceTypeDefinition(self, node, *args): # type: (Any, *Any) -> str - return ( + definition_str = ( "interface " + node.name + wrap(" ", join(node.directives, " ")) + " " + block(node.fields) ) + return join([node.description, definition_str], "\n") def leave_UnionTypeDefinition(self, node, *args): # type: (Any, *Any) -> str - return ( + definition_str = ( "union " + node.name + wrap(" ", join(node.directives, " ")) + " = " + join(node.types, " | ") ) + return join([node.description, definition_str], "\n") def leave_EnumTypeDefinition(self, node, *args): # type: (Any, *Any) -> str - return ( + definition_str = ( "enum " + node.name + wrap(" ", join(node.directives, " ")) + " " + block(node.values) ) + return join([node.description, definition_str], "\n") def leave_EnumValueDefinition(self, node, *args): # type: (Any, *Any) -> str - return node.name + wrap(" ", join(node.directives, " ")) + return join([ + node.description, + join([node.name, join(node.directives, " ")], " "), + ], "\n") def leave_InputObjectTypeDefinition(self, node, *args): # type: (Any, *Any) -> str - return ( + definition_str = ( "input " + node.name + wrap(" ", join(node.directives, " ")) + " " + block(node.fields) ) + return join([node.description, definition_str], "\n") def leave_TypeExtensionDefinition(self, node, *args): # type: (Any, *Any) -> str @@ -283,11 +303,14 @@ def leave_TypeExtensionDefinition(self, node, *args): def leave_DirectiveDefinition(self, node, *args): # type: (Any, *Any) -> str - return "directive @{}{} on {}".format( - node.name, - wrap("(", join(node.arguments, ", "), ")"), - " | ".join(node.locations), - ) + return join([ + node.description, + "directive @{}{} on {}".format( + node.name, + wrap("(", join(node.arguments, ", "), ")"), + " | ".join(node.locations), + ) + ], "\n") def join(maybe_list, separator=""): @@ -317,3 +340,13 @@ def indent(maybe_str): if maybe_str: return ' ' + maybe_str.replace("\n", "\n ") return "" + + +def print_block_string(value, is_description): + escaped = value.replace('"""', '\\"""') + if "\n" in value or (value[0] != " " and value[0] != "\t"): + if is_description: + return '"""\n' + escaped + '\n"""' + else: + return '"""\n' + indent(escaped) + '\n"""' + return '"""' + escaped.replace(r'"$', '"\n') + '"""' diff --git a/graphql/language/tests/fixtures.py b/graphql/language/tests/fixtures.py index b16653c4..290dc98d 100644 --- a/graphql/language/tests/fixtures.py +++ b/graphql/language/tests/fixtures.py @@ -58,7 +58,7 @@ } """ -SCHEMA_KITCHEN_SINK = """ +SCHEMA_KITCHEN_SINK = ''' # Copyright (c) 2015, Facebook, Inc. # All rights reserved. @@ -72,9 +72,23 @@ mutation: MutationType } +""" +This is a description +of the `Foo` type. +""" type Foo implements Bar { + "Description of the `one` field." one: Type - two(argument: InputType!): Type + """ + This is a description of the `two` field. + """ + two( + """ + This is a description of the `argument` argument. + """ + argument: InputType! + ): Type + """This is a description of the `three` field.""" three(argument: InputType, other: String): Int four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String @@ -103,8 +117,16 @@ scalar AnnotatedScalar @onScalar enum Site { + """ + This is a description of the `DESKTOP` value + """ + DESKTOP + """This is a description of the `MOBILE` value""" MOBILE + + "This is a description of the `WEB` value" + WEB } enum AnnotatedEnum @onEnum { @@ -129,7 +151,10 @@ type NoFields {} +""" +This is a description of the `@skip` directive +""" directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -""" +''' diff --git a/graphql/language/tests/test_block_string_value.py b/graphql/language/tests/test_block_string_value.py new file mode 100644 index 00000000..3e96ea7f --- /dev/null +++ b/graphql/language/tests/test_block_string_value.py @@ -0,0 +1,104 @@ +from graphql.language.lexer import block_string_value + +def test_uniform_indentation(): + _input = [ + '', + ' Hello,', + ' World!', + '', + ' Yours,', + ' GraphQL.', + ] + expectation = [ + 'Hello,', + ' World!', + '', + 'Yours,', + ' GraphQL.', + ] + _test_harness(_input, expectation) + + +def test_empty_leading_and_trailing_lines(): + _input = [ + '', + '', + ' Hello,', + ' World!', + '', + ' Yours,', + ' GraphQL.', + '', + '', + ] + expectation = [ + 'Hello,', + ' World!', + '', + 'Yours,', + ' GraphQL.', + ] + _test_harness(_input, expectation) + +def remove_blank_and_leading_lines(): + _input = [ + ' ', + ' ', + ' Hello,', + ' World!', + '', + ' Yours,', + ' GraphQL.', + ' ', + ' ', + ] + expectation = [ + 'Hello,', + ' World!', + '', + 'Yours,', + ' GraphQL.', + ] + _test_harness(_input, expectation) + +def test_retain_indentation_from_first_line(): + _input = [ + ' Hello,', + ' World!', + '', + ' Yours,', + ' GraphQL.', + ] + expectation = [ + ' Hello,', + ' World!', + '', + 'Yours,', + ' GraphQL.', + ] + _test_harness(_input, expectation) + +def test_no_alter_trailing_spaces(): + _input = [ + ' ', + ' Hello, ', + ' World! ', + ' ', + ' Yours, ', + ' GraphQL. ', + ' ', + ] + expectation = [ + 'Hello, ', + ' World! ', + ' ', + 'Yours, ', + ' GraphQL. ', + ] + _test_harness(_input, expectation) + + +def _test_harness(_input, expectation): + _input = "\n".join(_input) + expectation = "\n".join(expectation) + assert block_string_value(_input) == expectation diff --git a/graphql/language/tests/test_schema_printer.py b/graphql/language/tests/test_schema_printer.py index afa979cb..310fef55 100644 --- a/graphql/language/tests/test_schema_printer.py +++ b/graphql/language/tests/test_schema_printer.py @@ -38,14 +38,30 @@ def test_prints_kitchen_sink(): ast = parse(SCHEMA_KITCHEN_SINK) printed = print_ast(ast) - expected = """schema { + expected = '''schema { query: QueryType mutation: MutationType } +""" +This is a description +of the `Foo` type. +""" type Foo implements Bar { + "Description of the `one` field." one: Type - two(argument: InputType!): Type + """ + This is a description of the `two` field. + """ + two( + """ + This is a description of the `argument` argument. + """ + argument: InputType! + ): Type + """ + This is a description of the `three` field. + """ three(argument: InputType, other: String): Int four(argument: String = "string"): String five(argument: [String] = ["string", "string"]): String @@ -74,8 +90,16 @@ def test_prints_kitchen_sink(): scalar AnnotatedScalar @onScalar enum Site { + """ + This is a description of the `DESKTOP` value + """ DESKTOP + """ + This is a description of the `MOBILE` value + """ MOBILE + "This is a description of the `WEB` value" + WEB } enum AnnotatedEnum @onEnum { @@ -100,9 +124,12 @@ def test_prints_kitchen_sink(): type NoFields {} +""" +This is a description of the `@skip` directive +""" directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -""" +''' assert printed == expected diff --git a/graphql/language/visitor_meta.py b/graphql/language/visitor_meta.py index 37372c48..87f5ad37 100644 --- a/graphql/language/visitor_meta.py +++ b/graphql/language/visitor_meta.py @@ -31,17 +31,17 @@ ast.NonNullType: ("type",), ast.SchemaDefinition: ("directives", "operation_types"), ast.OperationTypeDefinition: ("type",), - ast.ScalarTypeDefinition: ("name", "directives"), - ast.ObjectTypeDefinition: ("name", "interfaces", "directives", "fields"), - ast.FieldDefinition: ("name", "arguments", "directives", "type"), - ast.InputValueDefinition: ("name", "type", "directives", "default_value"), - ast.InterfaceTypeDefinition: ("name", "directives", "fields"), - ast.UnionTypeDefinition: ("name", "directives", "types"), - ast.EnumTypeDefinition: ("name", "directives", "values"), - ast.EnumValueDefinition: ("name", "directives"), - ast.InputObjectTypeDefinition: ("name", "directives", "fields"), + ast.ScalarTypeDefinition: ("description", "name", "directives"), + ast.ObjectTypeDefinition: ("description", "name", "interfaces", "directives", "fields"), + ast.FieldDefinition: ("description", "name", "arguments", "directives", "type"), + ast.InputValueDefinition: ("description", "name", "type", "directives", "default_value"), + ast.InterfaceTypeDefinition: ("description", "name", "directives", "fields"), + ast.UnionTypeDefinition: ("description", "name", "directives", "types"), + ast.EnumTypeDefinition: ("description", "name", "directives", "values"), + ast.EnumValueDefinition: ("description", "name", "directives"), + ast.InputObjectTypeDefinition: ("description", "name", "directives", "fields"), ast.TypeExtensionDefinition: ("definition",), - ast.DirectiveDefinition: ("name", "arguments", "locations"), + ast.DirectiveDefinition: ("description", "name", "arguments", "locations"), } AST_KIND_TO_TYPE = {c.__name__: c for c in QUERY_DOCUMENT_KEYS.keys()}