diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 74126751835..d928c878a15 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5987,7 +5987,6 @@ def _strip_non_python_syntax(self, input, self.assertEqual(computed_clean_signature, clean_signature) self.assertEqual(computed_self_parameter, self_parameter) - @unittest.expectedFailure # TODO: RUSTPYTHON; + (module, /, path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True) def test_signature_strip_non_python_syntax(self): self._strip_non_python_syntax( "($module, /, path, mode, *, dir_fd=None, " + @@ -6318,7 +6317,6 @@ def test_weakref_module_has_signatures(self): no_signature = {'ReferenceType', 'ref'} self._test_module_has_signatures(weakref, no_signature) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: .func at 0xa4c07a580> builtin has invalid signature def test_python_function_override_signature(self): def func(*args, **kwargs): pass diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index cd1e88f5475..5eea014ccde 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1290,7 +1290,7 @@ def test_post_mortem_chained(): ... except Exception as e: ... pdb._post_mortem(e, instance) - >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE +EXPECTED_FAILURE + >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE ... 'exceptions', ... 'exceptions 0', ... '$_exception', @@ -2133,7 +2133,7 @@ def test_pdb_asynctask(): >>> def test_function(): ... asyncio.run(test(), loop_factory=asyncio.EventLoop) - >>> with PdbTestInput([ # TODO: RUSTPYTHON # doctest: +ELLIPSIS +EXPECTED_FAILURE + >>> with PdbTestInput([ # doctest: +ELLIPSIS ... '$_asynctask', ... 'continue', ... ]): @@ -4185,7 +4185,6 @@ def test_blocks_at_first_code_line(self): self.assertTrue(any("__main__.py(4)()" in l for l in stdout.splitlines()), stdout) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_file_modified_after_execution(self): script = """ print("hello") @@ -4259,7 +4258,6 @@ def test_file_modified_after_execution_with_multiple_instances(self): self.assertIn("WARNING:", stdout) self.assertIn("was edited", stdout) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_file_modified_after_execution_with_restart(self): script = """ import random diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 5c9059614de..d206e5a910d 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -1708,7 +1708,6 @@ def test_bound_builtin_classmethod_unrepresentable_default(self): "classmeth(a, b=) class method of " "_testcapi.DocStringUnrepresentableSignatureTest") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_overridden_text_signature(self): class C: def meth(*args, **kwargs): diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index 1c52eb0dcb2..1cfddb8e19c 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -890,7 +890,6 @@ def test_named_unicode_escapes(self): self.checkPatternError(br'\N{LESS-THAN SIGN}', r'bad escape \N', 0) self.checkPatternError(br'[\N{LESS-THAN SIGN}]', r'bad escape \N', 1) - @unittest.expectedFailure # TODO: RUSTPYTHON; re.search(r"\B", "") now returns a match in CPython 3.14 def test_word_boundaries(self): # See http://bugs.python.org/issue10713 self.assertEqual(re.search(r"\b(abc)\b", "abc").group(1), "abc") @@ -2493,7 +2492,6 @@ def test_search_anchor_at_beginning(self): # With optimization -- 0.0003 seconds. self.assertLess(stopwatch.seconds, 0.1) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_possessive_quantifiers(self): """Test Possessive Quantifiers Test quantifiers of the form @+ for some repetition operator @, @@ -2647,7 +2645,6 @@ def test_bug_gh100061(self): self.assertEqual(re.match("(?>(?:ab?c){1,3})", "aca").span(), (0, 2)) self.assertEqual(re.match("(?:ab?c){1,3}+", "aca").span(), (0, 2)) - @unittest.expectedFailure # TODO: RUSTPYTHON; self.assertEqual(re.match('((x)|y|z){3}+', 'xyz').groups(), ('z', 'x'))\n AssertionError: Tuples differ: ('x', 'x') != ('z', 'x') def test_bug_gh101955(self): # Possessive quantifier with nested alternative with capture groups self.assertEqual(re.match('((x)|y|z)*+', 'xyz').groups(), ('z', 'x')) diff --git a/Lib/test/test_unittest/testmock/testhelpers.py b/Lib/test/test_unittest/testmock/testhelpers.py index bcae14c5b3c..6877e92cb3c 100644 --- a/Lib/test/test_unittest/testmock/testhelpers.py +++ b/Lib/test/test_unittest/testmock/testhelpers.py @@ -929,6 +929,7 @@ def check_data_descriptor(mock_attr): check_data_descriptor(foo.desc) + @unittest.expectedFailure # TODO: RUSTPYTHON; time.ctime has an auto-generated __text_signature__, so inspect.signature() succeeds instead of raising ValueError def test_autospec_on_bound_builtin_function(self): meth = types.MethodType(time.ctime, time.time()) self.assertIsInstance(meth(), str) diff --git a/crates/codegen/src/symboltable.rs b/crates/codegen/src/symboltable.rs index bafd065ef93..a66410f9018 100644 --- a/crates/codegen/src/symboltable.rs +++ b/crates/codegen/src/symboltable.rs @@ -3356,7 +3356,14 @@ impl SymbolTableBuilder { flags.insert(SymbolFlags::USE); } SymbolUsage::Iter => { - flags.insert(SymbolFlags::ITER | SymbolFlags::DEF_COMP_ITER); + // CPython symtable_add_def_helper() records an inlined + // comprehension target as a local definition as well as a + // comprehension iterator. Keep ITER as the internal + // re-assignment check marker; DEF_LOCAL is part of the public + // ste_symbols flags exposed by _symtable. + flags.insert( + SymbolFlags::DEF_LOCAL | SymbolFlags::ITER | SymbolFlags::DEF_COMP_ITER, + ); } SymbolUsage::TypeParam => { flags.insert(SymbolFlags::DEF_LOCAL | SymbolFlags::DEF_TYPE_PARAM); diff --git a/crates/host_env/src/os.rs b/crates/host_env/src/os.rs index e5868871e8d..01711687c6c 100644 --- a/crates/host_env/src/os.rs +++ b/crates/host_env/src/os.rs @@ -4,6 +4,10 @@ use crate::crt_fd; #[cfg(windows)] use crate::fs; +#[cfg(windows)] +pub use crate::posix::rename; +#[cfg(any(unix, target_os = "wasi"))] +pub use crate::posix_unix_like::rename; #[cfg(any(unix, windows))] use core::ffi::CStr; use core::str::Utf8Error; @@ -28,6 +32,23 @@ use { }, }; +#[cfg(not(any(unix, windows, target_os = "wasi")))] +pub fn rename( + from: impl AsRef, + from_fd: Option>, + to: impl AsRef, + to_fd: Option>, +) -> io::Result<()> { + if from_fd.is_none() && to_fd.is_none() { + std::fs::rename(from, to) + } else { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "renameat is not available on this platform", + )) + } +} + /// Convert exit code to std::process::ExitCode /// /// On Windows, this supports the full u32 range including STATUS_CONTROL_C_EXIT (0xC000013A). diff --git a/crates/sre_engine/src/engine.rs b/crates/sre_engine/src/engine.rs index a91ea3455fb..a110a75d65f 100644 --- a/crates/sre_engine/src/engine.rs +++ b/crates/sre_engine/src/engine.rs @@ -110,6 +110,21 @@ impl Marks { self.marks_stack.pop(); } + fn stack_depth(&self) -> usize { + self.marks_stack.len() + } + + fn discard_to(&mut self, depth: usize) { + self.marks_stack.truncate(depth); + } + + fn restore_to(&mut self, depth: usize) { + let (marks, last_index) = self.marks_stack[depth].clone(); + self.marks = marks; + self.last_index = last_index; + self.marks_stack.truncate(depth); + } + fn clear(&mut self) { self.last_index = -1; self.marks.clear(); @@ -144,6 +159,7 @@ impl State { jump: Jump::OpCode, repeat_ctx_id: usize::MAX, count: -1, + marks_stack_base: usize::MAX, }; _match(req, self, ctx) } @@ -165,6 +181,7 @@ impl State { jump: Jump::OpCode, repeat_ctx_id: usize::MAX, count: -1, + marks_stack_base: usize::MAX, }; if ctx.peek_code(&req, 0) == SreOpcode::INFO as u32 { @@ -483,6 +500,7 @@ fn _match(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo } Jump::PossessiveRepeat2 => { if popped_result { + ctx.cursor = state.cursor; ctx.count += 1; ctx.jump = Jump::PossessiveRepeat1; continue 'context; @@ -495,6 +513,7 @@ fn _match(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo if ((ctx.count as usize) < max_count || max_count == MAXREPEAT) && ctx.cursor.position != state.cursor.position { + ctx.marks_stack_base = state.marks.stack_depth(); state.marks.push(); ctx.cursor = state.cursor; let mut next = ctx.next_offset(4, Jump::PossessiveRepeat4); @@ -507,12 +526,12 @@ fn _match(req: &Request<'_, S>, state: &mut State, mut ctx: MatchCo } Jump::PossessiveRepeat4 => { if popped_result { - state.marks.pop_discard(); + state.marks.discard_to(ctx.marks_stack_base); ctx.count += 1; ctx.jump = Jump::PossessiveRepeat3; continue 'context; } - state.marks.pop(); + state.marks.restore_to(ctx.marks_stack_base); state.cursor = ctx.cursor; ctx.skip_code_from(req, 1); ctx.skip_code(1); @@ -1057,6 +1076,7 @@ struct MatchContext { jump: Jump, repeat_ctx_id: usize, count: isize, + marks_stack_base: usize, } impl MatchContext { @@ -1147,7 +1167,9 @@ impl MatchContext { mut word_checker: F, ) -> bool { if self.at_beginning() && self.at_end(req) { - return false; + // Python 3.14 changed `\B` to match an empty input. Keep the + // boundary predicate false there, but its negation true. + return true; } let that = !self.at_beginning() && word_checker(self.back_peek_char::()); let this = !self.at_end(req) && word_checker(self.peek_char::()); diff --git a/crates/sre_engine/tests/tests.rs b/crates/sre_engine/tests/tests.rs index f1edb64cdbf..53f5225d4ad 100644 --- a/crates/sre_engine/tests/tests.rs +++ b/crates/sre_engine/tests/tests.rs @@ -2,6 +2,7 @@ #[cfg(test)] mod tests { use rustpython_sre_engine::{Request, State, StrDrive}; + use rustpython_wtf8::Wtf8Buf; struct Pattern { #[expect(unused, reason = "Needed for automated script")] @@ -44,7 +45,7 @@ mod tests { #[rustfmt::skip] let big_b = Pattern { pattern: "\\B", code: &[14, 4, 0, 0, 0, 6, 11, 1] }; // END GENERATED let (req, mut state) = big_b.state(""); - assert!(!state.search(req)); + assert!(state.search(req)); } #[test] @@ -169,6 +170,40 @@ mod tests { assert!(!state.py_match(&req)); } + #[test] + fn possessive_repeat_keeps_last_capture() { + use optional::Optioned; + + let single_code = &[17, 0, 24, 6, 0, 1, 16, 101, 1, 17, 1, 1]; + let req = Request::new("eeea", 3, usize::MAX, single_code, false); + let mut single_state = State::default(); + assert!(single_state.py_match(&req)); + assert_eq!( + single_state.marks.get(0), + (Optioned::some(3), Optioned::some(3)) + ); + + // (e?){2,4}+a: the fourth successful iteration is empty, so group 1 + // must retain its final empty span rather than the previous "e". + #[rustfmt::skip] let optional = Pattern { + pattern: "(e?){2,4}+a", + code: &[14, 4, 0, 1, 5, 28, 14, 2, 4, 17, 0, 24, 6, 0, 1, 16, 101, 1, 17, 1, 1, 16, 97, 1], + }; + let (req, mut state) = optional.state("eeea"); + assert!(state.py_match(&req)); + assert_eq!(state.marks.get(0), (Optioned::some(3), Optioned::some(3))); + + // ((x)|y|z){3}+: group 1 is the final "z"; group 2 retains "x". + #[rustfmt::skip] let alternation = Pattern { + pattern: "((x)|y|z){3}+", + code: &[14, 4, 0, 3, 3, 28, 28, 3, 3, 17, 0, 7, 9, 17, 2, 16, 120, 17, 3, 15, 12, 5, 16, 121, 15, 7, 5, 16, 122, 15, 2, 0, 17, 1, 1, 1], + }; + let (req, mut state) = alternation.state("xyz"); + assert!(state.py_match(&req)); + assert_eq!(state.marks.get(0), (Optioned::some(2), Optioned::some(3))); + assert_eq!(state.marks.get(1), (Optioned::some(0), Optioned::some(1))); + } + #[test] fn bug_20998() { // pattern p = re.compile('[a-c]+', re.I) @@ -181,6 +216,23 @@ mod tests { assert_eq!(state.cursor.position, 3); } + #[test] + fn ascii_ignore_keeps_nonascii_range_literal() { + // pattern p = re.compile(r'[\u0430-\u045f]', re.I | re.A) + // + // ASCII-only case folding must not discard an exact non-ASCII range: + // U+0450 lies in the compiled U+0430..U+045F interval. + #[rustfmt::skip] let p = Pattern { + pattern: "[\\u0430-\\u045f]", + code: &[14, 8, 4, 1, 1, 22, 1072, 1119, 0, 13, 5, 22, 1072, 1119, 0, 1], + }; + let (req, mut state) = p.state("\u{0450}"); + assert!(state.py_match(&req)); + let subject = Wtf8Buf::from("\u{0450}"); + let (req, mut state) = p.state(subject.as_ref()); + assert!(state.py_match(&req)); + } + #[test] fn bigcharset() { // pattern p = re.compile('[a-z]*', re.I) diff --git a/crates/stdlib/src/_tokenize.rs b/crates/stdlib/src/_tokenize.rs index 7106c527096..c071a2b62f2 100644 --- a/crates/stdlib/src/_tokenize.rs +++ b/crates/stdlib/src/_tokenize.rs @@ -237,7 +237,9 @@ mod _tokenize { } let raw_type = token_kind_value(kind); - let token_type = if extra_tokens && raw_type > TOKEN_DEDENT && raw_type < TOKEN_OP { + let token_type = if extra_tokens + && (kind == TokenKind::Unknown || (raw_type > TOKEN_DEDENT && raw_type < TOKEN_OP)) + { TOKEN_OP } else { raw_type