Skip to content

Commit

Permalink
Merge a0261ed into 084b7e9
Browse files Browse the repository at this point in the history
  • Loading branch information
ekampf committed Dec 20, 2018
2 parents 084b7e9 + a0261ed commit 073d045
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 110 deletions.
1 change: 1 addition & 0 deletions gql/query_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def enter_field(self, node, *_):
name = node.alias.value if node.alias else node.name.value
type_ = self.type_info.get_type()
new_node = MappingNode(query=self.query, node=node, name=name, graphql_type=type_, parent=self.current)
# TODO: Nullable fields should go to the end
self.current.children.append(new_node)
return node

Expand Down
179 changes: 79 additions & 100 deletions gql/renderer_dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,16 @@
import os
from typing import cast, Union
from graphql import GraphQLSchema, GraphQLWrappingType, GraphQLNonNull, OperationDefinitionNode, NonNullTypeNode, \
NamedTypeNode
from typing import cast
from graphql import GraphQLSchema, GraphQLWrappingType, GraphQLNonNull, OperationDefinitionNode, NonNullTypeNode, TypeNode

from gql.config import Config
from gql.utils_codegen import CodeChunk
from gql.query_parser import ParsedQuery, MappingNode

FILE_HEADER = """
# AUTOGENERATED file. Do not Change!
from typing import Any, Callable, Mapping
from dataclasses import dataclass
from dataclasses_json import dataclass_json
from gql.clients import Client, AsyncIOClient

{custom_header}
"""

OPERATION_TEMPLATE = """
@dataclass_json
@dataclass(frozen=True)
class {name}:
__QUERY__ = \"\"\"
{query}
\"\"\"
"""

OPERATION_EXEC_FUNC_TEMPLATE = """
@classmethod
def execute(cls, {vars_args} on_before_callback: Callable[[Mapping[str, str], Mapping[str, str]], None] = None):
client = Client('{url}')
variables = {vars_dict}
response_text = client.call(cls.__QUERY__, variables=variables, on_before_callback=on_before_callback)
return cls.from_json(response_text)
@classmethod
async def execute_async(cls, {vars_args} on_before_callback: Callable[[Mapping[str, str], Mapping[str, str]], None] = None):
client = AsyncClient('{url}')
variables = {vars_dict}
response_text = await client.call(cls.__QUERY__, variables=variables, on_before_callback=on_before_callback)
return cls.from_json(response_text)
"""


CLASS_TEMPLATE = """
@dataclass_json
@dataclass(frozen=True)
class {name}({parents}):
"""


Expand All @@ -59,84 +24,102 @@ def render(self, parsed_data: ParsedQuery):
# We sort fragment nodes to be first and operations to be last because of dependecies
sorted_data = sorted(parsed_data.parsed, key=lambda node: 1 if node.node.kind == 'operation_definition' else 0)

lines = []
buffer = CodeChunk()
buffer.write('# AUTOGENERATED file. Do not Change!')
buffer.write('from typing import Any, Callable, Mapping')
buffer.write('from dataclasses import dataclass')
buffer.write('from dataclasses_json import dataclass_json')
buffer.write('from gql.clients import Client, AsyncIOClient')

if self.config.custom_header:
buffer.write_lines(self.config.custom_header.split('\n'))

for node in sorted_data:
for line in self.__do_render(node, indent=0):
lines.append(line)
self.__do_render(buffer, node)

return FILE_HEADER.format(custom_header=self.config.custom_header) + '\n'.join(lines)
return str(buffer)

def render_common(self):
# pylint:disable=no-self-use
return ''

def __do_render(self, node: MappingNode, indent=0):
def __do_render(self, buffer: CodeChunk, node: MappingNode):
if node.node.kind == 'operation_definition':
yield from self.__render_operation(node, indent=indent)
self.__render_operation(buffer, node)

elif node.node.kind == 'fragment_definition':
yield from self.__render_python_class(node, indent=indent)
self.__render_python_class(buffer, node)

elif node.node.kind == 'field':
field_typename = self.__scalar_type_to_python(node.graphql_type) #f'{node.node.name.value.capitalize()}Response'
self.__render_field(buffer, node)

def __render_field(self, buffer: CodeChunk, node: MappingNode):
field_typename = self.__scalar_type_to_python(node.graphql_type)

if node.children or node.fragments:
yield from self.__render_python_class(node, indent=indent)
if node.children or node.fragments:
self.__render_python_class(buffer, node)

# render field
yield f'{self.indent(indent)}{node.name}: {field_typename}'
buffer.write(f'{node.name}: {field_typename}')

def __render_operation(self, node: MappingNode, indent: int = 0):

def __render_operation(self, buffer: CodeChunk, node: MappingNode):
assert node.node.kind == 'operation_definition'
graphql_node = cast(OperationDefinitionNode, node.node)
op_name = f'{graphql_node.operation.value.capitalize()}{graphql_node.name.value}'

ind = self.indent(indent)
ind1 = self.indent(indent+1)

op_def = OPERATION_TEMPLATE.format(name=op_name, query=node.query)
for line in op_def.split(os.linesep):
yield ind + line

yield from self.__render_python_class(node, indent=indent+1)

yield ind1 + f'data: {node.name} = None'
yield ind1 + 'errors: Any = None'

# Execution functions
variables = [(vdef.variable.name.value, self.__variable_type_to_python(vdef.type)) for vdef in graphql_node.variable_definitions]

if variables:
variables_arguments = ', '.join([f'{vdef.variable.name.value}: {self.__variable_type_to_python(vdef.type)}' for vdef in graphql_node.variable_definitions]) + ','
variables_dict = '{' + ', '.join(f'"{name}": {name}' for name, _ in variables) + '}'
else:
variables_arguments = ''
variables_dict = 'None'

for line in OPERATION_EXEC_FUNC_TEMPLATE.format(vars_args=variables_arguments, vars_dict=variables_dict, url=self.config.schema).split(os.linesep):
yield ind1 + line


def __render_python_class(self, node: MappingNode, indent: int = 0):
buffer.write('@dataclass_json')
buffer.write('@dataclass')
with buffer.write_block('class {name}:', name=op_name):
buffer.write('__QUERY__ = """')
buffer.write(node.query)
buffer.write('"""')
buffer.write('')

self.__render_python_class(buffer, node)

buffer.write(f'data: {node.name} = None')
buffer.write('errors: Any = None')
buffer.write('')

# Execution functions
variables = [(var_def.variable.name.value, self.__variable_type_to_python(var_def.type))
for var_def in
graphql_node.variable_definitions]

if variables:
vars_args = ', '.join([f'{vdef.variable.name.value}: {self.__variable_type_to_python(vdef.type)}' for vdef in graphql_node.variable_definitions]) + ','
variables_dict = '{' + ', '.join(f'"{name}": {name}' for name, _ in variables) + '}'
else:
vars_args = ''
variables_dict = 'None'

buffer.write('@classmethod')
with buffer.write_block(f'def execute(cls, {vars_args} on_before_callback: Callable[[Mapping[str, str], Mapping[str, str]], None] = None):'):
buffer.write(f'client = Client(\'{self.config.schema}\')')
buffer.write(f'variables = {variables_dict}')
buffer.write('response_text = client.call(cls.__QUERY__, variables=variables, on_before_callback=on_before_callback)')
buffer.write('return cls.from_json(response_text)')

buffer.write('@classmethod')
with buffer.write_block(f'async def execute_async(cls, {vars_args} on_before_callback: Callable[[Mapping[str, str], Mapping[str, str]], None] = None):'):
buffer.write(f'client = AsyncClient(\'{self.config.schema}\')')
buffer.write(f'variables = {variables_dict}')
buffer.write(f'response_text = await client.call(cls.__QUERY__, variables=variables, on_before_callback=on_before_callback)')
buffer.write(f'return cls.from_json(response_text)')

def __render_python_class(self, buffer: CodeChunk, node: MappingNode):
graphql_type = node.graphql_type if not isinstance(node.graphql_type, GraphQLWrappingType) else node.graphql_type.of_type

ind = self.indent(indent)
ind1 = self.indent(indent+1)

name = node.name if node.node.kind in ['operation_definition', 'fragment_definition'] else str(graphql_type)

class_def = CLASS_TEMPLATE.format(name=name, parents=','.join(node.fragments))
for line in class_def.split(os.linesep):
yield ind + line

if node.fragments and not node.children:
# Class has no fields of its own, only derive from fragment
yield ind1 + 'pass'
elif node.children:
for child in node.children:
yield from self.__do_render(child, indent=indent + 1)

yield '\n'
buffer.write('@dataclass_json')
buffer.write('@dataclass')
with buffer.write_block(f'class {name}({",".join(node.fragments)}):'):
if node.children:
for child in node.children:
self.__do_render(buffer, child)
else:
# Class has no fields of its own, only derive from fragment
buffer.write('pass')

@staticmethod
def __scalar_type_to_python(scalar, default=None):
Expand All @@ -151,15 +134,15 @@ def __scalar_type_to_python(scalar, default=None):
'Int': 'int',
'Float': 'float',
'Boolean': 'bool',
'DateTime': 'str' # TODO: add config for custom mapping of scalar -> curom python type
'DateTime': 'str' # TODO: add config for custom mapping of scalar -> custom python type
}

default = default or scalar
mapping = mapping.get(str(scalar), default)
return mapping if not nullable else f'{mapping} = None'

@staticmethod
def __variable_type_to_python(vartype: Union[NonNullTypeNode, NamedTypeNode]):
def __variable_type_to_python(vartype: TypeNode):
nullable = True
if isinstance(vartype, NonNullTypeNode):
nullable = False
Expand All @@ -176,7 +159,3 @@ def __variable_type_to_python(vartype: Union[NonNullTypeNode, NamedTypeNode]):

mapping = mapping.get(vartype.name.value, 'str')
return mapping if not nullable else f'{mapping} = None'

@staticmethod
def indent(count: int):
return ' ' * 4 * count
22 changes: 20 additions & 2 deletions gql/utils_codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,26 @@
SPACES = ' ' * 4


class CodeGenerator:
class CodeChunk:
class Block:
def __init__(self, codegen: 'CodeChunk'):
self.gen = codegen

def __enter__(self):
self.gen.indent()
return self.gen

def __exit__(self, exc_type, exc_val, exc_tb):
self.gen.unindent()

def __init__(self):
self.lines = []
self.level = 0

def indent(self):
self.level += 1

def undent(self):
def unindent(self):
if self.level > 0:
self.level -= 1

Expand All @@ -30,6 +41,13 @@ def write_lines(self, lines):
for line in lines:
self.lines.append(self.indent_string + line)

def block(self):
return self.Block(self)

def write_block(self, block_header: str, *args, **kwargs):
self.write(block_header, *args, **kwargs)
return self.block()

def __add__(self, value: str):
self.write(value)

Expand Down
6 changes: 3 additions & 3 deletions tests/test_renderer_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def execute(cls, id: str):
assert data.returnOfTheJedi.director == 'George Lucas'


def test_query_with_fragment(swapi_parser, swapi_dataclass_renderer, module_compiler):
def test_simple_query_with_fragment(swapi_parser, swapi_dataclass_renderer, module_compiler):
"""
@dataclass_json
Expand Down Expand Up @@ -201,7 +201,7 @@ class Film(FilmFields):
assert data.returnOfTheJedi.openingCrawl == 'la la la'


def test_query_with_complex_fragment(swapi_parser, swapi_dataclass_renderer, module_compiler):
def test_simple_query_with_complex_fragment(swapi_parser, swapi_dataclass_renderer, module_compiler):
"""
```
@dataclass_json
Expand Down Expand Up @@ -275,7 +275,7 @@ class Person(CharacterFields):
assert data.luke.home.name == 'Arakis'


def test_query_with_complex_inline_fragment(swapi_parser, swapi_dataclass_renderer, module_compiler):
def test_simple_query_with_complex_inline_fragment(swapi_parser, swapi_dataclass_renderer, module_compiler):
"""
'''
@dataclass_json
Expand Down
33 changes: 28 additions & 5 deletions tests/test_utils_codegen.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import pytest
from gql.utils_codegen import CodeGenerator
from gql.utils_codegen import CodeChunk


def test_codegen_write_simple_strings(module_compiler):
gen = CodeGenerator()
gen = CodeChunk()
gen.write('def sum(a, b):')
gen.indent()
gen.write('return a + b')
Expand All @@ -15,7 +15,7 @@ def test_codegen_write_simple_strings(module_compiler):


def test_codegen_write_template_strings_args(module_compiler):
gen = CodeGenerator()
gen = CodeChunk()
gen.write('def {0}(a, b):', 'sum')
gen.indent()
gen.write('return a + b')
Expand All @@ -27,7 +27,7 @@ def test_codegen_write_template_strings_args(module_compiler):


def test_codegen_write_template_strings_kwargs(module_compiler):
gen = CodeGenerator()
gen = CodeChunk()
gen.write('def {method}(a, b):', method='sum')
gen.indent()
gen.write('return a + b')
Expand All @@ -38,13 +38,36 @@ def test_codegen_write_template_strings_kwargs(module_compiler):
assert m.sum(2, 3) == 5


def test_codegen_block(module_compiler):
gen = CodeChunk()
gen.write('def sum(a, b):')
with gen.block():
gen.write('return a + b')

code = str(gen)

m = module_compiler(code)
assert m.sum(2, 3) == 5


def test_codegen_write_block(module_compiler):
gen = CodeChunk()
with gen.write_block('def {name}(a, b):', name='sum'):
gen.write('return a + b')

code = str(gen)

m = module_compiler(code)
assert m.sum(2, 3) == 5


def test_codegen_write_lines(module_compiler):
lines = [
'@staticmethod',
'def sum(a, b):'
' return a + b'
]
gen = CodeGenerator()
gen = CodeChunk()
gen.write('class Math:')
gen.indent()
gen.write_lines(lines)
Expand Down

0 comments on commit 073d045

Please sign in to comment.