Skip to content

Commit f8de240

Browse files
authored
Insert the item rather than its key in bisect.insort (#8565)
insort_left and insort_right rebound `x` to `key(x)` and then handed that same value to both the search and `a.insert`, so the object the caller passed in never reached the list: >>> words = ["a", "ccc"] >>> bisect.insort(words, "bb", key=len) >>> words ['a', 2, 'ccc'] The key now feeds the search only, and the insert keeps the original. Assisted-by: Claude Code:claude-opus-5
1 parent 3d2ee64 commit f8de240

2 files changed

Lines changed: 83 additions & 10 deletions

File tree

crates/stdlib/src/bisect.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,15 @@ mod _bisect {
106106

107107
#[pyfunction]
108108
fn insort_left(BisectArgs { a, x, lo, hi, key }: BisectArgs, vm: &VirtualMachine) -> PyResult {
109-
let x = if let Some(ref key) = key {
110-
key.call((x,), vm)?
111-
} else {
112-
x
109+
// The search runs on the key, the insert has to put back the item itself.
110+
let needle = match key {
111+
Some(ref key) => key.call((x.clone(),), vm)?,
112+
None => x.clone(),
113113
};
114114
let index = bisect_left(
115115
BisectArgs {
116116
a: a.clone(),
117-
x: x.clone(),
117+
x: needle,
118118
lo,
119119
hi,
120120
key,
@@ -126,15 +126,15 @@ mod _bisect {
126126

127127
#[pyfunction]
128128
fn insort_right(BisectArgs { a, x, lo, hi, key }: BisectArgs, vm: &VirtualMachine) -> PyResult {
129-
let x = if let Some(ref key) = key {
130-
key.call((x,), vm)?
131-
} else {
132-
x
129+
// The search runs on the key, the insert has to put back the item itself.
130+
let needle = match key {
131+
Some(ref key) => key.call((x.clone(),), vm)?,
132+
None => x.clone(),
133133
};
134134
let index = bisect_right(
135135
BisectArgs {
136136
a: a.clone(),
137-
x: x.clone(),
137+
x: needle,
138138
lo,
139139
hi,
140140
key,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from bisect import bisect_left, bisect_right, insort, insort_left, insort_right
2+
3+
# A key decides where the item goes, but the item is what gets stored.
4+
for insort_fn in (insort, insort_left, insort_right):
5+
words = ["a", "ccc"]
6+
insort_fn(words, "bb", key=len)
7+
assert words == ["a", "bb", "ccc"], (insort_fn.__name__, words)
8+
9+
numbers = [1, 3]
10+
insort_fn(numbers, -2, key=abs)
11+
assert numbers == [1, -2, 3], (insort_fn.__name__, numbers)
12+
13+
pairs = [(1, "a"), (3, "b")]
14+
insort_fn(pairs, (2, "x"), key=lambda pair: pair[0])
15+
assert pairs == [(1, "a"), (2, "x"), (3, "b")], (insort_fn.__name__, pairs)
16+
17+
18+
descending = [3, 1]
19+
insort(descending, 2, key=lambda value: -value)
20+
assert descending == [3, 2, 1], descending
21+
22+
23+
class Tagged:
24+
def __init__(self, size, tag):
25+
self.size = size
26+
self.tag = tag
27+
28+
29+
def sizes_and_tags(items):
30+
return [(item.size, item.tag) for item in items]
31+
32+
33+
by_size = [Tagged(1, "old"), Tagged(2, "old")]
34+
35+
# On a tie, left goes before the equal element and right goes after it.
36+
left = list(by_size)
37+
insort_left(left, Tagged(2, "new"), key=lambda item: item.size)
38+
assert sizes_and_tags(left) == [(1, "old"), (2, "new"), (2, "old")]
39+
40+
right = list(by_size)
41+
insort_right(right, Tagged(2, "new"), key=lambda item: item.size)
42+
assert sizes_and_tags(right) == [(1, "old"), (2, "old"), (2, "new")]
43+
44+
45+
# The key runs once on the new item, whatever the search does afterwards.
46+
seen = []
47+
48+
49+
def counting_key(value):
50+
seen.append(value)
51+
return value
52+
53+
54+
data = [1, 3, 5, 7]
55+
insort(data, 4, key=counting_key)
56+
assert data == [1, 3, 4, 5, 7], data
57+
assert seen.count(4) == 1, seen
58+
59+
60+
# lo and hi bound the search and leave the inserted item alone.
61+
bounded = [3, 1]
62+
insort(bounded, 2, 0, 1, key=lambda value: -value)
63+
assert bounded == [3, 2, 1], bounded
64+
65+
66+
plain = [1, 3]
67+
insort(plain, 2)
68+
assert plain == [1, 2, 3], plain
69+
70+
# The search takes the key value itself, so these two stay where they were.
71+
sizes = ["a", "bb", "ccc"]
72+
assert bisect_left(sizes, 2, key=len) == 1
73+
assert bisect_right(sizes, 2, key=len) == 2

0 commit comments

Comments
 (0)