From ff1138ca4c265ecce22c0b7307c46cd7c5d120c7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 11 Oct 2026 04:40:33 +0200 Subject: [PATCH] cultcache-py: union arm keys bounded to Int32 at definition; Missing slot typed Node; nil list elements, unit-arm extra slots and UnionArm frozen-ness pinned Co-Authored-By: Claude Opus 5.5 --- .../src/cultcache_py/documents.py | 8 +++- .../cultcache-py/tests/test_union_parity.py | 38 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/cultcache-py/src/cultcache_py/documents.py b/packages/cultcache-py/src/cultcache_py/documents.py index 382ddfd3..4b780e58 100644 --- a/packages/cultcache-py/src/cultcache_py/documents.py +++ b/packages/cultcache-py/src/cultcache_py/documents.py @@ -295,8 +295,8 @@ def define_union( raise ValueError(f"Union {name!r} requires at least one arm") if len(by_name) != len(by_key): raise ValueError(f"Union {name!r} has duplicate arm names") - if any(not _is_key(key) or key < 0 for key in by_key): - raise ValueError(f"Union {name!r} arm keys must be non-negative integers") + if any(not _is_key(key) or not _INT32_MIN <= key <= _INT32_MAX for key in by_key): + raise ValueError(f"Union {name!r} arm keys must be integers within Int32") def encode(value: Any) -> Any: if value is None: @@ -339,6 +339,10 @@ def extract_value(value: Any, extractor: str | Extractor[Any]) -> str | int | fl return getattr(value, extractor, None) +_INT32_MIN = -(2**31) +_INT32_MAX = 2**31 - 1 + + def _is_key(value: Any) -> bool: return isinstance(value, int) and not isinstance(value, bool) diff --git a/packages/cultcache-py/tests/test_union_parity.py b/packages/cultcache-py/tests/test_union_parity.py index 74e4e3e9..83a4ed3e 100644 --- a/packages/cultcache-py/tests/test_union_parity.py +++ b/packages/cultcache-py/tests/test_union_parity.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import shutil import tempfile import unittest @@ -57,7 +58,7 @@ surface_document = define_database_entry_type( database_entry_field("outcomes", 3, codec=outcome, many=True), database_entry_field("part", 4, codec=item), database_entry_field("crafted", 5, codec=crafted), - database_entry_field("missing", 6, codec=item), + database_entry_field("missing", 6, codec=node), ], ) @@ -146,6 +147,16 @@ class UnionSurfaceVectorTests(unittest.TestCase): [envelope] = SingleFileMessagePackBackingStore(fresh).pull_all() self.assertEqual(envelope.payload, _vector("union-surface.msgpack")) + def test_the_missing_slot_is_a_node_and_agrees_with_the_csharp_written_root(self) -> None: + payload = _vector("union-surface.msgpack") + slots = msgpack.unpackb(payload, raw=False) + slots[6] = slots[2] + decoded = surface_document.decode_payload(msgpack.packb(slots, use_bin_type=True)) + self.assertEqual(decoded["missing"], _surface_value()["root"]) + self.assertEqual(surface_document.encode_payload(decoded), msgpack.packb(slots, use_bin_type=True)) + with self.assertRaises(ValueError): + surface_document.encode_payload({**_surface_value(), "missing": UnionArm("Part", {"name": "gear", "count": 4})}) + def test_the_store_vector_payload_equals_the_surface_payload(self) -> None: [envelope] = SingleFileMessagePackBackingStore(VECTORS / "union-surface.cc").pull_all() self.assertEqual(envelope.payload, _vector("union-surface.msgpack")) @@ -242,6 +253,22 @@ class UnionCodecRuleTests(unittest.TestCase): use.decode(["u", None, self.CANARY]) self.assertEqual(str(caught.exception), "DatabaseEntry document type 'Use' slot 2 expected a MessagePack array") + def test_a_list_of_nested_records_keeps_a_nil_element_as_csharp_writes_it(self) -> None: + row = define_record_codec("Row", [("id", 0), database_entry_field("cells", 1, codec=text, many=True)]) + cells = [None, {"id": "c", "content": "x"}, None] + encoded = row.encode({"id": "r", "cells": cells}) + self.assertEqual(encoded, ["r", [None, ["c", "x"], None]]) + self.assertEqual(row.decode(encoded), {"id": "r", "cells": cells}) + + def test_a_unit_arm_with_extra_slots_is_read_past(self) -> None: + self.assertEqual(outcome.decode([2, ["extra"]]), UnionArm("Denied")) + + def test_union_arms_are_immutable_hashable_values(self) -> None: + arm = UnionArm("Denied") + with self.assertRaises(dataclasses.FrozenInstanceError): + arm.name = "Pending" # type: ignore[misc] + self.assertEqual(len({UnionArm("Denied"), UnionArm("Denied"), UnionArm("Pending", "wait")}), 2) + def test_a_nested_record_with_a_missing_required_slot_is_refused(self) -> None: with self.assertRaises(ValueError) as caught: node.decode([1, ["only-id"]]) @@ -251,14 +278,19 @@ class UnionCodecRuleTests(unittest.TestCase): cases = [ ("Empty", {}, "Union 'Empty' requires at least one arm"), ("Dup", {0: ("A", None), 1: ("A", None)}, "Union 'Dup' has duplicate arm names"), - ("Neg", {-1: ("A", None)}, "Union 'Neg' arm keys must be non-negative integers"), - ("Flag", {True: ("A", None), "x": ("B", None)}, "Union 'Flag' arm keys must be non-negative integers"), + ("Wide", {2**31: ("A", None)}, "Union 'Wide' arm keys must be integers within Int32"), + ("Narrow", {-(2**31) - 1: ("A", None)}, "Union 'Narrow' arm keys must be integers within Int32"), + ("Flag", {True: ("A", None), "x": ("B", None)}, "Union 'Flag' arm keys must be integers within Int32"), ] for union_name, arms, expected in cases: with self.subTest(union=union_name): with self.assertRaises(ValueError) as caught: define_union(union_name, arms) self.assertEqual(str(caught.exception), expected) + edge = define_union("Edge", {-(2**31): ("Low", None), -1: ("Negative", None), 2**31 - 1: ("High", None)}) + for arm_name, key in (("Low", -(2**31)), ("Negative", -1), ("High", 2**31 - 1)): + self.assertEqual(edge.encode(UnionArm(arm_name)), [key, []]) + self.assertEqual(edge.decode([key, []]), UnionArm(arm_name)) with self.assertRaises(ValueError) as caught: database_entry_field("loose", 1, many=True) self.assertEqual(str(caught.exception), "A many DatabaseEntry field requires a codec")