From 1ab62cba59e9dbde1279975b67dfaa0e5a55ca99 Mon Sep 17 00:00:00 2001 From: Minister944 Date: Thu, 2 Jul 2026 07:36:23 +0200 Subject: [PATCH 1/3] docs: consolidate plugin docs into docs site AR-7 Remove standalone PLUGINS.md and fold its content into docs/06-plugins/02-custom-plugins.md, expanding the hooks reference and plugin creating guide. --- PLUGINS.md | 327 --------------------- docs/06-plugins/02-custom-plugins.md | 412 ++++++++++++++++++--------- 2 files changed, 278 insertions(+), 461 deletions(-) delete mode 100644 PLUGINS.md 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/02-custom-plugins.md b/docs/06-plugins/02-custom-plugins.md index cdb1dcf6..5d2ee10e 100644 --- a/docs/06-plugins/02-custom-plugins.md +++ b/docs/06-plugins/02-custom-plugins.md @@ -22,84 +22,239 @@ 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 + +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. If plugin A and plugin B both implement `generate_client_class`, B sees the `ast.ClassDef` that A already modified. + +There is no priority mechanism. Every plugin executes every hook it defines. Order is controlled entirely by the position in the `plugins` list. + +### What every plugin instance has + +- `self.schema: GraphQLSchema` - the full GraphQL schema, kept up-to-date by `process_schema` +- `self.config_dict: dict` - the parsed `pyproject.toml` as a dict + +### What plugins cannot do + +- Control or skip other plugins in the chain +- Read the output of sibling plugins +- Prevent the hook chain from continuing +- Write files directly (transform content through code-string hooks; the framework handles writing) + +### Two hook levels + +**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. + +**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. + +### `process_schema` special case + +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. + +### Post-generation + +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. + +## Examples + +### Pipeline - two plugins on the same hook + +`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_init_module(self, module: ast.Module) -> ast.Module: +import ast +from ariadne_codegen.plugins.base import Plugin + + +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 + + +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 init module. Module has list of public, generated classes and reimports them all. Later this module will be saved as `__init__.py`. +Result in `client.py`: -### generate_init_import +```py +class GeneratedClient(AsyncBaseClient): + """Auto-generated client.""" + ... +``` + +### AST hook - adding a field to every result class + +`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 generate_init_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: +import ast +from ariadne_codegen.plugins.base import Plugin + + +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 addition of import to init module. Later this import will be placed in `__init__.py`. +### Code-string hook - adding a header to generated files -### generate_enum +`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 generate_enum(self, class_def: ast.ClassDef, enum_type: GraphQLEnumType) -> ast.ClassDef: +from ariadne_codegen.plugins.base import Plugin + + +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 + + def generate_enums_code(self, generated_code: str) -> str: + return self.generate_client_code(generated_code) ``` -Hook executed on generation of class definition of single enum. +### `process_schema` - inspecting the schema at startup -### generate_enums_module +`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 generate_enums_module(self, module: ast.Module) -> ast.Module: +from graphql import GraphQLSchema +from ariadne_codegen.plugins.base import Plugin + + +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 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. +### Reading config - VersionPlugin -### generate_client_module +Adds `__version__ = "..."` to the generated `__init__.py` by reading a value from `pyproject.toml`. ```py -def generate_client_module(self, module: ast.Module) -> ast.Module: +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 +``` + +```toml +[tool.version_plugin] +version = 0.21 ``` -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. +## Hooks + +Hooks below are listed in the order they fire during a single `ariadne-codegen` run. Implementing only the hooks you need is fine - the default implementation for every hook returns the input unchanged. -### generate_gql_function +### process_schema ```py -def generate_gql_function(self, function_def: ast.FunctionDef) -> ast.FunctionDef: +def process_schema(self, schema: GraphQLSchema) -> GraphQLSchema: ``` -Hook executed on generation of `gql` function. +Called once at startup, immediately after the schema is loaded from path or URL. `assume_valid` is set to `True` during parsing, so `graphql.assert_valid_schema` runs only after this hook returns. This is 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_class +### generate_enum ```py -def generate_client_class(self, class_def: ast.ClassDef) -> ast.ClassDef: +def generate_enum(self, class_def: ast.ClassDef, enum_type: GraphQLEnumType) -> ast.ClassDef: ``` -Hook executed on generation of client class. Class contains method for every graphql operation. +Called once per enum type in the schema, after the class definition is built but before it is added to the enums module. -### generate_client_import +### generate_enums_module ```py -def generate_client_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: +def generate_enums_module(self, module: ast.Module) -> ast.Module: ``` -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. +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_client_method +### generate_input_field ```py -def generate_client_method( +def generate_input_field( self, - method_def: Union[ast.FunctionDef, ast.AsyncFunctionDef], - operation_definition: OperationDefinitionNode, -) -> Union[ast.FunctionDef, ast.AsyncFunctionDef]: + 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: ``` -Hook executed on generation of client's method, which represents single graphql operation. Depends on the configuration method can be either async or not. -` +Called once per input type, after all its fields have been generated. Fires after all `generate_input_field` calls for the same class. + +### 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`. + +### 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. ### generate_arguments @@ -111,7 +266,7 @@ def generate_arguments( ) -> ast.arguments: ``` -Hook executed on generation of arguments for specific client's method. +Called once per GraphQL operation, after the method's argument list is built from the operation's variable definitions. ### generate_arguments_dict @@ -123,58 +278,56 @@ def generate_arguments_dict( ) -> ast.Dict: ``` -Hook executed on generation of dictionary with arguments of graphql operation. Serialized dictionary is later used as variables payload. +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_inputs_module +### generate_client_method ```py -def generate_inputs_module(self, module: ast.Module) -> ast.Module: +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 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. +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_input_class +### generate_gql_function ```py -def generate_input_class( - self, class_def: ast.ClassDef, input_type: GraphQLInputObjectType -) -> ast.ClassDef: +def generate_gql_function(self, function_def: ast.FunctionDef) -> ast.FunctionDef: ``` -Hook executed on generation of class definition for input from schema. +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_input_field +### generate_client_class ```py - def generate_input_field( - self, - field_implementation: ast.AnnAssign, - input_field: GraphQLInputField, - field_name: str, - ) -> ast.AnnAssign: +def generate_client_class(self, class_def: ast.ClassDef) -> ast.ClassDef: ``` -Hook executed on generation of representation for input field. +Called once, after all client methods have been added to the client class. At this point `class_def.body` contains every generated method. -### generate_result_types_module +### generate_client_module ```py -def generate_result_types_module( - self, module: ast.Module, operation_definition: ExecutableDefinitionNode -) -> ast.Module: +def generate_client_module(self, module: ast.Module) -> ast.Module: ``` -Hook executed on generation of module with models reprenting result of given operation. +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_operation_str +### generate_result_field ```py -def generate_operation_str( - self, operation_str: str, operation_definition: ExecutableDefinitionNode -) -> str: +def generate_result_field( + self, + field_implementation: ast.AnnAssign, + operation_definition: ExecutableDefinitionNode, + field: FieldNode, +) -> ast.AnnAssign: ``` -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. +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 @@ -187,143 +340,134 @@ def generate_result_class( ) -> ast.ClassDef: ``` -Hook executed on generation of single model, part of result of given query or mutation. +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_field +### generate_result_types_module ```py -def generate_result_field( - self, - field_implementation: ast.AnnAssign, - operation_definition: ExecutableDefinitionNode, - field: FieldNode, -) -> ast.AnnAssign: +def generate_result_types_module( + self, module: ast.Module, operation_definition: ExecutableDefinitionNode +) -> ast.Module: ``` -Hook executed on generation of single model field. +Called once per operation, after all result classes have been assembled into the module. Later this module is saved as `{operation_name}.py`. -### generate_client_code +### generate_operation_str ```py -def generate_client_code(self, generated_code: str) -> str: +def generate_operation_str( + self, operation_str: str, operation_definition: ExecutableDefinitionNode +) -> 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. +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_enums_code +### generate_fragments_module ```py -def generate_enums_code(self, generated_code: str) -> str: +def generate_fragments_module( + self, + module: ast.Module, + fragments_definitions: Dict[str, FragmentDefinitionNode], +) -> ast.Module: ``` -Hook executed on generation of enums code. Result is used as content of `{enums_module_name}.py`, `enums_module_name` is taken from config. +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. -### generate_inputs_code +### generate_custom_method ```py -def generate_inputs_code(self, generated_code: str) -> str: +def generate_custom_method(self, method_def: ast.FunctionDef) -> ast.FunctionDef: ``` -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. +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_result_types_code +### generate_custom_module ```py -def generate_result_types_code(self, generated_code: str) -> str: +def generate_custom_module(self, module: ast.Module) -> ast.Module: ``` -Hook executed on generation of result models code for one operation. Result is used as content of `{operation_name}.py`. +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. Note: `custom_fields.py` and `custom_typing_fields.py` have no plugin hook of any kind. -### copy_code +### generate_init_import ```py -def copy_code(self, copied_code: str) -> str: +def generate_init_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: ``` -Hook executed on copying file's content to result package. -Files hook is called for: +Called once per public symbol re-exported from `__init__.py`, as each import statement is registered. Fires before `generate_init_module`. -- `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 +### generate_init_module ```py -def generate_init_code(self, generated_code: str) -> str: +def generate_init_module(self, module: ast.Module) -> ast.Module: ``` -Hook executed on generation of init code. Result is used as content of `__init__.py`. +Called once, after all re-export imports have been added to the init module. Later this module is saved as `__init__.py`. -### process_name +### get_file_comment ```py -def process_name(self, name: str, node: Optional[Node] = None) -> str: +def get_file_comment( + self, comment: str, code: str, source: Optional[str] = None +) -> str: ``` -Hook executed on processing of GraphQL field, argument or operation name. +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_fragments_module +### generate_enums_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: ``` -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. +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. -### process_schema +### generate_inputs_code ```py -def process_schema(self, schema: GraphQLSchema) -> GraphQLSchema: +def generate_inputs_code(self, generated_code: str) -> str: ``` -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. +Called once with the complete `{input_types_module_name}.py` source as a string, immediately before the file is written to disk. -### get_file_comment +### generate_client_code ```py -def get_file_comment( - self, comment: str, code: str, source: Optional[str] = None -) -> str: +def generate_client_code(self, generated_code: str) -> str: ``` -Hook executed on generating comment included at the beginning of a generated code. +Called once with the complete `{client_file_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: +``` -## Example +Called once per operation with the complete `{operation_name}.py` source as a string, immediately before the file is written to disk. -This example plugin adds `__version__ = "..."` to generated `__init__.py` file. +### copy_code ```py -import ast +def copy_code(self, copied_code: str) -> str: +``` -from ariadne_codegen.plugins.base import Plugin +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_init_code -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 +```py +def generate_init_code(self, generated_code: str) -> str: ``` -`VersionPlugin` reads version from parsed `pyproject.toml`, eg. following entry will produce `__version__ = "0.21"`. +Called once with the complete `__init__.py` source as a string, immediately before the file is written to disk. -```toml -[tool.version_plugin] -version = 0.21 +### 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. From d5a1fc165aa937967a0b7ff38ca40ea2706e0b4a Mon Sep 17 00:00:00 2001 From: Minister944 Date: Thu, 9 Jul 2026 11:49:18 +0200 Subject: [PATCH 2/3] docs: restructure plugin docs and fix inaccuracies AR-7 --- docs/06-plugins/01-intro.md | 36 +++ docs/06-plugins/02-custom-plugins.md | 299 +----------------- docs/06-plugins/03-hooks.md | 292 +++++++++++++++++ ...dard-plugins.md => 04-standard-plugins.md} | 6 +- 4 files changed, 344 insertions(+), 289 deletions(-) create mode 100644 docs/06-plugins/01-intro.md create mode 100644 docs/06-plugins/03-hooks.md rename docs/06-plugins/{01-standard-plugins.md => 04-standard-plugins.md} (92%) 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 5d2ee10e..5f178371 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 @@ -56,6 +56,16 @@ After each plugin's `process_schema()` returns, `self.schema` is updated on **al 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. +### Custom operations and fields coverage + +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. + +| 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 | ✅ | + ## Examples ### Pipeline - two plugins on the same hook @@ -186,288 +196,5 @@ class VersionPlugin(Plugin): ```toml [tool.version_plugin] -version = 0.21 -``` - -## Hooks - -Hooks below are listed in the order they fire during a single `ariadne-codegen` run. Implementing only the hooks you need is fine - the default implementation for every hook returns the input unchanged. - -### process_schema - -```py -def process_schema(self, schema: GraphQLSchema) -> GraphQLSchema: -``` - -Called once at startup, immediately after the schema is loaded from path or URL. `assume_valid` is set to `True` during parsing, so `graphql.assert_valid_schema` runs only after this hook returns. This is 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_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_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_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_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`. - -### generate_client_import - -```py -def generate_client_import(self, import_: ast.ImportFrom) -> ast.ImportFrom: +version = "0.21" ``` - -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. - -### 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_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_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_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. - -### 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. Note: `custom_fields.py` and `custom_typing_fields.py` have no plugin hook of any kind. - -### 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_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`. - -### 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_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_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_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_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. - -### 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_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. - -### 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. diff --git a/docs/06-plugins/03-hooks.md b/docs/06-plugins/03-hooks.md new file mode 100644 index 00000000..69027d3f --- /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. `assume_valid` is set to `True` during parsing, so `graphql.assert_valid_schema` runs only after this hook returns. This is 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. From 09e86ece722ba2771d1241a90d7def2c08292190 Mon Sep 17 00:00:00 2001 From: Minister944 Date: Thu, 9 Jul 2026 12:23:28 +0200 Subject: [PATCH 3/3] docs: address plugin docs review feedback AR-7 --- docs/06-plugins/02-custom-plugins.md | 14 +------------- docs/06-plugins/03-hooks.md | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/docs/06-plugins/02-custom-plugins.md b/docs/06-plugins/02-custom-plugins.md index 5f178371..51693b0e 100644 --- a/docs/06-plugins/02-custom-plugins.md +++ b/docs/06-plugins/02-custom-plugins.md @@ -26,22 +26,10 @@ Every element of the list can enable plugins in 2 ways: ### Sequential pipeline -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. If plugin A and plugin B both implement `generate_client_class`, B sees the `ast.ClassDef` that A already modified. +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. There is no priority mechanism. Every plugin executes every hook it defines. Order is controlled entirely by the position in the `plugins` list. -### What every plugin instance has - -- `self.schema: GraphQLSchema` - the full GraphQL schema, kept up-to-date by `process_schema` -- `self.config_dict: dict` - the parsed `pyproject.toml` as a dict - -### What plugins cannot do - -- Control or skip other plugins in the chain -- Read the output of sibling plugins -- Prevent the hook chain from continuing -- Write files directly (transform content through code-string hooks; the framework handles writing) - ### Two hook levels **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. diff --git a/docs/06-plugins/03-hooks.md b/docs/06-plugins/03-hooks.md index 69027d3f..b4858aaa 100644 --- a/docs/06-plugins/03-hooks.md +++ b/docs/06-plugins/03-hooks.md @@ -17,7 +17,7 @@ Hooks below are listed in the order they fire during a single `ariadne-codegen` def process_schema(self, schema: GraphQLSchema) -> GraphQLSchema: ``` -Called once at startup, immediately after the schema is loaded from path or URL. `assume_valid` is set to `True` during parsing, so `graphql.assert_valid_schema` runs only after this hook returns. This is 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. +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