Skip to content

Commit 1049a4e

Browse files
authored
Renderer Fixes (see desc) (#17)
* Fix TODOs * Fix README example * Fix use of “pass” * nicer code * Nicer sort objects code * Nicer handling of fields rendering * Unused code
1 parent ac28d56 commit 1049a4e

3 files changed

Lines changed: 176 additions & 28 deletions

File tree

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Then go to your project folder and run `gql init`
2323

2424
## Quick Start
2525

26-
`gql` works by parsing query files (`**/*.graphql` by default) into their own Python module where
26+
`gql` works by parsing query files (`**/*.graphql` by default) into their own Python module where
2727
an class, named after the operation defined in the file, allows you to make that query and get a typed
2828
response.
2929

@@ -53,18 +53,17 @@ from gql.clients import Client, AsyncIOClient
5353
class GetFilm:
5454
@dataclass_json
5555
@dataclass
56-
class GetFilmData():
56+
class GetFilmData:
5757
@dataclass_json
5858
@dataclass
59-
class Film():
60-
pass
59+
class Film:
6160
title: str
6261
director: str
6362
film: Film = None
64-
63+
6564
data: GetFilmData = None
6665
errors: Any = None
67-
66+
6867
@classmethod
6968
def execute(cls, id: str, on_before_callback: Callable[[Mapping[str, str], Mapping[str, str]], None] = None) -> GetFilm:
7069
...

gql/renderer_dataclasses.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@
55
from gql.query_parser import ParsedQuery, ParsedField, ParsedObject, ParsedEnum, ParsedOperation, ParsedVariableDefinition
66

77

8-
CLASS_TEMPLATE = """
9-
@dataclass_json
10-
@dataclass(frozen=True)
11-
"""
12-
13-
148
class DataclassesRenderer:
159

1610
def __init__(self, schema: GraphQLSchema, config: Config):
@@ -42,8 +36,8 @@ def render(self, parsed_query: ParsedQuery):
4236
for enum in parsed_query.enums:
4337
self.__render_enum(buffer, enum)
4438

45-
# Iterate in reverse so that operation is last
46-
for obj in parsed_query.objects[::-1]:
39+
sorted_objects = sorted(parsed_query.objects, key=lambda obj: 1 if isinstance(obj, ParsedOperation) else 0)
40+
for obj in sorted_objects:
4741
if isinstance(obj, ParsedObject):
4842
self.__render_object(parsed_query, buffer, obj)
4943
elif isinstance(obj, ParsedOperation):
@@ -73,21 +67,26 @@ def __render_datetime_field(buffer: CodeChunk):
7367
buffer.write('')
7468

7569
def __render_object(self, parsed_query: ParsedQuery, buffer: CodeChunk, obj: ParsedObject):
70+
class_parents = '' if not obj.parents else f'({", ".join(obj.parents)})'
71+
7672
buffer.write('@dataclass_json')
7773
buffer.write('@dataclass')
78-
with buffer.write_block(f'class {obj.name}({", ".join(obj.parents)}):'):
74+
with buffer.write_block(f'class {obj.name}{class_parents}:'):
7975
# render child objects
80-
if not obj.children:
81-
buffer.write('pass')
82-
else:
83-
for child_object in obj.children:
84-
self.__render_object(parsed_query, buffer, child_object)
76+
for child_object in obj.children:
77+
self.__render_object(parsed_query, buffer, child_object)
8578

8679
# render fields
8780
sorted_fields = sorted(obj.fields, key=lambda f: 1 if f.nullable else 0)
8881
for field in sorted_fields:
8982
self.__render_field(parsed_query, buffer, field)
9083

84+
# pass if not children or fields
85+
if not (obj.children or obj.fields):
86+
buffer.write('pass')
87+
88+
buffer.write('')
89+
9190
def __render_operation(self, parsed_query: ParsedQuery, buffer: CodeChunk, parsed_op: ParsedOperation):
9291
buffer.write('@dataclass_json')
9392
buffer.write('@dataclass')
@@ -122,6 +121,8 @@ def __render_operation(self, parsed_query: ParsedQuery, buffer: CodeChunk, parse
122121
buffer.write('response_text = client.call(cls.__QUERY__, variables=variables, on_before_callback=on_before_callback)')
123122
buffer.write('return cls.from_json(response_text)')
124123

124+
buffer.write('')
125+
125126
buffer.write('@classmethod')
126127
with buffer.write_block(f'async def execute_async(cls, {vars_args} on_before_callback: Callable[[Mapping[str, str], Mapping[str, str]], None] = None):'):
127128
buffer.write(f'client = AsyncIOClient(\'{self.config.endpoint}\')')
@@ -143,18 +144,20 @@ def __render_variable_definition(var: ParsedVariableDefinition):
143144
def __render_field(parsed_query: ParsedQuery, buffer: CodeChunk, field: ParsedField):
144145
enum_names = [e.name for e in parsed_query.enums]
145146
is_enum = field.type in enum_names
147+
suffix = ''
148+
field_type = field.type
149+
146150
if is_enum:
147-
buffer.write(f'{field.name}: {field.type} = enum_field({field.type})')
148-
return
151+
suffix = f'= enum_field({field.type})'
149152

150153
if field.type == 'DateTime':
151-
buffer.write(f'{field.name}: datetime = DATETIME_FIELD')
152-
return
154+
suffix = '= DATETIME_FIELD'
155+
field_type = 'datetime'
153156

154157
if field.nullable:
155-
buffer.write(f'{field.name}: {field.type} = {field.default_value}')
156-
else:
157-
buffer.write(f'{field.name}: {field.type}')
158+
suffix = f'= {field.default_value}'
159+
160+
buffer.write(f'{field.name}: {field_type} {suffix}')
158161

159162
@staticmethod
160163
def __render_enum(buffer: CodeChunk, enum: ParsedEnum):

tests/test_renderer_dataclasses.py

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import pytest
22
from datetime import datetime
3-
from dataclasses import field
3+
4+
from graphql import GraphQLEnumType, GraphQLEnumValue, GraphQLField, GraphQLNonNull, GraphQLString, GraphQLInt, \
5+
GraphQLArgument, GraphQLSchema, GraphQLObjectType
6+
47
from gql.config import Config
8+
from gql.query_parser import QueryParser
59
from gql.renderer_dataclasses import DataclassesRenderer
610

11+
712
@pytest.fixture
813
def swapi_dataclass_renderer(swapi_schema):
914
return DataclassesRenderer(swapi_schema, Config(schema='schemaurl', endpoint='schemaurl', documents=''))
@@ -162,6 +167,54 @@ def test_simple_query_with_complex_fragment(swapi_parser, swapi_dataclass_render
162167
assert data.luke.home.name == 'Arakis'
163168

164169

170+
def test_simple_query_with_complex_fragments(swapi_parser, swapi_dataclass_renderer, module_compiler):
171+
query = """
172+
fragment PlanetFields on Planet {
173+
name
174+
population
175+
terrains
176+
}
177+
178+
fragment CharacterFields on Person {
179+
name
180+
home: homeworld {
181+
...PlanetFields
182+
}
183+
}
184+
185+
query GetPerson {
186+
luke: character(id: "luke") {
187+
...CharacterFields
188+
}
189+
}
190+
"""
191+
192+
parsed = swapi_parser.parse(query)
193+
rendered = swapi_dataclass_renderer.render(parsed)
194+
195+
m = module_compiler(rendered)
196+
response = m.GetPerson.from_json("""
197+
{
198+
"data": {
199+
"luke": {
200+
"name": "Luke Skywalker",
201+
"home": {
202+
"name": "Arakis",
203+
"population": "1,000,000",
204+
"terrains": ["Desert"]
205+
}
206+
}
207+
}
208+
}
209+
""")
210+
211+
assert response
212+
213+
data = response.data
214+
assert data.luke.name == 'Luke Skywalker'
215+
assert data.luke.home.name == 'Arakis'
216+
217+
165218
def test_simple_query_with_complex_inline_fragment(swapi_parser, swapi_dataclass_renderer, module_compiler):
166219
query = """
167220
query GetPerson {
@@ -246,6 +299,99 @@ def test_simple_query_with_enums(github_parser, github_dataclass_renderer, modul
246299
assert node.authorAssociation == m.CommentAuthorAssociation.FIRST_TIMER
247300

248301

302+
def test_simple_query_with_enums_default_value(module_compiler):
303+
"""
304+
enum LengthUnit {
305+
METER
306+
KM
307+
}
308+
309+
type Starship {
310+
id: ID!
311+
name: String!
312+
length(unit: LengthUnit = METER): Float
313+
}
314+
315+
type Query {
316+
ship(id: String!): Starship
317+
}
318+
"""
319+
320+
length_unit_enum = GraphQLEnumType(
321+
'LengthUnit',
322+
{
323+
'METER': GraphQLEnumValue('METER'),
324+
'KM': GraphQLEnumValue('KM'),
325+
},
326+
description='One of the films in the Star Wars Trilogy',
327+
)
328+
329+
starship_type = GraphQLObjectType(
330+
'Starship',
331+
lambda: {
332+
'id': GraphQLField(GraphQLNonNull(GraphQLString), description='The id of the ship.'),
333+
'name': GraphQLField(GraphQLString, description='The name of the ship.'),
334+
'length': GraphQLField(
335+
GraphQLInt,
336+
args={
337+
'unit': GraphQLArgument(
338+
GraphQLNonNull(length_unit_enum), default_value='METER', description='id of the droid'
339+
)
340+
}
341+
)
342+
}
343+
)
344+
345+
query_type = GraphQLObjectType(
346+
'Query',
347+
lambda: {
348+
'ship': GraphQLField(
349+
starship_type,
350+
args={
351+
'id': GraphQLArgument(GraphQLNonNull(GraphQLString), description='id of the ship')
352+
},
353+
)
354+
}
355+
)
356+
357+
schema = GraphQLSchema(query_type, types=[length_unit_enum, starship_type])
358+
359+
query = """
360+
query GetStarship {
361+
ship(id: "Enterprise") {
362+
id
363+
name
364+
length(unit: METER)
365+
}
366+
}
367+
"""
368+
query_parser = QueryParser(schema)
369+
query_renderer = DataclassesRenderer(schema, Config(schema='schemaurl', endpoint='schemaurl', documents=''))
370+
parsed = query_parser.parse(query)
371+
rendered = query_renderer.render(parsed)
372+
373+
m = module_compiler(rendered)
374+
response = m.GetStarship.from_json("""
375+
{
376+
"data": {
377+
"ship": {
378+
"id": "Enterprise",
379+
"name": "Enterprise",
380+
"length": 100
381+
}
382+
}
383+
}
384+
""")
385+
386+
assert response
387+
388+
ship = response.data.ship
389+
assert ship
390+
assert ship.id == 'Enterprise'
391+
assert ship.name == 'Enterprise'
392+
assert ship.length == 100
393+
394+
249395
def test_simple_query_with_datetime(swapi_dataclass_renderer, swapi_parser, module_compiler, mocker):
250396
query = """
251397
query GetFilm($id: ID!) {

0 commit comments

Comments
 (0)