From 8fda0eccee33f140899c2cddf9c39481f0e63a97 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" <69878+youknowone@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:53:53 +0900 Subject: [PATCH] Fix fork-child SIGSEGV and yield-from throw() args (#8759) * Unskip passing flaky CPython tests Remove RUSTPYTHON skip overlays after Linux release repeats passed for enum, threading, logging, subprocess, socket, int, re, strtod, and regrtest cases. Assisted-by: Grok:4.6 * Fix fork-child SIGSEGV and yield-from throw args Forget JoinHandle of threads that did not survive fork instead of dropping them. Zero-reinit the import lock and condvars in the child rather than unlock/notify. Resume the world if os.fork() fails. Call a yield-from subiterator throw() without None fillers so a 1-arg throw stays 1-arg. Drop the stale Windows expectedFailure on logging queue config and unskip test_reinit_tls_after_fork plus the asyncio local_addr family test. Assisted-by: Grok:4.6 * Restore import lock depth after fork; add WASI clock info Zeroing IMP_LOCK in the child discarded its recursion count, so imp.release_lock() raised and test_fork1 exited 1. Re-acquire the saved depth, then drop the extra pre-fork hold. Expose time.get_clock_info on WASI via clock_getres so test_int Stopwatch can run. Unskip lock_tests test_acquire_contended and test_thread_leak. Assisted-by: Grok:4.6 * Honor chmod follow_symlinks=False via fchmodat Linux chmod followed the symlink, so dangling links raised FileNotFoundError instead of NotImplementedError. Use fchmodat with AT_SYMLINK_NOFOLLOW and map ENOTSUP to NotImplementedError. Unskip httpservers truncated Content-Length (CPython already skips as root) and tempfile symlink-mode cleanup on Linux. Assisted-by: Grok:4.6 * Unskip httplib local bad-hostname HTTPS test The skip was leftover. With default ssl-rustls the test passes under -W error on Linux. Assisted-by: Grok:4.6 * Fix list cmp deadlock, wasm digit overflow, REPL prompt Do not hold list/deque locks across item comparison callbacks. Compute int-to-str digit bounds in u64 so the check cannot wrap on wasm32. Snapshot set contents when list.extend copies a set. For a non-TTY stdin, write the REPL prompt to stderr and read lines from stdin. Flush stdout/stderr after each interactive command. Set sys.ps1/ps2 to ">>> " and "... ". Unskip list mutation compares, asyncio all_tasks race, importlib deadlock/parallel/circular-import tests, and REPL stdout flush. Assisted-by: Grok:4.6 * Avoid unnecessary u64 cast in int-to-str digit check bits() is already u64, so `bits as u64` fails clippy unnecessary_cast under -Dwarnings. Assisted-by: Grok:4.6 --- Lib/test/lock_tests.py | 2 - Lib/test/test_asyncio/test_events.py | 1 - Lib/test/test_asyncio/test_free_threading.py | 8 ---- Lib/test/test_cmd_line_script.py | 6 +-- Lib/test/test_enum.py | 2 - Lib/test/test_httplib.py | 1 - Lib/test/test_httpservers.py | 1 - Lib/test/test_importlib/test_locks.py | 2 - .../test_importlib/test_threaded_import.py | 3 -- Lib/test/test_int.py | 1 - Lib/test/test_list.py | 2 - Lib/test/test_logging.py | 3 -- Lib/test/test_re.py | 1 - Lib/test/test_regrtest.py | 7 --- Lib/test/test_socket.py | 8 ---- Lib/test/test_strtod.py | 2 - Lib/test/test_subprocess.py | 2 - Lib/test/test_tempfile.py | 1 - Lib/test/test_threading.py | 14 ------ Lib/test/test_threading_local.py | 1 - crates/host_env/src/posix.rs | 10 ++++ crates/host_env/src/readline.rs | 37 +++++++++----- crates/host_env/src/time.rs | 11 +++++ crates/vm/src/builtins/int.rs | 7 ++- crates/vm/src/builtins/list.rs | 21 +++++--- crates/vm/src/frame.rs | 14 +++++- crates/vm/src/iter.rs | 48 +++++++++++++++++++ crates/vm/src/stdlib/_collections.rs | 19 +++++--- crates/vm/src/stdlib/_imp.rs | 41 +++++++++++----- crates/vm/src/stdlib/_thread.rs | 22 +++++++-- crates/vm/src/stdlib/posix.rs | 41 +++++++++++++--- crates/vm/src/stdlib/sys.rs | 4 +- crates/vm/src/stdlib/time.rs | 39 +++++++++++++++ crates/vm/src/vm/mod.rs | 11 ++++- src/shell.rs | 13 +++-- 35 files changed, 283 insertions(+), 123 deletions(-) diff --git a/Lib/test/lock_tests.py b/Lib/test/lock_tests.py index 8c7a4f765..fb11f4828 100644 --- a/Lib/test/lock_tests.py +++ b/Lib/test/lock_tests.py @@ -162,7 +162,6 @@ class BaseLockTests(BaseTestCase): self.assertFalse(result[0]) lock.release() - @unittest.skip("TODO: RUSTPYTHON; sometimes hangs") def test_acquire_contended(self): lock = self.locktype() lock.acquire() @@ -210,7 +209,6 @@ class BaseLockTests(BaseTestCase): with Bunch(f, 1): pass - @unittest.skip("TODO: RUSTPYTHON; sometimes hangs") def test_thread_leak(self): # The lock shouldn't leak a Thread instance when used from a foreign # (non-threading) thread. diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index 398ac99e3..919d543b0 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -789,7 +789,6 @@ class EventLoopTestsMixin: self.assertEqual(port, expected) tr.close() - @unittest.skipIf(sys.platform == "linux", "TODO: RUSTPYTHON; Flaky on CI") @socket_helper.skip_if_tcp_blackhole def test_create_connection_local_addr_skip_different_family(self): # See https://github.com/python/cpython/issues/86508 diff --git a/Lib/test/test_asyncio/test_free_threading.py b/Lib/test/test_asyncio/test_free_threading.py index c8de0d244..d874ed00b 100644 --- a/Lib/test/test_asyncio/test_free_threading.py +++ b/Lib/test/test_asyncio/test_free_threading.py @@ -189,10 +189,6 @@ class TestPyFreeThreading(TestFreeThreading, TestCase): def factory(self, loop, coro, **kwargs): return asyncio.tasks._PyTask(coro, loop=loop, **kwargs) - @unittest.skip("TODO: RUSTPYTHON; hangs - Python _current_tasks dict not thread-safe") - def test_all_tasks_race(self): - return super().test_all_tasks_race() - @unittest.skipUnless(hasattr(asyncio.tasks, "_c_all_tasks"), "requires _asyncio") class TestCFreeThreading(TestFreeThreading, TestCase): @@ -224,10 +220,6 @@ class TestEagerPyFreeThreading(TestPyFreeThreading): def factory(self, loop, coro, eager_start=True, **kwargs): return asyncio.tasks._PyTask(coro, loop=loop, **kwargs, eager_start=eager_start) - @unittest.skip("TODO: RUSTPYTHON; hangs - Python _current_tasks dict not thread-safe") - def test_all_tasks_race(self): - return super().test_all_tasks_race() - @unittest.skipUnless(hasattr(asyncio.tasks, "_c_all_tasks"), "requires _asyncio") class TestEagerCFreeThreading(TestCFreeThreading, TestCase): diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index 276dff080..a6310738f 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -208,19 +208,17 @@ class CmdLineTest(unittest.TestCase): self.assertIn(b' ~^~', stderr.readline()) self.assertIn(b'ZeroDivisionError', stderr.readline()) - @unittest.skip("TODO: RUSTPYTHON; test hang in middle") def test_repl_stdout_flush(self): self.check_repl_stdout_flush() - @unittest.skip("TODO: RUSTPYTHON; test hang in middle") def test_repl_stdout_flush_separate_stderr(self): self.check_repl_stdout_flush(True) - @unittest.skip("TODO: RUSTPYTHON; test hang in middle") + @unittest.skip("TODO: RUSTPYTHON; traceback omits the source line and caret") def test_repl_stderr_flush(self): self.check_repl_stderr_flush() - @unittest.skip("TODO: RUSTPYTHON; test hang in middle") + @unittest.skip("TODO: RUSTPYTHON; traceback omits the source line and caret") def test_repl_stderr_flush_separate_stderr(self): self.check_repl_stderr_flush(True) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 573db4393..57981ccc7 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -3972,7 +3972,6 @@ class OldTestFlag(unittest.TestCase): self.assertEqual(Color.ALL.value, 7) self.assertEqual(str(Color.BLUE), 'blue') - @unittest.skip("TODO: RUSTPYTHON; flaky test") @threading_helper.reap_threads @threading_helper.requires_working_threading() def test_unique_composite(self): @@ -4493,7 +4492,6 @@ class OldTestIntFlag(unittest.TestCase): self.assertEqual(Color.ALL.value, 7) self.assertEqual(str(Color.BLUE), 'blue') - @unittest.skip("TODO: RUSTPYTHON; flaky test") @threading_helper.reap_threads @threading_helper.requires_working_threading() def test_unique_composite(self): diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index f7ab3e576..ab06f3cf0 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -2166,7 +2166,6 @@ class HTTPSTest(TestCase): self.addCleanup(resp.close) self.assertEqual(resp.status, 404) - @unittest.skip("TODO: RUSTPYTHON; Flaky on CI") def test_local_bad_hostname(self): # The (valid) cert doesn't validate the HTTPS hostname import ssl diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index b7885f5d2..7b58c5ef5 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -1131,7 +1131,6 @@ class CGIHTTPServerTestCase(BaseTestCase): res = self.request('/cgi-bin/file7.py', 'POST', body, headers) self.assertEqual(res.read(), b'%d %d' % (size, size) + self.linesep) - @unittest.skip("TODO: RUSTPYTHON; flaky test") def test_large_content_length_truncated(self): with support.swap_attr(self.request_handler, 'timeout', 0.001): for w in range(18, 65): diff --git a/Lib/test/test_importlib/test_locks.py b/Lib/test/test_importlib/test_locks.py index d42f623b3..5bdb748a4 100644 --- a/Lib/test/test_importlib/test_locks.py +++ b/Lib/test/test_importlib/test_locks.py @@ -99,7 +99,6 @@ class DeadlockAvoidanceTests: self.assertEqual(len(results), NTHREADS) return results - @unittest.skip("TODO: RUSTPYTHON; sometimes hangs") def test_deadlock(self): results = self.run_deadlock_avoidance_test(True) # At least one of the threads detected a potential deadlock on its @@ -109,7 +108,6 @@ class DeadlockAvoidanceTests: self.assertGreaterEqual(nb_deadlocks, 1) self.assertEqual(results.count((True, True)), len(results) - nb_deadlocks) - @unittest.skip("TODO: RUSTPYTHON; flaky test") def test_no_deadlock(self): results = self.run_deadlock_avoidance_test(False) self.assertEqual(results.count((True, False)), 0) diff --git a/Lib/test/test_importlib/test_threaded_import.py b/Lib/test/test_importlib/test_threaded_import.py index befee06f4..0e0d0721e 100644 --- a/Lib/test/test_importlib/test_threaded_import.py +++ b/Lib/test/test_importlib/test_threaded_import.py @@ -135,7 +135,6 @@ class ThreadedImportTests(unittest.TestCase): if verbose: print("OK.") - @unittest.skip("TODO: RUSTPYTHON; flaky") @support.bigmemtest(size=50, memuse=76*2**20, dry_run=False) def test_parallel_module_init(self, size): self.check_parallel_module_init() @@ -247,7 +246,6 @@ class ThreadedImportTests(unittest.TestCase): __import__(TESTFN) del sys.modules[TESTFN] - @unittest.skip("TODO: RUSTPYTHON; hang; Suspected cause of crashes in Windows CI - PermissionError: [WinError 32] Permission denied: \"C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\test_python_0cdrhhs_\\test_python_6340æ\"") @support.bigmemtest(size=1, memuse=1.8*2**30, dry_run=False) def test_concurrent_futures_circular_import(self, size): # Regression test for bpo-43515 @@ -255,7 +253,6 @@ class ThreadedImportTests(unittest.TestCase): 'partial', 'cfimport.py') script_helper.assert_python_ok(fn) - @unittest.skip("TODO: RUSTPYTHON; hang") @support.bigmemtest(size=1, memuse=1.8*2**30, dry_run=False) def test_multiprocessing_pool_circular_import(self, size): # Regression test for bpo-41567 diff --git a/Lib/test/test_int.py b/Lib/test/test_int.py index e281763a6..64e4156f6 100644 --- a/Lib/test/test_int.py +++ b/Lib/test/test_int.py @@ -630,7 +630,6 @@ class IntStrDigitLimitsTests(unittest.TestCase): self.assertIn('conversion', str(err.exception)) self.assertLess(sw_fail_extra_huge.seconds, sw_convert.seconds/2) - @unittest.skip("TODO: RUSTPYTHON; flaky test") def test_denial_of_service_prevented_str_to_int(self): """Regression test: ensure we fail before performing O(N**2) work.""" maxdigits = sys.get_int_max_str_digits() diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py index d76247340..82e6056e7 100644 --- a/Lib/test/test_list.py +++ b/Lib/test/test_list.py @@ -226,7 +226,6 @@ class ListTest(list_tests.CommonTest): with self.assertRaises(TypeError): (3,) + L([1,2]) - @unittest.skip("TODO: RUSTPYTHON; hang") def test_equal_operator_modifying_operand(self): # test fix for seg fault reported in bpo-38588 part 2. class X: @@ -252,7 +251,6 @@ class ListTest(list_tests.CommonTest): list4 = [1] self.assertFalse(list3 == list4) - @unittest.skip("TODO: RUSTPYTHON; hang") def test_lt_operator_modifying_operand(self): # See gh-120298 class evil: diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 083934d31..69364b057 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -731,7 +731,6 @@ class HandlerTest(BaseTest): # based on os.fork existing because that is what users and this test use. # This helps ensure that when fork exists (the important concept) that the # register_at_fork mechanism is also present and used. - @unittest.skip("TODO: RUSTPYTHON; Flaky") @support.requires_fork() @threading_helper.requires_working_threading() @skip_if_asan_fork @@ -4109,8 +4108,6 @@ class ConfigDictTest(BaseTest): # log a message (this creates a record put in the queue) logging.getLogger().info(message_to_log) - @unittest.skip("TODO: RUSTPYTHON; flaky EOFError") - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; SemLock not implemented on Windows") @skip_if_tsan_fork @support.requires_subprocess() def test_multiprocessing_queues(self): diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index 502b3659e..21c846835 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -2476,7 +2476,6 @@ class ReTests(unittest.TestCase): # gh-117594: The test is not slow by itself, but it relies on # the absolute computation time and can fail on very slow computers. - @unittest.skip("TODO: RUSTPYTHON; flaky, improve perf") @requires_resource('cpu') def test_search_anchor_at_beginning(self): s = 'x'*10**7 diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index d56524d58..6f465b73f 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -915,7 +915,6 @@ class ProgramsTestCase(BaseTestCase): args = [*self.python_args, script, *self.regrtest_args, *self.tests] self.run_tests(args) - @unittest.skip("TODO: RUSTPYTHON; flaky") def test_module_test(self): # -m test args = [*self.python_args, '-m', 'test', @@ -928,14 +927,12 @@ class ProgramsTestCase(BaseTestCase): *self.regrtest_args, *self.tests] self.run_tests(args) - @unittest.skip("TODO: RUSTPYTHON; flaky") def test_module_autotest(self): # -m test.autotest args = [*self.python_args, '-m', 'test.autotest', *self.regrtest_args, *self.tests] self.run_tests(args) - @unittest.skip("TODO: RUSTPYTHON; flaky") def test_module_from_test_autotest(self): # from test import autotest code = 'from test import autotest' @@ -943,7 +940,6 @@ class ProgramsTestCase(BaseTestCase): *self.regrtest_args, *self.tests] self.run_tests(args) - @unittest.skip("TODO: RUSTPYTHON; flaky") def test_script_autotest(self): # Lib/test/autotest.py script = os.path.join(self.testdir, 'autotest.py') @@ -2033,7 +2029,6 @@ class ArgsTestCase(BaseTestCase): for name in names: self.assertFalse(os.path.exists(name), name) - @unittest.skip("TODO: RUSTPYTHON; flaky") @unittest.skipIf(support.is_wasi, 'checking temp files is not implemented on WASI') def test_leak_tmp_file(self): @@ -2185,11 +2180,9 @@ class ArgsTestCase(BaseTestCase): def test_random_seed(self): self._check_random_seed(run_workers=False) - @unittest.skip("TODO: RUSTPYTHON; flaky") def test_random_seed_workers(self): self._check_random_seed(run_workers=True) - @unittest.skip("TODO: RUSTPYTHON; flaky") def test_python_command(self): code = textwrap.dedent(r""" import sys diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index ecde37adc..b59709680 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -7066,14 +7066,6 @@ class SendfileUsingSendfileTest(SendfileUsingSendTest): def meth_from_sock(self, sock): return getattr(sock, "_sendfile_use_sendfile") - @unittest.skip("TODO: RUSTPYTHON; os.sendfile count parameter not handled correctly; flaky") - def testCount(self): - return super().testCount() - - @unittest.skip("TODO: RUSTPYTHON; os.sendfile count parameter not handled correctly; flaky") - def testWithTimeout(self): - return super().testWithTimeout() - @unittest.skipUnless(HAVE_SOCKET_ALG, 'AF_ALG required') class LinuxKernelCryptoAPI(unittest.TestCase): diff --git a/Lib/test/test_strtod.py b/Lib/test/test_strtod.py index f263b7ab4..570de390a 100644 --- a/Lib/test/test_strtod.py +++ b/Lib/test/test_strtod.py @@ -146,7 +146,6 @@ class StrtodTests(unittest.TestCase): digits *= 5 exponent -= 1 - @unittest.skip("TODO: RUSTPYTHON; fails on debug mode, flaky in release mode") def test_halfway_cases(self): # test halfway cases for the round-half-to-even rule for i in range(100 * TEST_SIZE): @@ -213,7 +212,6 @@ class StrtodTests(unittest.TestCase): s = '{}e{}'.format(digits, exponent) self.check_strtod(s) - @unittest.skip("TODO: RUSTPYTHON; flaky test") def test_parsing(self): # make '0' more likely to be chosen than other digits digits = '000000123456789' diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index f237508fb..4a7e52cbc 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -3051,7 +3051,6 @@ class POSIXProcessTestCase(BaseTestCase): p1.stdout.close() p2.stdout.close() - @unittest.skip("TODO: RUSTPYTHON; flaky test") def test_close_fds(self): fd_status = support.findfile("fd_status.py", subdir="subprocessdata") @@ -3183,7 +3182,6 @@ class POSIXProcessTestCase(BaseTestCase): # descriptor of a pipe closed in the parent process is valid in the # child process according to fstat(), but the mode of the file # descriptor is invalid, and read or write raise an error. - @unittest.skip("TODO: RUSTPYTHON; flaky test") @support.requires_mac_ver(10, 5) def test_pass_fds(self): fd_status = support.findfile("fd_status.py", subdir="subprocessdata") diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index 42d1cf283..6fac921f6 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -1751,7 +1751,6 @@ class TestTemporaryDirectory(BaseTestCase): d2.cleanup() @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; flaky, sometimes pollute env on CI") - @unittest.expectedFailureIf(sys.platform in ('android', 'linux'), "TODO: RUSTPYTHON; FileNotFoundError: [Errno 2] No such file or directory: ''") @os_helper.skip_unless_symlink def test_cleanup_with_symlink_modes(self): # cleanup() should not follow symlinks when fixing mode bits (#91133) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 4bebd6e83..bd8145f22 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -685,7 +685,6 @@ class ThreadTests(BaseTestCase): self.assertEqual(out, b'') self.assertEqual(err, b'') - @unittest.skip("TODO: RUSTPYTHON; flaky") @skip_unless_reliable_fork def test_is_alive_after_fork(self): # Try hard to trigger #18418: is_alive() could sometimes be True on @@ -749,7 +748,6 @@ class ThreadTests(BaseTestCase): "main ident True\n" "current is main True\n") - @unittest.skip("TODO: RUSTPYTHON; flaky; process timeout after fork") @skip_unless_reliable_fork @unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()") def test_main_thread_after_fork_from_nonmain_thread(self): @@ -1470,7 +1468,6 @@ class ThreadJoinOnShutdown(BaseTestCase): """ self._run_and_join(script) - @unittest.skip("TODO: RUSTPYTHON; flaky test") @skip_unless_reliable_fork def test_3_join_in_forked_from_thread(self): # Like the test above, but fork() was called from a worker thread @@ -1571,7 +1568,6 @@ class ThreadJoinOnShutdown(BaseTestCase): self.assertEqual(out.strip(), b"OK") self.assertEqual(rc, 0) - @unittest.skip("TODO: RUSTPYTHON; - flaky, parking_lot mutex not fork-safe") @skip_unless_reliable_fork def test_reinit_tls_after_fork(self): # Issue #13817: fork() would deadlock in a multithreaded program with @@ -1852,7 +1848,6 @@ class ThreadingExceptionTests(BaseTestCase): lock = threading.Lock() self.assertRaises(RuntimeError, lock.release) - @unittest.skip("TODO: RUSTPYTHON; flaky test") @requires_subprocess() def test_recursion_limit(self): # Issue 9670 @@ -2236,10 +2231,6 @@ class CRLockTests(lock_tests.RLockTests): CustomRLock(1, b=2) self.assertEqual(warnings_log, []) - @unittest.skip("TODO: RUSTPYTHON; flaky test") - def test_different_thread(self): - return super().test_different_thread() - class EventTests(lock_tests.EventTests): eventtype = staticmethod(threading.Event) @@ -2250,10 +2241,6 @@ class ConditionAsRLockTests(lock_tests.RLockTests): def test_recursion_count(self): self.skipTest("Condition does not expose _recursion_count()") - @unittest.skip("TODO: RUSTPYTHON; flaky test") - def test_different_thread(self): - return super().test_different_thread() - class ConditionTests(lock_tests.ConditionTests): condtype = staticmethod(threading.Condition) @@ -2413,7 +2400,6 @@ class InterruptMainTests(unittest.TestCase): # Restore original handler signal.signal(signum, handler) - @unittest.skip("TODO: RUSTPYTHON; flaky") @requires_gil_enabled("gh-118433: Flaky due to a longstanding bug") def test_interrupt_main_subthread(self): # Calling start_new_thread with a function that executes interrupt_main diff --git a/Lib/test/test_threading_local.py b/Lib/test/test_threading_local.py index 17f031def..3a58afd81 100644 --- a/Lib/test/test_threading_local.py +++ b/Lib/test/test_threading_local.py @@ -26,7 +26,6 @@ def target(local, weaklist): class BaseLocalTest: - @unittest.skip("TODO: RUSTPYTHON; flaky test") def test_local_refs(self): self._local_refs(20) self._local_refs(50) diff --git a/crates/host_env/src/posix.rs b/crates/host_env/src/posix.rs index 5636f1e9c..928f8df39 100644 --- a/crates/host_env/src/posix.rs +++ b/crates/host_env/src/posix.rs @@ -177,6 +177,16 @@ pub fn chroot(path: &Path) -> std::io::Result<()> { nix::unistd::chroot(path).map_err(std::io::Error::from) } +#[cfg(all(unix, not(target_os = "redox")))] +pub fn fchmodat(dirfd: i32, path: &CStr, mode: libc::mode_t, flags: i32) -> std::io::Result<()> { + let ret = unsafe { libc::fchmodat(dirfd, path.as_ptr(), mode, flags) }; + if ret == 0 { + Ok(()) + } else { + Err(std::io::Error::last_os_error()) + } +} + #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))] pub fn lchmod(path: &CStr, mode: libc::mode_t) -> std::io::Result<()> { unsafe extern "C" { diff --git a/crates/host_env/src/readline.rs b/crates/host_env/src/readline.rs index 1015ec093..f3f468cba 100644 --- a/crates/host_env/src/readline.rs +++ b/crates/host_env/src/readline.rs @@ -18,6 +18,22 @@ pub enum ReadlineResult { Other(OtherError), } +fn stdio_readline(prompt: &str) -> ReadlineResult { + use std::io::prelude::*; + eprint!("{prompt}"); + if let Err(e) = io::stderr().flush() { + return ReadlineResult::Io(e); + } + + let line = io::stdin().lock().lines().next(); + match line { + Some(Ok(line)) => ReadlineResult::Line(line), + None => ReadlineResult::Eof, + Some(Err(e)) if e.kind() == io::ErrorKind::Interrupted => ReadlineResult::Interrupt, + Some(Err(e)) => ReadlineResult::Io(e), + } +} + #[allow(unused)] pub mod basic_readline { use super::*; @@ -48,19 +64,7 @@ pub mod basic_readline { } pub fn readline(&mut self, prompt: &str) -> ReadlineResult { - use std::io::prelude::*; - print!("{prompt}"); - if let Err(e) = io::stdout().flush() { - return ReadlineResult::Io(e); - } - - let next_line = io::stdin().lock().lines().next(); - match next_line { - Some(Ok(line)) => ReadlineResult::Line(line), - None => ReadlineResult::Eof, - Some(Err(e)) if e.kind() == io::ErrorKind::Interrupted => ReadlineResult::Interrupt, - Some(Err(e)) => ReadlineResult::Io(e), - } + stdio_readline(prompt) } } } @@ -126,6 +130,13 @@ pub mod rustyline_readline { } pub fn readline(&mut self, prompt: &str) -> ReadlineResult { + use std::io::IsTerminal; + // Pipes have no termios. Write the prompt to stderr and read + // stdin, matching PyOS_StdioReadline, so `python -i` under + // spawn still emits `>>> `. + if !io::stdin().is_terminal() { + return stdio_readline(prompt); + } use rustyline::error::ReadlineError; loop { break match self.repl.readline(prompt) { diff --git a/crates/host_env/src/time.rs b/crates/host_env/src/time.rs index 16444e424..b2ab3bf63 100644 --- a/crates/host_env/src/time.rs +++ b/crates/host_env/src/time.rs @@ -302,6 +302,17 @@ pub fn clock_getres(id: ClockId) -> std::io::Result { .map_err(std::io::Error::from) } +#[cfg(target_os = "wasi")] +pub fn clock_getres(id: ClockId) -> std::io::Result { + let mut ts = core::mem::MaybeUninit::::uninit(); + let ret = unsafe { libc::clock_getres(id.as_raw(), ts.as_mut_ptr()) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + let ts = unsafe { ts.assume_init() }; + Ok(Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)) +} + #[cfg(all(unix, not(target_os = "redox"), not(target_vendor = "apple")))] pub fn clock_settime(id: ClockId, time: Duration) -> std::io::Result<()> { let ts = nix::sys::time::TimeSpec::from(time); diff --git a/crates/vm/src/builtins/int.rs b/crates/vm/src/builtins/int.rs index b840c174f..84af39e40 100644 --- a/crates/vm/src/builtins/int.rs +++ b/crates/vm/src/builtins/int.rs @@ -707,8 +707,11 @@ pub(crate) fn check_int_to_str_digits(value: &BigInt, vm: &VirtualMachine) -> Py return Ok(()); } // Upper bound on decimal digit count: ⌈bits × log10(2)⌉ + 1, with log10(2) ≈ 0.30103. - let digits_upper = (bits as usize * 30103 / 100000) + 1; - if digits_upper > limit { + // Multiply with the u64 bit count: `bits as usize * 30103` wraps on + // 32-bit (wasm32) once bits ≳ 142_500, which silently accepts over-limit + // conversions. + let digits_upper = bits.saturating_mul(30103) / 100_000 + 1; + if digits_upper > u64::try_from(limit).unwrap_or(u64::MAX) { return Err(vm.new_value_error(format!( "Exceeds the limit ({limit} digits) for integer string conversion; \ use sys.set_int_max_str_digits() to increase the limit" diff --git a/crates/vm/src/builtins/list.rs b/crates/vm/src/builtins/list.rs index 7939c828e..29b5b297c 100644 --- a/crates/vm/src/builtins/list.rs +++ b/crates/vm/src/builtins/list.rs @@ -14,7 +14,6 @@ use crate::{ class::PyClassImpl, convert::ToPyObject, function::{ArgSize, Either, FuncArgs, OptionalArg, PyComparisonValue}, - iter::PyExactSizeIterator, protocol::{PyIterReturn, PyMappingMethods, PySequenceMethods}, recursion::ReprGuard, sequence::{MutObjectSequenceOp, OptionalRangeArgs, SequenceExt, SequenceMutExt}, @@ -590,11 +589,21 @@ impl Comparable for PyList { return Ok(res.into()); } let other = class_or_notimplemented!(Self, other); - let a = &*zelf.borrow_vec(); - let b = &*other.borrow_vec(); - a.iter() - .richcompare(b.iter(), op, vm) - .map(PyComparisonValue::Implemented) + // Item comparison can mutate either list (clear, resize). Holding the + // element lock across that callback deadlocks on the write side. + crate::iter::richcompare_mutating_seqs( + |i| { + let a = zelf.borrow_vec(); + (a.len(), a.get(i).cloned()) + }, + |i| { + let b = other.borrow_vec(); + (b.len(), b.get(i).cloned()) + }, + op, + vm, + ) + .map(PyComparisonValue::Implemented) } } diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 9c27ef112..18cfe4158 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -4033,7 +4033,19 @@ impl ExecutingFrame<'_> { Either::A(coro) => coro .throw(jen, exc_type, exc_val, exc_tb, vm) .to_pyresult(vm), - Either::B(meth) => meth.call((exc_type, exc_val, exc_tb), vm), + Either::B(meth) => { + // Omit trailing None so a 1-arg throw() stays 1-arg. + // Passing None fillers makes throw() look like the + // deprecated 3-arg form and warns under -W error. + let args = if !vm.is_none(&exc_tb) { + vec![exc_type, exc_val, exc_tb] + } else if !vm.is_none(&exc_val) { + vec![exc_type, exc_val] + } else { + vec![exc_type] + }; + meth.call(args, vm) + } }; return ret.map(ExecutionResult::Yield).or_else(|err| { // Add traceback entry for the yield-from/await point. diff --git a/crates/vm/src/iter.rs b/crates/vm/src/iter.rs index 1e1324379..febbb8b33 100644 --- a/crates/vm/src/iter.rs +++ b/crates/vm/src/iter.rs @@ -51,3 +51,51 @@ pub trait PyExactSizeIterator<'a>: ExactSizeIterator + S } impl<'a, T> PyExactSizeIterator<'a> for T where T: ExactSizeIterator + Sized {} + +/// Compare two sequences whose item-comparison callbacks may mutate the +/// containers. `get(i)` must snapshot `(len, item_at_i)` and drop any +/// container lock before returning. +/// +/// After an unequal item, sizes are read again: if a callback shrank a +/// sequence so `i` is now past the end, the result is a length comparison +/// rather than the item inequality. +pub fn richcompare_mutating_seqs( + mut get_a: impl FnMut(usize) -> (usize, Option), + mut get_b: impl FnMut(usize) -> (usize, Option), + op: PyComparisonOp, + vm: &VirtualMachine, +) -> PyResult { + if matches!(op, PyComparisonOp::Eq | PyComparisonOp::Ne) { + let (a_len, _) = get_a(0); + let (b_len, _) = get_b(0); + if a_len != b_len { + return Ok(op == PyComparisonOp::Ne); + } + } + + let mut i = 0usize; + loop { + let (a_len, a_item) = get_a(i); + let (b_len, b_item) = get_b(i); + let (Some(a_item), Some(b_item)) = (a_item, b_item) else { + return Ok(op.eval_ord(a_len.cmp(&b_len))); + }; + + if vm.bool_eq(&a_item, &b_item)? { + i += 1; + continue; + } + + let (a_len, _) = get_a(i); + let (b_len, _) = get_b(i); + if i >= a_len || i >= b_len { + return Ok(op.eval_ord(a_len.cmp(&b_len))); + } + + return match op { + PyComparisonOp::Eq => Ok(false), + PyComparisonOp::Ne => Ok(true), + _ => a_item.rich_compare_bool(&b_item, op, vm), + }; + } +} diff --git a/crates/vm/src/stdlib/_collections.rs b/crates/vm/src/stdlib/_collections.rs index 592b67490..895059359 100644 --- a/crates/vm/src/stdlib/_collections.rs +++ b/crates/vm/src/stdlib/_collections.rs @@ -13,7 +13,6 @@ mod _collections { common::lock::{PyMutex, PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard}, convert::ToPyObject, function::{FuncArgs, KwArgs, OptionalArg, PyComparisonValue}, - iter::PyExactSizeIterator, object::{Traverse, TraverseFn}, protocol::{PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods}, recursion::ReprGuard, @@ -587,11 +586,19 @@ mod _collections { } let other = class_or_notimplemented!(Self, other); - let lhs = zelf.borrow_deque(); - let rhs = other.borrow_deque(); - lhs.iter() - .richcompare(rhs.iter(), op, vm) - .map(PyComparisonValue::Implemented) + crate::iter::richcompare_mutating_seqs( + |i| { + let lhs = zelf.borrow_deque(); + (lhs.len(), lhs.get(i).cloned()) + }, + |i| { + let rhs = other.borrow_deque(); + (rhs.len(), rhs.get(i).cloned()) + }, + op, + vm, + ) + .map(PyComparisonValue::Implemented) } } diff --git a/crates/vm/src/stdlib/_imp.rs b/crates/vm/src/stdlib/_imp.rs index cb3c1cbf8..4d053ee68 100644 --- a/crates/vm/src/stdlib/_imp.rs +++ b/crates/vm/src/stdlib/_imp.rs @@ -11,9 +11,22 @@ pub(super) use crate::vm::resolve_frozen_alias; #[pymodule(sub)] mod lock { use crate::{PyResult, VirtualMachine, stdlib::_thread::RawRMutex}; + use core::cell::Cell; static IMP_LOCK: RawRMutex = RawRMutex::INIT; + thread_local! { + static IMP_LOCK_DEPTH: Cell = const { Cell::new(0) }; + } + + fn bump_depth() { + IMP_LOCK_DEPTH.with(|c| c.set(c.get() + 1)); + } + + fn drop_depth() { + IMP_LOCK_DEPTH.with(|c| c.set(c.get().saturating_sub(1))); + } + #[pyfunction] fn acquire_lock(vm: &VirtualMachine) { // Detach while blocking on IMP_LOCK. The import lock is held across @@ -27,10 +40,11 @@ mod lock { #[pyfunction] fn release_lock(vm: &VirtualMachine) -> PyResult<()> { - if !IMP_LOCK.is_locked() { + if !IMP_LOCK.is_locked() || !IMP_LOCK.is_owned_by_current_thread() { Err(vm.new_runtime_error("Global import lock not held")) } else { unsafe { IMP_LOCK.unlock() }; + drop_depth(); Ok(()) } } @@ -42,41 +56,44 @@ mod lock { pub(super) fn acquire_lock_for_fork() { IMP_LOCK.lock(); + bump_depth(); } #[cfg(all(unix, feature = "host_env"))] pub(super) fn release_lock_after_fork_parent() { if IMP_LOCK.is_locked() && IMP_LOCK.is_owned_by_current_thread() { unsafe { IMP_LOCK.unlock() }; + drop_depth(); } } - /// Reset import lock after fork() — only if held by a dead thread. + /// Reset the import lock after `fork()`. /// - /// `IMP_LOCK` is a reentrant mutex. If the *current* (surviving) thread - /// held it at fork time, the child must be able to release it normally. - /// Only reset if a now-dead thread was the owner. + /// Zero the lock so waiter queues from dead threads are discarded, then + /// re-acquire it as many times as this thread held it. `unlock()` on the + /// inherited lock would unpark those waiters. /// /// # Safety /// /// Must only be called from single-threaded child after fork(). #[cfg(all(unix, feature = "host_env"))] pub(crate) unsafe fn reinit_after_fork() { - if IMP_LOCK.is_locked() && !IMP_LOCK.is_owned_by_current_thread() { - // Held by a dead thread — reset to unlocked. - unsafe { rustpython_common::lock::zero_reinit_after_fork(&IMP_LOCK) }; + let depth = IMP_LOCK_DEPTH.with(Cell::get); + unsafe { rustpython_common::lock::zero_reinit_after_fork(&IMP_LOCK) }; + for _ in 0..depth { + IMP_LOCK.lock(); } } - /// Match CPython's `_PyImport_ReInitLock()` + `_PyImport_ReleaseLock()` - /// behavior in the post-fork child: - /// 1) if ownership metadata is stale (dead owner / changed tid), reset; - /// 2) if current thread owns the lock, release it. + /// Restore the child's import lock, then drop the extra hold taken + /// before `fork()`. Nested `imp.acquire_lock()` holds stay so the + /// child's `release_lock()` can unwind them. #[cfg(all(unix, feature = "host_env"))] pub(super) unsafe fn after_fork_child_reinit_and_release() { unsafe { reinit_after_fork() }; if IMP_LOCK.is_locked() && IMP_LOCK.is_owned_by_current_thread() { unsafe { IMP_LOCK.unlock() }; + drop_depth(); } } } diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index 71175ccf0..22fb59a1e 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -1399,14 +1399,20 @@ pub(crate) mod _thread { } inner_guard.state = ThreadHandleState::Done; - inner_guard.join_handle = None; + // The OS thread did not survive the fork. Dropping JoinHandle + // would pthread_detach a copied thread descriptor. + if let Some(handle) = inner_guard.join_handle.take() { + core::mem::forget(handle); + } drop(inner_guard); - // Reinit and set the done event + // Reinit and set the done event. Do not notify: no other + // thread exists in the child, and notify_all would unpark + // waiters that did not survive the fork. let (lock, cvar) = &*done_event; reinit_parking_lot_mutex(lock); + reinit_parking_lot_condvar(cvar); *lock.lock() = true; - cvar.notify_all(); true }); @@ -1438,8 +1444,8 @@ pub(crate) mod _thread { let (lock, cvar) = &*done_event; reinit_parking_lot_mutex(lock); + reinit_parking_lot_condvar(cvar); *lock.lock() = true; - cvar.notify_all(); false }); @@ -1465,6 +1471,14 @@ pub(crate) mod _thread { unsafe { rustpython_common::lock::zero_reinit_after_fork(mutex.raw()) }; } + /// Reset a parking_lot::Condvar after fork. + /// + /// `notify_all()` would unpark waiters that did not survive the fork. + #[cfg(all(unix, feature = "host_env"))] + fn reinit_parking_lot_condvar(cvar: &parking_lot::Condvar) { + unsafe { rustpython_common::lock::zero_reinit_after_fork(cvar) }; + } + // Thread handle state enum #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ThreadHandleState { diff --git a/crates/vm/src/stdlib/posix.rs b/crates/vm/src/stdlib/posix.rs index 3f2457d86..6ad0df603 100644 --- a/crates/vm/src/stdlib/posix.rs +++ b/crates/vm/src/stdlib/posix.rs @@ -614,8 +614,8 @@ pub mod module { #[cfg(feature = "threading")] crate::stdlib::_thread::after_fork_child(vm); - // CPython parity: reinit import lock ownership metadata in child - // and release the lock acquired by PyOS_BeforeFork(). + // Reinit import lock ownership metadata in child and release the lock + // acquired before fork. #[cfg(feature = "threading")] unsafe { crate::stdlib::_imp::after_fork_child_imp_lock_release() @@ -817,15 +817,18 @@ pub mod module { Ok(0) } Ok(pid) => { - // Match CPython timing: capture this before parent after-fork hooks - // in case those hooks start threads. + // Capture this before parent after-fork hooks in case those + // hooks start threads. let num_os_threads = get_number_of_os_threads(); py_os_after_fork_parent(vm); - // Match CPython timing: warn only after parent callback path resumes world. + // Warn only after parent callback path resumes the world. warn_if_multi_threaded("fork", num_os_threads, vm); Ok(pid) } - Err(err) => Err(err.into_pyexception(vm)), + Err(err) => { + py_os_after_fork_parent(vm); + Err(err.into_pyexception(vm)) + } } } @@ -1002,6 +1005,32 @@ pub mod module { vm: &VirtualMachine, ) -> PyResult<()> { let [] = dir_fd.0; + #[cfg(all( + unix, + not(target_os = "redox"), + not(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd")) + ))] + if !follow_symlinks.0 { + let err_path = path.clone(); + let c_path = path.into_cstring(vm)?; + return rustpython_host_env::posix::fchmodat( + libc::AT_FDCWD, + &c_path, + mode as libc::mode_t, + libc::AT_SYMLINK_NOFOLLOW, + ) + .map_err(|err| { + let enotsup = err.raw_os_error() == Some(libc::EOPNOTSUPP) + || err.raw_os_error() == Some(libc::ENOTSUP); + if enotsup { + vm.new_not_implemented_error( + "chmod: follow_symlinks unavailable on this platform".to_owned(), + ) + } else { + OSErrorBuilder::with_filename(&err, err_path, vm) + } + }); + } let err_path = path.clone(); let body = move || { use std::os::unix::fs::PermissionsExt; diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index bfdc22588..a6ede7706 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -248,9 +248,9 @@ pub mod sys { }; #[pyattr(name = "ps1")] - const PS1: &str = ">>>>> "; + const PS1: &str = ">>> "; #[pyattr(name = "ps2")] - const PS2: &str = "..... "; + const PS2: &str = "... "; #[cfg(windows)] #[pyattr(name = "_vpath")] diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 4ddf31657..13914084e 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -21,6 +21,11 @@ mod decl { function::{Either, FuncArgs, OptionalArg}, types::{PyStructSequence, PyStructSequenceData, struct_sequence_new}, }; + #[cfg(target_os = "wasi")] + use crate::{ + PyRef, + builtins::{PyNamespace, PyUtf8StrRef}, + }; #[cfg(any(unix, windows))] use crate::{ common::wtf8::Wtf8Buf, @@ -75,6 +80,40 @@ mod decl { get_clock_time(ClockId::CLOCK_MONOTONIC, vm) } + #[cfg(target_os = "wasi")] + fn clock_getres(id: ClockId, vm: &VirtualMachine) -> PyResult { + host_time::clock_getres(id) + .map(|d| d.as_secs_f64()) + .map_err(|err| vm.new_os_error(err.to_string())) + } + + #[cfg(target_os = "wasi")] + #[pyfunction] + fn get_clock_info(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult> { + let (adj, imp, mono, res) = match name.as_str() { + "monotonic" | "perf_counter" => ( + false, + "time.clock_gettime(CLOCK_MONOTONIC)", + true, + clock_getres(ClockId::CLOCK_MONOTONIC, vm)?, + ), + "time" => ( + true, + "time.clock_gettime(CLOCK_REALTIME)", + false, + clock_getres(ClockId::CLOCK_REALTIME, vm)?, + ), + _ => return Err(vm.new_value_error("unknown clock")), + }; + + Ok(py_namespace!(vm, { + "implementation" => vm.new_pyobj(imp), + "monotonic" => vm.ctx.new_bool(mono), + "adjustable" => vm.ctx.new_bool(adj), + "resolution" => vm.ctx.new_float(res), + })) + } + #[cfg(not(any(unix, windows, target_os = "wasi")))] fn get_monotonic_time(vm: &VirtualMachine) -> PyResult { duration_since_system_now(vm) diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index be317f5ac..1a8ed3a85 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -24,8 +24,9 @@ mod vm_ops; use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, builtins::{ - self, PyBaseExceptionRef, PyBaseObject, PyDict, PyDictRef, PyInt, PyList, PyModule, PyStr, - PyStrInterned, PyStrRef, PyTypeRef, PyUtf8Str, PyUtf8StrInterned, PyWeak, + self, PyBaseExceptionRef, PyBaseObject, PyDict, PyDictRef, PyFrozenSet, PyInt, PyList, + PyModule, PySet, PyStr, PyStrInterned, PyStrRef, PyTypeRef, PyUtf8Str, PyUtf8StrInterned, + PyWeak, code::PyCode, dict::{PyDictItems, PyDictKeys, PyDictValues}, pystr::AsPyStr, @@ -3288,6 +3289,12 @@ impl VirtualMachine { i += 1; } return Ok(results); + } else if cls.is(self.ctx.types.set_type) { + let keys = value.downcast_ref::().unwrap().elements(); + return map_known_len(keys.into_iter(), func); + } else if cls.is(self.ctx.types.frozenset_type) { + let keys = value.downcast_ref::().unwrap().elements(); + return map_known_len(keys.into_iter(), func); } else if cls.is(self.ctx.types.dict_type) { let keys = value.downcast_ref::().unwrap().keys_vec(); return map_known_len(keys.into_iter(), func); diff --git a/src/shell.rs b/src/shell.rs index d99d38765..055a009ea 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -113,6 +113,14 @@ fn shell_exec( } } +fn flush_stdio(vm: &VirtualMachine) { + for name in ["stdout", "stderr"] { + if let Ok(stream) = vm.sys_module.get_attr(name, vm) { + let _ = vm.call_method(&stream, "flush", ()); + } + } +} + /// Enter a repl loop pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { let mut repl = Readline::new(helper::ShellHelper::new(vm, scope.globals.clone())); @@ -128,9 +136,7 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { None => ".repl_history.txt".into(), }; - if repl.load_history(&repl_history_path).is_err() { - println!("No previous history."); - } + let _ = repl.load_history(&repl_history_path); // We might either be waiting to know if a block is complete, or waiting to know if a multiline // statement is complete. In the former case, we need to ensure that we read one extra new line @@ -245,6 +251,7 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { } vm.print_exception(exc); } + flush_stdio(vm); } repl.save_history(&repl_history_path).unwrap();