Skip to content

Commit aa3b82d

Browse files
Add introspection parameters to available settings. (#413)
* Add introspection parameters to available settings. - Refactor logic unpacking response from endpoint to access errors. - Add tests for settings and schema to cover new parameters. - Update README to include new parameters. * add comment on input_object_one_of to remind to rename when bumping ariadne-core --------- Co-authored-by: Damian Czajkowski <d0.czajkowski@gmail.com>
1 parent af32081 commit aa3b82d

File tree

6 files changed

+357
-9
lines changed

6 files changed

+357
-9
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ Optional settings:
7979
- `plugins` (defaults to `[]`) - list of plugins to use during generation
8080
- `enable_custom_operations` (defaults to `false`) - enables building custom operations. Generates additional files that contains all the classes and methods for generation.
8181

82+
These options control which fields are included in the GraphQL introspection query when using `remote_schema_url`.
83+
84+
- `introspection_descriptions` (defaults to `false`) – include descriptions in the introspection result
85+
- `introspection_input_value_deprecation` (defaults to `false`) – include deprecation information for input values
86+
- `introspection_specified_by_url` (defaults to `false`) – include `specifiedByUrl` for custom scalars
87+
- `introspection_schema_description` (defaults to `false`) – include schema description
88+
- `introspection_directive_is_repeatable` (defaults to `false`) – include `isRepeatable` information for directives
89+
- `introspection_input_object_one_of` (defaults to `false`) – include `oneOf` information for input objects
90+
8291
## Custom operation builder
8392

8493
The custom operation builder allows you to create complex GraphQL queries in a structured and intuitive way.
@@ -178,7 +187,7 @@ Ariadne Codegen ships with optional plugins importable from the `ariadne_codegen
178187
[tool.ariadne-codegen]
179188
...
180189
plugins = ["ariadne_codegen.contrib.extract_operations.ExtractOperationsPlugin"]
181-
190+
182191
[tool.ariadne-codegen.extract_operations]
183192
operations_module_name = "custom_operations_module_name"
184193
```

ariadne_codegen/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def client(config_dict):
5151
headers=settings.remote_schema_headers,
5252
verify_ssl=settings.remote_schema_verify_ssl,
5353
timeout=settings.remote_schema_timeout,
54+
introspection_settings=settings.introspection_settings,
5455
)
5556

5657
plugin_manager = PluginManager(
@@ -95,6 +96,7 @@ def graphql_schema(config_dict):
9596
headers=settings.remote_schema_headers,
9697
verify_ssl=settings.remote_schema_verify_ssl,
9798
timeout=settings.remote_schema_timeout,
99+
introspection_settings=settings.introspection_settings,
98100
)
99101
)
100102
plugin_manager = PluginManager(

ariadne_codegen/schema.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections.abc import Generator
2+
from dataclasses import asdict
23
from pathlib import Path
34
from typing import Optional, cast
45

@@ -29,6 +30,7 @@
2930
InvalidGraphqlSyntax,
3031
InvalidOperationForSchema,
3132
)
33+
from .settings import IntrospectionSettings
3234

3335

3436
def filter_operations_definitions(
@@ -68,10 +70,15 @@ def get_graphql_schema_from_url(
6870
headers: Optional[dict[str, str]] = None,
6971
verify_ssl: bool = True,
7072
timeout: float = 5,
73+
introspection_settings: Optional[IntrospectionSettings] = None,
7174
) -> GraphQLSchema:
7275
return build_client_schema(
7376
introspect_remote_schema(
74-
url=url, headers=headers, verify_ssl=verify_ssl, timeout=timeout
77+
url=url,
78+
headers=headers,
79+
verify_ssl=verify_ssl,
80+
timeout=timeout,
81+
introspection_settings=introspection_settings,
7582
),
7683
assume_valid=True,
7784
)
@@ -82,11 +89,15 @@ def introspect_remote_schema(
8289
headers: Optional[dict[str, str]] = None,
8390
verify_ssl: bool = True,
8491
timeout: float = 5,
92+
introspection_settings: Optional[IntrospectionSettings] = None,
8593
) -> IntrospectionQuery:
94+
# If introspection settings are not provided, use default values.
95+
settings = introspection_settings or IntrospectionSettings()
96+
query = get_introspection_query(**asdict(settings))
8697
try:
8798
response = httpx.post(
8899
url,
89-
json={"query": get_introspection_query(descriptions=False)},
100+
json={"query": query},
90101
headers=headers,
91102
verify=verify_ssl,
92103
timeout=timeout,
@@ -105,14 +116,14 @@ def introspect_remote_schema(
105116
except ValueError as exc:
106117
raise IntrospectionError("Introspection result is not a valid json.") from exc
107118

108-
if (not isinstance(response_json, dict)) or ("data" not in response_json):
119+
if not isinstance(response_json, dict):
109120
raise IntrospectionError("Invalid introspection result format.")
110121

111122
errors = response_json.get("errors")
112123
if errors:
113124
raise IntrospectionError(f"Introspection errors: {errors}")
114125

115-
data = response_json["data"]
126+
data = response_json.get("data")
116127
if not isinstance(data, dict):
117128
raise IntrospectionError("Invalid data key in introspection result.")
118129

ariadne_codegen/settings.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import enum
22
import os
33
import re
4-
from dataclasses import dataclass, field
4+
from dataclasses import asdict, dataclass, field
55
from keyword import iskeyword
66
from pathlib import Path
77
from textwrap import dedent
@@ -31,6 +31,21 @@ class Strategy(str, enum.Enum):
3131
GRAPHQL_SCHEMA = "graphqlschema"
3232

3333

34+
@dataclass
35+
class IntrospectionSettings:
36+
"""
37+
Introspection settings for schema generation.
38+
"""
39+
40+
descriptions: bool = False
41+
input_value_deprecation: bool = False
42+
specified_by_url: bool = False
43+
schema_description: bool = False
44+
directive_is_repeatable: bool = False
45+
# graphql-core will rename this to one_of in a future version (update when bumping)
46+
input_object_one_of: bool = False
47+
48+
3449
@dataclass
3550
class BaseSettings:
3651
schema_path: str = ""
@@ -40,6 +55,12 @@ class BaseSettings:
4055
remote_schema_timeout: float = 5
4156
enable_custom_operations: bool = False
4257
plugins: list[str] = field(default_factory=list)
58+
introspection_descriptions: bool = False
59+
introspection_input_value_deprecation: bool = False
60+
introspection_specified_by_url: bool = False
61+
introspection_schema_description: bool = False
62+
introspection_directive_is_repeatable: bool = False
63+
introspection_input_object_one_of: bool = False
4364

4465
def __post_init__(self):
4566
if not self.schema_path and not self.remote_schema_url:
@@ -54,6 +75,37 @@ def __post_init__(self):
5475
if self.remote_schema_url:
5576
self.remote_schema_url = resolve_env_vars_in_string(self.remote_schema_url)
5677

78+
@property
79+
def using_remote_schema(self) -> bool:
80+
"""
81+
Return true if remote schema is used as source, false otherwise.
82+
"""
83+
return bool(self.remote_schema_url) and not bool(self.schema_path)
84+
85+
@property
86+
def introspection_settings(self) -> IntrospectionSettings:
87+
"""
88+
Return ``IntrospectionSettings`` instance build from provided configuration.
89+
"""
90+
return IntrospectionSettings(
91+
descriptions=self.introspection_descriptions,
92+
input_value_deprecation=self.introspection_input_value_deprecation,
93+
specified_by_url=self.introspection_specified_by_url,
94+
schema_description=self.introspection_schema_description,
95+
directive_is_repeatable=self.introspection_directive_is_repeatable,
96+
input_object_one_of=self.introspection_input_object_one_of,
97+
)
98+
99+
def _introspection_settings_message(self) -> str:
100+
"""
101+
Return human readable message with introspection settings values.
102+
"""
103+
formatted = ", ".join(
104+
f"{key}={str(value).lower()}"
105+
for key, value in asdict(self.introspection_settings).items()
106+
)
107+
return f"Introspection settings: {formatted}"
108+
57109

58110
@dataclass
59111
class ClientSettings(BaseSettings):
@@ -177,10 +229,14 @@ def used_settings_message(self) -> str:
177229
if self.include_typename
178230
else "Not including __typename fields in generated queries."
179231
)
232+
introspection_msg = (
233+
self._introspection_settings_message() if self.using_remote_schema else ""
234+
)
180235
return dedent(
181236
f"""\
182237
Selected strategy: {Strategy.CLIENT}
183238
Using schema from '{self.schema_path or self.remote_schema_url}'.
239+
{introspection_msg}
184240
Reading queries from '{self.queries_path}'.
185241
Using '{self.target_package_name}' as package name.
186242
Generating package into '{self.target_package_path}'.
@@ -221,12 +277,16 @@ def used_settings_message(self):
221277
if self.plugins
222278
else "No plugin is being used."
223279
)
280+
introspection_msg = (
281+
self._introspection_settings_message() if self.using_remote_schema else ""
282+
)
224283

225284
if self.target_file_format == "py":
226285
return dedent(
227286
f"""\
228287
Selected strategy: {Strategy.GRAPHQL_SCHEMA}
229288
Using schema from {self.schema_path or self.remote_schema_url}
289+
{introspection_msg}
230290
Saving graphql schema to: {self.target_file_path}
231291
Using {self.schema_variable_name} as variable name for schema.
232292
Using {self.type_map_variable_name} as variable name for type map.
@@ -238,6 +298,7 @@ def used_settings_message(self):
238298
f"""\
239299
Selected strategy: {Strategy.GRAPHQL_SCHEMA}
240300
Using schema from {self.schema_path or self.remote_schema_url}
301+
{introspection_msg}
241302
Saving graphql schema to: {self.target_file_path}
242303
{plugins_msg}
243304
"""

tests/test_schema.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
read_graphql_file,
1818
walk_graphql_files,
1919
)
20+
from ariadne_codegen.settings import IntrospectionSettings
2021

2122

2223
@pytest.fixture
@@ -298,9 +299,11 @@ def test_introspect_remote_schema_raises_introspection_error_for_not_dict_respon
298299
return_value=httpx.Response(status_code=200, content="[]"),
299300
)
300301

301-
with pytest.raises(IntrospectionError):
302+
with pytest.raises(IntrospectionError) as exc:
302303
introspect_remote_schema("http://testserver/graphql/")
303304

305+
assert "Invalid introspection result format." in str(exc.value)
306+
304307

305308
def test_introspect_remote_schema_raises_introspection_error_for_json_without_data_key(
306309
mocker,
@@ -310,9 +313,11 @@ def test_introspect_remote_schema_raises_introspection_error_for_json_without_da
310313
return_value=httpx.Response(status_code=200, content='{"not_data": null}'),
311314
)
312315

313-
with pytest.raises(IntrospectionError):
316+
with pytest.raises(IntrospectionError) as exc:
314317
introspect_remote_schema("http://testserver/graphql/")
315318

319+
assert "Invalid data key in introspection result." in str(exc.value)
320+
316321

317322
def test_introspect_remote_schema_raises_introspection_error_for_graphql_errors(mocker):
318323
mocker.patch(
@@ -349,9 +354,11 @@ def test_introspect_remote_schema_raises_introspection_error_for_invalid_data_va
349354
),
350355
)
351356

352-
with pytest.raises(IntrospectionError):
357+
with pytest.raises(IntrospectionError) as exc:
353358
introspect_remote_schema("http://testserver/graphql/")
354359

360+
assert "Invalid data key in introspection result." in str(exc.value)
361+
355362

356363
def test_introspect_remote_schema_returns_introspection_result(mocker):
357364
mocker.patch(
@@ -440,3 +447,74 @@ def test_get_graphql_queries_with_invalid_query_for_schema_raises_invalid_operat
440447
get_graphql_queries(
441448
invalid_query_for_schema_file.as_posix(), build_schema(schema_str)
442449
)
450+
451+
452+
def test_introspect_remote_schema_passes_introspection_settings_to_introspection_query(
453+
mocker,
454+
):
455+
"""
456+
Test that the introspection settings are passed to the get_introspection_query
457+
function when introspecting the remote schema.
458+
"""
459+
mocked_get_query = mocker.patch(
460+
"ariadne_codegen.schema.get_introspection_query",
461+
return_value="query { __schema { queryType { name } } }",
462+
)
463+
mocker.patch(
464+
"ariadne_codegen.schema.httpx.post",
465+
return_value=httpx.Response(
466+
status_code=200, content='{"data": {"__schema": {}}}'
467+
),
468+
)
469+
470+
settings = IntrospectionSettings(
471+
descriptions=True,
472+
input_value_deprecation=True,
473+
specified_by_url=True,
474+
schema_description=True,
475+
directive_is_repeatable=True,
476+
input_object_one_of=True,
477+
)
478+
479+
introspect_remote_schema(
480+
"http://testserver/graphql/", introspection_settings=settings
481+
)
482+
483+
mocked_get_query.assert_called_once_with(
484+
descriptions=True,
485+
specified_by_url=True,
486+
directive_is_repeatable=True,
487+
schema_description=True,
488+
input_value_deprecation=True,
489+
input_object_one_of=True,
490+
)
491+
492+
493+
def test_introspect_remote_schema_uses_default_introspection_settings_when_not_provided(
494+
mocker,
495+
):
496+
"""
497+
Test that when introspection settings are not provided, the default values are used
498+
in the get_introspection_query call.
499+
"""
500+
mocked_get_query = mocker.patch(
501+
"ariadne_codegen.schema.get_introspection_query",
502+
return_value="query { __schema { queryType { name } } }",
503+
)
504+
mocker.patch(
505+
"ariadne_codegen.schema.httpx.post",
506+
return_value=httpx.Response(
507+
status_code=200, content='{"data": {"__schema": {}}}'
508+
),
509+
)
510+
511+
introspect_remote_schema("http://testserver/graphql/")
512+
513+
mocked_get_query.assert_called_once_with(
514+
descriptions=False,
515+
specified_by_url=False,
516+
directive_is_repeatable=False,
517+
schema_description=False,
518+
input_value_deprecation=False,
519+
input_object_one_of=False,
520+
)

0 commit comments

Comments
 (0)