From 415e97d4d3d3862b5a3b52b4093b8d9ce6e7d729 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Wed, 15 Apr 2026 10:01:48 +0200 Subject: [PATCH 1/2] fix: use explicit UTF-8 encoding for ruff format subprocess (#422) --- ariadne_codegen/utils.py | 1 + .../test_custom_arguments.py | 73 ++++++++++++++++++ tests/test_utils.py | 75 +++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 tests/client_generators/test_custom_arguments.py diff --git a/ariadne_codegen/utils.py b/ariadne_codegen/utils.py index 3225a79c..b52a4d9b 100644 --- a/ariadne_codegen/utils.py +++ b/ariadne_codegen/utils.py @@ -78,6 +78,7 @@ def _format_code(code: str, *, remove_unused_imports: bool = True) -> str: input=code, capture_output=True, text=True, + encoding="utf-8", check=False, timeout=_SUBPROCESS_TIMEOUT, ) diff --git a/tests/client_generators/test_custom_arguments.py b/tests/client_generators/test_custom_arguments.py new file mode 100644 index 00000000..3865e5c1 --- /dev/null +++ b/tests/client_generators/test_custom_arguments.py @@ -0,0 +1,73 @@ +import ast + +from graphql import build_schema + +from ariadne_codegen.client_generators.custom_arguments import ArgumentGenerator +from ariadne_codegen.codegen import generate_import_from + +from ..utils import compare_ast + + +def _expected_enum_import(name: str) -> ast.ImportFrom: + """Matches generate_import_from for GraphQLEnumType in _parse_graphql_type_name.""" + return generate_import_from(names=[name], from_="enums", level=1) + + +def test_generate_arguments_records_enums_submodule_import_for_enum_field_argument(): + schema = build_schema( + """ + schema { query: Query } + enum Color { RED GREEN } + type Query { + paint(color: Color!): String + } + """ + ) + field = schema.query_type.fields["paint"] + generator = ArgumentGenerator(custom_scalars={}, convert_to_snake_case=False) + + generator.generate_arguments(field.args) + + expected = _expected_enum_import("Color") + assert len(generator.imports) == 1 + assert compare_ast(generator.imports[0], expected) + + +def test_generate_arguments_records_enums_submodule_import_for_list_of_enum_arguments(): + schema = build_schema( + """ + schema { query: Query } + enum SortOrder { ASC DESC } + type Query { + items(order: [SortOrder!]!): String + } + """ + ) + field = schema.query_type.fields["items"] + generator = ArgumentGenerator(custom_scalars={}, convert_to_snake_case=False) + + generator.generate_arguments(field.args) + + expected = _expected_enum_import("SortOrder") + assert len(generator.imports) == 1 + assert compare_ast(generator.imports[0], expected) + + +def test_generate_arguments_records_enums_submodule_import_for_optional_enum_argument(): + schema = build_schema( + """ + schema { query: Query } + enum Priority { LOW HIGH } + type Query { + task(priority: Priority): String + } + """ + ) + field = schema.query_type.fields["task"] + generator = ArgumentGenerator(custom_scalars={}, convert_to_snake_case=False) + + generator.generate_arguments(field.args) + + expected = _expected_enum_import("Priority") + assert len(generator.imports) == 1 + assert compare_ast(generator.imports[0], expected) diff --git a/tests/test_utils.py b/tests/test_utils.py index 42a82331..b1b6d293 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,9 +1,11 @@ import ast +import subprocess from textwrap import dedent import pytest from ariadne_codegen.utils import ( + _format_code, add_extra_to_base_model, ast_to_str, convert_to_multiline_string, @@ -78,6 +80,79 @@ class TestClass(Efg): assert not_used_imported_class not in generated_code +def _patch_subprocess_run(mocker, real_run=subprocess.run): + """Force a Windows-like default for ruff format when encoding is omitted.""" + + def wrapped(*args, **kwargs): + argv = args[0] if args else () + is_ruff_format = isinstance(argv, (list, tuple)) and tuple(argv[2:4]) == ( + "ruff", + "format", + ) + if ( + is_ruff_format + and kwargs.get("text") + and kwargs.get("input") is not None + and kwargs.get("encoding") is None + ): + kwargs = {**kwargs, "encoding": "cp1252"} + return real_run(*args, **kwargs) + + return mocker.patch("ariadne_codegen.utils.subprocess.run", side_effect=wrapped) + + +def test_ast_to_str_non_ascii_unicode_round_trip_issue_422(mocker): + """Regression for mirumee/ariadne-codegen#422 (Windows cp1252 / ruff stdin). + + Large schemas (e.g. Shopify) embed non-ASCII in descriptions; formatting must + pass explicit UTF-8 to ruff format. Without it, default locale encoding can raise + UnicodeEncodeError (reproduced here by simulating cp1252 when encoding is omitted). + """ + _patch_subprocess_run(mocker) + + description = "商店 line: émoji 🛍️ — characters outside cp1252" + module = ast.Module( + body=[ + ast.ClassDef( + name="ModelWithDescription", + bases=[], + keywords=[], + body=[ + ast.Expr(value=ast.Constant(value=description)), + ast.Pass(), + ], + decorator_list=[], + ), + ], + type_ignores=[], + ) + ast.fix_missing_locations(module) + + generated = ast_to_str(module, remove_unused_imports=False) + + assert description in generated + ast.parse(generated) + generated.encode("utf-8") + + +def test_format_code_ruff_format_uses_utf8_encoding_issue_422(mocker): + """Ensure ruff format stdin/stdout use UTF-8 (mirumee/ariadne-codegen#422).""" + spy = mocker.patch("ariadne_codegen.utils.subprocess.run", wraps=subprocess.run) + + _format_code("x = 1\n") + + format_calls = [ + call + for call in spy.call_args_list + if len(call[0][0]) >= 4 + and call[0][0][2] == "ruff" + and call[0][0][3] == "format" + ] + assert format_calls, "expected a ruff format subprocess.run" + _args, kwargs = format_calls[-1] + assert kwargs.get("encoding") == "utf-8" + + @pytest.mark.parametrize( "name, expected_result", [ From 1db210aca00a844e873d417beee98889a37737c8 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Wed, 15 Apr 2026 10:38:38 +0200 Subject: [PATCH 2/2] improve test cases --- tests/test_utils.py | 60 ++++----------------------------------------- 1 file changed, 5 insertions(+), 55 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index b1b6d293..84e57620 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -80,59 +80,12 @@ class TestClass(Efg): assert not_used_imported_class not in generated_code -def _patch_subprocess_run(mocker, real_run=subprocess.run): - """Force a Windows-like default for ruff format when encoding is omitted.""" - - def wrapped(*args, **kwargs): - argv = args[0] if args else () - is_ruff_format = isinstance(argv, (list, tuple)) and tuple(argv[2:4]) == ( - "ruff", - "format", - ) - if ( - is_ruff_format - and kwargs.get("text") - and kwargs.get("input") is not None - and kwargs.get("encoding") is None - ): - kwargs = {**kwargs, "encoding": "cp1252"} - return real_run(*args, **kwargs) - - return mocker.patch("ariadne_codegen.utils.subprocess.run", side_effect=wrapped) - - -def test_ast_to_str_non_ascii_unicode_round_trip_issue_422(mocker): - """Regression for mirumee/ariadne-codegen#422 (Windows cp1252 / ruff stdin). - - Large schemas (e.g. Shopify) embed non-ASCII in descriptions; formatting must - pass explicit UTF-8 to ruff format. Without it, default locale encoding can raise - UnicodeEncodeError (reproduced here by simulating cp1252 when encoding is omitted). - """ - _patch_subprocess_run(mocker) - +def test_ast_to_str_non_ascii_unicode_round_trip_issue_422(): + """Regression for mirumee/ariadne-codegen#422 (Windows cp1252 / ruff stdin).""" description = "商店 line: émoji 🛍️ — characters outside cp1252" - module = ast.Module( - body=[ - ast.ClassDef( - name="ModelWithDescription", - bases=[], - keywords=[], - body=[ - ast.Expr(value=ast.Constant(value=description)), - ast.Pass(), - ], - decorator_list=[], - ), - ], - type_ignores=[], - ) - ast.fix_missing_locations(module) - + module = ast.parse(f'"""{description}"""') generated = ast_to_str(module, remove_unused_imports=False) - assert description in generated - ast.parse(generated) - generated.encode("utf-8") def test_format_code_ruff_format_uses_utf8_encoding_issue_422(mocker): @@ -144,13 +97,10 @@ def test_format_code_ruff_format_uses_utf8_encoding_issue_422(mocker): format_calls = [ call for call in spy.call_args_list - if len(call[0][0]) >= 4 - and call[0][0][2] == "ruff" - and call[0][0][3] == "format" + if tuple(call[0][0][2:4]) == ("ruff", "format") ] assert format_calls, "expected a ruff format subprocess.run" - _args, kwargs = format_calls[-1] - assert kwargs.get("encoding") == "utf-8" + assert format_calls[-1][1].get("encoding") == "utf-8" @pytest.mark.parametrize(