Skip to content

gh-57684: Add the -p command line option #92361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Doc/c-api/init_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
34 changes: 32 additions & 2 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 4 additions & 6 deletions Lib/distutils/tests/test_bdist_rpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
38 changes: 32 additions & 6 deletions Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand 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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.')
Expand Down
6 changes: 2 additions & 4 deletions Lib/test/test_cmd_line_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions Lib/test/test_pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions Lib/test/test_runpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 7 additions & 1 deletion Misc/python.man
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ python \- an interpreted, interactive, object-oriented programming language
.B \-P
]
[
.B \-p
]
[
.B \-s
]
[
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<version> and
Expand Down
2 changes: 1 addition & 1 deletion Python/getopt.c
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
25 changes: 21 additions & 4 deletions Python/initconfig.c
Original file line number Diff line number Diff line change
Expand Up @@ -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\
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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':
Expand Down
3 changes: 1 addition & 2 deletions Tools/freeze/test/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down