Skip to content

Commit 76d8da1

Browse files
committed
output-position widening for invariant builtin containers
fresh-copy methods of invariant containers (list/set/dict) return a brand new object the caller owns, so widening its element type at the call site is sound — but `list[int]` isn't a `list[int | None]`, so today `a.copy()` can't be assigned to a wider specialization. encode the fix as a `Never`-defaulted output type parameter unioned into each invariant return position: `def copy[Widen = Never](self) -> list[Element | Widen]`. with an expected type it solves to the widening; without one it defaults to `Never` and collapses back, so inference is unchanged. new `output-widening` typeshed patch (post-pep695 pass, keyed on `in out` variance so covariant frozenset/tuple are left alone) + regenerated builtins.byi. it widens only methods reached by an ordinary call — `copy`, `set.union`/`difference`/`intersection`/`symmetric_difference` — where the caller's expected type already flows into inference. operator dunders (`__add__`, `__getitem__`, ...) are deliberately not widened: making their returns widen would require bidirectional inference on every binary op and subscript, which is far too expensive on real code (it times out xarray).
1 parent bada460 commit 76d8da1

8 files changed

Lines changed: 665 additions & 40 deletions

File tree

crates/by_typeshed_patch/src/lib.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,39 @@ pub struct Edit {
4242
pub replacement: String,
4343
}
4444

45-
/// registry of every patch the sync pipeline must apply, in declared order
45+
/// registry of every legacy-form patch the sync pipeline must apply, in
46+
/// declared order. these run before the pep 695 conversion and see the legacy
47+
/// `TypeVar` + `Generic[...]` form
4648
pub fn all_patches() -> Vec<Box<dyn Patch>> {
4749
// patches are added here as upstream syncs surface concrete drift. each
4850
// entry must have a corresponding module in `src/patches/` with tests
4951
vec![Box::new(patches::mapping::MappingKeyCovariance)]
5052
}
5153

54+
/// registry of patches that run *after* the pep 695 conversion, over the final
55+
/// form with explicit variance keywords. a patch belongs here when it needs the
56+
/// resolved variance (`in out` vs `out`) that only the converted form exposes
57+
pub fn all_post_patches() -> Vec<Box<dyn Patch>> {
58+
vec![Box::new(patches::output_widening::OutputWidening)]
59+
}
60+
61+
/// dotted module name for a typeshed file path relative to `stdlib/`, e.g.
62+
/// `typing.byi` -> `typing`, `os/path.byi` -> `os.path`,
63+
/// `asyncio/__init__.byi` -> `asyncio`
64+
pub(crate) fn module_qualname(path: &Path) -> Option<String> {
65+
let stem = path.file_stem()?.to_str()?;
66+
let mut parts: Vec<&str> = path
67+
.parent()
68+
.into_iter()
69+
.flat_map(Path::components)
70+
.filter_map(|component| component.as_os_str().to_str())
71+
.collect();
72+
if stem != "__init__" {
73+
parts.push(stem);
74+
}
75+
Some(parts.join("."))
76+
}
77+
5278
/// apply `edits` to `source`, returning the new text. edits must be disjoint;
5379
/// applied in reverse start order so earlier offsets remain valid
5480
pub fn apply_edits(source: &str, mut edits: Vec<Edit>) -> String {

crates/by_typeshed_patch/src/main.rs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
//! binary entry: walks the basedpython typeshed and rewrites each `.byi`
22
//! stub. invoked by `scripts/sync_typeshed_by.sh` after reverse-transpile
33
//!
4-
//! each file is rewritten in two passes:
4+
//! each file is rewritten in three passes:
55
//!
66
//! 1. the registered semantic [`Patch`]es (e.g. mapping key covariance), which
77
//! operate on the legacy `TypeVar` + `Generic[...]` form
88
//! 1. the pep 695 conversion ([`by_typeshed_patch::pep695`]), which turns
99
//! legacy generic classes into pep 695 headers with explicit variance and
1010
//! nice names
11+
//! 1. the post-conversion [`Patch`]es (e.g. output widening), which need the
12+
//! explicit variance keywords the conversion emits
1113
//!
12-
//! the passes run sequentially with a re-parse in between: a patch may rewrite
13-
//! a typevar reference (covariance) that the conversion then renames, so the
14-
//! conversion must see the patched source
14+
//! the passes run sequentially with a re-parse between each: a pass may rewrite
15+
//! a reference the next pass depends on, so each must see the prior output
1516
//!
1617
//! usage:
1718
//! `by_typeshed_patch` `<typeshed-stdlib-dir>`
@@ -27,7 +28,7 @@ use ruff_python_ast::PySourceType;
2728
use ruff_python_parser::parse_unchecked_source;
2829
use walkdir::WalkDir;
2930

30-
use by_typeshed_patch::{Patch, all_patches, apply_edits, pep695};
31+
use by_typeshed_patch::{Patch, all_patches, all_post_patches, apply_edits, pep695};
3132

3233
fn main() -> ExitCode {
3334
match run() {
@@ -50,7 +51,8 @@ fn run() -> Result<()> {
5051
}
5152

5253
let patches = all_patches();
53-
if patches.is_empty() {
54+
let post_patches = all_post_patches();
55+
if patches.is_empty() && post_patches.is_empty() {
5456
eprintln!("no patches registered; nothing to do");
5557
return Ok(());
5658
}
@@ -64,7 +66,7 @@ fn run() -> Result<()> {
6466
}
6567
visited += 1;
6668
let rel = path.strip_prefix(&root).unwrap_or(path);
67-
if apply_patches_to_file(path, rel, &patches)
69+
if apply_patches_to_file(path, rel, &patches, &post_patches)
6870
.with_context(|| format!("applying patches to {}", path.display()))?
6971
{
7072
patched += 1;
@@ -74,7 +76,12 @@ fn run() -> Result<()> {
7476
Ok(())
7577
}
7678

77-
fn apply_patches_to_file(path: &Path, rel: &Path, patches: &[Box<dyn Patch>]) -> Result<bool> {
79+
fn apply_patches_to_file(
80+
path: &Path,
81+
rel: &Path,
82+
patches: &[Box<dyn Patch>],
83+
post_patches: &[Box<dyn Patch>],
84+
) -> Result<bool> {
7885
let original = fs::read_to_string(path).with_context(|| format!("{}", path.display()))?;
7986

8087
// pass 1: registered semantic patches over the legacy form
@@ -98,12 +105,25 @@ fn apply_patches_to_file(path: &Path, rel: &Path, patches: &[Box<dyn Patch>]) ->
98105
// any typevar references the patches rewrote)
99106
let reparsed = parse_unchecked_source(&patched, PySourceType::BasedPythonStub);
100107
let conversion = pep695::convert_module(&reparsed, &patched);
101-
let final_source = if conversion.is_empty() {
108+
let converted = if conversion.is_empty() {
102109
patched
103110
} else {
104111
apply_edits(&patched, conversion)
105112
};
106113

114+
// pass 3: post-conversion patches over the final pep 695 form (re-parsed so
115+
// they see the explicit variance keywords the conversion emitted)
116+
let reparsed = parse_unchecked_source(&converted, PySourceType::BasedPythonStub);
117+
let mut post_edits = Vec::new();
118+
for patch in post_patches {
119+
post_edits.extend(patch.rewrite(rel, &reparsed, &converted));
120+
}
121+
let final_source = if post_edits.is_empty() {
122+
converted
123+
} else {
124+
apply_edits(&converted, post_edits)
125+
};
126+
107127
if final_source == original {
108128
return Ok(false);
109129
}

crates/by_typeshed_patch/src/patches/mapping.rs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use ruff_python_ast::visitor::source_order::{SourceOrderVisitor, walk_expr, walk
1818
use ruff_python_ast::{Expr, ModModule, Stmt};
1919
use ruff_python_parser::Parsed;
2020

21-
use crate::{Edit, Patch};
21+
use crate::{Edit, Patch, module_qualname};
2222

2323
/// module that owns the canonical `Mapping` definition
2424
const MODULE: &str = "typing";
@@ -97,23 +97,6 @@ impl<'a> SourceOrderVisitor<'a> for MappingKeyReferences {
9797
}
9898
}
9999

100-
/// dotted module name for a typeshed file path relative to `stdlib/`, e.g.
101-
/// `typing.byi` -> `typing`, `os/path.byi` -> `os.path`,
102-
/// `asyncio/__init__.byi` -> `asyncio`
103-
fn module_qualname(path: &Path) -> Option<String> {
104-
let stem = path.file_stem()?.to_str()?;
105-
let mut parts: Vec<&str> = path
106-
.parent()
107-
.into_iter()
108-
.flat_map(Path::components)
109-
.filter_map(|component| component.as_os_str().to_str())
110-
.collect();
111-
if stem != "__init__" {
112-
parts.push(stem);
113-
}
114-
Some(parts.join("."))
115-
}
116-
117100
#[cfg(test)]
118101
mod tests {
119102
use super::*;

crates/by_typeshed_patch/src/patches/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
//! each in `all_patches()` in the crate root
33
44
pub mod mapping;
5+
pub mod output_widening;

0 commit comments

Comments
 (0)