diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index 897705cec86fda..9413266b19e12e 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -560,6 +560,10 @@ PyConfig Set to ``1`` by the :option:`-P` command line option and the :envvar:`PYTHONSAFEPATH` environment variable. + Set to 0 by the :option:`-p` command line option, which takes precedence + over the :option:`-P` option and the :envvar:`PYTHONSAFEPATH` environment + variable (and the :option:`-P` option implied by the :option:`-I` option). + Default: ``0`` in Python config, ``1`` in isolated config. .. versionadded:: 3.11 diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 5f3b9b5776cb82..d3610dbf805b15 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -1142,9 +1142,9 @@ always available. the environment variable :envvar:`PYTHONPATH`, plus an installation-dependent default. - By default, as initialized upon program startup, a potentially unsafe path - is prepended to :data:`sys.path` (*before* the entries inserted as a result - of :envvar:`PYTHONPATH`): + By default, as initialized upon program startup, the following path is + prepended to :data:`sys.path` (*before* the entries inserted as a result of + :envvar:`PYTHONPATH`): * ``python -m module`` command line: prepend the current working directory. @@ -1154,7 +1154,9 @@ always available. string, which means the current working directory. To not prepend this potentially unsafe path, use the :option:`-P` command - line option or the :envvar:`PYTHONSAFEPATH` environment variable? + line option or the :envvar:`PYTHONSAFEPATH` environment variable. The + :option:`-p` option causes the :option:`-P` option and the + :envvar:`PYTHONSAFEPATH` environment variable to be ignored. A program is free to modify this list for its own purposes. Only strings and bytes should be added to :data:`sys.path`; all other data types are diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 07c05a94b99f98..c202f0b2396768 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -284,6 +284,11 @@ Miscellaneous options variables are ignored, too. Further restrictions may be imposed to prevent the user from injecting malicious code. + The :option:`-p` option can be used with the :option:`-I` option to ignore + the :option:`-P` implied by the :option:`-I` option: prepend a potentially + unsafe path to :data:`sys.path` such as the current directory, the script's + directory or an empty string. + .. versionadded:: 3.4 @@ -319,12 +324,30 @@ Miscellaneous options * ``python -c code`` and ``python`` (REPL) command lines: Don't prepend an empty string, which means the current working directory. - See also the :envvar:`PYTHONSAFEPATH` environment variable, and :option:`-E` + See also the :envvar:`PYTHONSAFEPATH` environment variable, and :option:`-p` and :option:`-I` (isolated) options. .. versionadded:: 3.11 +.. cmdoption:: -p + + Prepend a potentially unsafe path to :data:`sys.path` such as the current + directory, the script's directory or an empty string; opposite of the + :option:`-P` option. + + This is the default Python behavior. This option can be used to ignore the + :option:`-P` command line option and the :envvar:`PYTHONSAFEPATH` + environment variable. + + The :option:`-p` option takes precedence over the :option:`-P` option and + the :envvar:`PYTHONSAFEPATH` environment variable. It also takes precedence + over the :option:`-P` option implied by the :option:`-I` option (isolated + mode). + + .. versionadded:: 3.12 + + .. cmdoption:: -q Don't display the copyright and version messages even in interactive mode. @@ -606,11 +629,18 @@ conflict. :ref:`using-on-interface-options`. The search path can be manipulated from within a Python program as the variable :data:`sys.path`. + See also the :option:`-p` and :option:`-P` options and the + :envvar:`PYTHONSAFEPATH` environment variable. + .. envvar:: PYTHONSAFEPATH If this is set to a non-empty string, don't prepend a potentially unsafe - path to :data:`sys.path`: see the :option:`-P` option for details. + path to :data:`sys.path` such as the current directory, the script's + directory or an empty string; same effect as the :option:`-P` option. + + See also the :option:`-P` and :option:`-p` option and the + :envvar:`PYTHONPATH` environment variable. .. versionadded:: 3.11 diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 033de1780b3d18..6e32723e8a3b9d 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -79,6 +79,12 @@ New Features Other Language Changes ====================== +* Add the :option:`-p` command line option to prepend the potentially unsafe + path to :data:`sys.path`. It causes the :option:`-P` option and the + :envvar:`PYTHONSAFEPATH` environment variable to be ignored. It also causes + the :option:`-P` option implied by the :option:`-I` option (isolated mode) to + be ignored. + (Contributed by Victor Stinner in :gh:`57684`.) New Modules diff --git a/Lib/distutils/tests/test_bdist_rpm.py b/Lib/distutils/tests/test_bdist_rpm.py index 7eefa7b9cad84f..658482226123d9 100644 --- a/Lib/distutils/tests/test_bdist_rpm.py +++ b/Lib/distutils/tests/test_bdist_rpm.py @@ -49,10 +49,9 @@ def tearDown(self): 'the rpm command is not found') @unittest.skipIf(find_executable('rpmbuild') is None, 'the rpmbuild command is not found') - # import foo fails with safe path - @unittest.skipIf(sys.flags.safe_path, - 'PYTHONSAFEPATH changes default sys.path') def test_quiet(self): + os.environ.pop('PYTHONSAFEPATH', '') + # let's create a package tmp_dir = self.mkdtemp() os.environ['HOME'] = tmp_dir # to confine dir '.rpmdb' creation @@ -96,10 +95,9 @@ def test_quiet(self): 'the rpm command is not found') @unittest.skipIf(find_executable('rpmbuild') is None, 'the rpmbuild command is not found') - # import foo fails with safe path - @unittest.skipIf(sys.flags.safe_path, - 'PYTHONSAFEPATH changes default sys.path') def test_no_optimize_flag(self): + os.environ.pop('PYTHONSAFEPATH', '') + # let's create a package that breaks bdist_rpm tmp_dir = self.mkdtemp() os.environ['HOME'] = tmp_dir # to confine dir '.rpmdb' creation diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index ed733d2f616665..40d382be4732f5 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -360,8 +360,6 @@ def test_large_PYTHONPATH(self): self.assertIn(path1.encode('ascii'), out) self.assertIn(path2.encode('ascii'), out) - @unittest.skipIf(sys.flags.safe_path, - 'PYTHONSAFEPATH changes default sys.path') def test_empty_PYTHONPATH_issue16309(self): # On Posix, it is documented that setting PATH to the # empty string is equivalent to not setting PATH at all, @@ -373,8 +371,8 @@ def test_empty_PYTHONPATH_issue16309(self): path = ":".join(sys.path) path = path.encode("ascii", "backslashreplace") sys.stdout.buffer.write(path)""" - rc1, out1, err1 = assert_python_ok('-c', code, PYTHONPATH="") - rc2, out2, err2 = assert_python_ok('-c', code, __isolated=False) + rc1, out1, err1 = assert_python_ok('-p', '-c', code, PYTHONPATH="") + rc2, out2, err2 = assert_python_ok('-p', '-c', code, __isolated=False) # regarding to Posix specification, outputs should be equal # for empty and unset PYTHONPATH self.assertEqual(out1, out2) @@ -593,10 +591,9 @@ def test_isolatedmode(self): with open(main, "w", encoding="utf-8") as f: f.write("import uuid\n") f.write("print('ok')\n") - # Use -E to ignore PYTHONSAFEPATH env var self.assertRaises(subprocess.CalledProcessError, subprocess.check_output, - [sys.executable, '-E', main], cwd=tmpdir, + [sys.executable, '-p', main], cwd=tmpdir, stderr=subprocess.DEVNULL) out = subprocess.check_output([sys.executable, "-I", main], cwd=tmpdir) @@ -854,6 +851,35 @@ def test_parsing_error(self): self.assertTrue(proc.stderr.startswith(err_msg), proc.stderr) self.assertNotEqual(proc.returncode, 0) + def test_safe_path(self): + code = 'import sys; print(sys.flags.safe_path)' + for env in (False, True): + environ = dict(os.environ) + if env: + environ['PYTHONSAFEPATH'] = '1' + else: + environ.pop('PYTHONSAFEPATH', '') + for options, safe_path in ( + # PYTHONSAFEPATH env var + ([], env), + (["-E"], False), + (["-E", "-P"], True), + # isolated mode (-I) + (["-I"], True), + (["-I", "-P"], True), + (["-I", "-p"], False), + (["-I", "-p", "-P"], False), + # -p and -P options + (["-P"], True), + (["-p"], False), + (["-p", "-P"], False), + (["-P", "-p"], False), + ): + with self.subTest(env=env, options=options, safe_path=safe_path): + cmd = [sys.executable, *options, "-c", code] + out = subprocess.check_output(cmd, text=True, env=environ) + self.assertEqual(out.strip(), repr(safe_path)) + @unittest.skipIf(interpreter_requires_environment(), 'Cannot run -I tests when PYTHON env vars are required.') diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py index d783af65839ad9..1102d086b3d6c1 100644 --- a/Lib/test/test_cmd_line_script.py +++ b/Lib/test/test_cmd_line_script.py @@ -116,9 +116,7 @@ def _check_output(self, script_name, exit_code, data, self.assertIn(printed_file.encode('utf-8'), data) self.assertIn(printed_package.encode('utf-8'), data) self.assertIn(printed_argv0.encode('utf-8'), data) - # PYTHONSAFEPATH=1 changes the default sys.path[0] - if not sys.flags.safe_path: - self.assertIn(printed_path0.encode('utf-8'), data) + self.assertIn(printed_path0.encode('utf-8'), data) self.assertIn(printed_cwd.encode('utf-8'), data) def _check_script(self, script_exec_args, expected_file, @@ -127,7 +125,7 @@ def _check_script(self, script_exec_args, expected_file, *cmd_line_switches, cwd=None, **env_vars): if isinstance(script_exec_args, str): script_exec_args = [script_exec_args] - run_args = [*support.optim_args_from_interpreter_flags(), + run_args = [*support.optim_args_from_interpreter_flags(), '-p', *cmd_line_switches, *script_exec_args, *example_args] rc, out, err = assert_python_ok( *run_args, __isolated=False, __cwd=cwd, **env_vars diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index f1ca6da147376c..4f755637bf5e59 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -1143,10 +1143,11 @@ def test_init_dont_parse_argv(self): pre_config = { 'parse_argv': 0, } + argv = ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'] config = { 'parse_argv': 0, - 'argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'], - 'orig_argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'], + 'argv': argv, + 'orig_argv': argv, 'program_name': './argv0', } self.check_all_configs("test_init_dont_parse_argv", config, pre_config, diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 0141b739c25440..6a3c3f9a490d2f 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1369,11 +1369,9 @@ class PdbTestCase(unittest.TestCase): def tearDown(self): os_helper.unlink(os_helper.TESTFN) - @unittest.skipIf(sys.flags.safe_path, - 'PYTHONSAFEPATH changes default sys.path') def _run_pdb(self, pdb_args, commands): self.addCleanup(os_helper.rmtree, '__pycache__') - cmd = [sys.executable, '-m', 'pdb'] + pdb_args + cmd = [sys.executable, '-p', '-m', 'pdb'] + pdb_args with subprocess.Popen( cmd, stdout=subprocess.PIPE, diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py index 6aaa288c14e1d7..91f5153030476d 100644 --- a/Lib/test/test_runpy.py +++ b/Lib/test/test_runpy.py @@ -782,8 +782,7 @@ def run(self, *args, **kwargs): @requires_subprocess() def assertSigInt(self, cmd, *args, **kwargs): - # Use -E to ignore PYTHONSAFEPATH - cmd = [sys.executable, '-E', *cmd] + cmd = [sys.executable, '-p', *cmd] proc = subprocess.run(cmd, *args, **kwargs, text=True, stderr=subprocess.PIPE) self.assertTrue(proc.stderr.endswith("\nKeyboardInterrupt\n"), proc.stderr) self.assertEqual(proc.returncode, self.EXPECTED_CODE) diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-05-06-03-53-47.gh-issue-57684.ITjOhD.rst b/Misc/NEWS.d/next/Core and Builtins/2022-05-06-03-53-47.gh-issue-57684.ITjOhD.rst new file mode 100644 index 00000000000000..3c9d4fd6ff0961 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-05-06-03-53-47.gh-issue-57684.ITjOhD.rst @@ -0,0 +1,4 @@ +Add the :option:`-p` option to ignore the :option:`-P` option and the +:envvar:`PYTHONSAFEPATH` environment variable. It also causes the :option:`-P` +option implied by the :option:`-I` option (isolated mode) to be ignored. Patch +by Victor Stinner. diff --git a/Misc/python.man b/Misc/python.man index 69dab58a9aac26..dd79ea415a505d 100644 --- a/Misc/python.man +++ b/Misc/python.man @@ -46,6 +46,9 @@ python \- an interpreted, interactive, object-oriented programming language .B \-P ] [ +.B \-p +] +[ .B \-s ] [ @@ -185,6 +188,9 @@ Don't automatically prepend a potentially unsafe path to \fBsys.path\fP such as the current directory, the script's directory or an empty string. See also the \fBPYTHONSAFEPATH\fP environment variable. .TP +.B \-p +Ignore the \fB\-P\fP option and the \fBPYTHONSAFEPATH\fP environment variable. +.TP .B \-q Do not print the version and copyright messages. These messages are also suppressed in non-interactive mode. @@ -409,7 +415,7 @@ interpreter. .IP PYTHONSAFEPATH If this is set to a non-empty string, don't automatically prepend a potentially unsafe path to \fBsys.path\fP such as the current directory, the script's -directory or an empty string. See also the \fB\-P\fP option. +directory or an empty string. See also \fB\-P\fP and\fB\-p\fP options. .IP PYTHONHOME Change the location of the standard Python libraries. By default, the libraries are searched in ${prefix}/lib/python and diff --git a/Python/getopt.c b/Python/getopt.c index fcea60759d12cd..6e251c3bc9e648 100644 --- a/Python/getopt.c +++ b/Python/getopt.c @@ -41,7 +41,7 @@ static const wchar_t *opt_ptr = L""; /* Python command line short and long options */ -#define SHORT_OPTS L"bBc:dEhiIJm:OPqRsStuvVW:xX:?" +#define SHORT_OPTS L"bBc:dEhiIJm:OpPqRsStuvVW:xX:?" static const _PyOS_LongOption longopts[] = { {L"check-hash-based-pycs", 1, 0}, diff --git a/Python/initconfig.c b/Python/initconfig.c index a623973f953734..4ae0b478dc09a8 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -49,6 +49,8 @@ static const char usage_2[] = "\ .pyc extension; also PYTHONOPTIMIZE=x\n\ -OO : do -O changes and also discard docstrings; add .opt-2 before\n\ .pyc extension\n\ +-p : prepend a potentially unsafe path to sys.path\n\ + (override PYTHONSAFEPATH)\n\ -P : don't prepend a potentially unsafe path to sys.path\n\ -q : don't print version and copyright messages on interactive startup\n\ -s : don't add user site directory to sys.path; also PYTHONNOUSERSITE\n\ @@ -743,7 +745,7 @@ _PyConfig_InitCompatConfig(PyConfig *config) #else config->use_frozen_modules = 1; #endif - config->safe_path = 0; + config->safe_path = -1; config->_is_python_build = 0; config->code_debug_ranges = 1; } @@ -799,7 +801,6 @@ PyConfig_InitIsolatedConfig(PyConfig *config) config->use_hash_seed = 0; config->faulthandler = 0; config->tracemalloc = 0; - config->safe_path = 1; config->pathconfig_warnings = 0; #ifdef MS_WINDOWS config->legacy_windows_stdio = 0; @@ -1644,7 +1645,7 @@ config_read_env_vars(PyConfig *config) } } - if (config_get_env(config, "PYTHONSAFEPATH")) { + if (config_get_env(config, "PYTHONSAFEPATH") && config->safe_path < 0) { config->safe_path = 1; } @@ -2158,6 +2159,15 @@ config_read(PyConfig *config, int compute_path_config) config->parse_argv = 2; } + if (config->safe_path < 0) { + if (config->_config_init == _PyConfig_INIT_ISOLATED) { + config->safe_path = 1; + } + else { + config->safe_path = 0; + } + } + return _PyStatus_OK(); } @@ -2343,8 +2353,15 @@ config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, config->optimization_level++; break; + case 'p': + config->safe_path = 0; + break; + case 'P': - config->safe_path = 1; + // -p option takes precedence over -P option + if (config->safe_path < 0) { + config->safe_path = 1; + } break; case 'B': diff --git a/Tools/freeze/test/freeze.py b/Tools/freeze/test/freeze.py index ddbfd7fc9c2f41..b1cbc1f7bd07e3 100644 --- a/Tools/freeze/test/freeze.py +++ b/Tools/freeze/test/freeze.py @@ -172,8 +172,7 @@ def freeze(python, scriptfile, outdir): print(f'freezing {scriptfile}...') os.makedirs(outdir, exist_ok=True) - # Use -E to ignore PYTHONSAFEPATH - _run_quiet([python, '-E', FREEZE, '-o', outdir, scriptfile], outdir) + _run_quiet([python, '-p', FREEZE, '-o', outdir, scriptfile], outdir) _run_quiet([MAKE, '-C', os.path.dirname(scriptfile)]) name = os.path.basename(scriptfile).rpartition('.')[0]