From 4f8ced85472ce96847e4388060b7562a729d6196 Mon Sep 17 00:00:00 2001 From: txssu Date: Mon, 6 Apr 2026 17:09:05 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BE=D0=BA=20=D0=B8=D0=BC=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=BE=D0=B2=20=D0=BE=D0=B4=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/endpoint_module.py.jinja | 2 +- .../templates/model.py.jinja | 10 +-- tests/test_templates/test_imports_sorting.py | 81 +++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 tests/test_templates/test_imports_sorting.py diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 4db1c3546..c6d79b9a7 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -7,7 +7,7 @@ from ...client import AuthenticatedClient, Client from ...types import Response, UNSET from ... import errors -{% for relative in endpoint.relative_imports %} +{% for relative in endpoint.relative_imports | sort %} {{ relative }} {% endfor %} diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index d83bc4a71..6cf3b3fa8 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -13,11 +13,11 @@ import json from ..types import UNSET, Unset -{% for relative in model.relative_imports %} +{% for relative in model.relative_imports | sort %} {{ relative }} {% endfor %} -{% for lazy_import in model.lazy_imports %} +{% for lazy_import in model.lazy_imports | sort %} {{ lazy_import }} {% endfor %} @@ -121,7 +121,7 @@ return field_dict {% endmacro %} def to_dict(self) -> Dict[str, Any]: - {% for lazy_import in model.lazy_imports %} + {% for lazy_import in model.lazy_imports | sort %} {{ lazy_import }} {% endfor %} {{ _to_dict() | indent(8) }} @@ -133,7 +133,7 @@ return field_dict @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: - {% for lazy_import in model.lazy_imports %} + {% for lazy_import in model.lazy_imports | sort %} {{ lazy_import }} {% endfor %} d = src_dict.copy() @@ -162,7 +162,7 @@ return field_dict {% import "property_templates/" + model.additional_properties.template as prop_template %} {% if model.additional_properties.lazy_imports %} - {% for lazy_import in model.additional_properties.lazy_imports %} + {% for lazy_import in model.additional_properties.lazy_imports | sort %} {{ lazy_import }} {% endfor %} {% endif %} diff --git a/tests/test_templates/test_imports_sorting.py b/tests/test_templates/test_imports_sorting.py new file mode 100644 index 000000000..b20739b48 --- /dev/null +++ b/tests/test_templates/test_imports_sorting.py @@ -0,0 +1,81 @@ +"""Проверка, что импорты в сгенерированных файлах отсортированы одинаково.""" + +from types import SimpleNamespace + + +def _build_fake_model(*, relative_imports, lazy_imports): + return SimpleNamespace( + relative_imports=relative_imports, + lazy_imports=lazy_imports, + additional_properties=None, + is_multipart_body=False, + class_info=SimpleNamespace(name="MyClass", module_name="my_module"), + title=None, + description=None, + example=None, + required_properties=[], + optional_properties=[], + ) + + +def _ordered_indices(output: str, items): + """Return the index of the first occurrence of each item in ``output``.""" + return [output.index(item) for item in items] + + +def test_model_template_renders_relative_imports_sorted(env): + relative_imports = [ + "from ..models.charlie import Charlie", + "from ..models.alpha import Alpha", + "from ..models.bravo import Bravo", + ] + fake_model = _build_fake_model(relative_imports=relative_imports, lazy_imports=[]) + + output = env.get_template("model.py.jinja").render(model=fake_model) + + sorted_imports = sorted(relative_imports) + indices = _ordered_indices(output, sorted_imports) + assert indices == sorted(indices), ( + "relative_imports must be rendered in sorted order, got order: " + f"{[imp for _, imp in sorted(zip(indices, sorted_imports))]}" + ) + + +def test_model_template_renders_lazy_imports_sorted(env): + lazy_imports = [ + "from ..models.echo import Echo", + "from ..models.delta import Delta", + "from ..models.foxtrot import Foxtrot", + ] + fake_model = _build_fake_model(relative_imports=[], lazy_imports=lazy_imports) + + output = env.get_template("model.py.jinja").render(model=fake_model) + + sorted_imports = sorted(lazy_imports) + # lazy_imports appear at the top of the file *and* inside to_dict / from_dict. + # We only assert ordering on the first occurrence of each import. + indices = _ordered_indices(output, sorted_imports) + assert indices == sorted(indices), ( + "lazy_imports must be rendered in sorted order, got order: " + f"{[imp for _, imp in sorted(zip(indices, sorted_imports))]}" + ) + + +def test_model_template_render_is_deterministic_across_input_order(env): + """Rendering with the same imports in different input order produces the + same file - the bug being fixed.""" + imports_a = [ + "from ..models.alpha import Alpha", + "from ..models.beta import Beta", + "from ..models.gamma import Gamma", + ] + imports_b = list(reversed(imports_a)) + + out_a = env.get_template("model.py.jinja").render( + model=_build_fake_model(relative_imports=imports_a, lazy_imports=[]) + ) + out_b = env.get_template("model.py.jinja").render( + model=_build_fake_model(relative_imports=imports_b, lazy_imports=[]) + ) + + assert out_a == out_b, "model.py.jinja output depends on import iteration order"