Skip to content

Commit e6e8bbb

Browse files
jonasdeddenclaude
andcommitted
Avoid needless allocations when generating __all__
`dunder_all_stubs` now sizes the name vector upfront and returns before allocating it at all when the module has no member, as suggested in review. `natural_cmp` used to copy every run of digits it compared into a `String`, twice per comparison inside a sort comparator. It walks subslices instead now, so ordering `__all__` does not allocate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent da59625 commit e6e8bbb

1 file changed

Lines changed: 30 additions & 26 deletions

File tree

pyo3-introspection/src/stubs.rs

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ use std::borrow::Cow;
66
use std::cmp::Ordering;
77
use std::collections::{BTreeMap, BTreeSet, HashMap};
88
use std::fmt::Write;
9-
use std::iter::{once, Peekable};
9+
use std::iter::once;
1010
use std::path::PathBuf;
11-
use std::str::{Chars, FromStr};
11+
use std::str::FromStr;
1212

1313
/// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module.
1414
/// It returns a map between the file name and the file content.
@@ -140,18 +140,25 @@ fn dunder_all_stubs(module: &Module, imports: &Imports) -> Option<String> {
140140
if module.incomplete {
141141
return None;
142142
}
143-
let mut names = module
144-
.modules
145-
.iter()
146-
.map(|m| &m.name)
147-
.chain(module.classes.iter().map(|c| &c.name))
148-
.chain(module.functions.iter().map(|f| &f.name))
149-
.chain(module.attributes.iter().map(|a| &a.name))
150-
.collect::<Vec<_>>();
151-
if names.is_empty() {
143+
let member_count = module.modules.len()
144+
+ module.classes.len()
145+
+ module.functions.len()
146+
+ module.attributes.len();
147+
if member_count == 0 {
152148
// Nothing was ever added to the module, so it has no `__all__` at runtime either
153149
return None;
154150
}
151+
152+
let mut names = Vec::with_capacity(member_count);
153+
names.extend(
154+
module
155+
.modules
156+
.iter()
157+
.map(|m| &m.name)
158+
.chain(module.classes.iter().map(|c| &c.name))
159+
.chain(module.functions.iter().map(|f| &f.name))
160+
.chain(module.attributes.iter().map(|a| &a.name)),
161+
);
155162
names.sort_unstable_by(|a, b| isort_style_cmp(a, b));
156163

157164
let mut buffer = "__all__ = ".to_string();
@@ -214,40 +221,37 @@ fn isort_style_cmp(a: &str, b: &str) -> Ordering {
214221
/// (so `arg2` comes before `arg10`), everything else by code point.
215222
///
216223
/// [natural order]: https://en.wikipedia.org/wiki/Natural_sort_order
217-
fn natural_cmp(a: &str, b: &str) -> Ordering {
218-
fn take_digits(chars: &mut Peekable<Chars<'_>>) -> String {
219-
let mut digits = String::new();
220-
while let Some(c) = chars.next_if(char::is_ascii_digit) {
221-
digits.push(c);
222-
}
223-
digits
224+
fn natural_cmp(mut a: &str, mut b: &str) -> Ordering {
225+
/// Splits the leading run of digits from the rest
226+
fn split_digits(s: &str) -> (&str, &str) {
227+
s.split_at(s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len()))
224228
}
225229

226-
let mut a = a.chars().peekable();
227-
let mut b = b.chars().peekable();
228230
loop {
229231
// The one that is exhausted first comes first
230-
let (next_a, next_b) = match (a.peek().copied(), b.peek().copied()) {
232+
let (next_a, next_b) = match (a.chars().next(), b.chars().next()) {
231233
(None, None) => return Ordering::Equal,
232234
(None, Some(_)) => return Ordering::Less,
233235
(Some(_), None) => return Ordering::Greater,
234236
(Some(next_a), Some(next_b)) => (next_a, next_b),
235237
};
236238
let ordering = if next_a.is_ascii_digit() && next_b.is_ascii_digit() {
237-
let (digits_a, digits_b) = (take_digits(&mut a), take_digits(&mut b));
239+
let (digits_a, digits_b);
240+
(digits_a, a) = split_digits(a);
241+
(digits_b, b) = split_digits(b);
238242
if next_a == '0' || next_b == '0' {
239243
// A leading zero makes the run a fraction, compared digit by digit
240-
digits_a.cmp(&digits_b)
244+
digits_a.cmp(digits_b)
241245
} else {
242246
// The longest run spells the biggest number, ties go to the first differing digit
243247
digits_a
244248
.len()
245249
.cmp(&digits_b.len())
246-
.then_with(|| digits_a.cmp(&digits_b))
250+
.then_with(|| digits_a.cmp(digits_b))
247251
}
248252
} else {
249-
a.next();
250-
b.next();
253+
a = &a[next_a.len_utf8()..];
254+
b = &b[next_b.len_utf8()..];
251255
next_a.cmp(&next_b)
252256
};
253257
if ordering != Ordering::Equal {

0 commit comments

Comments
 (0)