name = "boltons"
description = "Pure-Python utility belt — bug fixes mined from upstream history."
language = "python"
strategies = ["hypothesis", "crosshair"]
crate = "boltons"
base_commit = "207651ee6055aabd0d9cdeac2e00140cdc208d44"

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["b8855d768baef2f14d832becdfdfa3bee7c9380d"]
commit_subjects = ["Fix barrellist sort with multiple lists"]
origin = "internal"
summary = "``BarrelList.sort()`` cleared ``self.lists`` via ``del self.lists[:]`` and then assigned ``self.lists[0] = tmp_sorted`` — but ``self.lists`` is now empty, so the indexed assignment raises IndexError. The fix uses ``self.lists.append(tmp_sorted)`` instead. Reproduces only when there are multiple internal lists (i.e. the BarrelList has spilled past one bucket)."

[tasks.injection]
kind = "patch"
files = ["boltons/listutils.py"]
locations = [{ file = "boltons/listutils.py", line = 311, symbol = "BarrelList.sort" }]
patch = "patches/barrellist_sort_drops_lists_b8855d7_1.patch"

[tasks.bug]
short_name = "barrellist_sort_drops_lists"
invariant = "When a ``BarrelList`` has more than one internal list, ``bl.sort()`` produces an output whose ``list(bl)`` equals ``sorted(<original elements>)``, without raising."
how_triggered = "The mutation reverts the ``self.lists.append(tmp_sorted)`` line to ``self.lists[0] = tmp_sorted``. Because ``del self.lists[:]`` runs immediately before, the indexed assignment hits an empty list and raises IndexError."

[[tasks.tasks]]
property = "BarrelListSortPreservesElements"
witnesses = [
  { test_fn = "witness_barrel_list_sort_preserves_elements_case_basic", note = "Two internal lists; sort must not raise and must preserve elements" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["0c88f25323de3ff85bcca844b9ce6cd171a80392"]
commit_subjects = ["Fix Table.to_text() sizing"]
origin = "internal"
summary = "``Table.to_text`` measured each *row's length* (the number of cells per row) when sizing columns instead of measuring the *contents* of each column. Tables with cells whose stringified width differed from the column count produced rows whose visible widths were uneven, with long cells unpadded and short cells over-padded."

[tasks.injection]
kind = "patch"
files = ["boltons/tableutils.py"]
locations = [{ file = "boltons/tableutils.py", line = 572, symbol = "Table.to_text" }]
patch = "patches/table_to_text_row_widths_0c88f25_1.patch"

[tasks.bug]
short_name = "table_to_text_row_widths"
invariant = "For each column ``j`` of ``Table.to_text(with_headers=False)``, every rendered cell width equals the maximum stringified width across all values in column ``j``."
how_triggered = "The mutation reverts ``[len(row[idx]) for row in text_data]`` to ``[len(cur) for cur in text_data]``. The latter measures the row's element count (a constant equal to the column count) instead of cell widths, leaving long cells un-padded and short cells over-padded."

[[tasks.tasks]]
property = "TableToTextColumnsAlign"
witnesses = [
  { test_fn = "witness_table_to_text_columns_align_case_uneven", note = "Two-column table with widely varying cell widths" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["6cac49ce4eba506a3582e8089bcc347c6b2fa70f"]
commit_subjects = ["fix OneToOne.update behavior with empty iterables"]
origin = "internal"
summary = "``OneToOne.update`` only assigned ``keys_vals`` inside the ``for`` loop that hashed the new values. With an empty ``dict_or_iterable`` the loop body never ran, so ``keys_vals`` was never bound and the subsequent ``keys_vals.extend(...)`` raised UnboundLocalError. The fix initialises ``keys_vals = []`` before the conditional."

[tasks.injection]
kind = "patch"
files = ["boltons/dictutils.py"]
locations = [{ file = "boltons/dictutils.py", line = 874, symbol = "OneToOne.update" }]
patch = "patches/onetoone_update_unbound_keys_vals_6cac49c_1.patch"

[tasks.bug]
short_name = "onetoone_update_unbound_keys_vals"
invariant = "``OneToOne.update`` accepts an empty mapping or iterable without raising, and afterwards the prior contents of the OneToOne remain intact."
how_triggered = "The mutation drops the ``keys_vals = []`` initialiser. With an empty dict argument, the for-loop hashing values never executes, so ``keys_vals`` is unbound when ``keys_vals.extend(kw.items())`` runs — UnboundLocalError."

[[tasks.tasks]]
property = "OneToOneUpdateEmptyOk"
witnesses = [
  { test_fn = "witness_one_to_one_update_empty_ok_case_basic", note = "Update with empty dict and empty iterable; original key intact" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["40a7b47c6e8235a1b2a4600d0ceec4f7ea0f9bb0"]
commit_subjects = ["bugfix in mathutils.Bits.as_list (#315)"]
prs = [315]
summary = "``Bits.as_list`` formatted the integer with ``'{0:b}'`` (no leading zero pad), so values whose binary representation was shorter than ``self.len`` came out truncated. ``Bits(0, 2).as_list()`` returned ``[False]`` instead of ``[False, False]``. The fix reuses ``self.as_bin()`` which zero-pads to ``self.len``."

[tasks.injection]
kind = "patch"
files = ["boltons/mathutils.py"]
locations = [{ file = "boltons/mathutils.py", line = 206, symbol = "Bits.as_list" }]
patch = "patches/bits_as_list_truncates_zero_pad_40a7b47_1.patch"

[tasks.bug]
short_name = "bits_as_list_truncates_zero_pad"
invariant = "For all ``Bits(val, n)`` with ``0 <= val < 2**n``, ``len(Bits(val, n).as_list()) == n`` and ``list(Bits(val, n)) == Bits(val, n).as_list()``."
how_triggered = "The mutation reverts ``as_list`` to ``[c == '1' for c in '{0:b}'.format(self.val)]``. The unpadded format strips leading zeros, so for values with fewer significant bits than ``self.len`` the returned list is shorter than ``n``."

[[tasks.tasks]]
property = "BitsAsListLengthMatches"
witnesses = [
  { test_fn = "witness_bits_as_list_length_matches_case_zero_pad", note = "Bits(0, 2): expected [False, False]; bug returns [False]" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["93185b224ebd60df01df88d64e574a3f651987c5"]
commit_subjects = ["Fix infinite daterange issue when start and stop is same (#302)"]
prs = [302]
summary = "``daterange(d, d, inclusive=True)`` looped forever. The branch selection used a strict ``elif start < stop``, so when ``start == stop`` it fell through to the descending branch with ``finished = operator.lt`` — and ``operator.lt(d, d)`` is always False. The fix relaxes to ``elif start <= stop`` so the equal case picks the ascending termination predicate."

[tasks.injection]
kind = "patch"
files = ["boltons/timeutils.py"]
locations = [{ file = "boltons/timeutils.py", line = 371, symbol = "daterange" }]
patch = "patches/daterange_inclusive_same_start_stop_93185b2_1.patch"

[tasks.bug]
short_name = "daterange_inclusive_same_start_stop"
invariant = "``daterange(d, d, inclusive=True)`` yields exactly ``[d]`` and then terminates; ``daterange(d, d, inclusive=False)`` yields ``[]`` and terminates."
how_triggered = "The mutation tightens ``start <= stop`` back to ``start < stop``. With equal endpoints the branch chooses the descending termination test (``operator.lt`` for inclusive=True), which never fires; the generator is infinite."

[[tasks.tasks]]
property = "DaterangeSameStartStopTerminates"
witnesses = [
  { test_fn = "witness_daterange_same_start_stop_terminates_case_zero_offset", note = "Bounded islice keeps the test from hanging on the infinite branch" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["4457decc614ee795804f2bb9c44ae9be30831d53"]
commit_subjects = ["fix IndexedSet.index() to account for removals, fixes #240"]
issues = [240]
summary = "``IndexedSet.index(val)`` returned ``self.item_index_map[val]`` directly. That raw position carries any pre-removal slot the value used to occupy; with un-compacted ``dead_indices`` the apparent index has shifted and the returned value is wrong. The fix routes the lookup through ``self._get_apparent_index`` so the post-removal index is reported."

[tasks.injection]
kind = "patch"
files = ["boltons/setutils.py"]
locations = [{ file = "boltons/setutils.py", line = 465, symbol = "IndexedSet.index" }]
patch = "patches/indexed_set_index_ignores_dead_4457dec_1.patch"

[tasks.bug]
short_name = "indexed_set_index_ignores_dead"
invariant = "For an ``IndexedSet`` with any removals, ``iset[iset.index(x)] == x`` for every ``x`` still in the set."
how_triggered = "The mutation drops the ``_get_apparent_index`` call. With the ``IndexedSet`` short of its compaction threshold (``dead_count <= len/_COMPACTION_FACTOR``), ``dead_indices`` survives across calls and ``index`` returns the pre-removal slot, off-by-(number of dead intervals before that slot)."

[[tasks.tasks]]
property = "IndexedSetIndexAfterRemovals"
witnesses = [
  { test_fn = "witness_indexed_set_index_after_removals_case_pop_one", note = "20-item set; popping a middle index leaves dead_indices populated" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["d056712a4a2abcdb977a6e9a3f80bef0fcaadedd"]
commit_subjects = ['Fix singularize("senses") == "sens"']
origin = "internal"
summary = "``singularize('senses')`` produced ``'sens'`` because 'senses' was missing from the irregular plural-to-singular table. The default singularization algorithm strips a trailing 's', which is wrong here. The fix adds ``'sense': 'senses'`` to ``_IRR_S2P``."

[tasks.injection]
kind = "patch"
files = ["boltons/strutils.py"]
locations = [{ file = "boltons/strutils.py", line = 300, symbol = "_IRR_S2P" }]
patch = "patches/singularize_senses_to_sens_d056712_1.patch"

[tasks.bug]
short_name = "singularize_senses_to_sens"
invariant = "``singularize('senses') == 'sense'``."
how_triggered = "The mutation removes the ``'sense': 'senses'`` entry from ``_IRR_S2P``. The fallback algorithm strips the trailing 's' to give 'sens'."

[[tasks.tasks]]
property = "SingularizeSensesIsSense"
witnesses = [
  { test_fn = "witness_singularize_senses_is_sense_case_basic", note = "Direct lookup against the irregular-plural table" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["b1df97114e94f930a28b18e756c1df436ea1ac85"]
commit_subjects = ["fix OMD.setdefault to default to None and not empty list, per documentation (and add a test to the same effect)"]
origin = "internal"
summary = "``OrderedMultiDict.setdefault(k, v)`` did not match dict semantics: it stored ``[v]`` (or ``[]`` for the no-default case) under the key and returned the raw ``default`` argument rather than the stored value. The fix stores ``v`` (or ``None``) directly and returns ``self[k]`` so the call mirrors ``dict.setdefault``."

[tasks.injection]
kind = "patch"
files = ["boltons/dictutils.py"]
locations = [{ file = "boltons/dictutils.py", line = 261, symbol = "OrderedMultiDict.setdefault" }]
patch = "patches/omd_setdefault_returns_default_b1df971_1.patch"

[tasks.bug]
short_name = "omd_setdefault_returns_default"
invariant = "After ``omd.setdefault(k, v)``, ``omd[k] == v`` and the call returns ``v``. For the no-default form, ``omd.setdefault(k)`` returns ``None`` and ``omd[k] is None``."
how_triggered = "The mutation reverts to ``self[k] = [] if default is _MISSING else [default]`` and ``return default``. The stored value is wrapped in a list, and the returned value is the sentinel rather than the actual stored value."

[[tasks.tasks]]
property = "OmdSetdefaultReturnsStored"
witnesses = [
  { test_fn = "witness_omd_setdefault_returns_stored_case_basic", note = "Stored value identity check + no-default returns None" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["139d5dcd3255eddb822aa2d876ce3120c34d81c7"]
commit_subjects = ["Fix bug in implementation of `timeutils.daterange`"]
origin = "internal"
summary = "``daterange`` installs a ``finished`` predicate to terminate the inner loop. With ``stop=None`` the predicate was a 1-arg lambda; with bounded ``stop`` it was 2-arg. The dispatch ``while not finished(now, stop)`` then crashed for the unbounded case. The fix gives both branches the same ``(now, stop) -> bool`` signature."

[tasks.injection]
kind = "patch"
files = ["boltons/timeutils.py"]
locations = [{ file = "boltons/timeutils.py", line = 370, symbol = "daterange" }]
patch = "patches/daterange_finished_arity_mismatch_139d5dc_1.patch"

[tasks.bug]
short_name = "daterange_finished_arity_mismatch"
invariant = "``daterange(d, None)`` yields a sequence starting at ``d``, ``d+1``, ``d+2``, ... without raising on any pull."
how_triggered = "The mutation reverts the unbounded-stop predicate to ``lambda t: False``. The first call inside the generator's main loop is ``finished(now, stop)`` (two args) — TypeError before the first yield."

[[tasks.tasks]]
property = "DaterangeInfiniteIterates"
witnesses = [
  { test_fn = "witness_daterange_infinite_iterates_case_today", note = "Three pulls verify both the first and the inner-loop termination calls" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["0082e13048dcf1574d4163ccc20b67b8a593277a"]
commit_subjects = ["Fix bytes2human when builtin zip returns iterators"]
origin = "internal"
summary = "Module-level ``_SIZE_RANGES = zip(...)`` was a one-shot iterator under Python 3 (zip returns an iterator, not a list). The first call to ``bytes2human`` drained it; subsequent calls saw an empty iterable, fell out of the size-bucket loop with ``size`` and ``symbol`` unbound, and raised UnboundLocalError. The fix wraps ``zip`` in ``list``."

[tasks.injection]
kind = "patch"
files = ["boltons/strutils.py"]
locations = [{ file = "boltons/strutils.py", line = 548, symbol = "_SIZE_RANGES" }]
patch = "patches/bytes2human_size_ranges_consumed_0082e13_1.patch"

[tasks.bug]
short_name = "bytes2human_size_ranges_consumed"
invariant = "``bytes2human(n)`` is referentially transparent: every invocation with the same input returns the same string, regardless of how many times it has been called before."
how_triggered = "The mutation reverts ``_SIZE_RANGES`` to the unwrapped ``zip(...)`` iterator. The first call iterates and drains it; the second call's ``for ... in _SIZE_RANGES`` immediately exits, leaving ``size`` unbound — UnboundLocalError on the divide."

[[tasks.tasks]]
property = "Bytes2HumanIsRepeatable"
witnesses = [
  { test_fn = "witness_bytes2_human_is_repeatable_case_one_kib", note = "Two consecutive 1024-byte calls; the second observes the drained iterator" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["f74b7e5a7d6cc985d47540c4437a072da6803851"]
commit_subjects = ["fix for default remap set support, fixes #84"]
issues = [84]
summary = "``default_exit`` for ``Set`` containers passed ``new_items`` (a list of ``(index, value)`` 2-tuples produced earlier in ``remap``) directly to ``set.update``. ``set.update`` adds each iterable element to the set, so the resulting set was full of 2-tuples instead of the original element values. The fix passes ``vals`` — the values stripped from those 2-tuples — instead."

[tasks.injection]
kind = "patch"
files = ["boltons/iterutils.py"]
locations = [{ file = "boltons/iterutils.py", line = 1083, symbol = "default_exit" }]
patch = "patches/remap_set_passes_pairs_to_update_f74b7e5_1.patch"

[tasks.bug]
short_name = "remap_set_passes_pairs_to_update"
invariant = "For any ``set`` or ``frozenset`` ``s``, ``remap(s) == s``."
how_triggered = "The mutation reverts the ``set.update`` argument to ``new_items``. Each ``new_items`` element is a ``(i, v)`` tuple, so the resulting set contains those tuples instead of the bare values."

[[tasks.tasks]]
property = "RemapPreservesSet"
witnesses = [
  { test_fn = "witness_remap_preserves_set_case_three_ints", note = "Plain ``{1,2,3}`` round-trips through remap" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["31873aebe9df299df76053f7f2e6527ae013de9a"]
commit_subjects = ["fix a bug where __eq__ would fail with non-iterable objects of comparison"]
origin = "internal"
summary = "``OrderedMultiDict.__eq__`` called ``len(other)`` unconditionally, so comparing against an int (or any object lacking ``__len__``) propagated TypeError. Python's data model expects ``__eq__`` to return ``NotImplemented`` or ``False`` in that situation, never raise. The fix wraps the length check in ``try/except TypeError: return False``."

[tasks.injection]
kind = "patch"
files = ["boltons/dictutils.py"]
locations = [{ file = "boltons/dictutils.py", line = 342, symbol = "OrderedMultiDict.__eq__" }]
patch = "patches/omd_eq_raises_on_non_iterable_31873ae_1.patch"

[tasks.bug]
short_name = "omd_eq_raises_on_non_iterable"
invariant = "``OMD == x`` and ``OMD != x`` return False/True (respectively) for any non-iterable ``x``, never raising."
how_triggered = "The mutation reverts the conditional length check to a bare ``elif len(other) != len(self)``. ``len(int)`` raises TypeError, which propagates."

[[tasks.tasks]]
property = "OmdEqHandlesNonIterable"
witnesses = [
  { test_fn = "witness_omd_eq_handles_non_iterable_case_int", note = "OMD([(1,1)]) == 5 must be False, not raise" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["a13bfb16fbf4f53ae23fb20cdafb69547fae8a02"]
commit_subjects = ["fix a bug with empty Stats and get_quantile"]
origin = "internal"
summary = "``Stats([]).get_quantile(q)`` raised because the inner ``_get_quantile`` indexed into the empty sorted-data list. Other accessors on empty ``Stats`` already returned the configured ``default``; the fix extends that convention to ``get_quantile``."

[tasks.injection]
kind = "patch"
files = ["boltons/statsutils.py"]
locations = [{ file = "boltons/statsutils.py", line = 475, symbol = "Stats.get_quantile" }]
patch = "patches/stats_quantile_empty_raises_a13bfb1_1.patch"

[tasks.bug]
short_name = "stats_quantile_empty_raises"
invariant = "``Stats([]).get_quantile(q)`` returns ``self.default`` for any in-range ``q``, never raising."
how_triggered = "The mutation drops the ``elif not self.data: return self.default`` guard. The call descends into ``_get_quantile`` over empty data, which raises (IndexError or ZeroDivision)."

[[tasks.tasks]]
property = "StatsQuantileEmptyReturnsDefault"
witnesses = [
  { test_fn = "witness_stats_quantile_empty_returns_default_case_median", note = "q=0.5 against Stats([]) returns the default" },
]

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

[tasks.source]
repo = "https://github.com/mahmoud/boltons"
commits = ["bad95b645c5fb7d14e02ad6ae479f1a05bc8185e"]
commit_subjects = ["fix LRU repr reversal, fixes #20"]
issues = [20]
summary = "``LRI.__repr__`` (inherited by LRU) interpolated its format arguments in the wrong order: ``self.on_miss`` filled the ``max_size=`` slot and ``self.max_size`` filled the ``on_miss=`` slot. The reproduced ``repr`` was a valid Python expression that, when ``eval``'d, produced an LRI with the wrong configuration."

[tasks.injection]
kind = "patch"
files = ["boltons/cacheutils.py"]
locations = [{ file = "boltons/cacheutils.py", line = 329, symbol = "LRI.__repr__" }]
patch = "patches/lru_repr_swaps_max_size_on_miss_bad95b6_1.patch"

[tasks.bug]
short_name = "lru_repr_swaps_max_size_on_miss"
invariant = "``repr(LRI(max_size=n))`` contains the literal substring ``max_size=<n>`` and ``on_miss=None`` for an LRI built without an on-miss callback."
how_triggered = "The mutation swaps the format-interpolation order back to ``(cn, self.on_miss, self.max_size, val_map)``. The repr now reports ``max_size=None`` and ``on_miss=<n>`` instead."

[[tasks.tasks]]
property = "LruReprFieldOrder"
witnesses = [
  { test_fn = "witness_lru_repr_field_order_case_basic", note = "max_size=42; repr must mention max_size=42 and on_miss=None" },
]
