Skip to content

Commit 0dcf601

Browse files
magicmarkclaude
andcommitted
Add models_only option to generate only Pydantic models without client code
When `models_only = true`, ariadne-codegen generates only input types, enums, result types, fragments, and the base model — skipping the client class, base client, and exceptions module. This removes the httpx dependency for type-only use cases. The `queries_path` and base client configuration become optional in this mode. Closes #418 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 66dc94d commit 0dcf601

File tree

4 files changed

+155
-45
lines changed

4 files changed

+155
-45
lines changed

ariadne_codegen/client_generators/package.py

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def __init__(
9292
default_optional_fields_to_none: bool = False,
9393
include_typename: bool = True,
9494
ignore_extra_fields: bool = True,
95+
models_only: bool = False,
9596
) -> None:
9697
self.package_path = Path(target_path) / package_name
9798

@@ -150,21 +151,23 @@ def __init__(
150151
self._unpacked_fragments: set[str] = set()
151152
self._used_enums: list[str] = []
152153

154+
self.models_only = models_only
153155
self.enable_custom_operations = enable_custom_operations
154156
if self.enable_custom_operations:
155157
self.files_to_include.append(self.base_schema_root_file_path)
156158

157159
def generate(self) -> list[str]:
158160
"""Generate package with graphql client."""
159-
self._include_exceptions()
161+
if not self.models_only:
162+
self._include_exceptions()
160163
self._validate_unique_file_names()
161164
if not self.package_path.exists():
162165
self.package_path.mkdir()
163166
self._generate_input_types()
164167
self._generate_result_types()
165168
self._generate_fragments()
166169
self._copy_files()
167-
if self.enable_custom_operations:
170+
if not self.models_only and self.enable_custom_operations:
168171
self._generate_custom_fields_typing()
169172
self._generate_custom_fields()
170173
self.client_generator.add_execute_custom_operation_method(self.async_client)
@@ -179,7 +182,8 @@ def generate(self) -> list[str]:
179182
"mutation", OperationType.MUTATION.value.upper(), self.async_client
180183
)
181184

182-
self._generate_client()
185+
if not self.models_only:
186+
self._generate_client()
183187
self._generate_enums()
184188
self._generate_init()
185189

@@ -223,14 +227,15 @@ def add_operation(self, definition: OperationDefinitionNode):
223227
query_types_generator.get_generated_public_names(), module_name, 1
224228
)
225229

226-
self.client_generator.add_method(
227-
definition=definition,
228-
name=method_name,
229-
return_type=return_type_name,
230-
return_type_module=module_name,
231-
operation_str=operation_str,
232-
async_=self.async_client,
233-
)
230+
if not self.models_only:
231+
self.client_generator.add_method(
232+
definition=definition,
233+
name=method_name,
234+
return_type=return_type_name,
235+
return_type_module=module_name,
236+
operation_str=operation_str,
237+
async_=self.async_client,
238+
)
234239

235240
def _include_exceptions(self):
236241
if self.base_client_file_path in (
@@ -247,18 +252,19 @@ def _include_exceptions(self):
247252
)
248253

249254
def _validate_unique_file_names(self):
250-
file_names = (
251-
[
255+
file_names = [
256+
self.base_model_file_path.name,
257+
f"{self.enums_module_name}.py",
258+
f"{self.input_types_module_name}.py",
259+
f"{self.fragments_module_name}.py",
260+
]
261+
if not self.models_only:
262+
file_names += [
252263
f"{self.client_file_name}.py",
253264
self.base_client_file_path.name,
254-
self.base_model_file_path.name,
255-
f"{self.enums_module_name}.py",
256-
f"{self.input_types_module_name}.py",
257-
f"{self.fragments_module_name}.py",
258265
]
259-
+ list(self._result_types_files.keys())
260-
+ [f.name for f in self.files_to_include]
261-
)
266+
file_names += list(self._result_types_files.keys())
267+
file_names += [f.name for f in self.files_to_include]
262268

263269
if len(file_names) != len(set(file_names)):
264270
seen = set()
@@ -359,10 +365,9 @@ def _generate_fragments(self):
359365
)
360366

361367
def _copy_files(self):
362-
files_to_copy = self.files_to_include + [
363-
self.base_client_file_path,
364-
self.base_model_file_path,
365-
]
368+
files_to_copy = self.files_to_include + [self.base_model_file_path]
369+
if not self.models_only:
370+
files_to_copy.append(self.base_client_file_path)
366371
for source_path in files_to_copy:
367372
code = self._add_comments_to_code(source_path.read_text(encoding="utf-8"))
368373
if not self.ignore_extra_fields and source_path.name == "base_model.py":
@@ -373,11 +378,12 @@ def _copy_files(self):
373378
target_path.write_text(code)
374379
self._generated_files.append(target_path.name)
375380

376-
self.init_generator.add_import(
377-
names=[self.base_client_name],
378-
from_=self.base_client_file_path.stem,
379-
level=1,
380-
)
381+
if not self.models_only:
382+
self.init_generator.add_import(
383+
names=[self.base_client_name],
384+
from_=self.base_client_file_path.stem,
385+
level=1,
386+
)
381387
self.init_generator.add_import(
382388
names=[BASE_MODEL_CLASS_NAME, UPLOAD_CLASS_NAME],
383389
from_=self.base_model_file_path.stem,
@@ -429,20 +435,30 @@ def get_package_generator(
429435
plugin_manager: PluginManager,
430436
) -> PackageGenerator:
431437
init_generator = InitFileGenerator(plugin_manager=plugin_manager)
432-
client_generator = ClientGenerator(
433-
base_client_import=generate_import_from(
438+
if settings.models_only:
439+
base_client_import = generate_import_from(
440+
names=["object"], from_="builtins", level=0
441+
)
442+
client_name = "Client"
443+
base_client = "object"
444+
else:
445+
base_client_import = generate_import_from(
434446
names=[settings.base_client_name],
435447
from_=Path(settings.base_client_file_path).stem,
436448
level=1,
437-
),
449+
)
450+
client_name = settings.client_name
451+
base_client = settings.base_client_name
452+
client_generator = ClientGenerator(
453+
base_client_import=base_client_import,
438454
arguments_generator=ArgumentsGenerator(
439455
schema=schema,
440456
convert_to_snake_case=settings.convert_to_snake_case,
441457
custom_scalars=settings.scalars,
442458
plugin_manager=plugin_manager,
443459
),
444-
name=settings.client_name,
445-
base_client=settings.base_client_name,
460+
name=client_name,
461+
base_client=base_client,
446462
enums_module_name=settings.enums_module_name,
447463
input_types_module_name=settings.input_types_module_name,
448464
unset_import=UNSET_IMPORT,
@@ -553,4 +569,5 @@ def get_package_generator(
553569
default_optional_fields_to_none=settings.default_optional_fields_to_none,
554570
include_typename=settings.include_typename,
555571
ignore_extra_fields=settings.ignore_extra_fields,
572+
models_only=settings.models_only,
556573
)

ariadne_codegen/settings.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,14 @@ class ClientSettings(BaseSettings):
7878
default_optional_fields_to_none: bool = False
7979
include_typename: bool = True
8080
ignore_extra_fields: bool = True
81+
models_only: bool = False
8182

8283
def __post_init__(self):
83-
if not self.queries_path and not self.enable_custom_operations:
84+
if (
85+
not self.queries_path
86+
and not self.enable_custom_operations
87+
and not self.models_only
88+
):
8489
raise TypeError("__init__ missing 1 required argument: 'queries_path'")
8590
super().__post_init__()
8691

@@ -93,24 +98,27 @@ def __post_init__(self):
9398
f"Valid options are: {valid_options}"
9499
) from exc
95100

96-
self._set_default_base_client_data()
101+
if not self.models_only:
102+
self._set_default_base_client_data()
97103

98104
for name, data in self.scalars.items():
99105
data.graphql_name = name
100106

101-
assert_path_exists(self.queries_path)
107+
if self.queries_path:
108+
assert_path_exists(self.queries_path)
102109

103110
assert_string_is_valid_python_identifier(self.target_package_name)
104111
assert_path_is_valid_directory(self.target_package_path)
105112

106-
assert_string_is_valid_python_identifier(self.client_name)
107-
assert_string_is_valid_python_identifier(self.client_file_name)
108-
assert_string_is_valid_python_identifier(self.base_client_name)
109-
assert_path_exists(self.base_client_file_path)
110-
assert_path_is_valid_file(self.base_client_file_path)
111-
assert_class_is_defined_in_file(
112-
Path(self.base_client_file_path), self.base_client_name
113-
)
113+
if not self.models_only:
114+
assert_string_is_valid_python_identifier(self.client_name)
115+
assert_string_is_valid_python_identifier(self.client_file_name)
116+
assert_string_is_valid_python_identifier(self.base_client_name)
117+
assert_path_exists(self.base_client_file_path)
118+
assert_path_is_valid_file(self.base_client_file_path)
119+
assert_class_is_defined_in_file(
120+
Path(self.base_client_file_path), self.base_client_name
121+
)
114122

115123
assert_string_is_valid_python_identifier(self.enums_module_name)
116124
assert_string_is_valid_python_identifier(self.input_types_module_name)
@@ -177,6 +185,23 @@ def used_settings_message(self) -> str:
177185
if self.include_typename
178186
else "Not including __typename fields in generated queries."
179187
)
188+
if self.models_only:
189+
return dedent(
190+
f"""\
191+
Selected strategy: {Strategy.CLIENT}
192+
Generating models only.
193+
Using schema from '{self.schema_path or self.remote_schema_url}'.
194+
Using '{self.target_package_name}' as package name.
195+
Generating package into '{self.target_package_path}'.
196+
Generating enums into '{self.enums_module_name}.py'.
197+
Generating inputs into '{self.input_types_module_name}.py'.
198+
Generating fragments into '{self.fragments_module_name}.py'.
199+
Comments type: {self.include_comments.value}
200+
{snake_case_msg}
201+
{files_to_include_msg}
202+
{plugins_msg}
203+
"""
204+
)
180205
return dedent(
181206
f"""\
182207
Selected strategy: {Strategy.CLIENT}

tests/client_generators/package_generator/test_generated_files.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,3 +764,52 @@ def test_generate_creates_client_with_custom_scalars_imports(
764764
f"{generator.client_file_name}.py"
765765
).open() as client_file:
766766
assert "from .abc import ScalarABC" in client_file.read()
767+
768+
769+
def test_generate_models_only(tmp_path, schema, async_base_client_import):
770+
package_name = "test_graphql_client"
771+
generator = PackageGenerator(
772+
package_name=package_name,
773+
target_path=tmp_path.as_posix(),
774+
schema=schema,
775+
init_generator=InitFileGenerator(),
776+
client_generator=ClientGenerator(
777+
base_client_import=async_base_client_import,
778+
arguments_generator=ArgumentsGenerator(schema=schema),
779+
),
780+
enums_generator=EnumsGenerator(schema=schema),
781+
input_types_generator=InputTypesGenerator(schema=schema),
782+
fragments_generator=FragmentsGenerator(schema=schema, fragments_definitions={}),
783+
models_only=True,
784+
)
785+
query_str = """
786+
query CustomQuery($id: ID!) {
787+
query1(id: $id) {
788+
field1
789+
}
790+
}
791+
"""
792+
generator.add_operation(parse(query_str).definitions[0])
793+
generated_files = generator.generate()
794+
795+
package_path = tmp_path / package_name
796+
# Model files should exist
797+
assert (package_path / "__init__.py").exists()
798+
assert (package_path / "base_model.py").exists()
799+
assert (package_path / f"{generator.enums_module_name}.py").exists()
800+
assert (package_path / f"{generator.input_types_module_name}.py").exists()
801+
# Result types from operations should still be generated
802+
assert (package_path / "custom_query.py").exists()
803+
assert "custom_query.py" in generated_files
804+
# Client runtime files should NOT exist
805+
assert not (package_path / "client.py").exists()
806+
assert not (package_path / generator.base_client_file_path.name).exists()
807+
assert not (package_path / EXCEPTIONS_FILE_PATH.name).exists()
808+
assert "client.py" not in generated_files
809+
assert EXCEPTIONS_FILE_PATH.name not in generated_files
810+
# __init__.py should not import client classes
811+
init_content = (package_path / "__init__.py").read_text()
812+
assert "from .base_model import BaseModel, Upload" in init_content
813+
assert "Client" not in init_content
814+
assert "AsyncBaseClient" not in init_content
815+
assert "GraphQLClientError" not in init_content

tests/test_settings.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,22 @@ def test_client_settings_include_typename_can_be_set_to_true(tmp_path):
479479
)
480480

481481
assert settings.include_typename is True
482+
483+
484+
def test_client_settings_models_only(tmp_path):
485+
schema_path = tmp_path / "schema.graphql"
486+
schema_path.touch()
487+
488+
settings = ClientSettings(
489+
schema_path=schema_path.as_posix(),
490+
models_only=True,
491+
)
492+
493+
assert settings.models_only is True
494+
assert settings.queries_path == ""
495+
assert settings.base_client_name == ""
496+
assert settings.base_client_file_path == ""
497+
498+
result = settings.used_settings_message
499+
assert "Generating models only." in result
500+
assert settings.schema_path in result

0 commit comments

Comments
 (0)