name = "more-itertools"
description = "Pure-Python iterator utilities — bug fixes mined from upstream history."
language = "python"
strategies = ["hypothesis", "crosshair"]
crate = "more-itertools"
base_commit = "bd8475cc1e9b2efdbc055ab4af4d4693a9c5cb7a"

# ---------------------------------------------------------------------------
# Variant: numeric_range_reversed_empty_edb3346_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["numeric_range_reversed_empty_edb3346_1"]

[tasks.source]
repo = "https://github.com/more-itertools/more-itertools"
commits = ["edb3346f835ca917efbfda5e2d6664ab952da369"]
commit_subjects = ["Fix empty ranges in numeric_range.__reversed__"]
origin = "internal"
summary = "``numeric_range.__reversed__`` calls ``self._get_by_index(-1)``, which raises ``IndexError`` for an empty range. The fix wraps the call in a ``try/except`` and returns ``iter([])`` on empty ranges."

[tasks.injection]
kind = "patch"
files = ["more_itertools/more.py"]
locations = [{ file = "more_itertools/more.py", symbol = "numeric_range.__reversed__" }]
patch = "patches/numeric_range_reversed_empty_edb3346_1.patch"

[tasks.bug]
short_name = "numeric_range_reversed_empty"
invariant = "For any valid (start, stop, step), ``list(reversed(numeric_range(start, stop, step)))`` equals ``list(numeric_range(start, stop, step))[::-1]`` and never raises."
how_triggered = "The mutation removes the empty-range guard; ``__reversed__`` calls ``self._get_by_index(-1)`` which raises ``IndexError`` whenever the range is empty (``start == stop``)."

[[tasks.tasks]]
property = "NumericRangeReversedHandlesEmpty"
witnesses = [
  { test_fn = "witness_numeric_range_reversed_handles_empty_case_basic", note = "Empty range numeric_range(0, 0, 1)." },
]

# ---------------------------------------------------------------------------
# Variant: numeric_range_negative_step_slice_a51da82_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["numeric_range_negative_step_slice_a51da82_1"]

[tasks.source]
repo = "https://github.com/more-itertools/more-itertools"
commits = ["a51da8244b6f9ec59ec05f81f1555f943d9506b2"]
commit_subjects = ["Fix numeric_range slicing with negative step returning empty range"]
origin = "internal"
summary = "``numeric_range.__getitem__`` did not normalise default ``start``/``stop`` for slices with a negative step direction, so ``nr[::-1]`` returned an empty range. The fix delegates to ``slice.indices`` and reconstructs the slice from the corrected indices."

[tasks.injection]
kind = "patch"
files = ["more_itertools/more.py"]
locations = [{ file = "more_itertools/more.py", symbol = "numeric_range.__getitem__" }]
patch = "patches/numeric_range_negative_step_slice_a51da82_1.patch"

[tasks.bug]
short_name = "numeric_range_negative_step_slice"
invariant = "For any valid ``numeric_range`` ``nr``, ``list(nr[::-1])`` equals ``list(nr)[::-1]``."
how_triggered = "The mutation reverts ``__getitem__`` to defaulting ``start`` to ``self._start`` and ``stop`` to ``self._stop`` regardless of step sign. With a negative slice step the resulting range is empty because ``start`` and ``stop`` are not swapped."

[[tasks.tasks]]
property = "NumericRangeSliceNegativeStep"
witnesses = [
  { test_fn = "witness_numeric_range_slice_negative_step_case_basic", note = "numeric_range(0, 10, 2)[::-1] should equal [8, 6, 4, 2, 0]." },
]

# ---------------------------------------------------------------------------
# Variant: last_reversed_truthy_check_cca3294_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["last_reversed_truthy_check_cca3294_1"]

[tasks.source]
repo = "https://github.com/more-itertools/more-itertools"
commits = ["cca32949f12d473fd823e37a5530c30d2faa1332"]
commit_subjects = ["fix last() when __reversed__ is None"]
origin = "internal"
summary = "``last`` checked ``hasattr(iterable, '__reversed__')`` to decide whether to call ``reversed()``. Python's protocol allows a class to opt out of ``reversed()`` by setting ``__reversed__ = None``, which still satisfies ``hasattr``, but ``reversed()`` then raises ``TypeError``. The fix uses ``getattr(iterable, '__reversed__', None)`` truthiness."

[tasks.injection]
kind = "patch"
files = ["more_itertools/more.py"]
locations = [{ file = "more_itertools/more.py", symbol = "last" }]
patch = "patches/last_reversed_truthy_check_cca3294_1.patch"

[tasks.bug]
short_name = "last_reversed_truthy_check"
invariant = "For any non-empty iterable ``it``, ``last(it)`` returns the same value as the last element produced by ``iter(it)`` — even when ``it`` declares ``__reversed__ = None``."
how_triggered = "The mutation reverts to ``hasattr(iterable, '__reversed__')``. For an iterable whose class sets ``__reversed__ = None`` (the official opt-out marker), the buggy branch calls ``reversed(iterable)`` which raises ``TypeError``."

[[tasks.tasks]]
property = "LastWithNoneReversed"
witnesses = [
  { test_fn = "witness_last_with_none_reversed_case_basic", note = "Custom iterable with __reversed__ = None over [1, 2, 3]." },
]

# ---------------------------------------------------------------------------
# Variant: split_before_empty_buffer_2e81a56_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["split_before_empty_buffer_2e81a56_1"]

[tasks.source]
repo = "https://github.com/more-itertools/more-itertools"
commits = ["2e81a562fbaccc996c19c069090a53f52ec894fe"]
commit_subjects = ["Fix split_before for an empty collections."]
origin = "internal"
summary = "``split_before`` unconditionally yielded the buffer after the loop, producing a trailing empty list when the input was empty. The fix gates the yield on ``if buf:``."

[tasks.injection]
kind = "patch"
files = ["more_itertools/more.py"]
locations = [{ file = "more_itertools/more.py", symbol = "split_before" }]
patch = "patches/split_before_empty_buffer_2e81a56_1.patch"

[tasks.bug]
short_name = "split_before_empty_buffer"
invariant = "``split_before([], pred)`` yields no chunks; in particular it never yields an empty list."
how_triggered = "The mutation removes the ``if buf:`` guard before the trailing ``yield buf``. When the input iterable is empty, ``buf`` is empty and the generator yields a stray ``[]``."

[[tasks.tasks]]
property = "SplitBeforeEmptyIterableNoEmpty"
witnesses = [
  { test_fn = "witness_split_before_empty_iterable_no_empty_case_basic", note = "split_before([], pred) must yield no chunks." },
]

# ---------------------------------------------------------------------------
# Variant: split_after_maxsplit_empty_9245cd0_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["split_after_maxsplit_empty_9245cd0_1"]

[tasks.source]
repo = "https://github.com/more-itertools/more-itertools"
commits = ["9245cd04c043d0d646497934df72549943d5f868"]
commit_subjects = ["Fix issue 658 for split_after"]
prs = [658]
summary = "When ``maxsplit == 1`` and the predicate matched the final element, ``split_after`` reached its ``yield list(it)`` branch with the iterator already exhausted — yielding a trailing ``[]`` chunk. The fix only yields if the trailing chunk is non-empty."

[tasks.injection]
kind = "patch"
files = ["more_itertools/more.py"]
locations = [{ file = "more_itertools/more.py", symbol = "split_after" }]
patch = "patches/split_after_maxsplit_empty_9245cd0_1.patch"

[tasks.bug]
short_name = "split_after_maxsplit_empty"
invariant = "``split_after`` never yields an empty chunk for any input, predicate, or ``maxsplit`` value."
how_triggered = "The mutation reverts to ``yield list(it)`` immediately when ``maxsplit == 1``. If the predicate just matched the final item, ``it`` is exhausted and the buggy branch yields ``[]``."

[[tasks.tasks]]
property = "SplitAfterMaxsplitNoEmpty"
witnesses = [
  { test_fn = "witness_split_after_maxsplit_no_empty_case_basic", note = "split_after([0], pred=x==0, maxsplit=1) must not yield a trailing []." },
]

# ---------------------------------------------------------------------------
# Variant: exactly_n_negative_input_adeda34_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["exactly_n_negative_input_adeda34_1"]

[tasks.source]
repo = "https://github.com/more-itertools/more-itertools"
commits = ["adeda34bd11ff636f5f19238cf422a73e1455483"]
commit_subjects = ["Fix bug for negative inputs to exactly_n(). Optimize code."]
origin = "internal"
summary = "``exactly_n(it, n)`` was implemented as ``ilen(islice(filter(pred, it), n + 1)) == n``. For ``n <= -2`` this passes a negative ``stop`` to ``islice`` which raises ``ValueError``. The fix returns ``False`` directly for ``n < 0``."

[tasks.injection]
kind = "patch"
files = ["more_itertools/more.py"]
locations = [{ file = "more_itertools/more.py", symbol = "exactly_n" }]
patch = "patches/exactly_n_negative_input_adeda34_1.patch"

[tasks.bug]
short_name = "exactly_n_negative_input"
invariant = "For any iterable and any ``n < 0``, ``exactly_n(iterable, n)`` returns ``False`` and does not raise."
how_triggered = "The mutation reverts ``exactly_n`` to its prior one-line ``islice``-based implementation. With ``n <= -2`` the call ``islice(..., n + 1)`` has a negative ``stop`` argument and raises ``ValueError``."

[[tasks.tasks]]
property = "ExactlyNRejectsNegative"
witnesses = [
  { test_fn = "witness_exactly_n_rejects_negative_case_basic", note = "exactly_n([True, False], -2) must return False, not raise." },
]

# ---------------------------------------------------------------------------
# Variant: iterate_func_stop_iteration_cd0a3a8_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["iterate_func_stop_iteration_cd0a3a8_1"]

[tasks.source]
repo = "https://github.com/more-itertools/more-itertools"
commits = ["cd0a3a87d231033db06a3c222fe893025e398a86"]
commit_subjects = ["Issue #707: fix iterate() to enable func to raise StopIteration"]
prs = [707]
summary = "Under PEP 479, a ``StopIteration`` raised inside a generator is converted to ``RuntimeError``. The bare ``while True: yield start; start = func(start)`` body in ``iterate`` therefore corrupted ``StopIteration`` raised from ``func``. The fix wraps the loop in ``with suppress(StopIteration):``."

[tasks.injection]
kind = "patch"
files = ["more_itertools/more.py"]
locations = [{ file = "more_itertools/more.py", symbol = "iterate" }]
patch = "patches/iterate_func_stop_iteration_cd0a3a8_1.patch"

[tasks.bug]
short_name = "iterate_func_stop_iteration"
invariant = "``iterate(func, start)`` yields ``start, func(start), func(func(start)), ...`` and stops cleanly when ``func`` raises ``StopIteration`` (no ``RuntimeError`` leak)."
how_triggered = "The mutation removes the ``with suppress(StopIteration):`` wrapper around the loop. Any ``StopIteration`` raised by ``func`` is converted to ``RuntimeError`` by PEP 479."

[[tasks.tasks]]
property = "IterateStopsOnStopIteration"
witnesses = [
  { test_fn = "witness_iterate_stops_on_stop_iteration_case_basic", note = "f(3) raises StopIteration; iterate must stop after yielding [0, 1, 2, 3]." },
]

# ---------------------------------------------------------------------------
# Variant: product_index_iterator_input_cf186b5_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["product_index_iterator_input_cf186b5_1"]

[tasks.source]
repo = "https://github.com/more-itertools/more-itertools"
commits = ["cf186b5de4797602fef76c3391da2d110b88a954"]
commit_subjects = ["Fix product_index() with iterator input"]
origin = "internal"
summary = "``product_index`` validated arity with ``len(element) != len(args)``. ``len(element)`` raises ``TypeError`` for bare iterators (e.g. generators); the fix uses the materialised tuples ``len(elements) != len(pools)`` instead."

[tasks.injection]
kind = "patch"
files = ["more_itertools/more.py"]
locations = [{ file = "more_itertools/more.py", symbol = "product_index" }]
patch = "patches/product_index_iterator_input_cf186b5_1.patch"

[tasks.bug]
short_name = "product_index_iterator_input"
invariant = "For any element-iterable that produces a tuple equal to a list ``L``, ``product_index(elem_iter, *pools)`` returns the same value as ``product_index(L, *pools)``."
how_triggered = "The mutation reverts the arity check to ``len(element) != len(pools)``. When ``element`` is a bare iterator, ``len(element)`` raises ``TypeError`` before any product index is computed."

[[tasks.tasks]]
property = "ProductIndexAcceptsIterator"
witnesses = [
  { test_fn = "witness_product_index_accepts_iterator_case_basic", note = "iter([2, 3, 1]) over three range(5) pools." },
]
