Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow codegen to process multiple query files in a single command #2911

Merged
merged 3 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Release type: minor

`strawberry codegen` can now operate on multiple input query files.
The previous behavior of naming the file `types.js` and `types.py`
for the builtin `typescript` and `python` plugins respectively is
preserved, but only if a single query file is passed. When more
than one query file is passed, the code generator will now use
the stem of the query file's name to construct the name of the
output files. e.g. `my_query.graphql` -> `my_query.js` or
`my_query.py`. Creators of custom plugins are responsible
for controlling the name of the output file themselves. To
accomodate this, if the `__init__` method of a `QueryCodegenPlugin`
has a parameter named `query` or `query_file`, the `pathlib.Path`
to the query file will be passed to the plugin's `__init__`
method.

Finally, the `ConsolePlugin` has also recieved two new lifecycle
methods. Unlike other `QueryCodegenPlugin`, the same instance of
the `ConsolePlugin` is used for each query file processed. This
allows it to keep state around how many total files were processed.
The `ConsolePlugin` recieved two new lifecycle hooks: `before_any_start`
and `after_all_finished` that get called at the appropriate times.
42 changes: 41 additions & 1 deletion docs/codegen/query-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ With the following command:
strawberry codegen --schema schema --output-dir ./output -p python query.graphql
```

We'll get the following output inside `output/types.py`:
We'll get the following output inside `output/query.py`:

```python
class MyQueryResultUserPost:
Expand Down Expand Up @@ -119,6 +119,13 @@ from strawberry.codegen.types import GraphQLType, GraphQLOperation


class QueryCodegenPlugin:
def __init__(self, query: Path) -> None:
"""Initialize the plugin.

The singular argument is the path to the file that is being processed by this plugin.
"""
self.query = query

def on_start(self) -> None:
...

Expand All @@ -137,3 +144,36 @@ class QueryCodegenPlugin:
- `generated_code` is called when the codegen starts and it receives the types
and the operation. You cans use this to generate code for each type and
operation.

### Console plugin

There is also a plugin that helps to orchestrate the codegen process and notify the
user about what the current codegen process is doing.

The interface for the ConsolePlugin looks like:

```python
class ConsolePlugin:
def __init__(self, output_dir: Path):
"""Initialize the plugin and tell it where the output should be written."""
...

def before_any_start(self) -> None:
"""This method is called before any plugins have been invoked or any queries have been processed."""
...

def after_all_finished(self) -> None:
"""This method is called after the full code generation is complete.

It can be used to report on all the things that have happened during the codegen.
"""
...

def on_start(self, plugins: Iterable[QueryCodegenPlugin], query: Path) -> None:
"""This method is called before any of the individual plugins have been started."""
...

def on_end(self, result: CodegenResult) -> None:
"""This method typically persists the results from a single query to the output directory."""
...
```
73 changes: 30 additions & 43 deletions strawberry/cli/commands/codegen.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
from __future__ import annotations

import functools
import importlib
import inspect
from pathlib import Path # noqa: TCH003
from typing import TYPE_CHECKING, List, Optional, Type
from typing import List, Optional, Type

import rich
import typer

from strawberry.cli.app import app
from strawberry.cli.utils import load_schema
from strawberry.codegen import QueryCodegen, QueryCodegenPlugin

if TYPE_CHECKING:
from strawberry.codegen import CodegenResult
from strawberry.codegen import ConsolePlugin, QueryCodegen, QueryCodegenPlugin


def _is_codegen_plugin(obj: object) -> bool:
return (
inspect.isclass(obj)
and issubclass(obj, QueryCodegenPlugin)
and issubclass(obj, (QueryCodegenPlugin, ConsolePlugin))
and obj is not QueryCodegenPlugin
)

Expand Down Expand Up @@ -62,6 +60,7 @@ def _import_plugin(plugin: str) -> Optional[Type[QueryCodegenPlugin]]:
return None


@functools.lru_cache
def _load_plugin(plugin_path: str) -> Type[QueryCodegenPlugin]:
# try to import plugin_name from current folder
# then try to import from strawberry.codegen.plugins
Expand All @@ -78,43 +77,21 @@ def _load_plugin(plugin_path: str) -> Type[QueryCodegenPlugin]:
return plugin


def _load_plugins(plugins: List[str]) -> List[QueryCodegenPlugin]:
return [_load_plugin(plugin)() for plugin in plugins]


class ConsolePlugin(QueryCodegenPlugin):
def __init__(
self, query: Path, output_dir: Path, plugins: List[QueryCodegenPlugin]
):
self.query = query
self.output_dir = output_dir
self.plugins = plugins

def on_start(self) -> None:
rich.print(
"[bold yellow]The codegen is experimental. Please submit any bug at "
"https://github.com/strawberry-graphql/strawberry\n",
)

plugin_names = [plugin.__class__.__name__ for plugin in self.plugins]

rich.print(
f"[green]Generating code for {self.query} using "
f"{', '.join(plugin_names)} plugin(s)",
)

def on_end(self, result: CodegenResult) -> None:
self.output_dir.mkdir(parents=True, exist_ok=True)
result.write(self.output_dir)
def _load_plugins(plugin_ids: List[str], query: Path) -> List[QueryCodegenPlugin]:
plugins = []
for ptype_id in plugin_ids:
ptype = _load_plugin(ptype_id)
plugin = ptype(query)
plugins.append(plugin)

rich.print(
f"[green] Generated {len(result.files)} files in {self.output_dir}",
)
return plugins


@app.command(help="Generate code from a query")
def codegen(
query: Path = typer.Argument(..., exists=True, dir_okay=False),
query: Optional[List[Path]] = typer.Argument(
default=None, exists=True, dir_okay=False
),
schema: str = typer.Option(..., help="Python path to the schema file"),
app_dir: str = typer.Option(
".",
Expand Down Expand Up @@ -143,12 +120,22 @@ def codegen(
),
cli_plugin: Optional[str] = None,
) -> None:
if not query:
return

schema_symbol = load_schema(schema, app_dir)

console_plugin = _load_plugin(cli_plugin) if cli_plugin else ConsolePlugin
console_plugin_type = _load_plugin(cli_plugin) if cli_plugin else ConsolePlugin
console_plugin = console_plugin_type(output_dir)
console_plugin.before_any_start()

for q in query:
plugins = _load_plugins(selected_plugins, q)
console_plugin.query = q # update the query in the console plugin.

plugins = _load_plugins(selected_plugins)
plugins.append(console_plugin(query, output_dir, plugins))
code_generator = QueryCodegen(
schema_symbol, plugins=plugins, console_plugin=console_plugin
)
code_generator.run(q.read_text())

code_generator = QueryCodegen(schema_symbol, plugins=plugins)
code_generator.run(query.read_text())
console_plugin.after_all_finished()
16 changes: 14 additions & 2 deletions strawberry/codegen/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
from .query_codegen import CodegenFile, CodegenResult, QueryCodegen, QueryCodegenPlugin
from .query_codegen import (
CodegenFile,
CodegenResult,
ConsolePlugin,
QueryCodegen,
QueryCodegenPlugin,
)

__all__ = ["QueryCodegen", "QueryCodegenPlugin", "CodegenFile", "CodegenResult"]
__all__ = [
"CodegenFile",
"CodegenResult",
"ConsolePlugin",
"QueryCodegen",
"QueryCodegenPlugin",
]
8 changes: 6 additions & 2 deletions strawberry/codegen/plugins/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
)

if TYPE_CHECKING:
from pathlib import Path

from strawberry.codegen.types import (
GraphQLArgumentValue,
GraphQLField,
Expand Down Expand Up @@ -46,8 +48,10 @@ class PythonPlugin(QueryCodegenPlugin):
"Decimal": PythonType("Decimal", "decimal"),
}

def __init__(self) -> None:
def __init__(self, query: Path) -> None:
self.imports: Dict[str, Set[str]] = defaultdict(set)
self.outfile_name: str = query.with_suffix(".py").name
self.query = query

def generate_code(
self, types: List[GraphQLType], operation: GraphQLOperation
Expand All @@ -57,7 +61,7 @@ def generate_code(

code = imports + "\n\n" + "\n\n".join(printed_types)

return [CodegenFile("types.py", code.strip())]
return [CodegenFile(self.outfile_name, code.strip())]

def _print_imports(self) -> str:
imports = [
Expand Down
8 changes: 7 additions & 1 deletion strawberry/codegen/plugins/typescript.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
)

if TYPE_CHECKING:
from pathlib import Path

from strawberry.codegen.types import GraphQLField, GraphQLOperation, GraphQLType


Expand All @@ -33,12 +35,16 @@ class TypeScriptPlugin(QueryCodegenPlugin):
float: "number",
}

def __init__(self, query: Path) -> None:
self.outfile_name: str = query.with_suffix(".ts").name
self.query = query

def generate_code(
self, types: List[GraphQLType], operation: GraphQLOperation
) -> List[CodegenFile]:
printed_types = list(filter(None, (self._print_type(type) for type in types)))

return [CodegenFile("types.ts", "\n\n".join(printed_types))]
return [CodegenFile(self.outfile_name, "\n\n".join(printed_types))]

def _get_type_name(self, type_: GraphQLType) -> str:
if isinstance(type_, GraphQLOptional):
Expand Down
Loading