From c41b2cac184fd9a5f62181bd333d60843f879e02 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 5 May 2026 17:12:47 +0200 Subject: [PATCH 1/3] fix: OrderedNamespaceSet.__setitem__ slice uses exhausted iterator slice branch stored exhausted islice iterator back into _order instead of the materialized successful_new_items list, resulting in an empty slice assignment that discarded all newly added items. Fixes #494 --- sdk/basyx/aas/model/base.py | 2 +- sdk/test/model/test_base.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/sdk/basyx/aas/model/base.py b/sdk/basyx/aas/model/base.py index c9c8c8fe..f668fc58 100644 --- a/sdk/basyx/aas/model/base.py +++ b/sdk/basyx/aas/model/base.py @@ -2248,7 +2248,7 @@ def __setitem__(self, s, o) -> None: for i in successful_new_items: super().remove(i) raise - self._order[s] = new_items + self._order[s] = successful_new_items for i in deleted_items: super().remove(i) diff --git a/sdk/test/model/test_base.py b/sdk/test/model/test_base.py index e300cc1f..8513c1c3 100644 --- a/sdk/test/model/test_base.py +++ b/sdk/test/model/test_base.py @@ -722,6 +722,35 @@ def test_OrderedNamespace(self) -> None: f"{self._namespace_class.__name__}[{self.namespace.id}]'", # type: ignore[has-type] str(cm2.exception)) + def test_ordered_namespaceset_slice_setitem_preserves_order(self) -> None: + # Replace a slice of items; the new items must appear in the correct positions after replacement + ns = ExampleOrderedNamespace() + sid1 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/sid1"),)) + sid2 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/sid2"),)) + sid3 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/sid3"),)) + sid4 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/sid4"),)) + sid5 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/sid5"),)) + p1 = model.Property("PA", model.datatypes.Int, semantic_id=sid1) + p2 = model.Property("PB", model.datatypes.Int, semantic_id=sid2) + p3 = model.Property("PC", model.datatypes.Int, semantic_id=sid3) + ns.set1.add(p1) + ns.set1.add(p2) + ns.set1.add(p3) + self.assertEqual([p1, p2, p3], list(ns.set1)) + + # Replace slice [0:2] (p1, p2) with two new items + new1 = model.Property("PX", model.datatypes.Int, semantic_id=sid4) + new2 = model.Property("PY", model.datatypes.Int, semantic_id=sid5) + ns.set1[0:2] = [new1, new2] + + # After replacement: [new1, new2, p3] + result = list(ns.set1) + self.assertEqual([new1, new2, p3], result) + self.assertIsNone(p1.parent) + self.assertIsNone(p2.parent) + self.assertIs(ns, new1.parent) + self.assertIs(ns, new2.parent) + class ExternalReferenceTest(unittest.TestCase): def test_constraints(self): From 31a07a90796d842c15f3051662081e5fbbfb1dd4 Mon Sep 17 00:00:00 2001 From: zrgt Date: Tue, 5 May 2026 17:18:34 +0200 Subject: [PATCH 2/3] fix: OrderedNamespaceSet.__setitem__ int removes old item before adding new add-before-remove caused false AASConstraintViolation when new item shares an attribute (e.g. id_short) with the item being replaced. Fix removes old item first, rolls back if the subsequent add fails. Fixes #498 --- sdk/basyx/aas/model/base.py | 10 ++++++++-- sdk/test/model/test_base.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/sdk/basyx/aas/model/base.py b/sdk/basyx/aas/model/base.py index c9c8c8fe..22b22317 100644 --- a/sdk/basyx/aas/model/base.py +++ b/sdk/basyx/aas/model/base.py @@ -2232,9 +2232,15 @@ def __setitem__(self, s: slice, o: Iterable[_NSO]) -> None: ... def __setitem__(self, s, o) -> None: if isinstance(s, int): - deleted_items = [self._order[s]] - super().add(o) + old_item = self._order[s] + super().remove(old_item) + try: + super().add(o) + except Exception: + super().add(old_item) + raise self._order[s] = o + return else: deleted_items = self._order[s] new_items = itertools.islice(o, len(deleted_items)) diff --git a/sdk/test/model/test_base.py b/sdk/test/model/test_base.py index e300cc1f..7780a3cb 100644 --- a/sdk/test/model/test_base.py +++ b/sdk/test/model/test_base.py @@ -722,6 +722,21 @@ def test_OrderedNamespace(self) -> None: f"{self._namespace_class.__name__}[{self.namespace.id}]'", # type: ignore[has-type] str(cm2.exception)) + def test_ordered_namespaceset_int_setitem_same_id_short(self) -> None: + # Replacing item at index with new item sharing same id_short must succeed; + # the add-before-remove order causes a false AASConstraintViolation otherwise + ns = ExampleOrderedNamespace() + sid1 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/s1"),)) + sid2 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/s2"),)) + old = model.Property("SameName", model.datatypes.Int, semantic_id=sid1) + new = model.Property("SameName", model.datatypes.Int, semantic_id=sid2) + ns.set1.add(old) + # Replace old with new — both have id_short "SameName"; must not raise AASConstraintViolation + ns.set1[0] = new + self.assertEqual([new], list(ns.set1)) + self.assertIsNone(old.parent) + self.assertIs(ns, new.parent) + class ExternalReferenceTest(unittest.TestCase): def test_constraints(self): From 60b36b5785a5564f87c17fb148b9852f7849ff7f Mon Sep 17 00:00:00 2001 From: zrgt Date: Thu, 7 May 2026 19:29:08 +0200 Subject: [PATCH 3/3] test: verify __setitem__ int preserves index of replaced item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test_ordered_namespaceset_int_setitem_preserves_index: build [p0, old, p2], replace index 1 with new (same id_short as old), assert result is [p0, new, p2] — no index shift and no AASConstraintViolation. Addresses review feedback on #517 --- sdk/test/model/test_base.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/sdk/test/model/test_base.py b/sdk/test/model/test_base.py index 7780a3cb..f0470ab6 100644 --- a/sdk/test/model/test_base.py +++ b/sdk/test/model/test_base.py @@ -722,20 +722,32 @@ def test_OrderedNamespace(self) -> None: f"{self._namespace_class.__name__}[{self.namespace.id}]'", # type: ignore[has-type] str(cm2.exception)) - def test_ordered_namespaceset_int_setitem_same_id_short(self) -> None: - # Replacing item at index with new item sharing same id_short must succeed; - # the add-before-remove order causes a false AASConstraintViolation otherwise + def test_ordered_namespaceset_int_setitem_preserves_index(self) -> None: + # __setitem__ int must place the new item at the exact index of the replaced item. + # Items before and after the replaced index must not shift. ns = ExampleOrderedNamespace() sid1 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/s1"),)) sid2 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/s2"),)) - old = model.Property("SameName", model.datatypes.Int, semantic_id=sid1) - new = model.Property("SameName", model.datatypes.Int, semantic_id=sid2) + sid3 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/s3"),)) + sid4 = model.ExternalReference((model.Key(model.KeyTypes.GLOBAL_REFERENCE, "http://example.org/s4"),)) + p0 = model.Property("PA", model.datatypes.Int, semantic_id=sid1) + old = model.Property("PB", model.datatypes.Int, semantic_id=sid2) + p2 = model.Property("PC", model.datatypes.Int, semantic_id=sid3) + new = model.Property("PB", model.datatypes.Int, semantic_id=sid4) # same id_short as old + ns.set1.add(p0) ns.set1.add(old) - # Replace old with new — both have id_short "SameName"; must not raise AASConstraintViolation - ns.set1[0] = new - self.assertEqual([new], list(ns.set1)) - self.assertIsNone(old.parent) + ns.set1.add(p2) + # set1 is [p0, old, p2] at indices [0, 1, 2] + + # Replace middle item (index 1) — same id_short must not raise AASConstraintViolation + ns.set1[1] = new + + # p0 stays at 0, new is at 1, p2 stays at 2 — no index shift + self.assertIs(p0, ns.set1[0]) + self.assertIs(new, ns.set1[1]) + self.assertIs(p2, ns.set1[2]) self.assertIs(ns, new.parent) + self.assertIsNone(old.parent) class ExternalReferenceTest(unittest.TestCase):