Breakwater

Backtest / #1030

perf: translate for canonicalize_name

pypa/packaging#1030 · merged 2026-01-05 · base 52e57f2003 → head 3803ce341b · ran on Docker

BLOCK

At least one behaviour change looks like an unintended regression. The “faster” code does up to 5× the work on valid inputs, growing with input size (complexity regression).

Known regressions in this PR

Counterexamples · 3 groups of 8 classes: 8 likely bug

P1

Output changed

known regression: unicode-lowercase
inputcanonicalize_name("İ")
basevalue: "i̇"
headvalue: "İ"
Likely bugvotes: 3 likely-bug

The PR aims only to speed up canonicalize_name while preserving the original semantics (lower‑casing per PEP 503). The new implementation only lower‑cases ASCII letters, so non‑ASCII characters like ‘İ’ are not lower‑cased, changing the output.

Emulates ``re.sub(r
perf: translate for canonicalize_name

Found by: Nemotron strategy search, Ultra's planned example, hostile mutation, search sample · also caught by: Plain Hypothesis, Mutations of the PR's own test strings, Nemotron plan only

ran onlocal Docker · image bw-pair:d09088ff31a7e098

Regression test verified passes on base, fails on head
def test_1030_p1_value_0():
    """Breakwater P1: base behaviour pinned (found by differential testing; label: likely-bug)."""
    from packaging.utils import canonicalize_name
    assert canonicalize_name('İ') == 'i̇'
Reproduce
uv run breakwater probe --repo https://github.com/pypa/packaging --base 52e57f20037f --head 3803ce341b59 --target packaging.utils:canonicalize_name --backend docker --input "İ"
2 near-duplicate classes same behaviour, inputs with extra traits
  • canonicalize_name("İ ") base value: "i̇ " · head value: "İ "unicode-lowercase test verified
  • canonicalize_name("İ\u0000") base value: "i̇\u0000" · head value: "İ\u0000"unicode-lowercase test verified
P2

Exception behaviour changed

inputcanonicalize_name(["İ"])
baseraises TypeError
headraises AttributeError

Neither exception type subclasses the other.

Likely bugvotes: 3 likely-bug

The change introduces a different exception type (AttributeError vs TypeError) when processing the same input, which is not mentioned in the PR title, docs, or tests and therefore is an unintended regression.

perf: translate for canonicalize_name
Emulates ``re.sub(r

Found by: Nemotron strategy search, search sample · no baseline found it

ran onlocal Docker · image bw-pair:d09088ff31a7e098

Regression test verified passes on base, errors on head
def test_1030_p2_outcome_3():
    """Breakwater P2: base behaviour pinned (found by differential testing; label: likely-bug)."""
    import pytest
    from packaging.utils import canonicalize_name
    with pytest.raises(TypeError) as excinfo:
        canonicalize_name(['İ'])
    assert type(excinfo.value) is TypeError  # exact type: a subclass would be a behaviour change
P3

Equality / ordering changed

acanonicalize_name("İ")
bcanonicalize_name("py3")
basea > b → False
heada > b → True
Likely bugvotes: 3 likely-bug

Ordering changes because the new code no longer lower‑cases Unicode letters, so comparison results differ from the original specification that required ``.lower()``.

Emulates ``re.sub(r
perf: translate for canonicalize_name

Found by: Ultra's planned example, relations pass · also caught by: Plain Hypothesis, Nemotron plan only

ran onlocal Docker · image bw-pair:d09088ff31a7e098

Regression test verified passes on base, fails on head
def test_1030_p3_gt_4():
    """Breakwater P3: base behaviour pinned (found by differential testing; label: likely-bug)."""
    from packaging.utils import canonicalize_name
    a = canonicalize_name('İ')
    b = canonicalize_name('py3')
    assert (a > b) is False
3 near-duplicate relation flipes same behaviour, inputs with extra traits
  • canonicalize_name("İ") vs canonicalize_name("py3") base a < b → True · head a < b → False test verified
  • canonicalize_name("ẞ") vs canonicalize_name("K") base a > b → True · head a > b → False test verified
  • canonicalize_name("ẞ") vs canonicalize_name("K") base a < b → False · head a < b → True test verified

Regression tests

8 of 8 classes have a generated pytest that pins the old behaviour. Each passed on the base commit and failed on the head commit in the sandbox before it was offered.

Download test_1030.py

Performance · canonicalize_name

Worst family with mostly valid inputs: 4.52× the instructions of base (many_underscores_and_dots).

Measured in backtest packaging-v2 (same commits, run bt-1030-e38d1c, image bw-pair:a959e7a1becfaee9). The semantic results above are from a later run; perf doesn't use the semantic planner, so it was not re-measured.

head slowerhead faster0.062×0.12×0.25×0.5×1×2×4×101001,00010,000input size (characters)R1.2R1.2: 1.28× at ~12 charsR1.2: 1.28× at ~12 charsR1.2: 2.28× at ~24 charsR1.2: 2.16× at ~72 charsR1.2: 0.82× at ~264 charsR1.2: 0.35× at ~1,032 charsR1.2: 0.22× at ~4,104 charsR4R4: 1.04× at ~8 charsR4: 1.28× at ~12 charsR4: 1.97× at ~36 charsR4: 3.03× at ~132 charsR4: 4.20× at ~516 charsR4: 4.36× at ~2,052 charsR4: 4.52× at ~8,196 charsR1R1: 0.65× at ~6 charsR1: 0.93× at ~8 charsR1: 1.66× at ~18 charsR1: 1.17× at ~64 charsR1: 0.36× at ~256 charsR1: 0.13× at ~1,024 charsR1: 0.07× at ~4,096 charsF3F3: 0.66× at ~8 charsF3: 0.91× at ~9 charsF3: 1.57× at ~21 charsF3: 1.90× at ~69 charsF3: 1.04× at ~261 charsF3: 0.35× at ~1,029 charsF3: 0.15× at ~4,101 charsF1F1: 1.13× at ~14 charsF1: 0.91× at ~25 charsF1: 0.50× at ~97 charsF1: 0.20× at ~409 charsF1: 0.12× at ~1,813 charsF1: 0.11× at ~7,597 charsF1: 0.10× at ~33,709 chars
  • R1.2
  • R4
  • R1
  • F3
  • F1
Head/base cachegrind instructions per call, by input size (log-log). Each line is one input family Nemotron proposed; points above 1× are where the "faster" code does more work.
FamilyInputsRatio range Slope base → headVerdict
R1.2Names starting/ending with multiple dots or underscoresclaimed speedup · expand:R10.22× – 2.28×0.86 → 0.52slower
R4many_underscores_and_dotssuspected slowdown · archive1.04× – 4.52×0.56 → 0.78complexity-regression
R1Normal package names are short‑ish alphanumerics with only a few '_' or '.' characters. The new translate‑table + single‑pass replace handles this in linear timclaimed speedup · archive0.07× – 1.66×0.86 → 0.44slower
F3typical_package_namesclaimed speedup · plan0.15× – 1.90×0.74 → 0.48slower
F1typical package names containing mixed case, dots and underscoresclaimed speedup · plan0.10× – 1.13×0.94 → 0.59slower

Receipt

TargetBackendImageRun AuditAdjudication
packaging.utils:canonicalize_namelocal Dockerbw-pair:d09088ff31a7e098bt-1030-0d870d$0.027$0.021

Nemotron cost on Nebius Token Factory. Backend: where the untrusted code ran. local Docker (an image cited by its content-addressed tag). Every image is content-addressed (repo at each commit + harness), so every card re-runs from the image and seed 0. Raw data: audit, adjudication.