From 716fd737f0a61c7dc17a68be2e4066e441cc8ab6 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Thu, 25 Jun 2026 01:54:21 +0200 Subject: [PATCH 1/2] fix: support negative indices in OrderedSet.__getitem__ Negative index access (e.g. ``s[-1]``) previously raised ``ValueError`` from ``itertools.islice``, which forbids negative start values. This is surprising because Python sequences uniformly accept negative indices (``s[-1]`` returns the last element) and the custom ``__getitem__`` on ``OrderedSet`` already raises ``IndexError`` for positive out-of-range indices. Normalise the index before passing it to ``islice``: add ``len(self)`` when ``index < 0``, and raise ``IndexError`` (not ``ValueError``) when the normalised index is still negative. The original index is preserved in the error message so the reported value matches what the caller passed. --- statemachine/orderedset.py | 13 ++++++++++++- tests/test_state.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/statemachine/orderedset.py b/statemachine/orderedset.py index 71d49779..931ea3ee 100644 --- a/statemachine/orderedset.py +++ b/statemachine/orderedset.py @@ -62,6 +62,12 @@ class OrderedSet(MutableSet[T]): >>> s[2] 3 + >>> s[-1] + 3 + + >>> s[-3] + 1 + >>> eval(repr(OrderedSet(['a', 'b', 'c']))) OrderedSet(['a', 'b', 'c']) @@ -84,10 +90,15 @@ def discard(self, x: T) -> None: self._d.pop(x, None) def __getitem__(self, index) -> T: + original_index = index + if index < 0: + index += len(self) + if index < 0: + raise IndexError(f"index {original_index} out of range") try: return next(itertools.islice(self._d, index, index + 1)) except StopIteration as e: - raise IndexError(f"index {index} out of range") from e + raise IndexError(f"index {original_index} out of range") from e def __contains__(self, x: object) -> bool: return self._d.__contains__(x) diff --git a/tests/test_state.py b/tests/test_state.py index c2f97c67..2e2d7f1c 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -56,6 +56,14 @@ def test_ordered_set_getitem(): assert s[2] == 30 +def test_ordered_set_getitem_negative_index(): + """OrderedSet supports negative index access like Python sequences.""" + s = OrderedSet([10, 20, 30]) + assert s[-1] == 30 + assert s[-2] == 20 + assert s[-3] == 10 + + def test_ordered_set_getitem_out_of_range(): """OrderedSet raises IndexError for out-of-range index.""" s = OrderedSet([10, 20]) @@ -63,6 +71,13 @@ def test_ordered_set_getitem_out_of_range(): s[5] +def test_ordered_set_getitem_negative_out_of_range(): + """OrderedSet raises IndexError for negative out-of-range index.""" + s = OrderedSet([10, 20]) + with pytest.raises(IndexError, match=r"index -3 out of range"): + s[-3] + + def test_ordered_set_union(): """OrderedSet.union returns new set with elements from both.""" s1 = OrderedSet([1, 2]) From d1f5d9b8223dbf69c1f3a12b8f8aa1e0a2ed99ee Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Thu, 25 Jun 2026 13:53:37 +0200 Subject: [PATCH 2/2] fix: remove extra blank lines in OrderedSet docstring --- statemachine/orderedset.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/statemachine/orderedset.py b/statemachine/orderedset.py index 931ea3ee..29f6fced 100644 --- a/statemachine/orderedset.py +++ b/statemachine/orderedset.py @@ -71,8 +71,6 @@ class OrderedSet(MutableSet[T]): >>> eval(repr(OrderedSet(['a', 'b', 'c']))) OrderedSet(['a', 'b', 'c']) - - """ __slots__ = ("_d",)