From 5586ebf1faa211b8ec608b462a513759c3c8c459 Mon Sep 17 00:00:00 2001 From: nawaaaaaAaar <198336470+nawaaaaaAaar@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:19:10 +0000 Subject: [PATCH 1/2] Support integer in-place multiplication and preserve empty cycle keys --- cycler/__init__.py | 18 ++++++++++++------ test_cycler.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index db476b8..b28e27e 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -77,10 +77,8 @@ def _process_keys( keys : set The keys in the composition of the two cyclers. """ - l_peek: dict[K, V] = next(iter(left)) if left != [] else {} - r_peek: dict[K, V] = next(iter(right)) if right is not None else {} - l_key: set[K] = set(l_peek.keys()) - r_key: set[K] = set(r_peek.keys()) + l_key: set[K] = left.keys if isinstance(left, Cycler) else set(next(iter(left), {})) + r_key: set[K] = right.keys if right is not None else set() if common_keys := l_key & r_key: raise ValueError( f"Cannot compose overlapping cycles, duplicate key(s): {common_keys}" @@ -166,6 +164,7 @@ def __init__( self._left: Cycler[K, V] | list[dict[K, V]] = Cycler( left._left, left._right, left._op ) + self._left._keys = left.keys elif left is not None: # Need to copy the dictionary or else that will be a residual # mutable that could lead to strange errors @@ -177,6 +176,7 @@ def __init__( self._right: Cycler[K, V] | None = Cycler( right._left, right._right, right._op ) + self._right._keys = right.keys else: self._right = None @@ -369,12 +369,18 @@ def __iadd__(self, other: Cycler[K, V]) -> Cycler[K, V]: # type: ignore[misc] def __imul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: # type: ignore[misc] """ - In-place outer product of two cyclers (`itertools.product`). + In-place outer product of two cyclers or integer multiplication. Parameters ---------- - other : Cycler + other : Cycler or int """ + if isinstance(other, int): + multiplied = self * other + self._left = multiplied._left + self._right = multiplied._right + self._op = multiplied._op + return self if not isinstance(other, Cycler): raise TypeError("Cannot *= with a non-Cycler object") # True shallow copy of self is fine since this is in-place diff --git a/test_cycler.py b/test_cycler.py index 1eccac3..fce29f1 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -80,6 +80,38 @@ def test_inplace_add_len_mismatch(): c1 += c3 +@pytest.mark.parametrize('factor', [-2, 0, 1, 2, 3]) +@pytest.mark.parametrize('make_cycle', [ + lambda: cycler(c='rb'), + lambda: cycler(c='rb') + cycler(lw=[1, 2]), + lambda: cycler(c='rb') * cycler(lw=[1, 2]), +], ids=['base', 'sum', 'product']) +def test_inplace_integer_mul(make_cycle, factor): + c = make_cycle() + alias = c + expected = c * factor + c *= factor + assert c is alias + _cycles_equal(c, expected) + assert c.keys == expected.keys + assert c.by_key() == expected.by_key() + assert repr(c) == repr(expected) + + +@pytest.mark.parametrize('factor', [-2, 0]) +@pytest.mark.parametrize('make_cycle', [ + lambda: cycler(c='rb'), + lambda: cycler(c='rb') + cycler(lw=[1, 2]), + lambda: cycler(c='rb') * cycler(lw=[1, 2]), +], ids=['base', 'sum', 'product']) +def test_integer_mul_empty_preserves_keys(make_cycle, factor): + c = make_cycle() + result = c * factor + assert list(result) == [] + assert len(result) == 0 + assert result.keys == c.keys + + def test_inplace_mul(): c3 = cycler(c='rgb') c4 = cycler(lw=range(3)) From 1bc923d1570dd94899c2fa3a216954e5d838bf5c Mon Sep 17 00:00:00 2001 From: nawaaaaaAaar <198336470+nawaaaaaAaar@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:46:51 +0000 Subject: [PATCH 2/2] Preserve iterable helper inputs in key processing --- cycler/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index b28e27e..ef4d5e1 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -78,7 +78,9 @@ def _process_keys( The keys in the composition of the two cyclers. """ l_key: set[K] = left.keys if isinstance(left, Cycler) else set(next(iter(left), {})) - r_key: set[K] = right.keys if right is not None else set() + r_key: set[K] = set() + if right is not None: + r_key = right.keys if isinstance(right, Cycler) else set(next(iter(right), {})) if common_keys := l_key & r_key: raise ValueError( f"Cannot compose overlapping cycles, duplicate key(s): {common_keys}"