From 632567d240e47e5cb1b735e17c0733bf285ad272 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Mon, 13 Nov 2023 12:49:48 +0100 Subject: [PATCH 01/11] Support PYTHONSAFEPATH --- coverage/execfile.py | 5 ++++- tests/test_execfile.py | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/coverage/execfile.py b/coverage/execfile.py index aac4d30bb..02c5b9aa6 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -90,7 +90,10 @@ def prepare(self) -> None: This needs to happen before any importing, and without importing anything. """ path0: Optional[str] - if self.as_module: + if os.environ.get('PYTHONSAFEPATH', ''): + # See https://docs.python.org/3/using/cmdline.html#cmdoption-P + path0 = None + elif self.as_module: path0 = os.getcwd() elif os.path.isdir(self.arg0): # Running a directory means running the __main__.py file in that diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 908857942..9e8841b04 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -13,7 +13,7 @@ import py_compile import re import sys - +from unittest import mock from typing import Any, Iterator import pytest @@ -306,6 +306,14 @@ def test_pkg1_init(self) -> None: assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" assert err == "" + def test_pythonpath(self) -> None: + with mock.patch.dict(os.environ, {"PYTHONSAFEPATH": "1"}): + run_python_module([TRY_EXECFILE]) + out, err = self.stdouterr() + mod_globs = json.loads(out) + assert os.cwd() not in mod_globs["path"] + assert err == "" + def test_no_such_module(self) -> None: with pytest.raises(NoSource, match="No module named '?i_dont_exist'?"): run_python_module(["i_dont_exist"]) From 8086c7049b71c1a4a9d4338866392028625667d3 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Mon, 13 Nov 2023 12:51:24 +0100 Subject: [PATCH 02/11] Fix --- tests/test_execfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 9e8841b04..3fcc88589 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -311,7 +311,7 @@ def test_pythonpath(self) -> None: run_python_module([TRY_EXECFILE]) out, err = self.stdouterr() mod_globs = json.loads(out) - assert os.cwd() not in mod_globs["path"] + assert os.getcwd() not in mod_globs["path"] assert err == "" def test_no_such_module(self) -> None: From dadd42f30244da8bc7850ca63cbf909fd082aa64 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Mon, 13 Nov 2023 14:41:37 +0100 Subject: [PATCH 03/11] make test mostly work --- tests/test_execfile.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 3fcc88589..116f915ac 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -23,6 +23,7 @@ from coverage.files import python_reported_file from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin +from tests.helpers import change_dir TRY_EXECFILE = os.path.join(TESTS_DIR, "modules/process_test/try_execfile.py") @@ -306,12 +307,13 @@ def test_pkg1_init(self) -> None: assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" assert err == "" - def test_pythonpath(self) -> None: - with mock.patch.dict(os.environ, {"PYTHONSAFEPATH": "1"}): - run_python_module([TRY_EXECFILE]) + def test_pythonpath(self, tmp_path) -> None: + env = {"PYTHONSAFEPATH": "1"} + with mock.patch.dict(os.environ, env), change_dir(tmp_path): + run_python_module(["process_test.try_execfile"]) out, err = self.stdouterr() mod_globs = json.loads(out) - assert os.getcwd() not in mod_globs["path"] + assert tmp_path not in mod_globs["path"] assert err == "" def test_no_such_module(self) -> None: From c495b96a73ebc59c603abd383420bf09061ebf09 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Tue, 14 Nov 2023 11:01:03 +0100 Subject: [PATCH 04/11] changes from code review --- coverage/execfile.py | 2 +- tests/test_execfile.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coverage/execfile.py b/coverage/execfile.py index 02c5b9aa6..953db2ead 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -90,7 +90,7 @@ def prepare(self) -> None: This needs to happen before any importing, and without importing anything. """ path0: Optional[str] - if os.environ.get('PYTHONSAFEPATH', ''): + if env.PYVERSION >= (3, 11) and os.environ.get('PYTHONSAFEPATH', ''): # See https://docs.python.org/3/using/cmdline.html#cmdoption-P path0 = None elif self.as_module: diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 116f915ac..0d575fcce 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -13,7 +13,7 @@ import py_compile import re import sys -from unittest import mock +from pathlib import Path from typing import Any, Iterator import pytest @@ -307,9 +307,9 @@ def test_pkg1_init(self) -> None: assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" assert err == "" - def test_pythonpath(self, tmp_path) -> None: - env = {"PYTHONSAFEPATH": "1"} - with mock.patch.dict(os.environ, env), change_dir(tmp_path): + def test_pythonpath(self, tmp_path: Path) -> None: + self.set_environ("PYTHONSAFEPATH", "1") + with change_dir(tmp_path): run_python_module(["process_test.try_execfile"]) out, err = self.stdouterr() mod_globs = json.loads(out) From 48759efd9ab16bb264e61cefaaced464c4bdcbe4 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Mon, 13 Nov 2023 12:49:48 +0100 Subject: [PATCH 05/11] Support PYTHONSAFEPATH --- coverage/execfile.py | 5 ++++- tests/test_execfile.py | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/coverage/execfile.py b/coverage/execfile.py index aac4d30bb..02c5b9aa6 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -90,7 +90,10 @@ def prepare(self) -> None: This needs to happen before any importing, and without importing anything. """ path0: Optional[str] - if self.as_module: + if os.environ.get('PYTHONSAFEPATH', ''): + # See https://docs.python.org/3/using/cmdline.html#cmdoption-P + path0 = None + elif self.as_module: path0 = os.getcwd() elif os.path.isdir(self.arg0): # Running a directory means running the __main__.py file in that diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 908857942..9e8841b04 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -13,7 +13,7 @@ import py_compile import re import sys - +from unittest import mock from typing import Any, Iterator import pytest @@ -306,6 +306,14 @@ def test_pkg1_init(self) -> None: assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" assert err == "" + def test_pythonpath(self) -> None: + with mock.patch.dict(os.environ, {"PYTHONSAFEPATH": "1"}): + run_python_module([TRY_EXECFILE]) + out, err = self.stdouterr() + mod_globs = json.loads(out) + assert os.cwd() not in mod_globs["path"] + assert err == "" + def test_no_such_module(self) -> None: with pytest.raises(NoSource, match="No module named '?i_dont_exist'?"): run_python_module(["i_dont_exist"]) From eb9b7e0bf19de1e4c3299c560151925ee4f68043 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Mon, 13 Nov 2023 12:51:24 +0100 Subject: [PATCH 06/11] Fix --- tests/test_execfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 9e8841b04..3fcc88589 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -311,7 +311,7 @@ def test_pythonpath(self) -> None: run_python_module([TRY_EXECFILE]) out, err = self.stdouterr() mod_globs = json.loads(out) - assert os.cwd() not in mod_globs["path"] + assert os.getcwd() not in mod_globs["path"] assert err == "" def test_no_such_module(self) -> None: From f682a3fa5b9a24a45670b900ae6a6931f3445d89 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Mon, 13 Nov 2023 14:41:37 +0100 Subject: [PATCH 07/11] make test mostly work --- tests/test_execfile.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 3fcc88589..116f915ac 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -23,6 +23,7 @@ from coverage.files import python_reported_file from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin +from tests.helpers import change_dir TRY_EXECFILE = os.path.join(TESTS_DIR, "modules/process_test/try_execfile.py") @@ -306,12 +307,13 @@ def test_pkg1_init(self) -> None: assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" assert err == "" - def test_pythonpath(self) -> None: - with mock.patch.dict(os.environ, {"PYTHONSAFEPATH": "1"}): - run_python_module([TRY_EXECFILE]) + def test_pythonpath(self, tmp_path) -> None: + env = {"PYTHONSAFEPATH": "1"} + with mock.patch.dict(os.environ, env), change_dir(tmp_path): + run_python_module(["process_test.try_execfile"]) out, err = self.stdouterr() mod_globs = json.loads(out) - assert os.getcwd() not in mod_globs["path"] + assert tmp_path not in mod_globs["path"] assert err == "" def test_no_such_module(self) -> None: From 28a62e783a3bf29dbfc6b2a555c08483cdb9c986 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Tue, 14 Nov 2023 11:01:03 +0100 Subject: [PATCH 08/11] changes from code review --- coverage/execfile.py | 2 +- tests/test_execfile.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coverage/execfile.py b/coverage/execfile.py index 02c5b9aa6..953db2ead 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -90,7 +90,7 @@ def prepare(self) -> None: This needs to happen before any importing, and without importing anything. """ path0: Optional[str] - if os.environ.get('PYTHONSAFEPATH', ''): + if env.PYVERSION >= (3, 11) and os.environ.get('PYTHONSAFEPATH', ''): # See https://docs.python.org/3/using/cmdline.html#cmdoption-P path0 = None elif self.as_module: diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 116f915ac..0d575fcce 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -13,7 +13,7 @@ import py_compile import re import sys -from unittest import mock +from pathlib import Path from typing import Any, Iterator import pytest @@ -307,9 +307,9 @@ def test_pkg1_init(self) -> None: assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" assert err == "" - def test_pythonpath(self, tmp_path) -> None: - env = {"PYTHONSAFEPATH": "1"} - with mock.patch.dict(os.environ, env), change_dir(tmp_path): + def test_pythonpath(self, tmp_path: Path) -> None: + self.set_environ("PYTHONSAFEPATH", "1") + with change_dir(tmp_path): run_python_module(["process_test.try_execfile"]) out, err = self.stdouterr() mod_globs = json.loads(out) From 66d3d34bbd734762baf1a73b9352b4dcbae97f32 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 18 Nov 2023 12:40:48 -0500 Subject: [PATCH 09/11] finish #1700 --- CHANGES.rst | 5 +++++ coverage/execfile.py | 2 +- tests/test_execfile.py | 9 --------- tests/test_process.py | 19 +++++++++++++++++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 440024230..22ce6c1cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,12 +20,17 @@ development at the same time, such as 4.5.x and 5.0. Unreleased ---------- +- Fix: the PYTHONSAFEPATH environment variable new in Python 3.11 is properly + supported, closing `issue 1696`_. Thanks, `Philipp A. `_. + - Added new :ref:`debug options `: - ``pytest`` writes the pytest test name into the debug output. - ``dataop2`` writes the full data being added to CoverageData objects. +.. _issue 1696: https://github.com/nedbat/coveragepy/issues/1696 +.. _pull 1700: https://github.com/nedbat/coveragepy/pull/1700 .. scriv-start-here diff --git a/coverage/execfile.py b/coverage/execfile.py index 953db2ead..7a2a1b102 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -90,7 +90,7 @@ def prepare(self) -> None: This needs to happen before any importing, and without importing anything. """ path0: Optional[str] - if env.PYVERSION >= (3, 11) and os.environ.get('PYTHONSAFEPATH', ''): + if env.PYVERSION >= (3, 11) and os.getenv("PYTHONSAFEPATH"): # See https://docs.python.org/3/using/cmdline.html#cmdoption-P path0 = None elif self.as_module: diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 0d575fcce..c532f9124 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -307,15 +307,6 @@ def test_pkg1_init(self) -> None: assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" assert err == "" - def test_pythonpath(self, tmp_path: Path) -> None: - self.set_environ("PYTHONSAFEPATH", "1") - with change_dir(tmp_path): - run_python_module(["process_test.try_execfile"]) - out, err = self.stdouterr() - mod_globs = json.loads(out) - assert tmp_path not in mod_globs["path"] - assert err == "" - def test_no_such_module(self) -> None: with pytest.raises(NoSource, match="No module named '?i_dont_exist'?"): run_python_module(["i_dont_exist"]) diff --git a/tests/test_process.py b/tests/test_process.py index 8fc45d930..3f67ad4bf 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -786,6 +786,25 @@ def test_coverage_zip_is_like_python(self) -> None: actual = self.run_command(f"python {cov_main} run run_me.py") self.assert_tryexecfile_output(expected, actual) + def test_pythonsafepath(self) -> None: + with open(TRY_EXECFILE) as f: + self.make_file("run_me.py", f.read()) + self.set_environ("PYTHONSAFEPATH", "1") + expected = self.run_command("python run_me.py") + actual = self.run_command("coverage run run_me.py") + self.assert_tryexecfile_output(expected, actual) + + @pytest.mark.skipif(env.PYVERSION < (3, 11), reason="PYTHONSAFEPATH is new in 3.11") + def test_pythonsafepath_dashm(self) -> None: + with open(TRY_EXECFILE) as f: + self.make_file("with_main/__main__.py", f.read()) + + self.set_environ("PYTHONSAFEPATH", "1") + expected = self.run_command("python -m with_main") + actual = self.run_command("coverage run -m with_main") + assert re.search(f"No module named '?with_main'?", actual) + assert re.search(f"No module named '?with_main'?", expected) + def test_coverage_custom_script(self) -> None: # https://github.com/nedbat/coveragepy/issues/678 # If sys.path[0] isn't the Python default, then coverage.py won't From bdde76299296b85ae222ca3e5e1ea36a06358111 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sun, 19 Nov 2023 18:47:54 +0100 Subject: [PATCH 10/11] getenv --- coverage/execfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/execfile.py b/coverage/execfile.py index 953db2ead..7f029c3ee 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -90,7 +90,7 @@ def prepare(self) -> None: This needs to happen before any importing, and without importing anything. """ path0: Optional[str] - if env.PYVERSION >= (3, 11) and os.environ.get('PYTHONSAFEPATH', ''): + if env.PYVERSION >= (3, 11) and os.getenv('PYTHONSAFEPATH'): # See https://docs.python.org/3/using/cmdline.html#cmdoption-P path0 = None elif self.as_module: From a98651b3f3a6d33695329fa8503058fd89227458 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sun, 19 Nov 2023 18:50:38 +0100 Subject: [PATCH 11/11] Fix lint error --- tests/test_process.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_process.py b/tests/test_process.py index 3f67ad4bf..f0a9d0b3c 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -802,8 +802,8 @@ def test_pythonsafepath_dashm(self) -> None: self.set_environ("PYTHONSAFEPATH", "1") expected = self.run_command("python -m with_main") actual = self.run_command("coverage run -m with_main") - assert re.search(f"No module named '?with_main'?", actual) - assert re.search(f"No module named '?with_main'?", expected) + assert re.search("No module named '?with_main'?", actual) + assert re.search("No module named '?with_main'?", expected) def test_coverage_custom_script(self) -> None: # https://github.com/nedbat/coveragepy/issues/678