diff --git a/.travis.yml b/.travis.yml index b40d377c..5e2f3ebb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: - pypy cache: pip install: -- pip install --cache-dir $HOME/.cache/pip pytest-cov coveralls flake8 import-order gevent==1.1b5 +- pip install --cache-dir $HOME/.cache/pip pytest-cov coveralls flake8 import-order gevent==1.1b5 six>=1.10.0 - pip install --cache-dir $HOME/.cache/pip pytest>=2.7.3 --upgrade - pip install -e . script: diff --git a/graphql/core/execution/executor.py b/graphql/core/execution/executor.py index 6a0783f0..df56355e 100644 --- a/graphql/core/execution/executor.py +++ b/graphql/core/execution/executor.py @@ -272,7 +272,7 @@ def complete_value(self, ctx, return_type, field_asts, info, result): if not runtime_type.is_type_of(result, info): raise GraphQLError( - u'Expected value of type "{}" but got {}.'.format(return_type, result), + u'Expected value of type "{}" but got {}.'.format(return_type, type(result).__name__), field_asts ) diff --git a/graphql/core/language/ast.py b/graphql/core/language/ast.py index c97d90ac..daa48ffd 100644 --- a/graphql/core/language/ast.py +++ b/graphql/core/language/ast.py @@ -32,6 +32,12 @@ def __repr__(self): 'definitions={self.definitions!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.definitions, + self.loc + ) + def __hash__(self): return id(self) @@ -70,6 +76,16 @@ def __repr__(self): ', selection_set={self.selection_set!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.operation, + self.selection_set, + self.name, + self.variable_definitions, + self.directives, + self.loc + ) + def __hash__(self): return id(self) @@ -102,6 +118,14 @@ def __repr__(self): ', default_value={self.default_value!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.variable, + self.type, + self.default_value, + self.loc + ) + def __hash__(self): return id(self) @@ -128,6 +152,12 @@ def __repr__(self): 'selections={self.selections!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.selections, + self.loc + ) + def __hash__(self): return id(self) @@ -170,6 +200,16 @@ def __repr__(self): ', selection_set={self.selection_set!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.name, + self.alias, + self.arguments, + self.directives, + self.selection_set, + self.loc + ) + def __hash__(self): return id(self) @@ -199,6 +239,13 @@ def __repr__(self): ', value={self.value!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.name, + self.value, + self.loc + ) + def __hash__(self): return id(self) @@ -228,6 +275,13 @@ def __repr__(self): ', directives={self.directives!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.name, + self.directives, + self.loc + ) + def __hash__(self): return id(self) @@ -260,6 +314,14 @@ def __repr__(self): ', selection_set={self.selection_set!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.type_condition, + self.selection_set, + self.directives, + self.loc + ) + def __hash__(self): return id(self) @@ -295,6 +357,15 @@ def __repr__(self): ', selection_set={self.selection_set!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.name, + self.type_condition, + self.selection_set, + self.directives, + self.loc + ) + def __hash__(self): return id(self) @@ -325,6 +396,12 @@ def __repr__(self): 'name={self.name!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.name, + self.loc + ) + def __hash__(self): return id(self) @@ -351,6 +428,12 @@ def __repr__(self): 'value={self.value!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.value, + self.loc + ) + def __hash__(self): return id(self) @@ -377,6 +460,12 @@ def __repr__(self): 'value={self.value!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.value, + self.loc + ) + def __hash__(self): return id(self) @@ -403,6 +492,12 @@ def __repr__(self): 'value={self.value!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.value, + self.loc + ) + def __hash__(self): return id(self) @@ -429,6 +524,12 @@ def __repr__(self): 'value={self.value!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.value, + self.loc + ) + def __hash__(self): return id(self) @@ -455,6 +556,12 @@ def __repr__(self): 'value={self.value!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.value, + self.loc + ) + def __hash__(self): return id(self) @@ -481,6 +588,12 @@ def __repr__(self): 'values={self.values!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.values, + self.loc + ) + def __hash__(self): return id(self) @@ -507,6 +620,12 @@ def __repr__(self): 'fields={self.fields!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.fields, + self.loc + ) + def __hash__(self): return id(self) @@ -536,6 +655,13 @@ def __repr__(self): ', value={self.value!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.name, + self.value, + self.loc + ) + def __hash__(self): return id(self) @@ -565,6 +691,13 @@ def __repr__(self): ', arguments={self.arguments!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.name, + self.arguments, + self.loc + ) + def __hash__(self): return id(self) @@ -595,6 +728,12 @@ def __repr__(self): 'name={self.name!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.name, + self.loc + ) + def __hash__(self): return id(self) @@ -621,6 +760,12 @@ def __repr__(self): 'type={self.type!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.type, + self.loc + ) + def __hash__(self): return id(self) @@ -647,6 +792,12 @@ def __repr__(self): 'type={self.type!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.type, + self.loc + ) + def __hash__(self): return id(self) @@ -673,5 +824,375 @@ def __repr__(self): 'value={self.value!r}' ')').format(self=self) + def __copy__(self): + return type(self)( + self.value, + self.loc + ) + + def __hash__(self): + return id(self) + + +class TypeDefinition(Node): + pass + + +class ObjectTypeDefinition(TypeDefinition): + __slots__ = ('loc', 'name', 'interfaces', 'fields',) + _fields = ('name', 'interfaces', 'fields',) + + def __init__(self, name, fields, interfaces=None, loc=None): + self.loc = loc + self.name = name + self.interfaces = interfaces + self.fields = fields + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, ObjectTypeDefinition) and + self.loc == other.loc and + self.name == other.name and + self.interfaces == other.interfaces and + self.fields == other.fields + ) + ) + + def __repr__(self): + return ('ObjectTypeDefinition(' + 'name={self.name!r}' + ', interfaces={self.interfaces!r}' + ', fields={self.fields!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.fields, + self.interfaces, + self.loc + ) + + def __hash__(self): + return id(self) + + +class FieldDefinition(Node): + __slots__ = ('loc', 'name', 'arguments', 'type',) + _fields = ('name', 'arguments', 'type',) + + def __init__(self, name, arguments, type, loc=None): + self.loc = loc + self.name = name + self.arguments = arguments + self.type = type + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, FieldDefinition) and + self.loc == other.loc and + self.name == other.name and + self.arguments == other.arguments and + self.type == other.type + ) + ) + + def __repr__(self): + return ('FieldDefinition(' + 'name={self.name!r}' + ', arguments={self.arguments!r}' + ', type={self.type!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.arguments, + self.type, + self.loc + ) + + def __hash__(self): + return id(self) + + +class InputValueDefinition(Node): + __slots__ = ('loc', 'name', 'type', 'default_value',) + _fields = ('name', 'type', 'default_value',) + + def __init__(self, name, type, default_value=None, loc=None): + self.loc = loc + self.name = name + self.type = type + self.default_value = default_value + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, InputValueDefinition) and + self.loc == other.loc and + self.name == other.name and + self.type == other.type and + self.default_value == other.default_value + ) + ) + + def __repr__(self): + return ('InputValueDefinition(' + 'name={self.name!r}' + ', type={self.type!r}' + ', default_value={self.default_value!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.type, + self.default_value, + self.loc + ) + + def __hash__(self): + return id(self) + + +class InterfaceTypeDefinition(TypeDefinition): + __slots__ = ('loc', 'name', 'fields',) + _fields = ('name', 'fields',) + + def __init__(self, name, fields, loc=None): + self.loc = loc + self.name = name + self.fields = fields + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, InterfaceTypeDefinition) and + self.loc == other.loc and + self.name == other.name and + self.fields == other.fields + ) + ) + + def __repr__(self): + return ('InterfaceTypeDefinition(' + 'name={self.name!r}' + ', fields={self.fields!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.fields, + self.loc + ) + + def __hash__(self): + return id(self) + + +class UnionTypeDefinition(TypeDefinition): + __slots__ = ('loc', 'name', 'types',) + _fields = ('name', 'types',) + + def __init__(self, name, types, loc=None): + self.loc = loc + self.name = name + self.types = types + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, UnionTypeDefinition) and + self.loc == other.loc and + self.name == other.name and + self.types == other.types + ) + ) + + def __repr__(self): + return ('UnionTypeDefinition(' + 'name={self.name!r}' + ', types={self.types!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.types, + self.loc + ) + + def __hash__(self): + return id(self) + + +class ScalarTypeDefinition(TypeDefinition): + __slots__ = ('loc', 'name',) + _fields = ('name',) + + def __init__(self, name, loc=None): + self.loc = loc + self.name = name + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, ScalarTypeDefinition) and + self.loc == other.loc and + self.name == other.name + ) + ) + + def __repr__(self): + return ('ScalarTypeDefinition(' + 'name={self.name!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.loc + ) + + def __hash__(self): + return id(self) + + +class EnumTypeDefinition(TypeDefinition): + __slots__ = ('loc', 'name', 'values',) + _fields = ('name', 'values',) + + def __init__(self, name, values, loc=None): + self.loc = loc + self.name = name + self.values = values + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, EnumTypeDefinition) and + self.loc == other.loc and + self.name == other.name and + self.values == other.values + ) + ) + + def __repr__(self): + return ('EnumTypeDefinition(' + 'name={self.name!r}' + ', values={self.values!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.values, + self.loc + ) + + def __hash__(self): + return id(self) + + +class EnumValueDefinition(Node): + __slots__ = ('loc', 'name',) + _fields = ('name',) + + def __init__(self, name, loc=None): + self.loc = loc + self.name = name + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, EnumValueDefinition) and + self.loc == other.loc and + self.name == other.name + ) + ) + + def __repr__(self): + return ('EnumValueDefinition(' + 'name={self.name!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.loc + ) + + def __hash__(self): + return id(self) + + +class InputObjectTypeDefinition(TypeDefinition): + __slots__ = ('loc', 'name', 'fields',) + _fields = ('name', 'fields',) + + def __init__(self, name, fields, loc=None): + self.loc = loc + self.name = name + self.fields = fields + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, InputObjectTypeDefinition) and + self.loc == other.loc and + self.name == other.name and + self.fields == other.fields + ) + ) + + def __repr__(self): + return ('InputObjectTypeDefinition(' + 'name={self.name!r}' + ', fields={self.fields!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.name, + self.fields, + self.loc + ) + + def __hash__(self): + return id(self) + + +class TypeExtensionDefinition(TypeDefinition): + __slots__ = ('loc', 'definition',) + _fields = ('definition',) + + def __init__(self, definition, loc=None): + self.loc = loc + self.definition = definition + + def __eq__(self, other): + return ( + self is other or ( + isinstance(other, TypeExtensionDefinition) and + self.loc == other.loc and + self.definition == other.definition + ) + ) + + def __repr__(self): + return ('TypeExtensionDefinition(' + 'definition={self.definition!r}' + ')').format(self=self) + + def __copy__(self): + return type(self)( + self.definition, + self.loc + ) + def __hash__(self): return id(self) diff --git a/graphql/core/language/parser.py b/graphql/core/language/parser.py index 33ec70f1..5f124a08 100644 --- a/graphql/core/language/parser.py +++ b/graphql/core/language/parser.py @@ -12,12 +12,26 @@ def parse(source, **kwargs): options = {'no_location': False, 'no_source': False} options.update(kwargs) source_obj = source + if isinstance(source, str_type): source_obj = Source(source) + parser = Parser(source_obj, options) return parse_document(parser) +def parse_value(source, **kwargs): + options = {'no_location': False, 'no_source': False} + options.update(kwargs) + source_obj = source + + if isinstance(source, str_type): + source_obj = Source(source) + + parser = Parser(source_obj, options) + return parse_value_literal(parser, False) + + class Parser(object): __slots__ = 'lexer', 'source', 'options', 'prev_end', 'token' @@ -81,6 +95,7 @@ def skip(parser, kind): match = parser.token.kind == kind if match: advance(parser) + return match @@ -92,6 +107,7 @@ def expect(parser, kind): if token.kind == kind: advance(parser) return token + raise LanguageError( parser.source, token.start, @@ -110,6 +126,7 @@ def expect_keyword(parser, value): if token.kind == TokenKind.NAME and token.value == value: advance(parser) return token + raise LanguageError( parser.source, token.start, @@ -137,6 +154,7 @@ def any(parser, open_kind, parse_fn, close_kind): nodes = [] while not skip(parser, close_kind): nodes.append(parse_fn(parser)) + return nodes @@ -149,6 +167,7 @@ def many(parser, open_kind, parse_fn, close_kind): nodes = [parse_fn(parser)] while not skip(parser, close_kind): nodes.append(parse_fn(parser)) + return nodes @@ -167,27 +186,35 @@ def parse_document(parser): start = parser.token.start definitions = [] while True: - if peek(parser, TokenKind.BRACE_L): - definitions.append(parse_operation_definition(parser)) - elif peek(parser, TokenKind.NAME): - if parser.token.value in ('query', 'mutation'): - definitions.append(parse_operation_definition(parser)) - elif parser.token.value == 'fragment': - definitions.append(parse_fragment_definition(parser)) - else: - raise unexpected(parser) - else: - raise unexpected(parser) + definitions.append(parse_definition(parser)) + if skip(parser, TokenKind.EOF): break + return ast.Document( definitions=definitions, loc=loc(parser, start) ) -# Implements the parsing rules in the Operations section. +def parse_definition(parser): + if peek(parser, TokenKind.BRACE_L): + return parse_operation_definition(parser) + if peek(parser, TokenKind.NAME): + name = parser.token.value + + if name in ('query', 'mutation', 'subscription'): + return parse_operation_definition(parser) + elif name == 'fragment': + return parse_fragment_definition(parser) + elif name in ('type', 'interface', 'union', 'scalar', 'enum', 'input', 'extend'): + return parse_type_definition(parser) + + raise unexpected(parser) + + +# Implements the parsing rules in the Operations section. def parse_operation_definition(parser): start = parser.token.start if peek(parser, TokenKind.BRACE_L): @@ -225,21 +252,17 @@ def parse_variable_definitions(parser): parse_variable_definition, TokenKind.PAREN_R ) + return [] def parse_variable_definition(parser): start = parser.token.start - variable = parse_variable(parser) - type = (expect(parser, TokenKind.COLON), parse_type(parser))[1] - if skip(parser, TokenKind.EQUALS): - default_value = parse_value(parser, True) - else: - default_value = None + return ast.VariableDefinition( - variable=variable, - type=type, - default_value=default_value, + variable=parse_variable(parser), + type=expect(parser, TokenKind.COLON) and parse_type(parser), + default_value=parse_value_literal(parser, True) if skip(parser, TokenKind.EQUALS) else None, loc=loc(parser, start) ) @@ -247,6 +270,7 @@ def parse_variable_definition(parser): def parse_variable(parser): start = parser.token.start expect(parser, TokenKind.DOLLAR) + return ast.Variable( name=parse_name(parser), loc=loc(parser, start) @@ -280,18 +304,12 @@ def parse_field(parser): alias = None name = name_or_alias - arguments = parse_arguments(parser) - directives = parse_directives(parser) - if peek(parser, TokenKind.BRACE_L): - selection_set = parse_selection_set(parser) - else: - selection_set = None return ast.Field( alias=alias, name=name, - arguments=arguments, - directives=directives, - selection_set=selection_set, + arguments=parse_arguments(parser), + directives=parse_directives(parser), + selection_set=parse_selection_set(parser) if peek(parser, TokenKind.BRACE_L) else None, loc=loc(parser, start) ) @@ -301,16 +319,16 @@ def parse_arguments(parser): return many( parser, TokenKind.PAREN_L, parse_argument, TokenKind.PAREN_R) + return [] def parse_argument(parser): start = parser.token.start + return ast.Argument( name=parse_name(parser), - value=( - expect(parser, TokenKind.COLON), - parse_value(parser, False))[1], + value=expect(parser, TokenKind.COLON) and parse_value_literal(parser, False), loc=loc(parser, start) ) @@ -321,6 +339,7 @@ def parse_fragment(parser): # Corresponds to both FragmentSpread and InlineFragment in the spec start = parser.token.start expect(parser, TokenKind.SPREAD) + if peek(parser, TokenKind.NAME) and parser.token.value != 'on': return ast.FragmentSpread( name=parse_fragment_name(parser), @@ -344,11 +363,10 @@ def parse_fragment(parser): def parse_fragment_definition(parser): start = parser.token.start expect_keyword(parser, 'fragment') + return ast.FragmentDefinition( name=parse_fragment_name(parser), - type_condition=( - expect_keyword(parser, 'on'), - parse_named_type(parser))[1], + type_condition=expect_keyword(parser, 'on') and parse_named_type(parser), directives=parse_directives(parser), selection_set=parse_selection_set(parser), loc=loc(parser, start) @@ -362,34 +380,31 @@ def parse_fragment_name(parser): return parse_name(parser) -# Implements the parsing rules in the Values section. -def parse_variable_value(parser): - return parse_value(parser, False) - - -def parse_const_value(parser): - return parse_value(parser, True) - - -def parse_value(parser, is_const): +def parse_value_literal(parser, is_const): token = parser.token if token.kind == TokenKind.BRACKET_L: - return parse_array(parser, is_const) + return parse_list(parser, is_const) + elif token.kind == TokenKind.BRACE_L: return parse_object(parser, is_const) + elif token.kind == TokenKind.INT: advance(parser) return ast.IntValue(value=token.value, loc=loc(parser, token.start)) + elif token.kind == TokenKind.FLOAT: advance(parser) return ast.FloatValue(value=token.value, loc=loc(parser, token.start)) + elif token.kind == TokenKind.STRING: advance(parser) return ast.StringValue(value=token.value, loc=loc(parser, token.start)) + elif token.kind == TokenKind.NAME: if token.value in ('true', 'false'): advance(parser) return ast.BooleanValue(value=token.value == 'true', loc=loc(parser, token.start)) + if token.value != 'null': advance(parser) return ast.EnumValue(value=token.value, loc=loc(parser, token.start)) @@ -397,15 +412,23 @@ def parse_value(parser, is_const): elif token.kind == TokenKind.DOLLAR: if not is_const: return parse_variable(parser) + raise unexpected(parser) -def parse_array(parser, is_const): +# Implements the parsing rules in the Values section. +def parse_variable_value(parser): + return parse_value_literal(parser, False) + + +def parse_const_value(parser): + return parse_value_literal(parser, True) + + +def parse_list(parser, is_const): start = parser.token.start - if is_const: - item = parse_const_value - else: - item = parse_variable_value + item = parse_const_value if is_const else parse_variable_value + return ast.ListValue( values=any( parser, TokenKind.BRACKET_L, @@ -418,8 +441,10 @@ def parse_object(parser, is_const): start = parser.token.start expect(parser, TokenKind.BRACE_L) fields = [] + while not skip(parser, TokenKind.BRACE_R): fields.append(parse_object_field(parser, is_const)) + return ast.ObjectValue(fields=fields, loc=loc(parser, start)) @@ -427,9 +452,7 @@ def parse_object_field(parser, is_const): start = parser.token.start return ast.ObjectField( name=parse_name(parser), - value=( - expect(parser, TokenKind.COLON), - parse_value(parser, is_const))[1], + value=expect(parser, TokenKind.COLON) and parse_value_literal(parser, is_const), loc=loc(parser, start) ) @@ -440,36 +463,38 @@ def parse_directives(parser): directives = [] while peek(parser, TokenKind.AT): directives.append(parse_directive(parser)) + return directives def parse_directive(parser): start = parser.token.start expect(parser, TokenKind.AT) - node = ast.Directive( + + return ast.Directive( name=parse_name(parser), arguments=parse_arguments(parser), loc=loc(parser, start), ) - return node # Implements the parsing rules in the Types section. - def parse_type(parser): """Handles the 'Type': TypeName, ListType, and NonNullType parsing rules.""" start = parser.token.start - type = None if skip(parser, TokenKind.BRACKET_L): - type = parse_type(parser) + ast_type = parse_type(parser) expect(parser, TokenKind.BRACKET_R) - type = ast.ListType(type=type, loc=loc(parser, start)) + ast_type = ast.ListType(type=ast_type, loc=loc(parser, start)) + else: - type = parse_named_type(parser) + ast_type = parse_named_type(parser) + if skip(parser, TokenKind.BANG): - return ast.NonNullType(type=type, loc=loc(parser, start)) - return type + return ast.NonNullType(type=ast_type, loc=loc(parser, start)) + + return ast_type def parse_named_type(parser): @@ -478,3 +503,177 @@ def parse_named_type(parser): name=parse_name(parser), loc=loc(parser, start), ) + + +def parse_type_definition(parser): + if not peek(parser, TokenKind.NAME): + raise unexpected(parser) + + name = parser.token.value + + if name == 'type': + return parse_object_type_definition(parser) + + elif name == 'interface': + return parse_interface_type_definition(parser) + + elif name == 'union': + return parse_union_type_definition(parser) + + elif name == 'scalar': + return parse_scalar_type_definition(parser) + + elif name == 'enum': + return parse_enum_type_definition(parser) + + elif name == 'input': + return parse_input_object_type_definition(parser) + + elif name == 'extend': + return parse_type_extension_definition(parser) + + raise unexpected(parser) + + +def parse_object_type_definition(parser): + start = parser.token.start + expect_keyword(parser, 'type') + return ast.ObjectTypeDefinition( + name=parse_name(parser), + interfaces=parse_implements_interfaces(parser), + fields=any( + parser, + TokenKind.BRACE_L, + parse_field_definition, + TokenKind.BRACE_R + ), + loc=loc(parser, start) + ) + + +def parse_implements_interfaces(parser): + types = [] + if parser.token.value == 'implements': + advance(parser) + + while True: + types.append(parse_named_type(parser)) + + if peek(parser, TokenKind.BRACE_L): + break + + return types + + +def parse_field_definition(parser): + start = parser.token.start + + return ast.FieldDefinition( + name=parse_name(parser), + arguments=parse_argument_defs(parser), + type=expect(parser, TokenKind.COLON) and parse_type(parser), + loc=loc(parser, start) + ) + + +def parse_argument_defs(parser): + if not peek(parser, TokenKind.PAREN_L): + return [] + + return many(parser, TokenKind.PAREN_L, parse_input_value_def, TokenKind.PAREN_R) + + +def parse_input_value_def(parser): + start = parser.token.start + + return ast.InputValueDefinition( + name=parse_name(parser), + type=expect(parser, TokenKind.COLON) and parse_type(parser), + default_value=parse_const_value(parser) if skip(parser, TokenKind.EQUALS) else None, + loc=loc(parser, start) + ) + + +def parse_interface_type_definition(parser): + start = parser.token.start + expect_keyword(parser, 'interface') + + return ast.InterfaceTypeDefinition( + name=parse_name(parser), + fields=any(parser, TokenKind.BRACE_L, parse_field_definition, TokenKind.BRACE_R), + loc=loc(parser, start) + ) + + +def parse_union_type_definition(parser): + start = parser.token.start + expect_keyword(parser, 'union') + + return ast.UnionTypeDefinition( + name=parse_name(parser), + types=expect(parser, TokenKind.EQUALS) and parse_union_members(parser), + loc=loc(parser, start) + ) + + +def parse_union_members(parser): + members = [] + + while True: + members.append(parse_named_type(parser)) + + if not skip(parser, TokenKind.PIPE): + break + + return members + + +def parse_scalar_type_definition(parser): + start = parser.token.start + expect_keyword(parser, 'scalar') + + return ast.ScalarTypeDefinition( + name=parse_name(parser), + loc=loc(parser, start) + ) + + +def parse_enum_type_definition(parser): + start = parser.token.start + expect_keyword(parser, 'enum') + + return ast.EnumTypeDefinition( + name=parse_name(parser), + values=many(parser, TokenKind.BRACE_L, parse_enum_value_definition, TokenKind.BRACE_R), + loc=loc(parser, start) + ) + + +def parse_enum_value_definition(parser): + start = parser.token.start + + return ast.EnumValueDefinition( + name=parse_name(parser), + loc=loc(parser, start) + ) + + +def parse_input_object_type_definition(parser): + start = parser.token.start + expect_keyword(parser, 'input') + + return ast.InputObjectTypeDefinition( + name=parse_name(parser), + fields=any(parser, TokenKind.BRACE_L, parse_input_value_def, TokenKind.BRACE_R), + loc=loc(parser, start) + ) + + +def parse_type_extension_definition(parser): + start = parser.token.start + expect_keyword(parser, 'extend') + + return ast.TypeExtensionDefinition( + definition=parse_object_type_definition(parser), + loc=loc(parser, start) + ) diff --git a/graphql/core/language/printer.py b/graphql/core/language/printer.py index 83a1c1f3..f5ead999 100644 --- a/graphql/core/language/printer.py +++ b/graphql/core/language/printer.py @@ -23,9 +23,11 @@ def leave_OperationDefinition(self, node, *args): selection_set = node.selection_set if not name: return selection_set + op = node.operation defs = wrap('(', join(node.variable_definitions, ', '), ')') directives = join(node.directives, ' ') + return join([op, join([name, defs]), directives, selection_set], ' ') def leave_VariableDefinition(self, node, *args): @@ -104,6 +106,42 @@ def leave_ListType(self, node, *args): def leave_NonNullType(self, node, *args): return node.type + '!' + # Type Definitions: + + def leave_ObjectTypeDefinition(self, node, *args): + return ( + 'type ' + node.name + ' ' + + wrap('implements ', join(node.interfaces, ', '), ' ') + + block(node.fields) + ) + + def leave_FieldDefinition(self, node, *args): + return node.name + wrap('(', join(node.arguments, ', '), ')') + ': ' + node.type + + def leave_InputValueDefinition(self, node, *args): + return node.name + ': ' + node.type + wrap(' = ', node.default_value) + + def leave_InterfaceTypeDefinition(self, node, *args): + return 'interface ' + node.name + ' ' + block(node.fields) + + def leave_UnionTypeDefinition(self, node, *args): + return 'union ' + node.name + ' = ' + join(node.types, ' | ') + + def leave_ScalarTypeDefinition(self, node, *args): + return 'scalar ' + node.name + + def leave_EnumTypeDefinition(self, node, *args): + return 'enum ' + node.name + ' ' + block(node.values) + + def leave_EnumValueDefinition(self, node, *args): + return node.name + + def leave_InputObjectTypeDefinition(self, node, *args): + return 'input ' + node.name + ' ' + block(node.fields) + + def leave_TypeExtensionDefinition(self, node, *args): + return 'extend ' + node.definition + def join(maybe_list, separator=''): if maybe_list: diff --git a/graphql/core/language/visitor.py b/graphql/core/language/visitor.py index fb8e90f3..d699f091 100644 --- a/graphql/core/language/visitor.py +++ b/graphql/core/language/visitor.py @@ -1,42 +1,23 @@ -from collections import namedtuple -from copy import copy, deepcopy +from copy import copy + +import six from . import ast +from .visitor_meta import QUERY_DOCUMENT_KEYS, VisitorMeta -QUERY_DOCUMENT_KEYS = { - ast.Name: (), - - ast.Document: ('definitions', ), - ast.OperationDefinition: ('name', 'variable_definitions', 'directives', 'selection_set'), - ast.VariableDefinition: ('variable', 'type', 'default_value'), - ast.Variable: ('name', ), - ast.SelectionSet: ('selections', ), - ast.Field: ('alias', 'name', 'arguments', 'directives', 'selection_set'), - ast.Argument: ('name', 'value'), - - ast.FragmentSpread: ('name', 'directives'), - ast.InlineFragment: ('type_condition', 'directives', 'selection_set'), - ast.FragmentDefinition: ('name', 'type_condition', 'directives', 'selection_set'), - - ast.IntValue: (), - ast.FloatValue: (), - ast.StringValue: (), - ast.BooleanValue: (), - ast.EnumValue: (), - ast.ListValue: ('values', ), - ast.ObjectValue: ('fields', ), - ast.ObjectField: ('name', 'value'), - - ast.Directive: ('name', 'arguments'), - - ast.NamedType: ('name', ), - ast.ListType: ('type', ), - ast.NonNullType: ('type', ), -} BREAK = object() REMOVE = object() -Stack = namedtuple('Stack', 'in_array index keys edits prev') + +class Stack(object): + __slots__ = 'in_array', 'index', 'keys', 'edits', 'prev' + + def __init__(self, in_array, index, keys, edits, prev): + self.in_array = in_array + self.index = index + self.keys = keys + self.edits = edits + self.prev = prev def visit(root, visitor, key_map=None): @@ -51,90 +32,111 @@ def visit(root, visitor, key_map=None): path = [] ancestors = [] new_root = root + leave = visitor.leave + enter = visitor.enter + path_pop = path.pop + ancestors_pop = ancestors.pop while True: index += 1 is_leaving = index == len(keys) - key = None - node = None - is_edited = is_leaving and len(edits) != 0 + is_edited = is_leaving and edits if is_leaving: - key = path.pop() if len(ancestors) != 0 else None + key = path_pop() if ancestors else None node = parent - parent = ancestors.pop() if len(ancestors) != 0 else None + parent = ancestors_pop() if ancestors else None + if is_edited: if in_array: node = list(node) + else: node = copy(node) + edit_offset = 0 for edit_key, edit_value in edits: if in_array: edit_key -= edit_offset + if in_array and edit_value is REMOVE: node.pop(edit_key) edit_offset += 1 + else: if isinstance(node, list): node[edit_key] = edit_value + else: - node = deepcopy(node) setattr(node, edit_key, edit_value) + index = stack.index keys = stack.keys edits = stack.edits in_array = stack.in_array stack = stack.prev + else: if parent: key = index if in_array else keys[index] if isinstance(parent, list): node = parent[key] + else: node = getattr(parent, key, None) + else: key = None node = new_root + if node is None: continue + if parent: path.append(key) result = None + if not isinstance(node, list): assert isinstance(node, ast.Node), 'Invalid AST Node: ' + repr(node) + if is_leaving: - result = visitor.leave(node, key, parent, path, ancestors) + result = leave(node, key, parent, path, ancestors) + else: - result = visitor.enter(node, key, parent, path, ancestors) + result = enter(node, key, parent, path, ancestors) + if result is BREAK: break if result is False: if not is_leaving: - path.pop() + path_pop() continue + elif result is not None: edits.append((key, result)) if not is_leaving: if result is not REMOVE: # TODO: check result is valid node node = result + else: - path.pop() + path_pop() continue if result is None and is_edited: edits.append((key, node)) if not is_leaving: - stack = Stack(in_array, index, keys, edits, prev=stack) + stack = Stack(in_array, index, keys, edits, stack) in_array = isinstance(node, list) - keys = node if in_array else visitor_keys.get(type(node), []) + keys = node if in_array else visitor_keys.get(type(node), None) or [] index = -1 edits = [] + if parent: ancestors.append(parent) + parent = node if not stack: @@ -146,16 +148,14 @@ def visit(root, visitor, key_map=None): return new_root +@six.add_metaclass(VisitorMeta) class Visitor(object): def enter(self, node, key, parent, path, ancestors): - return self._call_kind_specific_visitor('enter_', node, key, parent, path, ancestors) + method = self._get_enter_handler(type(node)) + if method: + return method(self, node, key, parent, path, ancestors) def leave(self, node, key, parent, path, ancestors): - return self._call_kind_specific_visitor('leave_', node, key, parent, path, ancestors) - - def _call_kind_specific_visitor(self, prefix, node, key, parent, path, ancestors): - node_kind = type(node).__name__ - method_name = prefix + node_kind - method = getattr(self, method_name, None) + method = self._get_leave_handler(type(node)) if method: - return method(node, key, parent, path, ancestors) + return method(self, node, key, parent, path, ancestors) diff --git a/graphql/core/language/visitor_meta.py b/graphql/core/language/visitor_meta.py new file mode 100644 index 00000000..c87c02c2 --- /dev/null +++ b/graphql/core/language/visitor_meta.py @@ -0,0 +1,72 @@ +from . import ast + +QUERY_DOCUMENT_KEYS = { + ast.Name: (), + + ast.Document: ('definitions',), + ast.OperationDefinition: ('name', 'variable_definitions', 'directives', 'selection_set'), + ast.VariableDefinition: ('variable', 'type', 'default_value'), + ast.Variable: ('name',), + ast.SelectionSet: ('selections',), + ast.Field: ('alias', 'name', 'arguments', 'directives', 'selection_set'), + ast.Argument: ('name', 'value'), + + ast.FragmentSpread: ('name', 'directives'), + ast.InlineFragment: ('type_condition', 'directives', 'selection_set'), + ast.FragmentDefinition: ('name', 'type_condition', 'directives', 'selection_set'), + + ast.IntValue: (), + ast.FloatValue: (), + ast.StringValue: (), + ast.BooleanValue: (), + ast.EnumValue: (), + ast.ListValue: ('values',), + ast.ObjectValue: ('fields',), + ast.ObjectField: ('name', 'value'), + + ast.Directive: ('name', 'arguments'), + + ast.NamedType: ('name',), + ast.ListType: ('type',), + ast.NonNullType: ('type',), + + ast.ObjectTypeDefinition: ('name', 'interfaces', 'fields'), + ast.FieldDefinition: ('name', 'arguments', 'type'), + ast.InputValueDefinition: ('name', 'type', 'default_value'), + ast.InterfaceTypeDefinition: ('name', 'fields'), + ast.UnionTypeDefinition: ('name', 'types'), + ast.ScalarTypeDefinition: ('name',), + ast.EnumTypeDefinition: ('name', 'values'), + ast.EnumValueDefinition: ('name',), + ast.InputObjectTypeDefinition: ('name', 'fields'), + ast.TypeExtensionDefinition: ('definition',), +} + +AST_KIND_TO_TYPE = {c.__name__: c for c in QUERY_DOCUMENT_KEYS.keys()} + + +class VisitorMeta(type): + def __new__(cls, name, bases, attrs): + enter_handlers = {} + leave_handlers = {} + + for base in bases: + if hasattr(base, '_enter_handlers'): + enter_handlers.update(base._enter_handlers) + if hasattr(base, '_leave_handlers'): + leave_handlers.update(base._leave_handlers) + + for attr, val in attrs.items(): + if attr.startswith('enter_'): + ast_kind = attr[6:] + ast_type = AST_KIND_TO_TYPE.get(ast_kind) + enter_handlers[ast_type] = val + + elif attr.startswith('leave_'): + ast_kind = attr[6:] + ast_type = AST_KIND_TO_TYPE.get(ast_kind) + leave_handlers[ast_type] = val + + attrs['_get_enter_handler'] = enter_handlers.get + attrs['_get_leave_handler'] = leave_handlers.get + return super(VisitorMeta, cls).__new__(cls, name, bases, attrs) diff --git a/graphql/core/utils/ast_to_code.py b/graphql/core/utils/ast_to_code.py new file mode 100644 index 00000000..8923fc40 --- /dev/null +++ b/graphql/core/utils/ast_to_code.py @@ -0,0 +1,47 @@ +from ..language.ast import Node +from ..language.parser import Loc + + +def ast_to_code(ast, indent=0): + """ + Converts an ast into a python code representation of the AST. + """ + code = [] + append = lambda line: code.append((' ' * indent) + line) + + if isinstance(ast, Node): + append('ast.{}('.format(ast.__class__.__name__)) + indent += 1 + for i, k in enumerate(ast._fields, 1): + v = getattr(ast, k) + append('{}={},'.format( + k, + ast_to_code(v, indent), + )) + if ast.loc: + append('loc={}'.format(ast_to_code(ast.loc, indent))) + + indent -= 1 + append(')') + + elif isinstance(ast, Loc): + append('loc({}, {})'.format(ast.start, ast.end)) + + elif isinstance(ast, list): + if ast: + append('[') + indent += 1 + + for i, it in enumerate(ast, 1): + is_last = i == len(ast) + append(ast_to_code(it, indent) + (',' if not is_last else '')) + + indent -= 1 + append(']') + else: + append('[]') + + else: + append(repr(ast)) + + return '\n'.join(code).strip() diff --git a/graphql/core/utils/type_info.py b/graphql/core/utils/type_info.py index 828a99b7..bb852641 100644 --- a/graphql/core/utils/type_info.py +++ b/graphql/core/utils/type_info.py @@ -1,4 +1,6 @@ -from ..language import ast +import six + +from ..language import visitor_meta from ..type.definition import ( GraphQLInputObjectType, GraphQLList, @@ -16,6 +18,8 @@ def pop(lst): lst.pop() +# noinspection PyPep8Naming +@six.add_metaclass(visitor_meta.VisitorMeta) class TypeInfo(object): def __init__(self, schema): self._schema = schema @@ -48,80 +52,105 @@ def get_directive(self): def get_argument(self): return self._argument + def leave(self, node): + method = self._get_leave_handler(type(node)) + if method: + return method(self) + def enter(self, node): - schema = self._schema - type = None - if isinstance(node, ast.SelectionSet): - named_type = get_named_type(self.get_type()) - composite_type = None - if is_composite_type(named_type): - composite_type = named_type - self._parent_type_stack.append(composite_type) - elif isinstance(node, ast.Field): - parent_type = self.get_parent_type() - field_def = None - if parent_type: - field_def = get_field_def(schema, parent_type, node) - self._field_def_stack.append(field_def) - self._type_stack.append(field_def and field_def.type) - elif isinstance(node, ast.Directive): - self._directive = schema.get_directive(node.name.value) - elif isinstance(node, ast.OperationDefinition): - if node.operation == 'query': - type = schema.get_query_type() - elif node.operation == 'mutation': - type = schema.get_mutation_type() - self._type_stack.append(type) - elif isinstance(node, (ast.InlineFragment, ast.FragmentDefinition)): - type_condition_ast = node.type_condition - type = type_from_ast(schema, type_condition_ast) if type_condition_ast else self.get_type() - self._type_stack.append(type) - elif isinstance(node, ast.VariableDefinition): - self._input_type_stack.append(type_from_ast(schema, node.type)) - elif isinstance(node, ast.Argument): - arg_def = None - arg_type = None - field_or_directive = self.get_directive() or self.get_field_def() - if field_or_directive: - arg_def = [arg for arg in field_or_directive.args if arg.name == node.name.value] - if arg_def: - arg_def = arg_def[0] - arg_type = arg_def.type - else: - arg_def = None - self._argument = arg_def - self._input_type_stack.append(arg_type) - elif isinstance(node, ast.ListValue): - list_type = get_nullable_type(self.get_input_type()) - self._input_type_stack.append( - list_type.of_type if isinstance(list_type, GraphQLList) else None - ) - elif isinstance(node, ast.ObjectField): - object_type = get_named_type(self.get_input_type()) - field_type = None - if isinstance(object_type, GraphQLInputObjectType): - input_field = object_type.get_fields().get(node.name.value) - field_type = input_field.type if input_field else None - self._input_type_stack.append(field_type) + method = self._get_enter_handler(type(node)) + if method: + return method(self, node) - def leave(self, node): - if isinstance(node, ast.SelectionSet): - pop(self._parent_type_stack) - elif isinstance(node, ast.Field): - pop(self._field_def_stack) - pop(self._type_stack) - elif isinstance(node, ast.Directive): - self._directive = None - elif isinstance(node, ( - ast.OperationDefinition, - ast.InlineFragment, - ast.FragmentDefinition, - )): - pop(self._type_stack) - elif isinstance(node, ast.VariableDefinition): - pop(self._input_type_stack) - elif isinstance(node, ast.Argument): - self._argument = None - pop(self._input_type_stack) - elif isinstance(node, (ast.ListType, ast.ObjectField)): - pop(self._input_type_stack) + def enter_SelectionSet(self, node): + named_type = get_named_type(self.get_type()) + composite_type = None + if is_composite_type(named_type): + composite_type = named_type + self._parent_type_stack.append(composite_type) + + def enter_Field(self, node): + parent_type = self.get_parent_type() + field_def = None + if parent_type: + field_def = get_field_def(self._schema, parent_type, node) + self._field_def_stack.append(field_def) + self._type_stack.append(field_def and field_def.type) + + def enter_Directive(self, node): + self._directive = self._schema.get_directive(node.name.value) + + def enter_OperationDefinition(self, node): + definition_type = None + if node.operation == 'query': + definition_type = self._schema.get_query_type() + elif node.operation == 'mutation': + definition_type = self._schema.get_mutation_type() + + self._type_stack.append(definition_type) + + def enter_InlineFragment(self, node): + type_condition_ast = node.type_condition + type = type_from_ast(self._schema, type_condition_ast) if type_condition_ast else self.get_type() + self._type_stack.append(type) + + enter_FragmentDefinition = enter_InlineFragment + + def enter_VariableDefinition(self, node): + self._input_type_stack.append(type_from_ast(self._schema, node.type)) + + def enter_Argument(self, node): + arg_def = None + arg_type = None + field_or_directive = self.get_directive() or self.get_field_def() + if field_or_directive: + arg_def = [arg for arg in field_or_directive.args if arg.name == node.name.value] + if arg_def: + arg_def = arg_def[0] + arg_type = arg_def.type + else: + arg_def = None + self._argument = arg_def + self._input_type_stack.append(arg_type) + + def enter_ListValue(self, node): + list_type = get_nullable_type(self.get_input_type()) + self._input_type_stack.append( + list_type.of_type if isinstance(list_type, GraphQLList) else None + ) + + def enter_ObjectField(self, node): + object_type = get_named_type(self.get_input_type()) + field_type = None + if isinstance(object_type, GraphQLInputObjectType): + input_field = object_type.get_fields().get(node.name.value) + field_type = input_field.type if input_field else None + self._input_type_stack.append(field_type) + + def leave_SelectionSet(self): + pop(self._parent_type_stack) + + def leave_Field(self): + pop(self._field_def_stack) + pop(self._type_stack) + + def leave_Directive(self): + self._directive = None + + def leave_OperationDefinition(self): + pop(self._type_stack) + + leave_InlineFragment = leave_OperationDefinition + leave_FragmentDefinition = leave_OperationDefinition + + def leave_VariableDefinition(self): + pop(self._input_type_stack) + + def leave_Argument(self): + self._argument = None + pop(self._input_type_stack) + + def leave_ListType(self): + pop(self._input_type_stack) + + leave_ObjectField = leave_ListType diff --git a/scripts/ast.ast b/scripts/ast.ast new file mode 100644 index 00000000..5d9caba6 --- /dev/null +++ b/scripts/ast.ast @@ -0,0 +1,176 @@ +# Mini-language for AST definition. +# All AST nodes extend AstNode. +# All AST nodes are visitible. +# All AST fields have a getter and a setter and are a constructor argument. +# We have concrete types (code T) and unions (code U). +# S for singular field, P for plural, ? for nullable. +# O for option in a union. +# Scalar type ontology: string, boolean + +# Location tracking for Identifier is probably useful for error messages. +U Definition +O OperationDefinition +O FragmentDefinition + +T Document +P Definition definitions + +T OperationDefinition +S OperationKind operation +S? Name name +P? VariableDefinition variableDefinitions +P? Directive directives +S SelectionSet selectionSet + +T VariableDefinition +S Variable variable +S Type type +S? Value defaultValue + +T SelectionSet +P Selection selections + +U Selection +O Field +O FragmentSpread +O InlineFragment + +T Field +S? Name alias +S Name name +P? Argument arguments +P? Directive directives +S? SelectionSet selectionSet + +T Argument +S Name name +S Value value + +T FragmentSpread +S Name name +P? Directive directives + +T InlineFragment +S NamedType typeCondition +P? Directive directives +S SelectionSet selectionSet + +T FragmentDefinition +S Name name +S NamedType typeCondition +P? Directive directives +S SelectionSet selectionSet + +U Value +O Variable +O IntValue +O FloatValue +O StringValue +O BooleanValue +O EnumValue +O ArrayValue +O ObjectValue + +T Variable +S Name name + +T IntValue +S string value + +T FloatValue +S string value + +T StringValue +S string value + +T BooleanValue +S boolean value + +T EnumValue +S string value + +T ArrayValue +P Value values + +T ObjectValue +P ObjectField fields + +T ObjectField +S Name name +S Value value + +T Directive +S Name name +P? Argument arguments + +U Type +O NamedType +O ListType +O NonNullType + +T NamedType +S Name name + +T ListType +S Type type + +T NonNullType +# JS version prohibits nesting nonnull in nonnull, we can't because we +# can't support multiple unions. Fix? +S Type type + +T Name +S string value + +# Implementation of Schema Parser AST Types +# This is not included in libgraphqlparser. +# Generated from https://github.com/graphql/graphql-js/blob/master/src/language/ast.js @ 3f1f9f5 + +U TypeDefinition +O ObjectTypeDefinition +O InterfaceTypeDefinition +O UnionTypeDefinition +O ScalarTypeDefinition +O EnumTypeDefinition +O InputObjectTypeDefinition +O TypeExtensionDefinition + +T ObjectTypeDefinition +S Name name +P? NamedType interfaces +P FieldDefinition fields + +T FieldDefinition +S Name name +P InputValueDefinition arguments +S Type type + +T InputValueDefinition +S Name name +S Type type +S? Value defaultValue + +T InterfaceTypeDefinition +S Name name +P FieldDefinition fields + +T UnionTypeDefinition +S Name name +P NamedType types + +T ScalarTypeDefinition +S Name name + +T EnumTypeDefinition +S Name name +P EnumValueDefinition values + +T EnumValueDefinition +S Name name + +T InputObjectTypeDefinition +S Name name +P InputValueDefinition fields + +T TypeExtensionDefinition +S ObjectTypeDefinition definition \ No newline at end of file diff --git a/scripts/generate_ast.py b/scripts/generate_ast.py index f4c6c2b2..9a791ede 100644 --- a/scripts/generate_ast.py +++ b/scripts/generate_ast.py @@ -5,7 +5,7 @@ project_root = os.path.join(os.path.dirname(__file__), '..') with open(os.path.join(project_root, 'graphql/core/language/ast.py'), 'w') as fp: process = subprocess.Popen( - ['python', '../libgraphqlparser/ast/ast.py', 'generate_ast', '../libgraphqlparser/ast/ast.ast'], + ['python', '../libgraphqlparser/ast/ast.py', 'generate_ast', './ast.ast'], stdout=fp, cwd=os.path.join(project_root, 'scripts'), env={'PYTHONPATH': '.'} @@ -59,6 +59,7 @@ def end_type(self, typename): self._print_ctor() self._print_comparator(typename) self._print_repr(typename) + self._print_copy(typename) self._print_hash() self._fields = [] @@ -95,6 +96,18 @@ def __eq__(self, other): print(' )') print(' )') + def _print_copy(self, typename): + fields = ( + [field for field in self._fields if not field[2]] + + [field for field in self._fields if field[2]]) + args = '\n'.join(''' self.{},'''.format(snake(name)) for (type, name, nullable, plural) in fields) + print (''' + def __copy__(self): + return type(self)( +{} + self.loc + )'''.format(args)) + def _print_repr(self, typename): print(''' def __repr__(self): diff --git a/setup.py b/setup.py index 09dd7cb3..200fea68 100644 --- a/setup.py +++ b/setup.py @@ -52,8 +52,8 @@ def run_tests(self): packages=find_packages(exclude=['tests']), - install_requires=[], - tests_require=['pytest>=2.7.3', 'gevent==1.1b5'], + install_requires=['six>=1.10.0'], + tests_require=['pytest>=2.7.3', 'gevent==1.1b5', 'six>=1.10.0'], cmdclass={'test': PyTest}, ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/core_execution/test_executor.py b/tests/core_execution/test_executor.py index 7d082c3f..7ed7ed6f 100644 --- a/tests/core_execution/test_executor.py +++ b/tests/core_execution/test_executor.py @@ -365,7 +365,7 @@ def test_does_not_include_arguments_that_were_not_set(): { 'field': GraphQLField( GraphQLString, - resolver=lambda data, args, *_: args and json.dumps(args, sort_keys=True, separators=(',',':')), + resolver=lambda data, args, *_: args and json.dumps(args, sort_keys=True, separators=(',', ':')), args={ 'a': GraphQLArgument(GraphQLBoolean), 'b': GraphQLArgument(GraphQLBoolean), @@ -382,3 +382,71 @@ def test_does_not_include_arguments_that_were_not_set(): assert result.data == { 'field': '{"a":true,"c":false,"e":0}' } + + +def test_fails_when_an_is_type_of_check_is_not_met(): + class Special(object): + def __init__(self, value): + self.value = value + + class NotSpecial(object): + def __init__(self, value): + self.value = value + + SpecialType = GraphQLObjectType( + 'SpecialType', + fields={ + 'value': GraphQLField(GraphQLString), + }, + is_type_of=lambda obj, info: isinstance(obj, Special) + ) + + schema = GraphQLSchema( + GraphQLObjectType( + name='Query', + fields={ + 'specials': GraphQLField( + GraphQLList(SpecialType), + resolver=lambda root, *_: root['specials'] + ) + } + ) + ) + + query = parse('{ specials { value } }') + value = { + 'specials': [Special('foo'), NotSpecial('bar')] + } + + result = execute(schema, value, query) + + assert result.data == { + 'specials': [ + {'value': 'foo'}, + None + ] + } + + assert 'Expected value of type "SpecialType" but got NotSpecial.' in str(result.errors) + + +def test_fails_to_execute_a_query_containing_a_type_definition(): + query = parse(''' + { foo } + + type Query { foo: String } + ''') + + schema = GraphQLSchema( + GraphQLObjectType( + name='Query', + fields={ + 'foo': GraphQLField(GraphQLString) + } + ) + ) + + with raises(GraphQLError) as excinfo: + result = execute(schema, None, query) + + assert excinfo.value.message == 'GraphQL cannot execute a request containing a ObjectTypeDefinition.' diff --git a/tests/core_language/__init__.py b/tests/core_language/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/core_language/fixtures.py b/tests/core_language/fixtures.py index 6112faba..629ede53 100644 --- a/tests/core_language/fixtures.py +++ b/tests/core_language/fixtures.py @@ -44,3 +44,45 @@ query } """ + +SCHEMA_KITCHEN_SINK = """ + +# Copyright (c) 2015, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +type Foo implements Bar { + one: Type + two(argument: InputType!): Type + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = {key: "value"}): Type +} + +interface Bar { + one: Type + four(argument: String = "string"): String +} + +union Feed = Story | Article | Advert + +scalar CustomScalar + +enum Site { + DESKTOP + MOBILE +} + +input InputType { + key: String! + answer: Int = 42 +} + +extend type Foo { + seven(argument: [String]): Type +} +""" \ No newline at end of file diff --git a/tests/core_language/test_parser.py b/tests/core_language/test_parser.py index 55b4d0db..d649aa89 100644 --- a/tests/core_language/test_parser.py +++ b/tests/core_language/test_parser.py @@ -4,7 +4,7 @@ from graphql.core.language.source import Source from graphql.core.language.parser import parse, Loc from graphql.core.language import ast -from fixtures import KITCHEN_SINK +from .fixtures import KITCHEN_SINK def test_parse_provides_useful_errors(): diff --git a/tests/core_language/test_printer.py b/tests/core_language/test_printer.py index 93665465..6419bf52 100644 --- a/tests/core_language/test_printer.py +++ b/tests/core_language/test_printer.py @@ -3,7 +3,7 @@ from graphql.core.language.parser import parse from graphql.core.language.printer import print_ast from pytest import raises -from fixtures import KITCHEN_SINK +from .fixtures import KITCHEN_SINK def test_does_not_alter_ast(): diff --git a/tests/core_language/test_schema_parser.py b/tests/core_language/test_schema_parser.py new file mode 100644 index 00000000..eb002b56 --- /dev/null +++ b/tests/core_language/test_schema_parser.py @@ -0,0 +1,694 @@ +from pytest import raises +from graphql.core import Source, parse +from graphql.core.language import ast +from graphql.core.language.error import LanguageError +from graphql.core.language.parser import Loc + + +def create_loc_fn(body): + source = Source(body) + return lambda start, end: Loc(start, end, source) + + +def test_parses_simple_type(): + body = ''' +type Hello { + world: String +}''' + + doc = parse(body) + loc = create_loc_fn(body) + + expected = ast.Document( + definitions=[ + ast.ObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(6, 11) + ), + interfaces=[], + fields=[ + ast.FieldDefinition( + name=ast.Name( + value='world', + loc=loc(16, 21) + ), + arguments=[], + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(23, 29) + ), + loc=loc(23, 29) + ), + loc=loc(16, 29) + ) + ], + loc=loc(1, 31) + ) + ], + loc=loc(1, 31) + ) + + assert doc == expected + + +def test_parses_simple_extension(): + body = ''' +extend type Hello { + world: String +}''' + doc = parse(body) + loc = create_loc_fn(body) + + expected = ast.Document( + definitions=[ + ast.TypeExtensionDefinition( + definition=ast.ObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(13, 18) + ), + interfaces=[], + fields=[ + ast.FieldDefinition( + name=ast.Name( + value='world', + loc=loc(23, 28) + ), + arguments=[], + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(30, 36) + ), + loc=loc(30, 36) + ), + loc=loc(23, 36) + ) + ], + loc=loc(8, 38) + ), + loc=loc(1, 38) + ) + ], + loc=loc(1, 38) + ) + + assert doc == expected + + +def test_simple_non_null_type(): + body = ''' +type Hello { + world: String! +}''' + + doc = parse(body) + loc = create_loc_fn(body) + expected = ast.Document( + definitions=[ + ast.ObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(6, 11) + ), + interfaces=[], + fields=[ + ast.FieldDefinition( + name=ast.Name( + value='world', + loc=loc(16, 21) + ), + arguments=[], + type=ast.NonNullType( + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(23, 29) + ), + loc=loc(23, 29) + ), + loc=loc(23, 30) + ), + loc=loc(16, 30) + ) + ], + loc=loc(1, 32) + ) + ], + loc=loc(1, 32) + ) + assert doc == expected + + +def test_parses_simple_type_inheriting_interface(): + body = 'type Hello implements World { }' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.ObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(5, 10) + ), + interfaces=[ + ast.NamedType( + name=ast.Name( + value='World', + loc=loc(22, 27) + ), + loc=loc(22, 27) + ) + ], + fields=[], + loc=loc(0, 31) + ) + ], + loc=loc(0, 31) + ) + + assert doc == expected + + +def test_parses_simple_type_inheriting_multiple_interfaces(): + body = 'type Hello implements Wo, rld { }' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.ObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(5, 10) + ), + interfaces=[ + ast.NamedType( + name=ast.Name( + value='Wo', + loc=loc(22, 24) + ), + loc=loc(22, 24) + ), + ast.NamedType( + name=ast.Name( + value='rld', + loc=loc(26, 29) + ), + loc=loc(26, 29) + ) + ], + fields=[], + loc=loc(0, 33) + ) + ], + loc=loc(0, 33) + ) + assert doc == expected + + +def test_parses_single_value_enum(): + body = 'enum Hello { WORLD }' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.EnumTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(5, 10) + ), + values=[ + ast.EnumValueDefinition( + name=ast.Name( + value='WORLD', + loc=loc(13, 18) + ), + loc=loc(13, 18) + ) + ], + loc=loc(0, 20) + ) + ], + loc=loc(0, 20) + ) + + assert doc == expected + + +def test_parses_double_value_enum(): + body = 'enum Hello { WO, RLD }' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.EnumTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(5, 10) + ), + values=[ + ast.EnumValueDefinition( + name=ast.Name( + value='WO', + loc=loc(13, 15) + ), + loc=loc(13, 15) + ), + ast.EnumValueDefinition( + name=ast.Name( + value='RLD', + loc=loc(17, 20) + ), + loc=loc(17, 20) + ) + ], + loc=loc(0, 22) + ) + ], + loc=loc(0, 22) + ) + + assert doc == expected + + +def test_parses_simple_interface(): + body = ''' +interface Hello { + world: String +} +''' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.InterfaceTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(11, 16) + ), + fields=[ + ast.FieldDefinition( + name=ast.Name( + value='world', + loc=loc(21, 26) + ), + arguments=[], + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(28, 34) + ), + loc=loc(28, 34) + ), + loc=loc(21, 34) + ) + ], + loc=loc(1, 36) + ) + ], + loc=loc(1, 37) + ) + + assert doc == expected + + +def test_parses_simple_field_with_arg(): + body = ''' +type Hello { + world(flag: Boolean): String +}''' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.ObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(6, 11) + ), + interfaces=[], + fields=[ + ast.FieldDefinition( + name=ast.Name( + value='world', + loc=loc(16, 21) + ), + arguments=[ + ast.InputValueDefinition( + name=ast.Name( + value='flag', + loc=loc(22, 26) + ), + type=ast.NamedType( + name=ast.Name( + value='Boolean', + loc=loc(28, 35) + ), + loc=loc(28, 35) + ), + default_value=None, + loc=loc(22, 35) + ) + ], + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(38, 44) + ), + loc=loc(38, 44) + ), + loc=loc(16, 44) + ) + ], + loc=loc(1, 46) + ) + ], + loc=loc(1, 46) + ) + + assert doc == expected + + +def test_parses_simple_field_with_arg_with_default_value(): + body = ''' +type Hello { + world(flag: Boolean = true): String +}''' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.ObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(6, 11) + ), + interfaces=[], + fields=[ + ast.FieldDefinition( + name=ast.Name( + value='world', + loc=loc(16, 21) + ), + arguments=[ + ast.InputValueDefinition( + name=ast.Name( + value='flag', + loc=loc(22, 26) + ), + type=ast.NamedType( + name=ast.Name( + value='Boolean', + loc=loc(28, 35) + ), + loc=loc(28, 35) + ), + default_value=ast.BooleanValue( + value=True, + loc=loc(38, 42) + ), + loc=loc(22, 42) + ) + ], + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(45, 51) + ), + loc=loc(45, 51) + ), + loc=loc(16, 51) + ) + ], + loc=loc(1, 53) + ) + ], + loc=loc(1, 53) + ) + + assert doc == expected + + +def test_parses_simple_field_with_list_arg(): + body = ''' +type Hello { + world(things: [String]): String +}''' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.ObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(6, 11) + ), + interfaces=[], + fields=[ + ast.FieldDefinition( + name=ast.Name( + value='world', + loc=loc(16, 21) + ), + arguments=[ + ast.InputValueDefinition( + name=ast.Name( + value='things', + loc=loc(22, 28) + ), + type=ast.ListType( + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(31, 37) + ), + loc=loc(31, 37) + ), + loc=loc(30, 38) + ), + default_value=None, + loc=loc(22, 38) + ) + ], + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(41, 47) + ), + loc=loc(41, 47) + ), + loc=loc(16, 47) + ) + ], + loc=loc(1, 49) + ) + ], + loc=loc(1, 49) + ) + assert doc == expected + + +def test_parses_simple_field_with_two_args(): + body = ''' +type Hello { + world(argOne: Boolean, argTwo: Int): String +}''' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.ObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(6, 11) + ), + interfaces=[], + fields=[ + ast.FieldDefinition( + name=ast.Name( + value='world', + loc=loc(16, 21) + ), + arguments=[ + ast.InputValueDefinition( + name=ast.Name( + value='argOne', + loc=loc(22, 28) + ), + type=ast.NamedType( + name=ast.Name( + value='Boolean', + loc=loc(30, 37) + ), + loc=loc(30, 37) + ), + default_value=None, + loc=loc(22, 37) + ), + ast.InputValueDefinition( + name=ast.Name( + value='argTwo', + loc=loc(39, 45) + ), + type=ast.NamedType( + name=ast.Name( + value='Int', + loc=loc(47, 50) + ), + loc=loc(47, 50) + ), + default_value=None, + loc=loc(39, 50) + ) + ], + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(53, 59) + ), + loc=loc(53, 59) + ), + loc=loc(16, 59) + ) + ], + loc=loc(1, 61) + ) + ], + loc=loc(1, 61) + ) + assert doc == expected + + +def test_parses_simple_union(): + body = 'union Hello = World' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.UnionTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(6, 11) + ), + types=[ + ast.NamedType( + name=ast.Name( + value='World', + loc=loc(14, 19) + ), + loc=loc(14, 19) + ) + ], + loc=loc(0, 19) + ) + ], + loc=loc(0, 19) + ) + assert doc == expected + + +def test_parses_union_with_two_types(): + body = 'union Hello = Wo | Rld' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.UnionTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(6, 11) + ), + types=[ + ast.NamedType( + name=ast.Name( + value='Wo', + loc=loc(14, 16) + ), + loc=loc(14, 16) + ), + ast.NamedType( + name=ast.Name( + value='Rld', + loc=loc(19, 22) + ), + loc=loc(19, 22) + ) + ], + loc=loc(0, 22) + ) + ], + loc=loc(0, 22) + ) + assert doc == expected + + +def test_parses_scalar(): + body = 'scalar Hello' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.ScalarTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(7, 12) + ), + loc=loc(0, 12) + ) + ], + loc=loc(0, 12) + ) + assert doc == expected + + +def test_parses_simple_input_object(): + body = ''' +input Hello { + world: String +}''' + loc = create_loc_fn(body) + doc = parse(body) + expected = ast.Document( + definitions=[ + ast.InputObjectTypeDefinition( + name=ast.Name( + value='Hello', + loc=loc(7, 12) + ), + fields=[ + ast.InputValueDefinition( + name=ast.Name( + value='world', + loc=loc(17, 22) + ), + type=ast.NamedType( + name=ast.Name( + value='String', + loc=loc(24, 30) + ), + loc=loc(24, 30) + ), + default_value=None, + loc=loc(17, 30) + ) + ], + loc=loc(1, 32) + ) + ], + loc=loc(1, 32) + ) + assert doc == expected + + +def test_parsing_simple_input_object_with_args_should_fail(): + body = ''' +input Hello { + world(foo: Int): String +} +''' + with raises(LanguageError) as excinfo: + parse(body) + + assert 'Syntax Error GraphQL (3:8) Expected :, found (' in excinfo.value.message diff --git a/tests/core_language/test_schema_printer.py b/tests/core_language/test_schema_printer.py new file mode 100644 index 00000000..4be5af20 --- /dev/null +++ b/tests/core_language/test_schema_printer.py @@ -0,0 +1,69 @@ +from copy import deepcopy +from graphql.core import parse +from pytest import raises +from graphql.core.language.printer import print_ast +from graphql.core.language import ast +from .fixtures import SCHEMA_KITCHEN_SINK + + +def test_prints_minimal_ast(): + node = ast.ScalarTypeDefinition( + name=ast.Name('foo') + ) + + assert print_ast(node) == 'scalar foo' + + +def test_print_produces_helpful_error_messages(): + bad_ast = {'random': 'Data'} + with raises(AssertionError) as excinfo: + print_ast(bad_ast) + + assert "Invalid AST Node: {'random': 'Data'}" in str(excinfo.value) + + +def test_does_not_alter_ast(): + ast = parse(SCHEMA_KITCHEN_SINK) + ast_copy = deepcopy(ast) + print_ast(ast) + assert ast == ast_copy + + +def test_prints_kitchen_sink(): + ast = parse(SCHEMA_KITCHEN_SINK) + printed = print_ast(ast) + + expected = '''type Foo implements Bar { + one: Type + two(argument: InputType!): Type + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = {key: "value"}): Type +} + +interface Bar { + one: Type + four(argument: String = "string"): String +} + +union Feed = Story | Article | Advert + +scalar CustomScalar + +enum Site { + DESKTOP + MOBILE +} + +input InputType { + key: String! + answer: Int = 42 +} + +extend type Foo { + seven(argument: [String]): Type +} +''' + + assert printed == expected diff --git a/tests/core_language/test_visitor.py b/tests/core_language/test_visitor.py index 2d0a8061..a57fd0da 100644 --- a/tests/core_language/test_visitor.py +++ b/tests/core_language/test_visitor.py @@ -1,7 +1,7 @@ from graphql.core.language.ast import Field, Name, SelectionSet from graphql.core.language.parser import parse from graphql.core.language.visitor import visit, Visitor, REMOVE, BREAK -from fixtures import KITCHEN_SINK +from .fixtures import KITCHEN_SINK def test_allows_for_editing_on_enter(): diff --git a/tests/core_utils/test_ast_to_code.py b/tests/core_utils/test_ast_to_code.py new file mode 100644 index 00000000..4f0bf9bd --- /dev/null +++ b/tests/core_utils/test_ast_to_code.py @@ -0,0 +1,15 @@ +from graphql.core import parse, Source +from graphql.core.language.parser import Loc +from graphql.core.language import ast +from graphql.core.utils.ast_to_code import ast_to_code +from tests.core_language import fixtures + + +def test_ast_to_code_using_kitchen_sink(): + doc = parse(fixtures.KITCHEN_SINK) + code_ast = ast_to_code(doc) + source = Source(fixtures.KITCHEN_SINK) + loc = lambda start, end: Loc(start, end, source) + + parsed_code_ast = eval(code_ast, {}, {'ast': ast, 'loc': loc}) + assert doc == parsed_code_ast diff --git a/tox.ini b/tox.ini index fc8c9241..f42f1f9e 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = flake8,import-order,py27,py33,py34,py35,pypy,docs deps = pytest>=2.7.2 gevent==1.1b5 + six>=1.10.0 commands = py{27,33,34,py}: py.test tests {posargs} py35: py.test tests tests_py35 {posargs}