name = "toolz"
description = "Pure-Python functional utilities — bug fixes mined from upstream history."
language = "python"
strategies = ["hypothesis", "crosshair"]
crate = "toolz"
base_commit = "568c2b8393973cd172a466546c9d95779c452438"

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

[tasks.source]
repo = "https://github.com/pytoolz/toolz"
commits = ["5a7e078c941c990dcaca53ac6920e5d5c0b1475f"]
commit_subjects = ["Raise in partition_all when length is invalid (#603)"]
prs = [603]
issues = [602]
origin = "internal"
summary = "``partition_all`` blindly trusts ``len(seq) % n`` to decide where the no-pad sentinels start in the final tuple. If the iterable's ``__len__`` lies (e.g. a list subclass that returns ``super().__len__() + 1``), the slice ``prev[:len(seq) % n]`` either drops a real value or includes a ``no_pad`` sentinel — silently bad output. The fix asserts that ``prev[end-1]`` is a real value and ``prev[end]`` is the sentinel before yielding the slice, raising ``LookupError`` when the invariant fails."

[tasks.injection]
kind = "patch"
files = ["toolz/itertoolz.py"]
locations = [{ file = "toolz/itertoolz.py", line = 729, symbol = "partition_all" }]
patch = "patches/partition_all_invalid_length_5a7e078_1.patch"

[tasks.bug]
short_name = "partition_all_invalid_length"
invariant = "When a sequence's ``__len__`` is inconsistent with its iterated contents, ``partition_all(n, seq)`` must raise ``LookupError`` rather than silently emitting tuples that contain ``no_pad`` sentinels or drop real elements."
how_triggered = "The mutation drops the ``end-1``/``end`` sentinel check from the fast path. With a list subclass whose ``__len__`` is off-by-one, ``len(seq) % n`` lands at the wrong slice boundary and the function yields a tuple containing the internal ``no_pad`` sentinel (or drops a value)."

[[tasks.tasks]]
property = "PartitionAllValidatesLength"
witnesses = [
  { test_fn = "witness_partition_all_validates_length_case_off_by_one", note = "List subclass whose __len__ over-reports by one — fix raises, bug emits sentinel" },
]

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

[tasks.source]
repo = "https://github.com/pytoolz/toolz"
commits = ["80ae19783d10852619d435253cfc2e052be7a73c"]
commit_subjects = ["Fix missed items call."]
origin = "internal"
summary = "The full / right-default branch of ``join`` iterated unmatched left keys with ``iteritems(d)``. ``iteritems`` is defined in ``toolz.compatibility`` but never imported into ``toolz.itertoolz``, so any ``join(..., right_default=...)`` call where some left keys went unmatched would raise ``NameError`` at iteration time. The fix uses ``d.items()`` directly."

[tasks.injection]
kind = "patch"
files = ["toolz/itertoolz.py"]
locations = [{ file = "toolz/itertoolz.py", line = 918, symbol = "join" }]
patch = "patches/join_iteritems_namerror_80ae197_1.patch"

[tasks.bug]
short_name = "join_iteritems_namerror"
invariant = "``join(leftkey, leftseq, rightkey, rightseq, right_default=...)`` yields one ``(left_match, right_default)`` tuple per left key with no matching right element — without raising."
how_triggered = "The mutation reverts ``d.items()`` back to ``iteritems(d)``. Since ``iteritems`` is not imported in ``toolz/itertoolz.py``, the unmatched-keys loop raises NameError as soon as it is reached."

[[tasks.tasks]]
property = "JoinRightDefaultIterates"
witnesses = [
  { test_fn = "witness_join_right_default_iterates_case_basic", note = "Full outer join with one unmatched left key — bug NameErrors, fix yields the unmatched pair" },
]

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

[tasks.source]
repo = "https://github.com/pytoolz/toolz"
commits = ["e9c2724108a1487a1e781c86999ccd9d73471653"]
commit_subjects = ["raise if chunksize less than two"]
origin = "internal"
summary = "``toolz.sandbox.parallel.fold`` partitions the sequence into chunks of size ``chunksize`` and reduces each chunk before combining. With ``chunksize <= 1`` the per-chunk reduce never sees two elements, so it returns the single element instead of applying ``binop`` — and the overall result is wrong (e.g. ``fold(add, [1,2,3,4], chunksize=1)`` returns the last element rather than the sum). The fix asserts ``chunksize > 1`` so the misuse fails loudly."

[tasks.injection]
kind = "patch"
files = ["toolz/sandbox/parallel.py"]
locations = [{ file = "toolz/sandbox/parallel.py", line = 53, symbol = "fold" }]
patch = "patches/fold_chunksize_one_e9c2724_1.patch"

[tasks.bug]
short_name = "fold_chunksize_one"
invariant = "``toolz.sandbox.parallel.fold(binop, seq, chunksize=k)`` raises ``AssertionError`` for any ``k <= 1``."
how_triggered = "The mutation drops the ``assert chunksize > 1`` line. With ``chunksize=1`` the function silently produces a wrong reduction (the sequence is not actually combined), so the test that expects ``AssertionError`` instead observes a return value."

[[tasks.tasks]]
property = "FoldChunksizeOneRejected"
witnesses = [
  { test_fn = "witness_fold_chunksize_one_rejected_case_basic", note = "fold(add, [1,2,3,4], chunksize=1) must raise; bug returns silently" },
]

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

[tasks.source]
repo = "https://github.com/pytoolz/toolz"
commits = ["dd87e4f20f6ef591a57f3d28f43c1d1eb1318794"]
commit_subjects = ["Fix topk to work with 0 for key"]
origin = "internal"
summary = "``topk(k, seq, key=0)`` should sort tuples by their first element, but the buggy guard ``if key and not callable(key)`` treats ``key=0`` as falsy and skips the ``getter(0)`` conversion. ``key=0`` then propagates to ``heapq.nlargest`` as a non-callable integer, which raises ``TypeError: 'int' object is not callable``. The fix uses ``if key is not None and not callable(key)`` so integer indexers are honoured."

[tasks.injection]
kind = "patch"
files = ["toolz/itertoolz.py"]
locations = [{ file = "toolz/itertoolz.py", line = 990, symbol = "topk" }]
patch = "patches/topk_zero_key_falsy_dd87e4f_1.patch"

[tasks.bug]
short_name = "topk_zero_key_falsy"
invariant = "``topk(k, seq, key=0)`` ranks tuples in ``seq`` by their zeroth field, never raising — i.e. the integer 0 is a valid key indexer."
how_triggered = "The mutation reverts ``key is not None`` to ``key`` (truthiness). ``key=0`` is falsy, so ``getter(0)`` is never wrapped and ``heapq.nlargest`` receives ``key=0``, which is not callable: TypeError."

[[tasks.tasks]]
property = "TopkZeroKeyAccepted"
witnesses = [
  { test_fn = "witness_topk_zero_key_accepted_case_basic", note = "topk(2, [(0,4),(1,3),(2,2),(3,1),(4,0)], 0) — bug TypeErrors, fix returns the two highest-zeroth-field tuples" },
]

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

[tasks.source]
repo = "https://github.com/pytoolz/toolz"
commits = ["46b17268bbd3c960f5471ea4dc9d68af976fc0cd"]
commit_subjects = ["Make dissoc idempotent per #258."]
issues = [258]
origin = "internal"
summary = "``dissoc(d, *keys)`` deleted each key unconditionally on the small-keys fast path (``len(keys) < len(d) * .6``). Calling ``dissoc({'x': 1}, 'y')`` raised ``KeyError`` even though dropping a missing key should be a no-op. The fix wraps the ``del`` in ``if key in d2``, matching the semantics of the slow set-difference path."

[tasks.injection]
kind = "patch"
files = ["toolz/dicttoolz.py"]
locations = [{ file = "toolz/dicttoolz.py", line = 220, symbol = "dissoc" }]
patch = "patches/dissoc_missing_key_keyerror_46b1726_1.patch"

[tasks.bug]
short_name = "dissoc_missing_key_keyerror"
invariant = "``dissoc(d, *keys)`` is idempotent and ignores missing keys: ``dissoc({'x': 1}, 'y')`` returns ``{'x': 1}`` and ``dissoc(dissoc({'x': 1}, 'x'), 'x')`` returns ``{}``."
how_triggered = "The mutation drops the ``if key in d2`` guard on the fast path. With one key in ``d`` and one missing key supplied, the fast path is taken (1 < 1*.6 is False — actually fast path covers up to 60% — for a single-key dict where supplied key is missing, ``len(keys)=1 < .6`` is False, so it takes the SET branch). To reliably hit the fast path we need a larger ``d`` so 1 < .6*N is True; the witness uses a 5-element dict and a single missing key."

[[tasks.tasks]]
property = "DissocMissingKeyOk"
witnesses = [
  { test_fn = "witness_dissoc_missing_key_ok_case_basic", note = "5-key dict, drop one missing key (fast path) — bug raises KeyError, fix returns the dict" },
]

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

[tasks.source]
repo = "https://github.com/pytoolz/toolz"
commits = ["060cc4bf8627c35e733c8037d9a4eebd0edb7fab"]
commit_subjects = ["`isdistinct` now works on iterators.  See #89"]
issues = [89]
origin = "internal"
summary = "``isdistinct(seq)`` was implemented as ``len(seq) == len(set(seq))``. For iterators (anything where ``iter(seq) is seq``) this is wrong on two counts: ``len`` raises ``TypeError`` for iterators with no ``__len__``, and even when it doesn't, ``set(seq)`` consumes the iterator so ``len(seq)`` is then computed against an empty iterator. The fix dispatches on whether the input is its own iterator, and walks it once."

[tasks.injection]
kind = "patch"
files = ["toolz/itertoolz.py"]
locations = [{ file = "toolz/itertoolz.py", line = 287, symbol = "isdistinct" }]
patch = "patches/isdistinct_iter_broken_060cc4b_1.patch"

[tasks.bug]
short_name = "isdistinct_iter_broken"
invariant = "``isdistinct(iter(xs))`` returns the same boolean as ``isdistinct(xs)`` for any sequence ``xs``."
how_triggered = "The mutation collapses ``isdistinct`` back to ``return len(seq) == len(set(seq))``. Passing ``iter([1,2,3])`` raises ``TypeError`` (iterators have no ``__len__``); passing a generator likewise."

[[tasks.tasks]]
property = "IsdistinctOnIterators"
witnesses = [
  { test_fn = "witness_isdistinct_on_iterators_case_basic", note = "isdistinct(iter([1,2,3])) — bug TypeErrors, fix returns True" },
]

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

[tasks.source]
repo = "https://github.com/pytoolz/toolz"
commits = ["1d78a46bffd43c9a232715303fdfc61ef9a1b424"]
commit_subjects = ["Fixed bug in the accumulate function"]
origin = "internal"
summary = "``accumulate`` started with ``result = next(iter(seq))`` and then iterated the rest with ``itertools.islice(seq, 1, None)``. For a true iterator argument, ``next(iter(seq))`` and ``islice(seq, 1, None)`` operate on the same underlying iterator: ``next`` consumes element 0, then ``islice(seq, 1, None)`` skips one MORE — so element 1 is dropped and accumulation starts from element 2. The fix binds ``seq = iter(seq)`` once and iterates it directly, yielding the correct result for both list and iterator inputs."

[tasks.injection]
kind = "patch"
files = ["toolz/itertoolz.py"]
locations = [{ file = "toolz/itertoolz.py", line = 57, symbol = "accumulate" }]
patch = "patches/accumulate_iter_double_consume_1d78a46_1.patch"

[tasks.bug]
short_name = "accumulate_iter_double_consume"
invariant = "For any list ``xs`` and any binary operator ``binop``, ``list(accumulate(binop, iter(xs)))`` equals ``list(accumulate(binop, xs))``."
how_triggered = "The mutation reverts ``seq = iter(seq)`` + ``for elem in seq`` to the buggy double-consume pattern (``next(iter(seq))`` + ``itertools.islice(seq, 1, None)``). On iterators, element 1 is silently skipped — ``list(accumulate(add, iter([1,2,3])))`` returns ``[1, 4]`` instead of ``[1, 3, 6]``."

[[tasks.tasks]]
property = "AccumulateOnIterators"
witnesses = [
  { test_fn = "witness_accumulate_on_iterators_case_basic", note = "accumulate(add, iter([1,2,3])) — bug returns [1,4], fix returns [1,3,6]" },
]

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

[tasks.source]
repo = "https://github.com/pytoolz/toolz"
commits = ["c696ac6f3c2962b370fe8dda78c64cf2564e1457"]
commit_subjects = ["Also fix merge_with"]
origin = "internal"
summary = "``merge_with(func, *dicts)`` accepts either a list of dicts or a single iterable-of-dicts. To dispatch, it tested ``not isinstance(dicts[0], dict)`` — but a custom Mapping subclass that does NOT subclass ``dict`` would be wrongly treated as ``iterable-of-dicts`` and unpacked, raising ``TypeError`` (or producing nonsense). The fix tests against ``collections.abc.Mapping``, which is the correct ABC."

[tasks.injection]
kind = "patch"
files = ["toolz/dicttoolz.py"]
locations = [{ file = "toolz/dicttoolz.py", line = 57, symbol = "merge_with" }]
patch = "patches/merge_with_mapping_typecheck_c696ac6_1.patch"

[tasks.bug]
short_name = "merge_with_mapping_typecheck"
invariant = "``merge_with(func, m)`` where ``m`` is any ``Mapping`` (not necessarily a ``dict``) returns a result equal to ``merge_with(func, dict(m))``."
how_triggered = "The mutation reverts the isinstance check from ``Mapping`` back to ``dict``. A custom ``Mapping`` subclass that does not inherit from ``dict`` is now treated as an iterable of dicts; ``merge_with`` then iterates the mapping (which yields keys, not dicts) and crashes."

[[tasks.tasks]]
property = "MergeWithCustomMapping"
witnesses = [
  { test_fn = "witness_merge_with_custom_mapping_case_basic", note = "Custom Mapping with one key — bug TypeErrors, fix returns {key: func([value])}" },
]
