From 7570383c967a4700b0d4933ffe929e27642496e3 Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Sun, 26 Jul 2026 18:43:56 -0600 Subject: [PATCH] Make CI green: version-independent flag names, SWR corpus skip CI has been red on every master push since 2026-07-13, failing 11 tests. Two causes: 1. EnumNameConverter relied on composite Flag .name, which is None before Python 3.11, so 3.10 emitted 'ROM_ACT_TYPES.3' where 3.11+ emits 'ROM_ACT_TYPES.IS_NPC|SENTINEL'. The converter now decomposes unnamed composites into single-bit member names in ascending bit order, matching 3.11+ output exactly on every version. The named-combinations test also derived its expectation from .name; it now builds it from single-bit members directly. 2. test_fuss_fields_are_editable_declaratively was the only SWR test missing the corpus-unavailable skip guard, so it crashed on the hardcoded local path when C:\Users\Q\src\swrfuss is absent. Verified: full suite passes on CPython 3.10.11, 3.11, and 3.13 locally (938 passed each), interpreter versions confirmed in-run. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HfQ678eD83tiAALqNkct1U --- area_reader/__init__.py | 20 +++++++++++++++++++- test_jsonification.py | 17 ++++++++++++----- test_swr_writer.py | 2 ++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/area_reader/__init__.py b/area_reader/__init__.py index 10a0685..4b2f3c3 100644 --- a/area_reader/__init__.py +++ b/area_reader/__init__.py @@ -3901,7 +3901,25 @@ def __init__(self, *args, **kwargs): ) def _unstructure_enum(self, obj): - name = obj.name if obj.name is not None else str(obj.value) + # Composite Flag values have no .name before Python 3.11; decompose + # into single-bit member names in ascending bit order, matching the + # names 3.11+ produces, so JSON output is identical on every version. + name = obj.name + if name is None and isinstance(obj, enum.Flag): + bits = { + member.value: member.name + for member in type(obj).__members__.values() + if member.value and not member.value & (member.value - 1) + } + names = [bits[bit] for bit in sorted(bits) if obj.value & bit] + covered = 0 + for bit in bits: + if obj.value & bit: + covered |= bit + if names and covered == obj.value: + name = "|".join(names) + if name is None: + name = str(obj.value) return obj.__class__.__name__ + "." + name diff --git a/test_jsonification.py b/test_jsonification.py index 974bdb2..b41fcbc 100644 --- a/test_jsonification.py +++ b/test_jsonification.py @@ -101,14 +101,21 @@ def test_every_int_flag_type_jsonifies_zero(flag_type): ids=lambda flag_type: flag_type.__name__, ) def test_every_int_flag_type_jsonifies_named_combinations(flag_type): - members = list(flag_type) - if len(members) < 2: - pytest.skip("flag type has fewer than two iterable members") - value = members[0] | members[1] + bits = sorted( + { + member.value: member + for member in flag_type.__members__.values() + if member.value and not member.value & (member.value - 1) + }.values(), + key=lambda member: member.value, + ) + if len(bits) < 2: + pytest.skip("flag type has fewer than two single-bit members") + value = bits[0] | bits[1] result = area_reader.EnumNameConverter().unstructure(value) - assert result == f"{flag_type.__name__}.{value.name}" + assert result == f"{flag_type.__name__}.{bits[0].name}|{bits[1].name}" assert json.loads(json.dumps(result)) == result diff --git a/test_swr_writer.py b/test_swr_writer.py index 28c3ef4..253b0e4 100644 --- a/test_swr_writer.py +++ b/test_swr_writer.py @@ -96,6 +96,8 @@ def test_swr_write_uses_the_canonical_rendering(tmp_path: Path) -> None: def test_fuss_fields_are_editable_declaratively(tmp_path: Path) -> None: + if not swr_paths(): + pytest.skip("upstream SWR/FUSS corpus is unavailable") source = load_swr(UPSTREAM_SWR / "area" / "kashyyyk") mob = next(iter(source.area.mobs.values())) room = next(iter(source.area.rooms.values()))