Skip to content

Commit 8960caa

Browse files
committed
fix(codegen): stabilize Used By sort order
After resolving type name collisions across themes (101596f), two referrers from different modules can share a display name. The sort key (kind, name) produced ties, and Python's sorted() preserved set iteration order for tied elements -- which depends on id()-based hashing and varies across process invocations. Add the source module as a tiebreaker: (kind, name, module). Expose TypeIdentity.module property to encapsulate the getattr(obj, "__module__") access pattern.
1 parent 6a1c70c commit 8960caa

File tree

3 files changed

+50
-2
lines changed

3 files changed

+50
-2
lines changed

packages/overture-schema-codegen/src/overture/schema/codegen/reverse_references.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,15 @@ def collect_from_newtype_spec(spec: NewTypeSpec, referrer: TypeIdentity) -> None
159159
elif isinstance(supp_spec, ModelSpec):
160160
collect_from_model_spec(supp_spec, tid)
161161

162-
# Sort sets into lists
162+
# Sort into deterministic lists. (kind, name) handles the common case;
163+
# module breaks ties when two referrers share the same display name
164+
# (e.g. identically-named types from different themes/modules).
163165
result: dict[TypeIdentity, list[UsedByEntry]] = {}
164166
for target, ref_set in references.items():
165-
entries = sorted(ref_set, key=lambda e: (e.kind.value, e.identity.name))
167+
entries = sorted(
168+
ref_set,
169+
key=lambda e: (e.kind.value, e.identity.name, e.identity.module),
170+
)
166171
result[target] = entries
167172

168173
return result

packages/overture-schema-codegen/src/overture/schema/codegen/specs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ def __eq__(self, other: object) -> bool:
5858
def __hash__(self) -> int:
5959
return id(self.obj)
6060

61+
@property
62+
def module(self) -> str:
63+
"""Source module of the underlying object, or empty string."""
64+
return getattr(self.obj, "__module__", "")
65+
6166

6267
class _SourceTypeIdentityMixin:
6368
"""Mixin providing ``identity`` from ``source_type`` and ``name``.

packages/overture-schema-codegen/tests/test_reverse_references.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for reverse reference computation."""
22

3+
from enum import Enum as PyEnum
34
from typing import NewType
45

56
import pytest
@@ -14,6 +15,7 @@
1415
lookup_by_name,
1516
make_union_spec,
1617
)
18+
from overture.schema.codegen.enum_extraction import extract_enum
1719
from overture.schema.codegen.model_extraction import expand_model_tree, extract_model
1820
from overture.schema.codegen.newtype_extraction import extract_newtype
1921
from overture.schema.codegen.reverse_references import (
@@ -24,6 +26,7 @@
2426
from overture.schema.codegen.type_collection import collect_all_supplementary_types
2527
from overture.schema.system.ref import Id
2628
from overture.schema.system.string import NoWhitespaceString
29+
from pydantic import BaseModel
2730

2831

2932
@pytest.mark.parametrize(
@@ -150,6 +153,41 @@ def test_pydantic_type_has_used_by_from_feature() -> None:
150153
assert any(e.identity.name == "FeatureWithUrl" for e in entries)
151154

152155

156+
def test_sort_tiebreaker_uses_module_for_same_name_referrers() -> None:
157+
"""Referrers with the same name sort deterministically by module."""
158+
159+
# Two model classes named "Feature" from different modules.
160+
class SharedEnum(PyEnum):
161+
A = "a"
162+
163+
class FeatureAlpha(BaseModel):
164+
value: SharedEnum
165+
166+
class FeatureBeta(BaseModel):
167+
value: SharedEnum
168+
169+
FeatureAlpha.__name__ = "Feature"
170+
FeatureAlpha.__module__ = "alpha.models"
171+
FeatureBeta.__name__ = "Feature"
172+
FeatureBeta.__module__ = "beta.models"
173+
174+
spec_a = extract_model(FeatureAlpha, entry_point="Feature")
175+
spec_b = extract_model(FeatureBeta, entry_point="Feature")
176+
expand_model_tree(spec_a)
177+
expand_model_tree(spec_b)
178+
179+
enum_id = TypeIdentity(SharedEnum, "SharedEnum")
180+
all_specs = {enum_id: extract_enum(SharedEnum)}
181+
182+
result = compute_reverse_references([spec_a, spec_b], all_specs)
183+
184+
entries = lookup_by_name(result, "SharedEnum")
185+
assert len(entries) == 2
186+
# Both named "Feature" — module provides the tiebreaker
187+
modules = [e.identity.module for e in entries]
188+
assert modules == ["alpha.models", "beta.models"]
189+
190+
153191
def test_sorting_models_before_newtypes() -> None:
154192
"""Sorting produces models before NewTypes, alphabetical within groups."""
155193
# Create a test where the same type (Id) is referenced by:

0 commit comments

Comments
 (0)