Skip to content

Commit

Permalink
#71 mutations support (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
fishi0x01 authored Feb 22, 2023
1 parent 9165fca commit 84198f5
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 39 deletions.
1 change: 1 addition & 0 deletions docs/plugins/pydantic_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Supported definitions are:

- `fragment`
- `query`
- `mutation` (only response data structures - input data structures are still a [TODO](https://github.com/app-sre/qenerate/issues/71))

## Opinionated Custom Scalars

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "qenerate"
version = "0.4.7"
version = "0.5.0"
description = "Code Generator for GraphQL Query and Fragment Data Classes"
authors = [
"Red Hat - Service Delivery - AppSRE <[email protected]>"
Expand Down
2 changes: 1 addition & 1 deletion qenerate/core/code_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def generate_code(self, introspection_file_path: str, dir: str):
schema=schema,
)

rendered_queries = plugin.generate_queries(
rendered_queries = plugin.generate_operations(
definitions=queries_by_plugin[plugin_name],
fragments=rendered_fragments,
schema=schema,
Expand Down
2 changes: 1 addition & 1 deletion qenerate/core/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Fragment(GeneratedFile):


class Plugin:
def generate_queries(
def generate_operations(
self,
definitions: list[GQLDefinition],
schema: GraphQLSchema,
Expand Down
33 changes: 21 additions & 12 deletions qenerate/core/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(self, message: str):
class GQLDefinitionType(Enum):
QUERY = 1
FRAGMENT = 2
MUTATION = 3


@dataclass
Expand Down Expand Up @@ -79,23 +80,31 @@ def enter_operation_definition(self, node: OperationDefinitionNode, *_):
body = self._node_body(node)
name = self._node_name(node)

if node.operation != OperationType.QUERY:
if node.operation == OperationType.QUERY:
definition = GQLDefinition(
kind=GQLDefinitionType.QUERY,
definition=body,
source_file=self._source_file_path,
feature_flags=self._feature_flags,
fragment_dependencies=set(),
name=name,
)
elif node.operation == OperationType.MUTATION:
definition = GQLDefinition(
kind=GQLDefinitionType.MUTATION,
definition=body,
source_file=self._source_file_path,
feature_flags=self._feature_flags,
fragment_dependencies=set(),
name=name,
)
else:
# TODO: logger
# TODO: raise
print(
"[WARNING] Skipping operation definition because"
f" it is not a query: \n{body}"
f" it is neither a query nor a mutation: \n{body}"
)
return

definition = GQLDefinition(
kind=GQLDefinitionType.QUERY,
definition=body,
source_file=self._source_file_path,
feature_flags=self._feature_flags,
fragment_dependencies=set(),
name=name,
)
self._stack.append(definition)

def leave_operation_definition(self, *_):
Expand Down
50 changes: 37 additions & 13 deletions qenerate/plugins/pydantic_v1/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
ParsedClassNode,
)
from qenerate.core.feature_flag_parser import NamingCollisionStrategy, FeatureFlags
from qenerate.core.preprocessor import GQLDefinition
from qenerate.core.preprocessor import GQLDefinition, GQLDefinitionType

from qenerate.plugins.pydantic_v1.mapper import (
graphql_class_name_to_python,
Expand Down Expand Up @@ -66,7 +66,7 @@
)


def convenience_function(cls: str) -> str:
def query_convenience_function(cls: str) -> str:
return f"""
def query(query_func: Callable, **kwargs: Any) -> {cls}:
{INDENT}\"\"\"
Expand All @@ -88,6 +88,29 @@ def query(query_func: Callable, **kwargs: Any) -> {cls}:
"""


def mutation_convenience_function(cls: str) -> str:
return f"""
def mutate(mutation_func: Callable, **kwargs: Any) -> {cls}:
{INDENT}\"\"\"
{INDENT}This is a convenience function which executes a mutation and parses the response
{INDENT}into concrete types. It should be compatible with most GQL clients.
{INDENT}You do not have to use it to consume the generated data classes.
{INDENT}Alternatively, you can also mime and alternate the behavior
{INDENT}of this function in the caller.
{INDENT}Parameters:
{INDENT}{INDENT}mutation_func (Callable): Function which executes the mutation.
{INDENT}{INDENT}kwargs: Arguments that will be passed to the mutation function.
{INDENT}{INDENT}{INDENT}This must include the mutation parameters.
{INDENT}Returns:
{INDENT}{INDENT}{cls}: mutation response parsed into generated classes
{INDENT}\"\"\"
{INDENT}raw_data: dict[Any, Any] = mutation_func(DEFINITION, **kwargs)
{INDENT}return {cls}(**raw_data)
"""


class PydanticV1Error(Exception):
pass

Expand All @@ -97,7 +120,7 @@ def __init__(
self,
schema: GraphQLSchema,
type_info: TypeInfo,
definition: str,
definition: GQLDefinition,
feature_flags: FeatureFlags,
):
Visitor.__init__(self)
Expand Down Expand Up @@ -140,6 +163,7 @@ def enter_operation_definition(self, node: OperationDefinitionNode, *_):
current = ParsedOperationNode(
parent=self.parent,
fields=[],
operation_type=self.definition.kind,
parsed_type=ParsedFieldType(
unwrapped_python_type=node.name.value,
wrapped_python_type=node.name.value,
Expand Down Expand Up @@ -266,9 +290,9 @@ def _to_python_type(self, graphql_type: GraphQLOutputType) -> str:
class QueryParser:
@staticmethod
def parse(
definition: str, schema: GraphQLSchema, feature_flags: FeatureFlags
definition: GQLDefinition, schema: GraphQLSchema, feature_flags: FeatureFlags
) -> ParsedNode:
document_ast = parse(definition)
document_ast = parse(definition.definition)
type_info = TypeInfo(schema)
visitor = FieldToTypeMatcherVisitor(
schema=schema,
Expand Down Expand Up @@ -339,9 +363,8 @@ def generate_fragments(
result += fragment_imports
qf = definition.source_file
parser = QueryParser()
fragment_definition = definition.definition
ast = parser.parse(
definition=fragment_definition,
definition=definition,
schema=schema,
feature_flags=definition.feature_flags,
)
Expand Down Expand Up @@ -400,7 +423,7 @@ def _assemble_definition(
)
return ans

def generate_queries(
def generate_operations(
self,
definitions: list[GQLDefinition],
schema: GraphQLSchema,
Expand Down Expand Up @@ -433,17 +456,18 @@ def generate_queries(
)
result += 'DEFINITION = """\n' f"{assembled_definition}" '\n"""'
parser = QueryParser()
query = definition.definition
ast = parser.parse(
definition=query,
definition=definition,
schema=schema,
feature_flags=definition.feature_flags,
)
result += self._traverse(ast)
result += "\n\n"
result += convenience_function(
cls=f"{ast.fields[0].parsed_type.unwrapped_python_type}QueryData"
)
cls = ast.fields[0].parsed_type.unwrapped_python_type
if definition.kind == GQLDefinitionType.QUERY:
result += query_convenience_function(cls=f"{cls}QueryData")
else:
result += mutation_convenience_function(cls=f"{cls}MutationResponse")
generated_files.append(
GeneratedFile(file=qf.with_suffix(".py"), content=result)
)
Expand Down
9 changes: 8 additions & 1 deletion qenerate/plugins/pydantic_v1/typed_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from dataclasses import dataclass
from typing import Any, Optional

from qenerate.core.preprocessor import GQLDefinitionType


INDENT = " "

Expand Down Expand Up @@ -155,10 +157,15 @@ def field_type(self) -> str:

@dataclass
class ParsedOperationNode(ParsedNode):
operation_type: GQLDefinitionType

def class_code_string(self) -> str:
lines = ["\n\n"]
class_suffix = "QueryData"
if self.operation_type == GQLDefinitionType.MUTATION:
class_suffix = "MutationResponse"
lines.append(
f"class {self.parsed_type.unwrapped_python_type}QueryData(BaseModel):"
f"class {self.parsed_type.unwrapped_python_type}{class_suffix}(BaseModel):"
)
for field in self.fields:
if isinstance(field, ParsedClassNode):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

setup_kwargs = {
"name": "qenerate",
"version": "0.4.7",
"version": "0.5.0",
"description": "Code Generator for GraphQL Query and Fragment Data Classes",
"long_description": "Qenerate is a Code Generator for GraphQL Query and Fragment Data Classes. Documentation is at https://github.com/app-sre/qenerate .",
"author": "Service Delivery - AppSRE",
Expand Down
2 changes: 1 addition & 1 deletion tests/core/preprocessor/queries/mutation.gql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# qenerate: plugin=test
# This should be ignored by qenerate

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
Expand Down
2 changes: 1 addition & 1 deletion tests/core/test_code_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def generate_fragments(
) -> list[Fragment]:
return []

def generate_queries(
def generate_operations(
self,
definitions: list[GQLDefinition],
fragments: list[Fragment],
Expand Down
11 changes: 10 additions & 1 deletion tests/core/test_preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,16 @@ def normalize_definition(definition: str) -> str:
# Test a file containing a mutation
[
Path("tests/core/preprocessor/queries/mutation.gql"),
[],
[
GQLDefinition(
feature_flags=FeatureFlags(plugin="test"),
kind=GQLDefinitionType.MUTATION,
name="CreateReviewForEpisode",
definition="mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { stars commentary } }",
fragment_dependencies=set(),
source_file="", # adjusted in test
),
],
],
# Test a file containing a single fragment
[
Expand Down
14 changes: 14 additions & 0 deletions tests/generator/definitions/github/comment_mutation.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mutation AddComment($body: String = "", $subjectId: ID = "") {
addComment(input: {subjectId: $subjectId, body: $body}) {
subject {
... on Topic {
id
name
}
... on User {
id
email
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
"""
from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
from datetime import datetime # noqa: F401 # pylint: disable=W0611
from enum import Enum # noqa: F401 # pylint: disable=W0611
from typing import ( # noqa: F401 # pylint: disable=W0611
Any,
Optional,
Union,
)

from pydantic import ( # noqa: F401 # pylint: disable=W0611
BaseModel,
Extra,
Field,
Json,
)


DEFINITION = """
mutation AddComment($body: String = "", $subjectId: ID = "") {
addComment(input: {subjectId: $subjectId, body: $body}) {
subject {
... on Topic {
id
name
}
... on User {
id
email
}
}
}
}

"""


class Node(BaseModel):

class Config:
smart_union = True
extra = Extra.forbid


class Topic(Node):
q_id: str = Field(..., alias="id")
name: str = Field(..., alias="name")

class Config:
smart_union = True
extra = Extra.forbid


class User(Node):
q_id: str = Field(..., alias="id")
email: str = Field(..., alias="email")

class Config:
smart_union = True
extra = Extra.forbid


class AddCommentPayload(BaseModel):
subject: Optional[Union[Topic, User, Node]] = Field(..., alias="subject")

class Config:
smart_union = True
extra = Extra.forbid


class AddCommentMutationResponse(BaseModel):
add_comment: Optional[AddCommentPayload] = Field(..., alias="addComment")

class Config:
smart_union = True
extra = Extra.forbid


def mutate(mutation_func: Callable, **kwargs: Any) -> AddCommentMutationResponse:
"""
This is a convenience function which executes a mutation and parses the response
into concrete types. It should be compatible with most GQL clients.
You do not have to use it to consume the generated data classes.
Alternatively, you can also mime and alternate the behavior
of this function in the caller.

Parameters:
mutation_func (Callable): Function which executes the mutation.
kwargs: Arguments that will be passed to the mutation function.
This must include the mutation parameters.

Returns:
AddCommentMutationResponse: mutation response parsed into generated classes
"""
raw_data: dict[Any, Any] = mutation_func(DEFINITION, **kwargs)
return AddCommentMutationResponse(**raw_data)
Loading

0 comments on commit 84198f5

Please sign in to comment.