├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── dev_requirements.txt ├── graphql ├── __init__.py ├── grammar.py └── graphql.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_dumps.py ├── test_grammar.py └── test_loads.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.swp 3 | *.pyc 4 | *~ 5 | .tox 6 | dist 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include dev_requirements.txt 3 | include README.md 4 | include LICENSE 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GraphQL parser for Python 2 | ========================= 3 | 4 | This is an experimental, generic parser for the [GraphQL language][1] in Python. Consider it unstable since there's no spec for the language yey (that's why it's *experimental* ;) 5 | 6 | 7 | Install and usage 8 | ----------------- 9 | 10 | $ pip install graphql-python 11 | 12 | And just `import graphql` to get started. This library mimics the standard `json` module, so there's a `dumps` and a `loads` function. Consider this query: 13 | 14 | query = """ 15 | { 16 | user(232) { 17 | id, 18 | name, 19 | photo(size: 50) { 20 | url, 21 | width, 22 | height 23 | } 24 | } 25 | } 26 | """ 27 | 28 | objects = graphql.loads(query) 29 | # objects has now: 30 | [ 31 | { 32 | "name": "user", 33 | "params": 232, 34 | "properties": [ 35 | {"name": "id"}, 36 | {"name": "name"}, 37 | { 38 | "name": "photo", 39 | "params": {"size": 50}, 40 | "properties": [ 41 | {"name": "url"}, 42 | {"name": "width"}, 43 | {"name": "height"} 44 | ] 45 | }, 46 | ] 47 | } 48 | ] 49 | 50 | To understand how `loads` works, let's split this query into small parts: 51 | 52 | friends(user_id: 232).first(10) { 53 | url, 54 | name, 55 | address 56 | } 57 | 58 | This query represents an *object* composed of: 59 | 60 | - a name (`friends`) 61 | - can be anything that matches the `r'`[a-zA-Z_][a-zA-Z0-9_/]*'` regex. 62 | - some parameters (`user_id: 232`) 63 | - can match literals (strings, numbers with or without signal, true, false and null) and pairs of values 64 | - `(true)` will be loaded as `"params": True`. Likewise, `(232)` will be `"params": 232`. 65 | - `(foo: "bar", bar: "baz")` will be loaded as `"params": {"foo": "bar", "bar": "baz"}`. Any valid identifier (the regex for *name*) can be used as an argument. 66 | - some custom filters (`first(10)`) 67 | - it's a sequence of identifiers followed by a list of parameters. Order is important, so for example, `.after(id: 243442).first(10)` will be loaded as: 68 | 69 | 70 | "filters": [ 71 | ("after", {"id": 243442}), 72 | ("first", 10) 73 | ] 74 | 75 | 76 | - and a list of properties (`url, name, address`)... basically either an identifier or another nested object. 77 | 78 | 79 | Some advices 80 | ------------ 81 | 82 | Right now, this parser is *strict* (at least until the spec is released, obviously). It'll yell at you for not using commas in the right places. For example: 83 | 84 | # Will fail :( 85 | graphql.loads(""" 86 | user(42) { 87 | id, 88 | name 89 | } 90 | """) 91 | 92 | # Much better :) ... don't forget those { } in the beginning and end of the query 93 | graphql.loads(""" 94 | { 95 | user(42) { 96 | id, 97 | name 98 | } 99 | } 100 | """) 101 | 102 | # Will fail :( 103 | graphql.loads(""" 104 | { 105 | user(42) { 106 | id, 107 | name 108 | } 109 | 110 | company(2) { 111 | address 112 | } 113 | } 114 | """) 115 | 116 | # Much better :) ... see that little comma right after the first object? 117 | graphql.loads(""" 118 | { 119 | user(42) { 120 | id, 121 | name 122 | }, # <-- right here 123 | 124 | company(2) { 125 | address 126 | } 127 | } 128 | """) 129 | 130 | 131 | A note about performance & pyParsing 132 | ------------------------------------ 133 | 134 | I didn't test the numbers, just dropping [this link][2] for you to tell there's a way to improve pyParsing's performance. 135 | This flag isn't enabled because it's global, so [YMMV][3]. 136 | 137 | [1]: https://facebook.github.io/react/blog/2015/05/01/graphql-introduction.html 138 | [2]: http://stackoverflow.com/a/21371472 139 | [3]: http://www.urbandictionary.com/define.php?term=ymmv 140 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | tox==2.0.2 2 | virtualenv==13.0.3 3 | pytest==2.7.1 4 | mock==1.0.1 5 | -------------------------------------------------------------------------------- /graphql/__init__.py: -------------------------------------------------------------------------------- 1 | from .graphql import * 2 | from . import grammar 3 | -------------------------------------------------------------------------------- /graphql/grammar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import pyparsing as pp 4 | 5 | 6 | def set_debug(debug): 7 | root.setDebug(debug) 8 | 9 | 10 | OPEN_BRACE = pp.Suppress('{') 11 | CLOSE_BRACE = pp.Suppress('}') 12 | OPEN_PAREN = pp.Suppress('(') 13 | CLOSE_PAREN = pp.Suppress(')') 14 | COMMA = pp.Suppress(',') 15 | COLON = pp.Suppress(':') 16 | DOT = pp.Suppress('.') 17 | EOF = pp.Suppress(pp.LineEnd()) 18 | 19 | identifier = pp.Regex(r'[a-zA-Z_][a-zA-Z0-9_/]*').setName('identifier') 20 | 21 | string = pp.quotedString.setName('quoted string') 22 | number = pp.Regex(r'-?\d+(\.\d+)?').setName('number') 23 | literal = (number | string | "null" | "true" | "false").setName('literal') 24 | 25 | param = pp.Group(identifier + COLON + literal).setName('param') 26 | param_pairs_list = (param + pp.ZeroOrMore(COMMA + param)) 27 | params_list = ( OPEN_PAREN 28 | + pp.Optional(literal | param_pairs_list) 29 | + CLOSE_PAREN).setName('params list') 30 | 31 | filter_param = pp.Group(identifier + params_list).setName('filter param') 32 | 33 | gql_header = ( identifier 34 | + pp.Optional(pp.Group(params_list).setResultsName('params')) 35 | + pp.Group(pp.ZeroOrMore(DOT + filter_param)).setResultsName('filters')) 36 | 37 | gql_object = pp.Forward() 38 | gql_property = pp.Group(gql_object) | identifier 39 | gql_properties_list = gql_property + pp.ZeroOrMore(COMMA + gql_property) 40 | gql_object << ( pp.Group(gql_header).setResultsName('header') 41 | + OPEN_BRACE 42 | + pp.Group(gql_properties_list).setResultsName('properties') 43 | + CLOSE_BRACE) 44 | 45 | gql_objects_list = pp.Group(gql_object) + pp.ZeroOrMore(COMMA + pp.Group(gql_object)) 46 | root = pp.Suppress(pp.LineStart()) + OPEN_BRACE + gql_objects_list + CLOSE_BRACE + EOF 47 | -------------------------------------------------------------------------------- /graphql/graphql.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import six 4 | import json 5 | import pyparsing as pp 6 | from . import grammar 7 | 8 | __all__ = ['loads', 'dumps'] 9 | 10 | 11 | def loads(query): 12 | """ 13 | Converts a GraphQL string into a Python dictionary. 14 | 15 | >>> graphql.loads(\"\"\" 16 | { 17 | user(id: 232) { 18 | id, 19 | name 20 | } 21 | } 22 | \"\"\") 23 | 24 | [ 25 | { 26 | "name": "user", 27 | "params": {"id": 232}, 28 | "properties": [ 29 | {"name": "id"}, 30 | {"name": "name"} 31 | ] 32 | } 33 | ] 34 | """ 35 | 36 | data = [] 37 | 38 | for parsed_obj in grammar.root.parseString(query): 39 | data.append(load_obj(parsed_obj)) 40 | 41 | return data 42 | 43 | 44 | def dumps(ast, compact=False, indent=2): 45 | """ 46 | Converts a Python dict representing a GraphQL structure to 47 | its string form. The `compact` argument is a shorthand for `indent=0`, 48 | so `graphql.dumps(x, compact=True) == graphql.dumps(x, indent=0)`. 49 | 50 | >>> graphql.dumps([ 51 | { 52 | "name": "user", 53 | "params": {"id": 232}, 54 | "properties": [ 55 | {"name": "id"}, 56 | {"name": "name"} 57 | ] 58 | } 59 | ], indent=4) 60 | 61 | { 62 | user(id: 232) { 63 | id, 64 | name 65 | } 66 | } 67 | """ 68 | 69 | if compact: 70 | indent = 0 71 | 72 | chunks = ['{'] 73 | if indent > 0: 74 | chunks.append('\n') 75 | 76 | for obj in ast: 77 | chunks.extend(dump_object(obj, indent)) 78 | chunks.append(',\n\n' if indent > 0 else ',') 79 | 80 | chunks.pop(-1) # remove the last comma 81 | if indent > 0: 82 | chunks.append('\n') 83 | chunks.append('}') 84 | 85 | return ''.join(chunks) 86 | 87 | 88 | def dump_object(obj, indent, indent_level=1): 89 | """ 90 | Converts a python object to a GraphQL string 91 | """ 92 | 93 | is_compact = indent == 0 94 | chunks = [] if is_compact else [' ' * indent * indent_level] 95 | 96 | chunks.append(obj['name']) 97 | 98 | if 'params' in obj: 99 | chunks.extend(dump_params(obj['params'], is_compact)) 100 | 101 | if 'filters' in obj: 102 | for filter_name, params in iter(obj['filters'].items()): 103 | chunks.extend(['.', filter_name]) 104 | chunks.extend(dump_params(params, is_compact)) 105 | 106 | if 'properties' in obj: 107 | chunks.append('{' if is_compact else ' {\n') 108 | 109 | for prop in obj['properties']: 110 | chunks.extend(dump_object(prop, indent, indent_level + 1)) 111 | chunks.append(',' if is_compact else ',\n') 112 | 113 | chunks.pop(-1) # remove the last comma 114 | if is_compact: 115 | chunks.append('}') 116 | else: 117 | chunks.extend(['\n', ' ' * indent * indent_level, '}']) 118 | 119 | return chunks 120 | 121 | 122 | def dump_params(params, compact): 123 | """ 124 | Converts list of params to a GraphQL representation. 125 | """ 126 | 127 | SEP = ': ' if not compact else ':' 128 | 129 | if not isinstance(params, dict): 130 | return ['(', json.dumps(params), ')'] 131 | 132 | chunks = ['('] 133 | for k, v in iter(params.items()): 134 | chunks.extend([k, SEP, json.dumps(v)]) 135 | chunks.append(')') 136 | 137 | return chunks 138 | 139 | 140 | def load_obj(parsed_obj): 141 | """ 142 | Converts a parsed GraphQL object into a Python dict 143 | """ 144 | 145 | name, params, filters = get_header(parsed_obj) 146 | properties = get_properties(parsed_obj) 147 | 148 | obj = {'name': name} 149 | if params: 150 | obj['params'] = load_args(params) 151 | 152 | if filters: 153 | obj['filters'] = [(filter_[0], load_args(filter_[1:])) 154 | for filter_ in filters] 155 | 156 | obj['properties'] = [] 157 | for p in properties: 158 | if isinstance(p, pp.ParseResults): 159 | obj['properties'].append(load_obj(p)) 160 | else: 161 | obj['properties'].append({'name': p}) 162 | 163 | return obj 164 | 165 | 166 | def load_args(args): 167 | """ 168 | Converts parsed values inside parens into its Python equivalents. 169 | """ 170 | 171 | if len(args) == 1 and isinstance(args[0], six.string_types): 172 | return json.loads(args[0]) 173 | 174 | parsed_args = {} 175 | for arg in args: 176 | if not isinstance(arg, list): 177 | arg = arg.asList() 178 | 179 | param = arg[0] 180 | arguments = arg[1:] 181 | parsed_args[param] = json.loads(arguments[0]) 182 | 183 | return parsed_args 184 | 185 | 186 | def get_header(obj): 187 | header, _ = obj 188 | name = header[0] 189 | 190 | return name, header.get('params', []), header.get('filters', []) 191 | 192 | 193 | def get_properties(obj): 194 | _, properties = obj 195 | 196 | return properties 197 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyparsing==2.0.3 2 | six>=1.9.0 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from setuptools import setup 5 | 6 | 7 | def read_req(filename): 8 | return open(filename).read().split() 9 | 10 | 11 | setup( 12 | name="graphql-python", 13 | version="0.0.1", 14 | url='https://github.com/lsmag/graphql-python', 15 | author='Lucas Sampaio', 16 | author_email='lucas@lsmagalhaes.com', 17 | description='Python implementation of GraphQL markup language', 18 | packages=['graphql'], 19 | include_package_data=True, 20 | install_requires=read_req('requirements.txt'), 21 | tests_require=read_req('dev_requirements.txt'), 22 | classifiers=[ 23 | "Development Status :: 3 - Alpha", 24 | "Programming Language :: Python :: 2.7", 25 | "Programming Language :: Python :: 3.0", 26 | "Programming Language :: Python :: 3.1", 27 | "Programming Language :: Python :: 3.2", 28 | "Programming Language :: Python :: 3.3", 29 | "Programming Language :: Python :: 3.4", 30 | "Intended Audience :: Developers", 31 | "Topic :: Text Processing :: Markup", 32 | "Topic :: Software Development :: Libraries", 33 | "Topic :: Software Development :: Libraries :: Python Modules", 34 | "License :: OSI Approved :: MIT License" 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsmag/graphql-python/6f1ba6e26da9667c963bc4993e133dc39cc7ec5c/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_dumps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from textwrap import dedent 4 | import graphql 5 | 6 | 7 | def test_simple_dumps(): 8 | query = graphql.dumps([ 9 | { 10 | "name": "user", 11 | "params": 232, 12 | "properties": [ 13 | {"name": "id"}, 14 | {"name": "name"} 15 | ] 16 | } 17 | ], indent=4) 18 | 19 | expected = """{ 20 | user(232) { 21 | id, 22 | name 23 | } 24 | }""" 25 | 26 | assert query == expected 27 | 28 | 29 | def test_dumps_with_filters(): 30 | query = graphql.dumps([ 31 | { 32 | "name": "user", 33 | "params": 232, 34 | "filters": {"active": True}, 35 | "properties": [ 36 | {"name": "id"}, 37 | {"name": "name"} 38 | ] 39 | } 40 | ], indent=4) 41 | 42 | expected = """{ 43 | user(232).active(true) { 44 | id, 45 | name 46 | } 47 | }""" 48 | 49 | assert query == expected 50 | 51 | 52 | def test_dumps_with_named_args(): 53 | query = graphql.dumps([ 54 | { 55 | "name": "user", 56 | "params": {"id": 232}, 57 | "properties": [ 58 | {"name": "id"}, 59 | {"name": "name"} 60 | ] 61 | } 62 | ], indent=4) 63 | 64 | expected = """{ 65 | user(id: 232) { 66 | id, 67 | name 68 | } 69 | }""" 70 | 71 | assert query == expected 72 | 73 | 74 | def test_dumps_nested_objects(): 75 | query = graphql.dumps([ 76 | { 77 | "name": "user", 78 | "params": {"id": 232}, 79 | "properties": [ 80 | {"name": "id"}, 81 | {"name": "name"}, 82 | { 83 | "name": "photo", 84 | "params": {"size": 50}, 85 | "properties": [ 86 | {"name": "url"}, 87 | {"name": "width"}, 88 | {"name": "height"} 89 | ] 90 | } 91 | ] 92 | } 93 | ], indent=4) 94 | 95 | expected = """{ 96 | user(id: 232) { 97 | id, 98 | name, 99 | photo(size: 50) { 100 | url, 101 | width, 102 | height 103 | } 104 | } 105 | }""" 106 | 107 | assert query == expected 108 | 109 | 110 | def test_dumps_multiple_objects(): 111 | query = graphql.dumps([ 112 | { 113 | "name": "user", 114 | "params": {"id": 232}, 115 | "properties": [ 116 | {"name": "id"}, 117 | {"name": "name"}, 118 | ] 119 | }, 120 | { 121 | "name": "photo", 122 | "params": {"size": 50}, 123 | "properties": [ 124 | {"name": "url"}, 125 | {"name": "width"}, 126 | {"name": "height"} 127 | ] 128 | } 129 | ], indent=4) 130 | 131 | expected = """{ 132 | user(id: 232) { 133 | id, 134 | name 135 | }, 136 | 137 | photo(size: 50) { 138 | url, 139 | width, 140 | height 141 | } 142 | }""" 143 | 144 | assert query == expected 145 | 146 | 147 | def test_dumps_compact(): 148 | query = graphql.dumps([ 149 | { 150 | "name": "user", 151 | "params": {"id": 232}, 152 | "properties": [ 153 | {"name": "id"}, 154 | {"name": "name"}, 155 | ] 156 | }, 157 | { 158 | "name": "photo", 159 | "params": {"size": 50}, 160 | "properties": [ 161 | {"name": "url"}, 162 | {"name": "width"}, 163 | {"name": "height"} 164 | ] 165 | } 166 | ], compact=True) 167 | 168 | assert query == ('{user(id:232){id,name},' 169 | 'photo(size:50){url,width,height}}') 170 | -------------------------------------------------------------------------------- /tests/test_grammar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from contextlib import contextmanager 3 | from pyparsing import ParseException 4 | from graphql import grammar 5 | 6 | 7 | @contextmanager 8 | def assert_raises(exc): 9 | raised = True 10 | try: 11 | yield 12 | raised = False 13 | except Exception as e: 14 | assert isinstance(e, exc) 15 | 16 | assert raised, "Exception %s not raised" % exc 17 | 18 | 19 | def test_literals(): 20 | assert grammar.literal.parseString('null').asList() == ['null'] 21 | assert grammar.literal.parseString('true').asList() == ['true'] 22 | assert grammar.literal.parseString('false').asList() == ['false'] 23 | 24 | assert grammar.literal.parseString('2.334').asList() == ['2.334'] 25 | assert grammar.literal.parseString('-12.334').asList() == ['-12.334'] 26 | assert grammar.literal.parseString('42').asList() == ['42'] 27 | 28 | assert grammar.literal.parseString('"Foobar"').asList() == ['"Foobar"'] 29 | assert grammar.literal.parseString("'Barbaz'").asList() == ["'Barbaz'"] 30 | 31 | 32 | def test_identifier(): 33 | assert grammar.identifier.parseString('fooBar').asList() == ['fooBar'] 34 | assert grammar.identifier.parseString('foo23_45').asList() == ['foo23_45'] 35 | assert grammar.identifier.parseString('entity/23').asList() == ['entity/23'] 36 | assert grammar.identifier.parseString('_foo').asList() == ['_foo'] 37 | 38 | with assert_raises(ParseException): 39 | grammar.identifier.parseString('/fas').asList() 40 | 41 | with assert_raises(ParseException): 42 | grammar.identifier.parseString('42as').asList() 43 | 44 | 45 | def test_params_list(): 46 | # Testing literals 47 | assert grammar.params_list.parseString('(null)').asList() == ['null'] 48 | assert grammar.params_list.parseString('(true)').asList() == ['true'] 49 | assert grammar.params_list.parseString('(false)').asList() == ['false'] 50 | assert grammar.params_list.parseString('(2333.43)').asList() == ['2333.43'] 51 | assert grammar.params_list.parseString('("foo")').asList() == ['"foo"'] 52 | 53 | # Pairs 54 | assert grammar.params_list.parseString('(id: 12, name: "Adalberto")').asList() == [['id', '12'], ['name', '"Adalberto"']] 55 | assert grammar.params_list.parseString('(component/name: "textarea")').asList() == [['component/name', '"textarea"']] 56 | 57 | # Empty params list 58 | assert grammar.params_list.parseString('()').asList() == [] 59 | 60 | 61 | def test_gql_header(): 62 | # Valid headers: 63 | # user { 64 | # user(232) { 65 | # user(id: 232) { 66 | # photos.first(2) { 67 | # friends(recent: true).first(50) { 68 | assert grammar.gql_header.parseString('user').asList() == ['user', []] 69 | assert grammar.gql_header.parseString('user(232)').asList() == ['user', ['232'], []] 70 | assert grammar.gql_header.parseString('user(id: 232, name: "Adalberto")').asList() == ['user', [['id', '232'], ['name', '"Adalberto"']], [] ] 71 | 72 | assert grammar.gql_header.parseString('photos.first(2)').asList() == ['photos', [ ['first', '2'] ]] 73 | 74 | assert grammar.gql_header.parseString('friends.after(2434423).first(10)').asList() == ['friends', [ ['after', '2434423'], ['first', '10'] ]] 75 | 76 | assert grammar.gql_header.parseString('friends(recent: true).first(50)').asList() == ['friends', [['recent', 'true']], [['first', '50']] ] 77 | 78 | # We need to test if groups are labeled properly 79 | res = grammar.gql_header.parseString('friends(id: 42, recent: true).first(50)') 80 | assert res['params'].asList() == [['id', '42'], ['recent', 'true']] 81 | assert res['filters'].asList() == [['first', '50']] 82 | 83 | res = grammar.gql_header.parseString('user') 84 | assert 'params' not in res 85 | assert ('filters' not in res or not res['filters']) 86 | 87 | 88 | def test_gql_object(): 89 | assert grammar.gql_object.parseString(""" 90 | user { 91 | id, 92 | name 93 | } 94 | """).asList() == [['user', []], ['id', 'name']] 95 | 96 | with assert_raises(ParseException): 97 | grammar.gql_object.parseString('user {}') 98 | 99 | obj = """ 100 | user(232) { 101 | id, 102 | name 103 | } 104 | """ 105 | assert grammar.gql_object.parseString(obj).asList() == [['user', ['232'], []], ['id', 'name']] 106 | assert grammar.gql_object.parseString(obj)['header'].asList() == ['user', ['232'], []] 107 | assert grammar.gql_object.parseString(obj)['properties'].asList() == ['id', 'name'] 108 | 109 | assert grammar.gql_object.parseString(""" 110 | user(232) { 111 | id, 112 | name, 113 | photo(size: 50) { 114 | url, 115 | id 116 | } 117 | } 118 | """).asList() == ( 119 | [ 120 | ['user', ['232'], []], 121 | ['id', 122 | 'name', 123 | [ 124 | ['photo', [['size', '50']], []], 125 | ['url', 'id'] 126 | ] 127 | ] 128 | ] 129 | ) 130 | 131 | 132 | def test_root(): 133 | # root is the main grammar, it defines a list of objects 134 | assert grammar.root.parseString(""" 135 | { 136 | User(f: "123", g: 223) { 137 | id, 138 | name 139 | }, 140 | 141 | AnotherUser { 142 | id, 143 | name 144 | } 145 | } 146 | """).asList() == [ 147 | [ 148 | ['User', [['f', '"123"'], ['g', '223']], []], 149 | ['id', 'name'] 150 | ], 151 | [ 152 | ['AnotherUser', []], 153 | ['id', 'name'] 154 | ] 155 | ] 156 | 157 | # Missing commas between objects will raise an exception 158 | with assert_raises(ParseException): 159 | grammar.root.parseString(""" 160 | { 161 | User {id, name} 162 | AnotherUser {id, name} 163 | }""") 164 | 165 | # Anything after the end of the document will raise an exception 166 | with assert_raises(ParseException): 167 | grammar.root.parseString(""" 168 | { 169 | User {id, name}, 170 | AnotherUser {id, name} 171 | } foo bar""") 172 | 173 | # Likewise, anything before also will raise an exception 174 | with assert_raises(ParseException): 175 | grammar.root.parseString(""" 176 | foo bar{ 177 | User {id, name}, 178 | AnotherUser {id, name} 179 | }""") 180 | -------------------------------------------------------------------------------- /tests/test_loads.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import graphql 4 | 5 | 6 | def test_load_simple_object(): 7 | assert graphql.loads(""" 8 | { 9 | user { 10 | id, 11 | name 12 | } 13 | }""") == [ 14 | { 15 | "name": "user", 16 | "properties": [ 17 | {"name": "id"}, 18 | {"name": "name"} 19 | ] 20 | } 21 | ] 22 | 23 | 24 | def test_load_simple_object_with_params(): 25 | assert graphql.loads(""" 26 | { 27 | user(232) { 28 | id, 29 | name 30 | } 31 | }""") == [ 32 | { 33 | "name": "user", 34 | "params": 232, 35 | "properties": [ 36 | {"name": "id"}, 37 | {"name": "name"} 38 | ] 39 | } 40 | ] 41 | 42 | 43 | def test_load_simple_object_with_filters(): 44 | assert graphql.loads(""" 45 | { 46 | photos.first(2) { 47 | url, 48 | width, 49 | height 50 | } 51 | }""") == [ 52 | { 53 | "name": "photos", 54 | "filters": [("first", 2)], 55 | "properties": [ 56 | {"name": "url"}, 57 | {"name": "width"}, 58 | {"name": "height"}, 59 | ] 60 | } 61 | ] 62 | 63 | 64 | def test_load_simple_object_with_full_header(): 65 | assert graphql.loads(""" 66 | { 67 | photos(username: "Louro Jose").after(id: 232).sortBy("username") { 68 | url, 69 | width, 70 | height 71 | } 72 | }""") == [ 73 | { 74 | "name": "photos", 75 | "params": {"username": "Louro Jose"}, 76 | "filters": [ 77 | ("after", {"id": 232}), 78 | ("sortBy", "username") 79 | ], 80 | "properties": [ 81 | {"name": "url"}, 82 | {"name": "width"}, 83 | {"name": "height"}, 84 | ] 85 | } 86 | ] 87 | 88 | 89 | def test_load_simple_object_with_full_header_multiple_args(): 90 | assert graphql.loads(""" 91 | { 92 | photos(username: "Louro Jose").after(id: 232, username: "Hebe").sortBy("url") { 93 | url, 94 | width, 95 | height 96 | } 97 | }""") == [ 98 | { 99 | "name": "photos", 100 | "params": {"username": "Louro Jose"}, 101 | "filters": [ 102 | ("after", {"id": 232, "username": "Hebe"}), 103 | ("sortBy", "url") 104 | ], 105 | "properties": [ 106 | {"name": "url"}, 107 | {"name": "width"}, 108 | {"name": "height"}, 109 | ] 110 | } 111 | ] 112 | 113 | 114 | def test_load_multiple_objects(): 115 | assert graphql.loads(""" 116 | { 117 | user(232) { 118 | name, 119 | id 120 | }, 121 | company(userId: 232) { 122 | address 123 | } 124 | }""") == [ 125 | { 126 | "name": "user", 127 | "params": 232, 128 | "properties": [ 129 | {"name": "name"}, 130 | {"name": "id"} 131 | ] 132 | }, 133 | { 134 | "name": "company", 135 | "params": {"userId": 232}, 136 | "properties": [ 137 | {"name": "address"} 138 | ] 139 | } 140 | ] 141 | 142 | 143 | def test_load_objects_with_constant_args(): 144 | assert graphql.loads('{ user(null) { id } }') == [ 145 | { 146 | "name": "user", 147 | "params": None, 148 | "properties": [ {"name": "id"} ] 149 | } 150 | ] 151 | 152 | assert graphql.loads('{ user(true) { id } }') == [ 153 | { 154 | "name": "user", 155 | "params": True, 156 | "properties": [ {"name": "id"} ] 157 | } 158 | ] 159 | 160 | assert graphql.loads('{ user(false) { id } }') == [ 161 | { 162 | "name": "user", 163 | "params": False, 164 | "properties": [ {"name": "id"} ] 165 | } 166 | ] 167 | 168 | assert graphql.loads('{ user(id: null) { id } }') == [ 169 | { 170 | "name": "user", 171 | "params": {"id": None}, 172 | "properties": [ {"name": "id"} ] 173 | } 174 | ] 175 | 176 | def test_nested_objects(): 177 | assert graphql.loads(""" 178 | { 179 | user(232) { 180 | id, 181 | name, 182 | photo(size: 50) { 183 | url, 184 | width, 185 | height 186 | } 187 | } 188 | } 189 | """) == [ 190 | { 191 | "name": "user", 192 | "params": 232, 193 | "properties": [ 194 | {"name": "id"}, 195 | {"name": "name"}, 196 | { 197 | "name": "photo", 198 | "params": {"size": 50}, 199 | "properties": [ 200 | {"name": "url"}, 201 | {"name": "width"}, 202 | {"name": "height"} 203 | ] 204 | } 205 | ] 206 | } 207 | ] 208 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py30,py31,py32,py33,py34 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | changedir=tests 7 | deps= 8 | -rrequirements.txt 9 | -rdev_requirements.txt 10 | commands= 11 | py.test --basetemp={envtmpdir} {posargs} 12 | --------------------------------------------------------------------------------