Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
44 changes: 44 additions & 0 deletions _python_utils_tests/test_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 6 additions & 4 deletions python_utils/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down