diff --git a/PLUGINS.md b/PLUGINS.md deleted file mode 100644 index 691e5b9f..00000000 --- a/PLUGINS.md +++ /dev/null @@ -1,327 +0,0 @@ -# Plugins - -## How to implement plugin - -Plugin is a class extending `ariadne_codegen.plugins.base.Plugin`. - -Plugins are instantiated once, at the beginning of `ariadne-codegen` command, with following arguments: - -- `schema: GraphQLSchema`: parsed graphql schema -- `config_dict: dict`: parsed `pyproject.toml` file represented as a dictionary - -To handle specific events custom plugins need to override [hook methods](#hooks) from default `Plugin`. Default hook methods from `Plugin` don't implement any logic on their own. - -## Enabling plugins - -Plugins can be enabled in `ariadne-codegen` by assigning list of strings to `plugins` key in config file. -Every element of the list can enable plugins in 2 ways: - -1. Providing full import path to class, eg. `my_package.my_plugins.MyPlugin`. - -2. Providing only package name, eg. `my_package`. In this case all plugins from selected package will be used. For class to be accessible this way, it needs to be in package's public api. Plugin has to be importable from package, eg. `from my_package import MyPlugin` has to work. - -## Hooks - -### generate_init_module - -```py -def generate_init_module(self, module: ast.Module) -> ast.Module: -``` - -Hook executed on generation of init module. Module has list of public, generated classes and reimports them all. Later this module will be saved as `__init__.py`. - -### generate_init_import - -```py -def generate_init_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: -``` - -Hook executed on addition of import to init module. Later this import will be placed in `__init__.py`. - -### generate_enum - -```py -def generate_enum(self, class_def: ast.ClassDef, enum_type: GraphQLEnumType) -> ast.ClassDef: -``` - -Hook executed on generation of class definition of single enum. - -### generate_enums_module - -```py -def generate_enums_module(self, module: ast.Module) -> ast.Module: -``` - -Hook executed on generation of enums module. Module has all classes representing enums from schema. Later this module will be saved as `{enums_module_name}.py`, `enums_module_name` is taken from config. - -### generate_client_module - -```py -def generate_client_module(self, module: ast.Module) -> ast.Module: -``` - -Hook executed on generation of client module. Module contains `gql` function definition and client class. Later this module will be saved as `{client_file_name}.py`, `client_file_name` is taken from config. - -### generate_gql_function - -```py -def generate_gql_function(self, function_def: ast.FunctionDef) -> ast.FunctionDef: -``` - -Hook executed on generation of `gql` function. - -### generate_client_class - -```py -def generate_client_class(self, class_def: ast.ClassDef) -> ast.ClassDef: -``` - -Hook executed on generation of client class. Class contains method for every graphql operation. - -### generate_client_import - -```py -def generate_client_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: -``` - -Hook executed on addition of import to client module. Later this import will be placed in `{client_file_name}.py`, `client_file_name` is taken from config. - -### generate_client_method - -```py -def generate_client_method( - self, - method_def: Union[ast.FunctionDef, ast.AsyncFunctionDef], - operation_definition: OperationDefinitionNode, -) -> Union[ast.FunctionDef, ast.AsyncFunctionDef]: -``` - -Hook executed on generation of client's method, which represents single graphql operation. Depends on the configuration method can be either async or not. -` - -### generate_arguments - -```py -def generate_arguments( - self, - arguments: ast.arguments, - variable_definitions: tuple[VariableDefinitionNode, ...], -) -> ast.arguments: -``` - -Hook executed on generation of arguments for specific client's method. - -### generate_arguments_dict - -```py -def generate_arguments_dict( - self, - dict_: ast.Dict, - variable_definitions: tuple[VariableDefinitionNode, ...], -) -> ast.Dict: -``` - -Hook executed on generation of dictionary with arguments of graphql operation. Serialized dictionary is later used as variables payload. - -### generate_inputs_module - -```py -def generate_inputs_module(self, module: ast.Module) -> ast.Module: -``` - -Hook executed on generation of inputs module. Module has all classes representing inputs from schema. Later this module will be saved as `{input_types_module_name}.py`, `input_types_module_name` is taken from config. - -### generate_input_class - -```py -def generate_input_class( - self, class_def: ast.ClassDef, input_type: GraphQLInputObjectType -) -> ast.ClassDef: -``` - -Hook executed on generation of class definition for input from schema. - -### generate_input_field - -```py - def generate_input_field( - self, - field_implementation: ast.AnnAssign, - input_field: GraphQLInputField, - field_name: str, - ) -> ast.AnnAssign: -``` - -Hook executed on generation of representation for input field. - -### generate_result_types_module - -```py -def generate_result_types_module( - self, module: ast.Module, operation_definition: ExecutableDefinitionNode -) -> ast.Module: -``` - -Hook executed on generation of module with models reprenting result of given operation. - -### generate_operation_str - -```py -def generate_operation_str( - self, operation_str: str, operation_definition: ExecutableDefinitionNode -) -> str: -``` - -Hook executed on generation of string representation of given operation. Result is later used by generated client as part of payload sent to graphql server. - -### generate_result_class - -```py -def generate_result_class( - self, - class_def: ast.ClassDef, - operation_definition: ExecutableDefinitionNode, - selection_set: SelectionSetNode, -) -> ast.ClassDef: -``` - -Hook executed on generation of single model, part of result of given query or mutation. - -### generate_result_field - -```py -def generate_result_field( - self, - field_implementation: ast.AnnAssign, - operation_definition: ExecutableDefinitionNode, - field: FieldNode, -) -> ast.AnnAssign: -``` - -Hook executed on generation of single model field. - -### generate_client_code - -```py -def generate_client_code(self, generated_code: str) -> str: -``` - -Hook executed on generation of client code. Result is used as content of `{client_file_name}.py`, `client_file_name` is taken from config. - -### generate_enums_code - -```py -def generate_enums_code(self, generated_code: str) -> str: -``` - -Hook executed on generation of enums code. Result is used as content of `{enums_module_name}.py`, `enums_module_name` is taken from config. - -### generate_inputs_code - -```py -def generate_inputs_code(self, generated_code: str) -> str: -``` - -Hook executed on generation of input models code. Result is used as content of `{input_types_module_name}.py`, `input_types_module_name` is taken from config. - -### generate_result_types_code - -```py -def generate_result_types_code(self, generated_code: str) -> str: -``` - -Hook executed on generation of result models code for one operation. Result is used as content of `{operation_name}.py`. - -### copy_code - -```py -def copy_code(self, copied_code: str) -> str: -``` - -Hook executed on coping file's content to result package. -Files hook is called for: - -- `base_client.py` or `async_base_client.py` or custom base client `base_client_file_path` -- `base_model.py` -- `exceptions.py` -- all files from config's `files_to_include` - -### generate_init_code - -```py -def generate_init_code(self, generated_code: str) -> str: -``` - -Hook executed on generation of init code. Result is used as content of `__init__.py`. - -### process_name - -```py -def process_name(self, name: str, node: Optional[Node] = None) -> str: -``` - -Hook executed on processing of GraphQL field, argument or operation name. - -### generate_fragments_module - -```py -def generate_fragments_module( - self, - module: ast.Module, - fragments_definitions: dict[str, FragmentDefinitionNode], -) -> ast.Module: -``` - -Hook executed on generation of fragments module. Module has classes representing all fragments from provided queries. Later this module will be saved as `{fragments_module_name}.py`, `fragments_module_name` is taken from config. - -### process_schema - -```py -def process_schema(self, schema: GraphQLSchema) -> GraphQLSchema: -``` - -Hook executed on creating `GraphQLSchema` object from path or url provided in settings. During parsing `assume_valid` is set to `True`. Then this hook is called, and only after that `graphql.assert_valid_schema` is used to validate schema. -To ensure all plugins have current version of schema, result of this hook is propagated to all plugins, updating their `schema` field. - -### get_file_comment - -```py -def get_file_comment( - self, comment: str, code: str, source: Optional[str] = None -) -> str: -``` - -Hook executed on generating comment included at the beginning of a generated code. - -## Example - -This example plugin adds `__version__ = "..."` to generated `__init__.py` file. - -```py -import ast - -from ariadne_codegen.plugins.base import Plugin - - -class VersionPlugin(Plugin): - def generate_init_module(self, module: ast.Module) -> ast.Module: - version = ( - self.config_dict.get("tool", {}) - .get("version_plugin", {}) - .get("version", "0.1") - ) - assign = ast.Assign( - targets=[ast.Name(id="__version__")], - value=ast.Constant(value=version), - lineno=len(module.body) + 1, - ) - module.body.append(assign) - return module -``` - -`VersionPlugin` reads version from parsed `pyproject.toml`, eg. following entry will produce `__version__ = "0.21"`. - -```toml -[tool.version_plugin] -version = 0.21 -``` diff --git a/docs/06-plugins/01-intro.md b/docs/06-plugins/01-intro.md new file mode 100644 index 00000000..809130a9 --- /dev/null +++ b/docs/06-plugins/01-intro.md @@ -0,0 +1,36 @@ +--- +title: Introduction +--- + +# Plugins + +Plugins let you hook into `ariadne-codegen` to inspect or modify the GraphQL schema and the generated code before it is written to disk. A plugin is a Python class extending `ariadne_codegen.plugins.base.Plugin` that overrides one or more [hooks](./03-hooks.md) - each hook fires at a specific point of a generation run and receives the value being produced (a schema, an `ast` node, or a source string), returning it modified or unchanged. + +## Why use a plugin + +Plugins are the supported extension point when the built-in configuration is not enough. Common use cases: + +- **Add file headers** or license banners to every generated module. +- **Inject fields or metadata** into generated Pydantic models (e.g. an `__operation__` marker on result classes). +- **Transform the final source** - rename symbols, rewrite imports, reformat output. +- **Inspect or rewrite the schema** at startup before any code is generated. +- **Read your own configuration** from `pyproject.toml` to drive any of the above. + +## Real-world examples + +The best worked examples are the [standard plugins](./04-standard-plugins.md) shipped in `ariadne_codegen.contrib` - each is a small, production plugin whose source is worth reading: + +- [`ShorterResultsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/shorter_results.py) - rewrites generated client methods to return a single top-level field directly instead of the full result type. +- [`ExtractOperationsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/extract_operations.py) - moves query strings out into a separate module and rewrites the client's imports; also reads its own `[tool.ariadne-codegen.extract-operations]` config. +- [`ClientForwardRefsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/client_forward_refs.py) - moves Pydantic model imports under `TYPE_CHECKING` to speed up importing the client module. +- [`NoReimportsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/no_reimports.py) - empties the generated `__init__.py` to avoid eager initialization of large packages. + +## How they work + +Plugins are instantiated once, at the start of the `ariadne-codegen` command, and run as a **sequential pipeline** - each plugin receives the output of the previous one. See [Plugin execution model](./02-custom-plugins.md#plugin-execution-model) for the details. + +## Next steps + +- [Custom plugins](./02-custom-plugins.md) - write and enable your own plugin. +- [Hooks](./03-hooks.md) - the full reference of every hook, in the order it fires. +- [Standard plugins](./04-standard-plugins.md) - ready-to-use plugins shipped with `ariadne-codegen`. diff --git a/docs/06-plugins/02-custom-plugins.md b/docs/06-plugins/02-custom-plugins.md index cdb1dcf6..51693b0e 100644 --- a/docs/06-plugins/02-custom-plugins.md +++ b/docs/06-plugins/02-custom-plugins.md @@ -2,7 +2,7 @@ title: Custom plugins --- -# How to implement plugin +# Custom plugins Plugin is a class extending `ariadne_codegen.plugins.base.Plugin`. @@ -11,7 +11,7 @@ Plugins are instantiated once, at the beginning of `ariadne-codegen` command, wi - `schema: GraphQLSchema`: parsed graphql schema - `config_dict: dict`: parsed `pyproject.toml` file represented as a dictionary -To handle specific events custom plugins need to override [hook methods](#hooks) from default `Plugin`. Default hook methods from `Plugin` don't implement any logic on their own. +To handle specific events custom plugins need to override [hook methods](./03-hooks.md) from default `Plugin`. Default hook methods from `Plugin` don't implement any logic on their own. ## Enabling plugins @@ -22,282 +22,143 @@ Every element of the list can enable plugins in 2 ways: 2. Providing only package name, eg. `my_package`. In this case all plugins from selected package will be used. For class to be accessible this way, it needs to be in package's public api. Plugin has to be importable from package, eg. `from my_package import MyPlugin` has to work. -## Hooks +## Plugin execution model -### generate_init_module +### Sequential pipeline -```py -def generate_init_module(self, module: ast.Module) -> ast.Module: -``` - -Hook executed on generation of init module. Module has list of public, generated classes and reimports them all. Later this module will be saved as `__init__.py`. - -### generate_init_import - -```py -def generate_init_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: -``` - -Hook executed on addition of import to init module. Later this import will be placed in `__init__.py`. - -### generate_enum - -```py -def generate_enum(self, class_def: ast.ClassDef, enum_type: GraphQLEnumType) -> ast.ClassDef: -``` - -Hook executed on generation of class definition of single enum. +All plugins run in the order they are listed under `plugins` in `pyproject.toml`. Each plugin receives the **output of the previous plugin** as its input, not the original value. -### generate_enums_module +There is no priority mechanism. Every plugin executes every hook it defines. Order is controlled entirely by the position in the `plugins` list. -```py -def generate_enums_module(self, module: ast.Module) -> ast.Module: -``` +### Two hook levels -Hook executed on generation of enums module. Module has all classes representing enums from schema. Later this module will be saved as `{enums_module_name}.py`, `enums_module_name` is taken from config. +**AST-level hooks** (`generate_*_module`, `generate_*_class`, `generate_*_field`, `generate_*_method`, etc.) - fire during code generation, before the AST is serialised to a string. You work with Python `ast` node objects. -### generate_client_module +**Code-string hooks** (`generate_*_code`, `copy_code`) - fire after AST→string conversion, immediately before each file is written to disk. You work with the final Python source as a plain string. These are the closest equivalent to a file-emission interceptor. -```py -def generate_client_module(self, module: ast.Module) -> ast.Module: -``` +### `process_schema` special case -Hook executed on generation of client module. Module contains `gql` function definition and client class. Later this module will be saved as `{client_file_name}.py`, `client_file_name` is taken from config. +After each plugin's `process_schema()` returns, `self.schema` is updated on **all** plugin instances - not just the ones that haven't run yet. This guarantees that every subsequent hook in every plugin sees the schema as last modified. -### generate_gql_function +### Post-generation -```py -def generate_gql_function(self, function_def: ast.FunctionDef) -> ast.FunctionDef: -``` +There is currently no hook that fires after **all** files have been written. The `generate_*_code` / `copy_code` hooks cover per-file interception. If you need a post-generation callback (e.g. to run a formatter over the whole output directory), open an issue. -Hook executed on generation of `gql` function. +### Custom operations and fields coverage -### generate_client_class +The files generated with `enable_custom_operations = true` are reached by only a few hooks. None of them have a code-string hook, and the custom-fields files have no dedicated AST hook. -```py -def generate_client_class(self, class_def: ast.ClassDef) -> ast.ClassDef: -``` - -Hook executed on generation of client class. Class contains method for every graphql operation. - -### generate_client_import +| File | AST hook | `generate_client_import` | `get_file_comment` | +|------|----------|:---:|:---:| +| `custom_queries.py` / `custom_mutations.py` | `generate_custom_method`, `generate_custom_module` | ✅ | ✅ | +| `custom_fields.py` | none | ✅ | ✅ | +| `custom_typing_fields.py` | none | none | ✅ | -```py -def generate_client_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: -``` +## Examples -Hook executed on addition of import to client module. Later this import will be placed in `{client_file_name}.py`, `client_file_name` is taken from config. +### Pipeline - two plugins on the same hook -### generate_client_method +`PluginA` and `PluginB` both implement `generate_client_class`. When registered as `plugins = ["my_pkg.PluginA", "my_pkg.PluginB"]`, `PluginB` receives the `class_def` already modified by `PluginA`. ```py -def generate_client_method( - self, - method_def: Union[ast.FunctionDef, ast.AsyncFunctionDef], - operation_definition: OperationDefinitionNode, -) -> Union[ast.FunctionDef, ast.AsyncFunctionDef]: -``` - -Hook executed on generation of client's method, which represents single graphql operation. Depends on the configuration method can be either async or not. -` - -### generate_arguments - -```py -def generate_arguments( - self, - arguments: ast.arguments, - variable_definitions: Tuple[VariableDefinitionNode, ...], -) -> ast.arguments: -``` - -Hook executed on generation of arguments for specific client's method. - -### generate_arguments_dict - -```py -def generate_arguments_dict( - self, - dict_: ast.Dict, - variable_definitions: Tuple[VariableDefinitionNode, ...], -) -> ast.Dict: -``` - -Hook executed on generation of dictionary with arguments of graphql operation. Serialized dictionary is later used as variables payload. - -### generate_inputs_module - -```py -def generate_inputs_module(self, module: ast.Module) -> ast.Module: -``` - -Hook executed on generation of inputs module. Module has all classes representing inputs from schema. Later this module will be saved as `{input_types_module_name}.py`, `input_types_module_name` is taken from config. - -### generate_input_class - -```py -def generate_input_class( - self, class_def: ast.ClassDef, input_type: GraphQLInputObjectType -) -> ast.ClassDef: -``` - -Hook executed on generation of class definition for input from schema. - -### generate_input_field - -```py - def generate_input_field( - self, - field_implementation: ast.AnnAssign, - input_field: GraphQLInputField, - field_name: str, - ) -> ast.AnnAssign: -``` - -Hook executed on generation of representation for input field. - -### generate_result_types_module - -```py -def generate_result_types_module( - self, module: ast.Module, operation_definition: ExecutableDefinitionNode -) -> ast.Module: -``` - -Hook executed on generation of module with models reprenting result of given operation. - -### generate_operation_str - -```py -def generate_operation_str( - self, operation_str: str, operation_definition: ExecutableDefinitionNode -) -> str: -``` - -Hook executed on generation of string representation of given operation. Result is later used by generated client as part of payload sent to graphql server. - -### generate_result_class - -```py -def generate_result_class( - self, - class_def: ast.ClassDef, - operation_definition: ExecutableDefinitionNode, - selection_set: SelectionSetNode, -) -> ast.ClassDef: -``` - -Hook executed on generation of single model, part of result of given query or mutation. - -### generate_result_field - -```py -def generate_result_field( - self, - field_implementation: ast.AnnAssign, - operation_definition: ExecutableDefinitionNode, - field: FieldNode, -) -> ast.AnnAssign: -``` - -Hook executed on generation of single model field. - -### generate_client_code - -```py -def generate_client_code(self, generated_code: str) -> str: -``` +import ast +from ariadne_codegen.plugins.base import Plugin -Hook executed on generation of client code. Result is used as content of `{client_file_name}.py`, `client_file_name` is taken from config. -### generate_enums_code +class PluginA(Plugin): + def generate_client_class(self, class_def: ast.ClassDef) -> ast.ClassDef: + docstring = ast.Expr(value=ast.Constant(value="Auto-generated client.")) + class_def.body.insert(0, docstring) + return class_def -```py -def generate_enums_code(self, generated_code: str) -> str: -``` -Hook executed on generation of enums code. Result is used as content of `{enums_module_name}.py`, `enums_module_name` is taken from config. - -### generate_inputs_code - -```py -def generate_inputs_code(self, generated_code: str) -> str: +class PluginB(Plugin): + def generate_client_class(self, class_def: ast.ClassDef) -> ast.ClassDef: + # class_def.body[0] is already the docstring inserted by PluginA + class_def.name = "Generated" + class_def.name + return class_def ``` -Hook executed on generation of input models code. Result is used as content of `{input_types_module_name}.py`, `input_types_module_name` is taken from config. - -### generate_result_types_code +Result in `client.py`: ```py -def generate_result_types_code(self, generated_code: str) -> str: +class GeneratedClient(AsyncBaseClient): + """Auto-generated client.""" + ... ``` -Hook executed on generation of result models code for one operation. Result is used as content of `{operation_name}.py`. +### AST hook - adding a field to every result class -### copy_code +`generate_result_class` fires once per Pydantic model class inside a result file (e.g. `get_user.py`). There can be multiple classes per operation - one for the top-level result and one for each nested type. This plugin adds `__operation__: str = "GetUser"` as the first field of every result class. ```py -def copy_code(self, copied_code: str) -> str: -``` - -Hook executed on copying file's content to result package. -Files hook is called for: - -- `base_client.py` or `async_base_client.py` or custom base client `base_client_file_path` -- `base_model.py` -- `exceptions.py` -- all files from config's `files_to_include` +import ast +from ariadne_codegen.plugins.base import Plugin -### generate_init_code -```py -def generate_init_code(self, generated_code: str) -> str: +class ASTPlugin(Plugin): + def generate_result_class( + self, class_def, operation_definition, selection_set + ) -> ast.ClassDef: + op_name = ( + operation_definition.name.value if operation_definition.name else "unknown" + ) + attr = ast.AnnAssign( + target=ast.Name(id="__operation__"), + annotation=ast.Name(id="str"), + value=ast.Constant(value=op_name), + simple=1, + lineno=1, + col_offset=0, + ) + class_def.body.insert(0, attr) + return class_def ``` -Hook executed on generation of init code. Result is used as content of `__init__.py`. +### Code-string hook - adding a header to generated files -### process_name +`generate_client_code` and `generate_enums_code` receive the final Python source as a plain string, immediately before the file is written to disk. This is the right place for text-based transformations that are simpler to do on a string than on an AST. ```py -def process_name(self, name: str, node: Optional[Node] = None) -> str: -``` +from ariadne_codegen.plugins.base import Plugin -Hook executed on processing of GraphQL field, argument or operation name. -### generate_fragments_module +class HeaderPlugin(Plugin): + def generate_client_code(self, generated_code: str) -> str: + header = "\n".join([ + "# ──────────────────────────────────────────", + "# Auto-generated - do not edit manually", + "# Generated by ariadne-codegen", + "# ──────────────────────────────────────────", + "", + ]) + return header + generated_code -```py -def generate_fragments_module( - self, - module: ast.Module, - fragments_definitions: Dict[str, FragmentDefinitionNode], -) -> ast.Module: + def generate_enums_code(self, generated_code: str) -> str: + return self.generate_client_code(generated_code) ``` -Hook executed on generation of fragments module. Module has classes representing all fragments from provided queries. Later this module will be saved as `{fragments_module_name}.py`, `fragments_module_name` is taken from config. +### `process_schema` - inspecting the schema at startup -### process_schema +`process_schema` runs once, before any file is generated. `self.schema` on all plugin instances is updated after each plugin's `process_schema` returns, so every subsequent hook call sees the latest schema. ```py -def process_schema(self, schema: GraphQLSchema) -> GraphQLSchema: -``` - -Hook executed on creating `GraphQLSchema` object from path or url provided in settings. During parsing `assume_valid` is set to `True`. Then this hook is called, and only after that `graphql.assert_valid_schema` is used to validate schema. -To ensure all plugins have current version of schema, result of this hook is propagated to all plugins, updating their `schema` field. +from graphql import GraphQLSchema +from ariadne_codegen.plugins.base import Plugin -### get_file_comment -```py -def get_file_comment( - self, comment: str, code: str, source: Optional[str] = None -) -> str: +class SchemaPlugin(Plugin): + def process_schema(self, schema: GraphQLSchema) -> GraphQLSchema: + query_type = schema.query_type + if query_type and "_version" not in query_type.fields: + print("[SchemaPlugin] Query type is missing _version field") + # Return the schema unmodified, or return a new GraphQLSchema to replace it. + return schema ``` -Hook executed on generating comment included at the beginning of a generated code. - -## Example +### Reading config - VersionPlugin -This example plugin adds `__version__ = "..."` to generated `__init__.py` file. +Adds `__version__ = "..."` to the generated `__init__.py` by reading a value from `pyproject.toml`. ```py import ast @@ -321,9 +182,7 @@ class VersionPlugin(Plugin): return module ``` -`VersionPlugin` reads version from parsed `pyproject.toml`, eg. following entry will produce `__version__ = "0.21"`. - ```toml [tool.version_plugin] -version = 0.21 +version = "0.21" ``` diff --git a/docs/06-plugins/03-hooks.md b/docs/06-plugins/03-hooks.md new file mode 100644 index 00000000..b4858aaa --- /dev/null +++ b/docs/06-plugins/03-hooks.md @@ -0,0 +1,292 @@ +--- +title: Hooks +--- + +# Hooks + +Hooks are the methods a custom plugin overrides to take part in code generation. They are defined on the base `Plugin` class in [`ariadne_codegen/plugins/base.py`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/plugins/base.py) (importable as `ariadne_codegen.plugins.base.Plugin`). + +Every hook has a default implementation that returns its input unchanged, so a plugin only needs to override the hooks it cares about. See [Custom plugins](./02-custom-plugins.md) for how to write and enable a plugin. + +Hooks below are listed in the order they fire during a single `ariadne-codegen` run. + + +### process_schema + +```py +def process_schema(self, schema: GraphQLSchema) -> GraphQLSchema: +``` + +Called once at startup, immediately after the schema is loaded from path or URL and before it is validated. The schema is built with `assume_valid=True` (in [`ariadne_codegen/schema.py`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/schema.py)), which defers validation: `ariadne-codegen` only calls `graphql.assert_valid_schema` *after* this hook returns (see [`main.py`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/main.py)). That makes `process_schema` the one place where a plugin can repair a schema that would otherwise fail validation. It is also the earliest point at which plugins can inspect or modify the schema. After each plugin's `process_schema` returns, `self.schema` is updated on **all** plugin instances. + +### generate_client_import + +```py +def generate_client_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: +``` + +Called every time an import statement is registered in any of the generated modules: the client module (`client.py`), and the custom operation modules (`custom_queries.py`, `custom_mutations.py`, `custom_fields.py`). Fires before the import is appended to the module's import list. + +### process_name + +```py +def process_name(self, name: str, node: Optional[Node] = None) -> str: +``` + +Called throughout the entire run, every time a GraphQL field, argument, or operation name is converted to a Python identifier. This hook fires far more frequently than any other - once for each name across all types, operations, and fields. The optional `node` parameter identifies the GraphQL AST node being named. + +### generate_enum + +```py +def generate_enum(self, class_def: ast.ClassDef, enum_type: GraphQLEnumType) -> ast.ClassDef: +``` + +Called once per enum type in the schema, after the class definition is built but before it is added to the enums module. + +### generate_input_field + +```py +def generate_input_field( + self, + field_implementation: ast.AnnAssign, + input_field: GraphQLInputField, + field_name: str, +) -> ast.AnnAssign: +``` + +Called once per field in each input type, after the field annotation and default value are generated. Fires before `generate_input_class` for the same class. + +### generate_input_class + +```py +def generate_input_class( + self, class_def: ast.ClassDef, input_type: GraphQLInputObjectType +) -> ast.ClassDef: +``` + +Called once per input type, after all its fields have been generated. Fires after all `generate_input_field` calls for the same class. + +### generate_result_field + +```py +def generate_result_field( + self, + field_implementation: ast.AnnAssign, + operation_definition: ExecutableDefinitionNode, + field: FieldNode, +) -> ast.AnnAssign: +``` + +Called once per field in each result class, after the field type annotation and pydantic metadata (aliases, discriminators, defaults) are applied. Fires before `generate_result_class` for the containing class. Called multiple times per operation - once per field across all result classes in that operation's output file. + +### generate_result_class + +```py +def generate_result_class( + self, + class_def: ast.ClassDef, + operation_definition: ExecutableDefinitionNode, + selection_set: SelectionSetNode, +) -> ast.ClassDef: +``` + +Called once per Pydantic model class in the result file for an operation. There is one class for the top-level result and one for each nested object type selected. Fires after all `generate_result_field` calls for the same class. Called multiple times per operation. + +### generate_result_types_module + +```py +def generate_result_types_module( + self, module: ast.Module, operation_definition: ExecutableDefinitionNode +) -> ast.Module: +``` + +Called once per operation, after all result classes have been assembled into the module. Later this module is saved as `{operation_name}.py`. + +### generate_operation_str + +```py +def generate_operation_str( + self, operation_str: str, operation_definition: ExecutableDefinitionNode +) -> str: +``` + +Called once per operation, for the raw GraphQL query/mutation string that the generated client embeds and sends to the server. Modifying this string changes what the client actually sends - not just the generated Python code. + +### generate_init_import + +```py +def generate_init_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: +``` + +Called once per public symbol re-exported from `__init__.py`, as each import statement is registered. Fires before `generate_init_module`. + +### generate_arguments + +```py +def generate_arguments( + self, + arguments: ast.arguments, + variable_definitions: Tuple[VariableDefinitionNode, ...], +) -> ast.arguments: +``` + +Called once per GraphQL operation, after the method's argument list is built from the operation's variable definitions. + +### generate_arguments_dict + +```py +def generate_arguments_dict( + self, + dict_: ast.Dict, + variable_definitions: Tuple[VariableDefinitionNode, ...], +) -> ast.Dict: +``` + +Called once per GraphQL operation, immediately after `generate_arguments`, for the `variables` dictionary that is serialised and sent as payload to the GraphQL server. + +### generate_client_method + +```py +def generate_client_method( + self, + method_def: Union[ast.FunctionDef, ast.AsyncFunctionDef], + operation_definition: OperationDefinitionNode, +) -> Union[ast.FunctionDef, ast.AsyncFunctionDef]: +``` + +Called once per GraphQL operation, after the full method definition (arguments, body, return type) is assembled. Whether the method is async depends on the `async_client` config option. + +### generate_inputs_module + +```py +def generate_inputs_module(self, module: ast.Module) -> ast.Module: +``` + +Called once, after all input classes have been generated and collected. Later this module is saved as `{input_types_module_name}.py`. + +### get_file_comment + +```py +def get_file_comment( + self, comment: str, code: str, source: Optional[str] = None +) -> str: +``` + +Called once per output file - including copied files (`base_client.py`, `base_model.py`, `exceptions.py`, `files_to_include`) and files that have no code-string hook (`fragments.py`, `custom_queries.py`, `custom_mutations.py`). Fires immediately before the header comment is prepended to the file content. `source` contains the path or URL the file was generated from, or `None` for copied files. + +### generate_inputs_code + +```py +def generate_inputs_code(self, generated_code: str) -> str: +``` + +Called once with the complete `{input_types_module_name}.py` source as a string, immediately before the file is written to disk. + +### generate_result_types_code + +```py +def generate_result_types_code(self, generated_code: str) -> str: +``` + +Called once per operation with the complete `{operation_name}.py` source as a string, immediately before the file is written to disk. + +### generate_fragments_module + +```py +def generate_fragments_module( + self, + module: ast.Module, + fragments_definitions: Dict[str, FragmentDefinitionNode], +) -> ast.Module: +``` + +Called once, after all fragment classes have been generated. Later this module is saved as `{fragments_module_name}.py`. There is no corresponding code-string hook for this file - `fragments.py` is written directly to disk after AST serialisation. Use this hook if you need to modify the fragments output. + +### copy_code + +```py +def copy_code(self, copied_code: str) -> str: +``` + +Called once per copied file, after reading the source but before writing to the output directory. Files: the base client (`*base_client*.py`, selected by config or overridden via `base_client_file_path`), `base_model.py`, `exceptions.py` (built-in clients only), and any `files_to_include`. **This is the only hook that fires for copied files** - AST-level hooks have no effect on them. + +### generate_custom_method + +```py +def generate_custom_method(self, method_def: ast.FunctionDef) -> ast.FunctionDef: +``` + +Called once per method in the custom operation builder class. Only fires when `enable_custom_operations = true` is set in config. Fires before `generate_custom_module` for the containing module. + +### generate_custom_module + +```py +def generate_custom_module(self, module: ast.Module) -> ast.Module: +``` + +Called once per custom operation module (`custom_queries.py` for queries, `custom_mutations.py` for mutations), after all builder methods have been generated. Only fires when `enable_custom_operations = true`. There is no corresponding code-string hook - these files are written directly after AST serialisation. See [Custom operations and fields coverage](./02-custom-plugins.md#custom-operations-and-fields-coverage) for which hooks reach the other custom files. + +### generate_gql_function + +```py +def generate_gql_function(self, function_def: ast.FunctionDef) -> ast.FunctionDef: +``` + +Called once, after all client methods have been added and the `gql()` helper function is generated. Fires before the function is inserted into the client module. + +### generate_client_class + +```py +def generate_client_class(self, class_def: ast.ClassDef) -> ast.ClassDef: +``` + +Called once, after all client methods have been added to the client class. At this point `class_def.body` contains every generated method. + +### generate_client_module + +```py +def generate_client_module(self, module: ast.Module) -> ast.Module: +``` + +Called once, after the complete client module AST is assembled (imports, `gql` function, client class). Later this module is saved as `{client_file_name}.py`. + +### generate_client_code + +```py +def generate_client_code(self, generated_code: str) -> str: +``` + +Called once with the complete `{client_file_name}.py` source as a string, immediately before the file is written to disk. + +### generate_enums_module + +```py +def generate_enums_module(self, module: ast.Module) -> ast.Module: +``` + +Called once, after all enum classes have been generated and collected into the module. Later this module is saved as `{enums_module_name}.py`. + +### generate_enums_code + +```py +def generate_enums_code(self, generated_code: str) -> str: +``` + +Called once with the complete `{enums_module_name}.py` source as a string, immediately before the file is written to disk. This is the last point at which the enums file can be modified. + +### generate_init_module + +```py +def generate_init_module(self, module: ast.Module) -> ast.Module: +``` + +Called once, after all re-export imports have been added to the init module. Later this module is saved as `__init__.py`. + +### generate_init_code + +```py +def generate_init_code(self, generated_code: str) -> str: +``` + +Called once with the complete `__init__.py` source as a string, immediately before the file is written to disk. + diff --git a/docs/06-plugins/01-standard-plugins.md b/docs/06-plugins/04-standard-plugins.md similarity index 92% rename from docs/06-plugins/01-standard-plugins.md rename to docs/06-plugins/04-standard-plugins.md index 1dbcaf6b..62e93f4d 100644 --- a/docs/06-plugins/01-standard-plugins.md +++ b/docs/06-plugins/04-standard-plugins.md @@ -8,17 +8,17 @@ Ariadne Codegen ships with optional plugins importable from the `ariadne_codegen - [`ariadne_codegen.contrib.shorter_results.ShorterResultsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/shorter_results.py#L61) - This plugin processes generated client methods for operations where only single top field is requested, so they return this field's value directly instead of operation's result type. For example get_user method generated for query `GetUser() { user(...) { ... }}` will return value of user field directly instead of `GetUserResult`. -- [`ariadne_codegen.contrib.extract_operations.ExtractOperationsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/extract_operations.py#L29) - This extracts query strings from generated client's methods into separate `operations.py` module. It also modifies the generated client to import these definitions. Generated module name can be customized by adding `operations_module_name="custom_name"` to the `[tool.ariadne-codegen.operations]` section in config. Eg.: +- [`ariadne_codegen.contrib.extract_operations.ExtractOperationsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/extract_operations.py#L29) - This extracts query strings from generated client's methods into separate `operations.py` module. It also modifies the generated client to import these definitions. Generated module name can be customized by adding `operations_module_name="custom_name"` to the `[tool.ariadne-codegen.extract-operations]` section in config. Eg.: ```toml [tool.ariadne-codegen] ... plugins = ["ariadne_codegen.contrib.extract_operations.ExtractOperationsPlugin"] - [tool.ariadne-codegen.extract_operations] + [tool.ariadne-codegen.extract-operations] operations_module_name = "custom_operations_module_name" ``` - [`ariadne_codegen.contrib.client_forward_refs.ClientForwardRefsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/client_forward_refs.py#L32) - This plugin changes generated client module moving all Pydantic models imports under the `TYPE_CHECKING` condition, making them forward references. This greatly improves the import performance of the `client` module. -- [`ariadne_codegen.contrib.no_reimports.NoReimportsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/no_reimports.py#L6) - This plugin removes content of generated `__init__.py`. This is useful in scenarios where generated plugins contain so many Pydantic models that client's eager initialization of entire package on first import is very slow. +- [`ariadne_codegen.contrib.no_reimports.NoReimportsPlugin`](https://github.com/mirumee/ariadne-codegen/blob/main/ariadne_codegen/contrib/no_reimports.py#L6) - This plugin removes content of generated `__init__.py`. This is useful in scenarios where generated modules contain so many Pydantic models that client's eager initialization of entire package on first import is very slow.