From 4d90688095f6c23a8efe13a0ac98049e857d4e89 Mon Sep 17 00:00:00 2001 From: Alan Date: Thu, 1 Oct 2026 09:23:50 +0800 Subject: [PATCH] Keep UniqueList membership in sync on indexed replacement --- CHANGELOG.md | 5 +++ _python_utils_tests/test_containers.py | 44 ++++++++++++++++++++++++++ python_utils/containers.py | 10 +++--- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55c979f..f815ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +- Keep `UniqueList` membership in sync when replacing an indexed item, and + leave membership unchanged when the index is out of range. + ## 4.0.1 - 2026-08-30 - Allow `uv_build` 0.12.x, contributed by @felixonmars in PR #49. diff --git a/_python_utils_tests/test_containers.py b/_python_utils_tests/test_containers.py index ddee07e..cce8258 100644 --- a/_python_utils_tests/test_containers.py +++ b/_python_utils_tests/test_containers.py @@ -78,3 +78,47 @@ def test_sliceable_deque_eq() -> None: assert d == {1, 2, 3} assert d == d assert d == containers.SliceableDeque([1, 2, 3]) + + +@pytest.mark.parametrize('on_duplicate', ['ignore', 'raise']) +@pytest.mark.parametrize('index', [0, -2]) +def test_unique_list_replace_membership( + on_duplicate: containers.OnDuplicate, index: int +) -> None: + """Release the old value after replacing an indexed item.""" + values = containers.UniqueList(1, 2, on_duplicate=on_duplicate) + values[index] = 3 + + assert values == [3, 2] + assert 1 not in values + assert 3 in values + values.append(1) + assert values == [3, 2, 1] + + +@pytest.mark.parametrize('on_duplicate', ['ignore', 'raise']) +@pytest.mark.parametrize('index', [2, -3]) +def test_unique_list_failed_replace_preserves_membership( + on_duplicate: containers.OnDuplicate, index: int +) -> None: + """Do not reserve a value when indexed assignment fails.""" + values = containers.UniqueList(1, 2, on_duplicate=on_duplicate) + with pytest.raises(IndexError): + values[index] = 3 + + assert values == [1, 2] + assert 3 not in values + values.append(3) + assert values == [1, 2, 3] + + +@pytest.mark.parametrize('on_duplicate', ['ignore', 'raise']) +def test_unique_list_replace_same_value( + on_duplicate: containers.OnDuplicate, +) -> None: + """Replacing an item with itself keeps membership intact.""" + values = containers.UniqueList(1, 2, on_duplicate=on_duplicate) + values[0] = 1 + assert values == [1, 2] + assert 1 in values + assert 2 in values diff --git a/python_utils/containers.py b/python_utils/containers.py index 9318d95..3f19461 100644 --- a/python_utils/containers.py +++ b/python_utils/containers.py @@ -494,17 +494,19 @@ def __setitem__( self._set.update(values) else: values = typing.cast(HT, values) - if values in self._set and values != self[indices]: + old_value = self[indices] + if values in self._set and values != old_value: if self.on_duplicate == 'raise': raise ValueError(f'Duplicate value: {values}') else: return + super().__setitem__(indices, values) + self._set.remove(old_value) self._set.add(values) + return - super().__setitem__( - typing.cast(slice, indices), typing.cast(list[HT], values) - ) + super().__setitem__(indices, typing.cast(list[HT], values)) def __delitem__(self, index: typing.SupportsIndex | slice) -> None: """