Files
Jeong, YunWon be23b2c562 Store database docstrings xz-compressed behind an optional doc feature (#8836)
* Skip Python-defined classes when collecting builtin type docs

generate.py recorded every object.__subclasses__() entry as builtins.<name>
after importing the whole stdlib, so classes written in Python (tkinter,
unittest, tarfile, ...) ended up in the doc database. Classes that are
mutable heap types without C method descriptors and whose module is not a
C extension are now skipped, and their entries are removed from
data.inc.rs.

Assisted-by: Claude:claude-opus-5-5
Assisted-by: Grok:grok-4.7

* Store database docstrings xz-compressed behind an optional doc feature

- rustpython-doc's build script compresses all database docstrings into
  one xz blob with an offset index; identical texts share one offset.
- Macros emit blob references for database docs; vm decompresses the
  blob on first access.
- New `doc` cargo feature (default on) in rustpython, rustpython-vm and
  rustpython-stdlib. Without it the blob and xz are not linked and
  database-backed __doc__ is None; __text_signature__ is unaffected.
- Native types without a docstring get __doc__ = None in their dict.
- Workflows that build with --no-default-features enable `doc` where
  tests or releases need docstrings.

Assisted-by: Claude:claude-opus-5-5
Assisted-by: Grok:grok-4.7
2026-09-27 20:45:45 +09:00

146 lines
4.8 KiB
Rust

extern crate alloc;
use alloc::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=src/data.inc.rs");
let src = fs::read_to_string("src/data.inc.rs").expect("read data.inc.rs");
let entries = parse_db(&src);
let mut text = String::new();
let mut seen: BTreeMap<&str, (u32, u32)> = BTreeMap::new();
let mut spans = Vec::with_capacity(entries.len());
for (key, doc) in &entries {
let span = if doc.is_empty() {
(0, 0)
} else if let Some(span) = seen.get(doc.as_str()) {
*span
} else {
let offset = u32::try_from(text.len()).expect("doc offset");
text.push_str(doc);
let len = u32::try_from(doc.len()).expect("doc len");
seen.insert(doc.as_str(), (offset, len));
(offset, len)
};
spans.push((key.as_str(), span));
}
let mut index = String::from(
"#[derive(Clone, Copy, Debug)]\n\
pub struct DocRef {\n\
\x20 pub offset: u32,\n\
\x20 pub len: u32,\n\
}\n\
\n\
pub static DB: &[(&str, DocRef)] = &[\n",
);
for (key, (offset, len)) in spans {
index.push_str(" (");
index.push_str(&rust_string(key));
index.push_str(", DocRef { offset: ");
index.push_str(&offset.to_string());
index.push_str(", len: ");
index.push_str(&len.to_string());
index.push_str(" }),\n");
}
index.push_str("];\n");
let blob = xz::encode_all(text.as_bytes(), 9).expect("xz encode");
let out = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
fs::write(out.join("docs.xz"), blob).expect("write blob");
fs::write(out.join("index.rs"), index).expect("write index");
}
fn rust_string(s: &str) -> String {
let mut out = String::from("\"");
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
fn parse_db(src: &str) -> Vec<(String, String)> {
let mut entries = Vec::new();
for line in src.lines() {
let line = line.trim();
if !line.starts_with('(') {
continue;
}
let rest = &line[1..];
let (key, rest) = parse_rust_string(rest).expect("doc key");
let rest = rest
.trim_start()
.strip_prefix(',')
.expect("comma")
.trim_start();
let (doc, rest) = parse_rust_string(rest).expect("doc text");
let rest = rest.trim_start();
assert!(rest.starts_with(')'), "{rest}");
entries.push((key, doc));
}
assert!(!entries.is_empty());
entries
}
fn parse_rust_string(input: &str) -> Option<(String, &str)> {
let bytes = input.as_bytes();
if bytes.first() != Some(&b'"') {
return None;
}
let mut i = 1;
let mut out = Vec::new();
while i < bytes.len() {
match bytes[i] {
b'\\' => {
i += 1;
match *bytes.get(i)? {
b'n' => out.push(b'\n'),
b'r' => out.push(b'\r'),
b't' => out.push(b'\t'),
b'\\' => out.push(b'\\'),
b'"' => out.push(b'"'),
b'\'' => out.push(b'\''),
b'0' => out.push(0),
b'x' => {
let hex = core::str::from_utf8(bytes.get(i + 1..i + 3)?).ok()?;
out.push(u8::from_str_radix(hex, 16).ok()?);
i += 2;
}
b'u' => {
if bytes.get(i + 1) != Some(&b'{') {
return None;
}
let start = i + 2;
let end = bytes[start..].iter().position(|b| *b == b'}')? + start;
let hex = core::str::from_utf8(&bytes[start..end]).ok()?;
let code = u32::from_str_radix(hex, 16).ok()?;
let ch = char::from_u32(code)?;
out.extend_from_slice(ch.encode_utf8(&mut [0; 4]).as_bytes());
i = end;
}
_ => return None,
}
i += 1;
}
b'"' => {
let text = String::from_utf8(out).ok()?;
return Some((text, &input[i + 1..]));
}
byte => {
out.push(byte);
i += 1;
}
}
}
None
}