From 51d0a4b4b4968c0f6cd91a285a2f00a932040aef Mon Sep 17 00:00:00 2001 From: reginaldalfret Date: Tue, 20 Oct 2026 16:23:35 +0530 Subject: [PATCH] fix: avoid printing globals dictionary to stderr on console script exit When executing a console script with env.script, shiv previously passed partial(runpy.run_path, ...) directly to run(), which calls sys.exit(module()). Because runpy.run_path returns the executed module's globals dictionary, passing a dict to sys.exit() caused Python to print the string representation of the dictionary to stderr and terminate with exit status 1. Introduce _run_script helper that invokes runpy.run_path without returning its globals dict so that sys.exit receives None, cleanly exiting with code 0. Add regression unit test in test_bootstrap.py. Resolves #240 Signed-off-by: reginaldalfret --- src/shiv/bootstrap/__init__.py | 7 ++++++- test/test_bootstrap.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/shiv/bootstrap/__init__.py b/src/shiv/bootstrap/__init__.py index b6ad546..1abac52 100644 --- a/src/shiv/bootstrap/__init__.py +++ b/src/shiv/bootstrap/__init__.py @@ -38,6 +38,11 @@ def run(module): # pragma: no cover sys.exit(module()) +def _run_script(script): + """Execute a script via runpy without returning its module dictionary to sys.exit.""" + runpy.run_path(script, run_name="__main__") + + @contextmanager def current_zipfile(): """A function to vend the current zipfile, if any""" @@ -262,7 +267,7 @@ def bootstrap(): # pragma: no cover run(import_string(env.entry_point)) elif env.script is not None: - run(partial(runpy.run_path, str(site_packages / "bin" / env.script), run_name="__main__")) + run(partial(_run_script, str(site_packages / "bin" / env.script))) # all other options exhausted, drop into interactive mode execute_interpreter() diff --git a/test/test_bootstrap.py b/test/test_bootstrap.py index 8bc2994..e0aed63 100644 --- a/test/test_bootstrap.py +++ b/test/test_bootstrap.py @@ -11,7 +11,10 @@ import pytest +from functools import partial + from shiv.bootstrap import ( + _run_script, cache_path, current_zipfile, ensure_no_modify, @@ -20,6 +23,7 @@ get_first_sitedir_index, import_string, prepend_pythonpath, + run, ) from shiv.bootstrap.environment import Environment from shiv.bootstrap.filelock import FileLock @@ -27,6 +31,19 @@ class TestBootstrap: + def test_run_script(self, tmp_path): + script_file = tmp_path / "myscript.py" + output_file = tmp_path / "output.txt" + script_file.write_text(f"from pathlib import Path\nPath({repr(str(output_file))}).write_text('executed')\n") + + ret = _run_script(str(script_file)) + assert ret is None + assert output_file.read_text() == "executed" + + with mock.patch("sys.exit") as mock_exit: + run(partial(_run_script, str(script_file))) + mock_exit.assert_called_once_with(None) + def test_import_string(self): assert import_string("site.addsitedir") == addsitedir assert import_string("site:addsitedir") == addsitedir