From af0fa212ccdfe6ee077a3ea004e68e1a653cb173 Mon Sep 17 00:00:00 2001 From: YujinBae Date: Mon, 13 Jul 2026 14:10:23 +0900 Subject: [PATCH] _sre: fix Match.expand to use _compile_template (#8229) (#8256) Match.expand delegated to re._expand, which CPython removed in 3.12 during the template-expansion refactor. With the 3.14 stdlib that helper no longer exists, so expand raised AttributeError. Route expand through the compiled-template path (Template::compile -> re._compile_template) and share the template-filling logic with Pattern.sub via a Match::expand_template helper. Each caller still compiles the template once, so sub keeps its no-recompile-per-match fast path. Remove the expectedFailure marker on test_re.test_expand. Assisted-by: Claude Code:claude-opus-4-8 --- Lib/test/test_re.py | 1 - crates/vm/src/stdlib/_sre.rs | 47 ++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index 1d396e4f3..eef630754 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -726,7 +726,6 @@ class ReTests(unittest.TestCase): 'first second').groupdict(), {'first':'first', 'second':'second'}) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_expand(self): self.assertEqual(re.match("(?Pfirst) (?Psecond)", "first second") diff --git a/crates/vm/src/stdlib/_sre.rs b/crates/vm/src/stdlib/_sre.rs index f7bc10e66..c90ff4d4f 100644 --- a/crates/vm/src/stdlib/_sre.rs +++ b/crates/vm/src/stdlib/_sre.rs @@ -471,15 +471,7 @@ mod _sre { } FilterType::Template(template) => { let m = Match::new(&mut iter.state, zelf.clone(), string.clone()); - // template.expand(m)? - // let mut list = vec![template.literal.clone()]; - sub_list.push(template.literal.clone()); - for (index, literal) in template.items.iter().cloned() { - if let Some(item) = m.get_slice(index, s, vm) { - sub_list.push(item); - } - sub_list.push(literal); - } + m.expand_template(template, s, &mut sub_list, vm); } }; @@ -702,10 +694,19 @@ mod _sre { } #[pymethod] - fn expand(zelf: PyRef, template: PyStrRef, vm: &VirtualMachine) -> PyResult { - let re = vm.import("re", 0)?; - let func = re.get_attr("_expand", vm)?; - func.call((zelf.pattern.clone(), zelf, template), vm) + fn expand(zelf: PyRef, template: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let template = Template::compile(zelf.pattern.clone(), template, vm)?; + with_sre_str!(zelf.pattern, &zelf.string, vm, |s| { + let mut list: Vec = Vec::new(); + zelf.expand_template(&template, s, &mut list, vm); + + let join_type: PyObjectRef = if zelf.pattern.isbytes { + vm.ctx.new_bytes(vec![]).into() + } else { + vm.ctx.new_str(ascii!("")).into() + }; + vm.call_method(&join_type, "join", (PyList::from(list).into_pyobject(vm),)) + }) } #[pymethod] @@ -820,6 +821,26 @@ mod _sre { Some(str_drive.slice(start as usize, end as usize, vm)) } + /// Expand an already-compiled template against this match, appending the + /// resulting literal/group segments to `list`. Shared by `expand` and + /// `Pattern.sub` so the template-filling logic lives in one place; the + /// caller is responsible for compiling the template (once) beforehand. + fn expand_template( + &self, + template: &Template, + str_drive: S, + list: &mut Vec, + vm: &VirtualMachine, + ) { + list.push(template.literal.clone()); + for (index, literal) in template.items.iter().cloned() { + if let Some(item) = self.get_slice(index, str_drive, vm) { + list.push(item); + } + list.push(literal); + } + } + #[pyclassmethod] fn __class_getitem__( cls: PyTypeRef,