diff --git a/Doc/library/concurrent.interpreters.rst b/Doc/library/concurrent.interpreters.rst index 41ea6af3b226e9..09a9d975468090 100644 --- a/Doc/library/concurrent.interpreters.rst +++ b/Doc/library/concurrent.interpreters.rst @@ -243,6 +243,16 @@ Interpreter objects Generally, :class:`Interpreter` shouldn't be called directly. Instead, use :func:`create` or one of the other module functions. + In addition to interpreters created by this module (via + :func:`create`), an :class:`!Interpreter` object may wrap + interpreters not created by the module. Some methods fail for such + interpreters, as noted below for each such method. + + An interpreter may be marked as currently executing code in its + :mod:`!__main__` module. See :meth:`~Interpreter.is_running`. + Some methods of a running :class:`!interpreter` fail, as noted + below for each such method. + .. attribute:: id (read-only) @@ -271,21 +281,33 @@ Interpreter objects Some objects are actually shared and some are copied efficiently, but most are copied via :mod:`pickle`. See :ref:`interp-object-sharing`. + This method fails for unsupported modules. + It also fails for running modules. + .. method:: exec(code, /, dedent=True) Run the given source code in the interpreter (in the current thread). + This method fails for unsupported modules. + It also fails for running modules. + .. method:: call(callable, /, *args, **kwargs) Return the result of calling running the given function in the interpreter (in the current thread). + This method fails for unsupported modules. + It also fails for running modules. + .. _interp-call-in-thread: .. method:: call_in_thread(callable, /, *args, **kwargs) Run the given function in the interpreter (in a new thread). + This method fails for unsupported modules. + It also fails for running modules. + Exceptions ^^^^^^^^^^ diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py index 9a5ee03e4722c0..8c223149a10461 100644 --- a/Lib/test/test_interpreters/test_api.py +++ b/Lib/test/test_interpreters/test_api.py @@ -717,18 +717,18 @@ def test_created_with_capi(self): with self.subTest('running __main__ (from self)'): with self.interpreter_from_capi() as interpid: with self.assertRaisesRegex(ExecutionFailed, - 'InterpreterError.*unrecognized'): + 'InterpreterError.*not supported'): self.run_from_capi(interpid, script, main=True) with self.subTest('running, but not __main__ (from self)'): with self.assertRaisesRegex(ExecutionFailed, - 'InterpreterError.*unrecognized'): + 'InterpreterError.*not supported'): self.run_temp_from_capi(script) with self.subTest('running __main__ (from other)'): with self.interpreter_obj_from_capi() as (interp, interpid): with self.running_from_capi(interpid, main=True): - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): interp.close() # Make sure it wssn't closed. self.assertTrue( @@ -741,7 +741,7 @@ def test_created_with_capi(self): with self.subTest('running, but not __main__ (from other)'): with self.interpreter_obj_from_capi() as (interp, interpid): with self.running_from_capi(interpid, main=False): - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): interp.close() # Make sure it wssn't closed. self.assertTrue( @@ -749,7 +749,7 @@ def test_created_with_capi(self): with self.subTest('not running (from other)'): with self.interpreter_obj_from_capi() as (interp, interpid): - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): interp.close() self.assertTrue( self.interp_exists(interpid)) @@ -819,6 +819,43 @@ def task(): class TestInterpreterPrepareMain(TestBase): + def test_main(self): + interp0 = interpreters.get_main() + if interp0 is interpreters.get_current(): + import __main__ as mainmod + mainns = vars(mainmod) + self.assertNotIn('prepare_main_spam', mainns) + + with self.subTest('in current'): + try: + with self.assertRaisesRegex(InterpreterError, 'running'): + interp0.prepare_main(prepare_main_spam='spam!!!') + finally: + mainns.pop('prepare_main_spam', None) + self.assertNotIn('prepare_main_spam', mainns) + + with self.subTest('in other'): + interp = interpreters.create() + try: + with self.assertRaisesRegex(ExecutionFailed, 'running'): + interp.exec(dedent(""" + from concurrent import interpreters + interp0 = interpreters.get_main() + interp0.prepare_main(prepare_main_spam='spam!!!') + """)) + self.assertNotIn('prepare_main_spam', mainns) + finally: + mainns.pop('prepare_main_spam', None) + self.assertNotIn('prepare_main_spam', mainns) + else: + with self.subTest('in other'): + interp = interpreters.create() + interp.exec(dedent(""" + from concurrent import interpreters + interp0 = interpreters.get_main() + interp0.prepare_main(prepare_main_spam='spam!!!') + """)) + def test_empty(self): interp = interpreters.create() with self.assertRaises(ValueError): @@ -883,7 +920,7 @@ def test_running(self): @requires_test_modules def test_created_with_capi(self): with self.interpreter_obj_from_capi() as (interp, interpid): - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): interp.prepare_main({'spam': True}) with self.assertRaisesRegex(ExecutionFailed, 'NameError'): self.run_from_capi(interpid, 'assert spam is True') @@ -907,6 +944,11 @@ def test_failure(self): with self.assertRaises(ExecutionFailed): interp.exec('raise Exception') + def test_main(self): + interp = interpreters.get_main() + with self.assertRaisesRegex(InterpreterError, 'running'): + interp.exec('print("spam")') + @force_not_colorized def test_display_preserved_exception(self): tempdir = self.temp_dir() @@ -1056,7 +1098,7 @@ def task(): def test_created_with_capi(self): with self.interpreter_obj_from_capi() as (interp, _): - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): interp.exec('raise Exception("it worked!")') def test_list_comprehension(self): @@ -1256,6 +1298,11 @@ def assert_exceptions_equal(self, exc1, exc2): self.assertIs(type(exc1), type(exc2)) self.assertEqual(exc1.args, exc2.args) + def test_main(self): + interp = interpreters.get_main() + with self.assertRaisesRegex(InterpreterError, 'running'): + interp.call(call_func_noop) + def test_stateless_funcs(self): interp = interpreters.create() @@ -2224,7 +2271,7 @@ def test_destroy(self): with self.subTest('from C-API'): interpid = _testinternalcapi.create_interpreter() - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): _interpreters.destroy(interpid, restrict=True) self.assertTrue( self.interp_exists(interpid)) @@ -2268,7 +2315,7 @@ def test_get_config(self): with self.subTest('from C-API'): orig = _interpreters.new_config('isolated') with self.interpreter_from_capi(orig) as interpid: - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): _interpreters.get_config(interpid, restrict=True) config = _interpreters.get_config(interpid) self.assert_ns_equal(config, orig) @@ -2329,7 +2376,7 @@ def test_contextvars_missing(self): def test_is_running(self): def check(interpid, expected): - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): _interpreters.is_running(interpid, restrict=True) running = _interpreters.is_running(interpid) self.assertIs(running, expected) @@ -2347,7 +2394,8 @@ def check(interpid, expected): with self.subTest('main'): interpid, *_ = _interpreters.get_main() - check(interpid, True) + running = _interpreters.is_running(interpid) + self.assertTrue(running) with self.subTest('from C-API (running __main__)'): with self.interpreter_from_capi() as interpid: @@ -2394,7 +2442,7 @@ def test_exec(self): with self.subTest('from C-API'): with self.interpreter_from_capi() as interpid: - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): _interpreters.exec(interpid, 'raise Exception("it worked!")', restrict=True) exc = _interpreters.exec(interpid, 'raise Exception("it worked!")') @@ -2473,7 +2521,7 @@ def test_set___main___attrs(self): with self.subTest('from C-API'): with self.interpreter_from_capi() as interpid: - with self.assertRaisesRegex(InterpreterError, 'unrecognized'): + with self.assertRaisesRegex(InterpreterError, 'not supported'): _interpreters.set___main___attrs(interpid, {'spam': True}, restrict=True) _interpreters.set___main___attrs(interpid, {'spam': True}) diff --git a/Misc/NEWS.d/next/Library/2025-10-02-12-52-51.gh-issue-139458.HGegAs.rst b/Misc/NEWS.d/next/Library/2025-10-02-12-52-51.gh-issue-139458.HGegAs.rst new file mode 100644 index 00000000000000..23c7627138aedd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-02-12-52-51.gh-issue-139458.HGegAs.rst @@ -0,0 +1,2 @@ +Errors and documentation are clearer now relative to interpreters not +created by the :mod:`~concurrent.interpreters` module. diff --git a/Modules/_interpretersmodule.c b/Modules/_interpretersmodule.c index 2aee8b07891c91..ea87dca404d17b 100644 --- a/Modules/_interpretersmodule.c +++ b/Modules/_interpretersmodule.c @@ -755,6 +755,11 @@ resolve_interp(PyObject *idobj, int restricted, int reqready, const char *op) } } + if (_Py_IsMainInterpreter(interp)) { + assert(_PyInterpreterState_IsReady(interp)); + return interp; + } + if (reqready && !_PyInterpreterState_IsReady(interp)) { if (idobj == NULL) { PyErr_Format(PyExc_InterpreterError, @@ -770,11 +775,11 @@ resolve_interp(PyObject *idobj, int restricted, int reqready, const char *op) if (restricted && get_whence(interp) != _PyInterpreterState_WHENCE_STDLIB) { if (idobj == NULL) { PyErr_Format(PyExc_InterpreterError, - "cannot %s unrecognized current interpreter", op); + "cannot %s current interpreter (not supported)", op); } else { PyErr_Format(PyExc_InterpreterError, - "cannot %s unrecognized interpreter %R", op, idobj); + "cannot %s interpreter %R (not supported)", op, idobj); } return NULL; }