Skip to content

Commit 1d37e2b

Browse files
feat: alias generator (#449)
* feat: add use_alias_generator setting * fix: don't silently ship wrong aliases or unformatted code * fix: keep fragment forward references resolvable in operation modules A generated client whose operation spreads a fragment could not be imported on Python 3.10, with or without `defer_model_build`: PydanticUndefinedAnnotation: name 'UserFieldsFriends' is not defined A fragment names its nested selections by forward reference, and an operation that spreads it subclasses the fragment class from another module: # fragments.py class UserFields(BaseModel): friends: list["UserFieldsFriends"] # list_users.py class ListUsersUsers(UserFields): pass Pydantic resolves the annotations a subclass inherits against the subclass's own module, and `UserFieldsFriends` lives in `fragments`. On 3.11+ the inherited `ForwardRef` evaluation is reused and this happens to work; on 3.10 it is re-evaluated and fails. `requires-python` is `>= 3.10` and the default hatch environment pins 3.10, so this failed on the version CI runs. With `defer_model_build` the same shape failed on every supported version, because dropping the eager `model_rebuild()` calls removes the only thing that ever resolved those references. Emit the names the mixin bases need into the module that has to resolve them: from .fragments import ( UserFields, UserFieldsFriends, # noqa: F401 ) The `noqa` is required because the name appears only inside an inherited annotation. Keeping the build deferred rather than restoring the eager fragment rebuilds preserves the whole import-time win - `defer_model_build` now measures 1.79x faster to import on a 1448-type schema, against 1.74x before, because no fragment models are built at import. Add `ForwardRefNamesVisitor` and `collect_class_forward_ref_names()` in `codegen.py`; `_map_fragment_forward_refs()` and `_mixin_forward_ref_import()` in `package.py`. `ast.unparse` drops comments, so the line is injected as raw source through the new `prepend_code` argument of `_queue_module`. Clients with no fragment mixin are byte-for-byte unchanged. The suite never caught this because every `expected_client` fixture is inert: the tests diff generated source and never import it. Add two tests that import a generated package and validate a payload, one per path. Both fail only on Python 3.10, which the default hatch environment provides. * docs: record graphql-core dependency and the two import-performance settings `enable_custom_operations` makes the generated `base_operation.py` and `client.py` import graphql-core to build and print a query AST at runtime, so the generated package needs it alongside pydantic and httpx. It was missing from "Generated code dependencies" and costs roughly 25 ms of import. `defer_model_build` and `use_alias_generator` were documented in the README but absent from the optional-settings list in `docs/02-configuration.md`, where every other setting lives. Also describe the `# noqa: F401` fragment import that generated operation modules now carry, so it does not read as a code-generation bug to anyone running a linter over the output. * test: cover alias_generator in the multipart_uploads=false base-model test Now that use_alias_generator exists, extend the no-upload base-model config test to also enable it and assert alias_generator=to_camel lands on the generated BaseModel - exercising the base-model source-path detection for all three config rewrites in the uploads-disabled path. * build: require pydantic >=2.8 for use_alias_generator `use_alias_generator` sets `alias_generator=to_camel` and omits the explicit `Field(alias=...)` for fields whose GraphQL name `to_camel` can reconstruct. `to_camel`'s case handling was fixed in pydantic 2.8 (pydantic/pydantic#9561); on earlier versions it could mangle names, so the generated package would rebuild the wrong aliases. Raise the floor to 2.8 to match. * docs: tighten the to_camel / pydantic 2.8 floor comments * docs: move import-performance and fragments docs out of README * fix: don't let the BaseModel config rewrite go missing unnoticed * docs: drop the defer_model_build aside from the fragments page * test: consolidate the rewrite_base_model config tests * docs: place the import-performance and fragments guides in the docs site The use_alias_generator branch predates the docs-site refactor (#446), so it added these two guides as flat docs/07 and docs/08 pages and linked them from a monolithic README. Move them under docs/02-guides/ to match the current structure and repoint the config-reference links. * fix: satisfy ty 0.0.62 in extract_operations and shorter_results ty 0.0.62 (stricter than the version CI ran previously) flags two spots in existing contrib plugins: the `ast.Constant.value` union widens the list passed to `sorted()` past `SupportsRichComparison`, and `ImportFrom.module` is `str | None` so indexing the `dict[str, set]` of extended imports with it is rejected. Narrow the constant to `str` and bind/guard the module name. No behaviour change.
1 parent c16dc19 commit 1d37e2b

46 files changed

Lines changed: 1731 additions & 364 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ We use [Hatch](https://github.com/pypa/hatch) for project management.
1919
Formatting and linting (including typing-related rules) use [ruff](https://github.com/astral-sh/ruff). To format the code, run:
2020

2121
```bash
22-
hatch fmt
22+
hatch check code --fix
23+
hatch check fmt --fix
2324
```
2425

2526
The contents of the `ariadne-codegen` package are annotated with types and validated using [ty](https://github.com/astral-sh/ty). To run type checking, use:

ariadne_codegen/client_generators/fragments.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def __init__(
2323
default_optional_fields_to_none: bool = False,
2424
include_typename: bool = True,
2525
defer_model_build: bool = False,
26+
use_alias_generator: bool = False,
2627
) -> None:
2728
self.schema = schema
2829
self.enums_module_name = enums_module_name
@@ -34,6 +35,7 @@ def __init__(
3435
self.default_optional_fields_to_none = default_optional_fields_to_none
3536
self.include_typename = include_typename
3637
self.defer_model_build = defer_model_build
38+
self.use_alias_generator = use_alias_generator
3739

3840
self._fragments_names = set(self.fragments_definitions.keys())
3941
self._generated_public_names: list[str] = []
@@ -60,6 +62,7 @@ def generate(self, exclude_names: Optional[set[str]] = None) -> ast.Module:
6062
plugin_manager=self.plugin_manager,
6163
default_optional_fields_to_none=self.default_optional_fields_to_none,
6264
include_typename=self.include_typename,
65+
use_alias_generator=self.use_alias_generator,
6366
)
6467
imports.extend(generator.get_imports())
6568
class_defs = generator.get_classes()

ariadne_codegen/client_generators/input_types.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,23 @@
1414
generate_ann_assign,
1515
generate_class_def,
1616
generate_constant,
17-
generate_expr,
1817
generate_import_from,
1918
generate_keyword,
20-
generate_method_call,
19+
generate_model_rebuild_calls,
2120
generate_module,
2221
generate_name,
2322
generate_pass,
2423
generate_pydantic_field,
25-
model_has_forward_refs,
2624
)
2725
from ..plugins.manager import PluginManager
28-
from ..utils import process_name
26+
from ..utils import needs_explicit_alias, process_name
2927
from .constants import (
3028
ALIAS_KEYWORD,
3129
ANNOTATED,
3230
ANY,
3331
BASE_MODEL_CLASS_NAME,
3432
BASE_MODEL_IMPORT,
3533
FIELD_CLASS,
36-
MODEL_REBUILD_METHOD,
3734
OPTIONAL,
3835
PLAIN_SERIALIZER,
3936
PYDANTIC_MODULE,
@@ -56,13 +53,15 @@ def __init__(
5653
custom_scalars: Optional[dict[str, ScalarData]] = None,
5754
plugin_manager: Optional[PluginManager] = None,
5855
defer_model_build: bool = False,
56+
use_alias_generator: bool = False,
5957
) -> None:
6058
self.schema = schema
6159
self.convert_to_snake_case = convert_to_snake_case
6260
self.enums_module = enums_module
6361
self.custom_scalars = custom_scalars if custom_scalars else {}
6462
self.plugin_manager = plugin_manager
6563
self.defer_model_build = defer_model_build
64+
self.use_alias_generator = use_alias_generator
6665

6766
self._imports = [
6867
generate_import_from([OPTIONAL, ANY, UNION, ANNOTATED], TYPING_MODULE),
@@ -91,16 +90,8 @@ def generate(self, types_to_include: Optional[list[str]] = None) -> ast.Module:
9190
scalar_data = self.custom_scalars[scalar_name]
9291
self._imports.extend(generate_scalar_imports(scalar_data))
9392

94-
model_rebuild_calls = (
95-
[]
96-
if self.defer_model_build
97-
else [
98-
generate_expr(
99-
generate_method_call(class_def.name, MODEL_REBUILD_METHOD)
100-
)
101-
for class_def in class_defs
102-
if model_has_forward_refs(class_def)
103-
]
93+
model_rebuild_calls = generate_model_rebuild_calls(
94+
class_defs, self.defer_model_build
10495
)
10596

10697
module_body = (
@@ -190,7 +181,7 @@ def _parse_input_definition(
190181
),
191182
lineno=lineno,
192183
)
193-
if name != org_name:
184+
if needs_explicit_alias(name, org_name, self.use_alias_generator):
194185
field_implementation.value = self._process_field_value(
195186
field_implementation=field_implementation, alias=org_name
196187
)

0 commit comments

Comments
 (0)