Commit d5d9e02d authored by Nick Coghlan's avatar Nick Coghlan Committed by GitHub

bpo-33053: -m now adds *starting* directory to sys.path (GH-6231)

Historically, -m added the empty string as sys.path
zero, meaning it resolved imports against the current
working directory, the same way -c and the interactive
prompt do.

This changes the sys.path initialisation to add the
*starting* working directory as sys.path[0] instead,
such that changes to the working directory while the
program is running will have no effect on imports
when using the -m switch.
parent bc77eff8
...@@ -1332,8 +1332,8 @@ script execution tests. ...@@ -1332,8 +1332,8 @@ script execution tests.
.. function:: run_python_until_end(*args, **env_vars) .. function:: run_python_until_end(*args, **env_vars)
Set up the environment based on *env_vars* for running the interpreter Set up the environment based on *env_vars* for running the interpreter
in a subprocess. The values can include ``__isolated``, ``__cleavenv``, in a subprocess. The values can include ``__isolated``, ``__cleanenv``,
and ``TERM``. ``__cwd``, and ``TERM``.
.. function:: assert_python_ok(*args, **env_vars) .. function:: assert_python_ok(*args, **env_vars)
......
...@@ -421,6 +421,12 @@ Other Language Changes ...@@ -421,6 +421,12 @@ Other Language Changes
writable. writable.
(Contributed by Nathaniel J. Smith in :issue:`30579`.) (Contributed by Nathaniel J. Smith in :issue:`30579`.)
* When using the :option:`-m` switch, ``sys.path[0]`` is now eagerly expanded
to the full starting directory path, rather than being left as the empty
directory (which allows imports from the *current* working directory at the
time when an import occurs)
(Contributed by Nick Coghlan in :issue:`33053`.)
New Modules New Modules
=========== ===========
...@@ -1138,6 +1144,11 @@ Changes in Python behavior ...@@ -1138,6 +1144,11 @@ Changes in Python behavior
parentheses can be omitted only on calls. parentheses can be omitted only on calls.
(Contributed by Serhiy Storchaka in :issue:`32012` and :issue:`32023`.) (Contributed by Serhiy Storchaka in :issue:`32012` and :issue:`32023`.)
* When using the ``-m`` switch, the starting directory is now added to sys.path,
rather than the current working directory. Any programs that are found to be
relying on the previous behaviour will need to be updated to manipulate
:data:`sys.path` appropriately.
Changes in the Python API Changes in the Python API
------------------------- -------------------------
......
...@@ -87,6 +87,7 @@ class _PythonRunResult(collections.namedtuple("_PythonRunResult", ...@@ -87,6 +87,7 @@ class _PythonRunResult(collections.namedtuple("_PythonRunResult",
# Executing the interpreter in a subprocess # Executing the interpreter in a subprocess
def run_python_until_end(*args, **env_vars): def run_python_until_end(*args, **env_vars):
env_required = interpreter_requires_environment() env_required = interpreter_requires_environment()
cwd = env_vars.pop('__cwd', None)
if '__isolated' in env_vars: if '__isolated' in env_vars:
isolated = env_vars.pop('__isolated') isolated = env_vars.pop('__isolated')
else: else:
...@@ -125,7 +126,7 @@ def run_python_until_end(*args, **env_vars): ...@@ -125,7 +126,7 @@ def run_python_until_end(*args, **env_vars):
cmd_line.extend(args) cmd_line.extend(args)
proc = subprocess.Popen(cmd_line, stdin=subprocess.PIPE, proc = subprocess.Popen(cmd_line, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=env) env=env, cwd=cwd)
with proc: with proc:
try: try:
out, err = proc.communicate() out, err = proc.communicate()
......
...@@ -524,13 +524,13 @@ def run_test(modules, set_list, skip=None): ...@@ -524,13 +524,13 @@ def run_test(modules, set_list, skip=None):
test.id = lambda : None test.id = lambda : None
test.expect_set = list(gen(repeat(()), iter(sl))) test.expect_set = list(gen(repeat(()), iter(sl)))
with create_modules(modules): with create_modules(modules):
sys.path.append(os.getcwd())
with TracerRun(test, skip=skip) as tracer: with TracerRun(test, skip=skip) as tracer:
tracer.runcall(tfunc_import) tracer.runcall(tfunc_import)
@contextmanager @contextmanager
def create_modules(modules): def create_modules(modules):
with test.support.temp_cwd(): with test.support.temp_cwd():
sys.path.append(os.getcwd())
try: try:
for m in modules: for m in modules:
fname = m + '.py' fname = m + '.py'
...@@ -542,6 +542,7 @@ def create_modules(modules): ...@@ -542,6 +542,7 @@ def create_modules(modules):
finally: finally:
for m in modules: for m in modules:
test.support.forget(m) test.support.forget(m)
sys.path.pop()
def break_in_func(funcname, fname=__file__, temporary=False, cond=None): def break_in_func(funcname, fname=__file__, temporary=False, cond=None):
return 'break', (fname, None, temporary, cond, funcname) return 'break', (fname, None, temporary, cond, funcname)
......
This diff is collapsed.
...@@ -9,7 +9,7 @@ import os ...@@ -9,7 +9,7 @@ import os
import sys import sys
import importlib import importlib
import unittest import unittest
import tempfile
# NOTE: There are some additional tests relating to interaction with # NOTE: There are some additional tests relating to interaction with
# zipimport in the test_zipimport_support test module. # zipimport in the test_zipimport_support test module.
...@@ -688,10 +688,16 @@ class TestDocTestFinder(unittest.TestCase): ...@@ -688,10 +688,16 @@ class TestDocTestFinder(unittest.TestCase):
def test_empty_namespace_package(self): def test_empty_namespace_package(self):
pkg_name = 'doctest_empty_pkg' pkg_name = 'doctest_empty_pkg'
os.mkdir(pkg_name) with tempfile.TemporaryDirectory() as parent_dir:
mod = importlib.import_module(pkg_name) pkg_dir = os.path.join(parent_dir, pkg_name)
assert doctest.DocTestFinder().find(mod) == [] os.mkdir(pkg_dir)
os.rmdir(pkg_name) sys.path.append(parent_dir)
try:
mod = importlib.import_module(pkg_name)
finally:
support.forget(pkg_name)
sys.path.pop()
assert doctest.DocTestFinder().find(mod) == []
def test_DocTestParser(): r""" def test_DocTestParser(): r"""
......
...@@ -118,7 +118,7 @@ class ImportTests(unittest.TestCase): ...@@ -118,7 +118,7 @@ class ImportTests(unittest.TestCase):
f.write("__all__ = [b'invalid_type']") f.write("__all__ = [b'invalid_type']")
globals = {} globals = {}
with self.assertRaisesRegex( with self.assertRaisesRegex(
TypeError, f"{re.escape(name)}\.__all__ must be str" TypeError, f"{re.escape(name)}\\.__all__ must be str"
): ):
exec(f"from {name} import *", globals) exec(f"from {name} import *", globals)
self.assertNotIn(b"invalid_type", globals) self.assertNotIn(b"invalid_type", globals)
...@@ -127,7 +127,7 @@ class ImportTests(unittest.TestCase): ...@@ -127,7 +127,7 @@ class ImportTests(unittest.TestCase):
f.write("globals()[b'invalid_type'] = object()") f.write("globals()[b'invalid_type'] = object()")
globals = {} globals = {}
with self.assertRaisesRegex( with self.assertRaisesRegex(
TypeError, f"{re.escape(name)}\.__dict__ must be str" TypeError, f"{re.escape(name)}\\.__dict__ must be str"
): ):
exec(f"from {name} import *", globals) exec(f"from {name} import *", globals)
self.assertNotIn(b"invalid_type", globals) self.assertNotIn(b"invalid_type", globals)
...@@ -847,8 +847,11 @@ class PycacheTests(unittest.TestCase): ...@@ -847,8 +847,11 @@ class PycacheTests(unittest.TestCase):
unload(TESTFN) unload(TESTFN)
importlib.invalidate_caches() importlib.invalidate_caches()
m = __import__(TESTFN) m = __import__(TESTFN)
self.assertEqual(m.__file__, try:
os.path.join(os.curdir, os.path.relpath(pyc_file))) self.assertEqual(m.__file__,
os.path.join(os.curdir, os.path.relpath(pyc_file)))
finally:
os.remove(pyc_file)
def test___cached__(self): def test___cached__(self):
# Modules now also have an __cached__ that points to the pyc file. # Modules now also have an __cached__ that points to the pyc file.
......
When using the -m switch, sys.path[0] is now explicitly expanded as the
*starting* working directory, rather than being left as the empty path
(which allows imports from the current working directory at the time of the
import)
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
#include "Python.h" #include "Python.h"
#include "osdefs.h" #include "osdefs.h"
#include "internal/pystate.h" #include "internal/pystate.h"
#include <wchar.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
...@@ -255,11 +256,6 @@ Py_GetProgramName(void) ...@@ -255,11 +256,6 @@ Py_GetProgramName(void)
return _Py_path_config.program_name; return _Py_path_config.program_name;
} }
#define _HAVE_SCRIPT_ARGUMENT(argc, argv) \
(argc > 0 && argv0 != NULL && \
wcscmp(argv0, L"-c") != 0 && wcscmp(argv0, L"-m") != 0)
/* Compute argv[0] which will be prepended to sys.argv */ /* Compute argv[0] which will be prepended to sys.argv */
PyObject* PyObject*
_PyPathConfig_ComputeArgv0(int argc, wchar_t **argv) _PyPathConfig_ComputeArgv0(int argc, wchar_t **argv)
...@@ -267,6 +263,8 @@ _PyPathConfig_ComputeArgv0(int argc, wchar_t **argv) ...@@ -267,6 +263,8 @@ _PyPathConfig_ComputeArgv0(int argc, wchar_t **argv)
wchar_t *argv0; wchar_t *argv0;
wchar_t *p = NULL; wchar_t *p = NULL;
Py_ssize_t n = 0; Py_ssize_t n = 0;
int have_script_arg = 0;
int have_module_arg = 0;
#ifdef HAVE_READLINK #ifdef HAVE_READLINK
wchar_t link[MAXPATHLEN+1]; wchar_t link[MAXPATHLEN+1];
wchar_t argv0copy[2*MAXPATHLEN+1]; wchar_t argv0copy[2*MAXPATHLEN+1];
...@@ -278,11 +276,25 @@ _PyPathConfig_ComputeArgv0(int argc, wchar_t **argv) ...@@ -278,11 +276,25 @@ _PyPathConfig_ComputeArgv0(int argc, wchar_t **argv)
wchar_t fullpath[MAX_PATH]; wchar_t fullpath[MAX_PATH];
#endif #endif
argv0 = argv[0]; argv0 = argv[0];
if (argc > 0 && argv0 != NULL) {
have_module_arg = (wcscmp(argv0, L"-m") == 0);
have_script_arg = !have_module_arg && (wcscmp(argv0, L"-c") != 0);
}
if (have_module_arg) {
#if defined(HAVE_REALPATH) || defined(MS_WINDOWS)
_Py_wgetcwd(fullpath, Py_ARRAY_LENGTH(fullpath));
argv0 = fullpath;
n = wcslen(argv0);
#else
argv0 = L".";
n = 1;
#endif
}
#ifdef HAVE_READLINK #ifdef HAVE_READLINK
if (_HAVE_SCRIPT_ARGUMENT(argc, argv)) if (have_script_arg)
nr = _Py_wreadlink(argv0, link, MAXPATHLEN); nr = _Py_wreadlink(argv0, link, MAXPATHLEN);
if (nr > 0) { if (nr > 0) {
/* It's a symlink */ /* It's a symlink */
...@@ -310,7 +322,7 @@ _PyPathConfig_ComputeArgv0(int argc, wchar_t **argv) ...@@ -310,7 +322,7 @@ _PyPathConfig_ComputeArgv0(int argc, wchar_t **argv)
#if SEP == '\\' #if SEP == '\\'
/* Special case for Microsoft filename syntax */ /* Special case for Microsoft filename syntax */
if (_HAVE_SCRIPT_ARGUMENT(argc, argv)) { if (have_script_arg) {
wchar_t *q; wchar_t *q;
#if defined(MS_WINDOWS) #if defined(MS_WINDOWS)
/* Replace the first element in argv with the full path. */ /* Replace the first element in argv with the full path. */
...@@ -334,7 +346,7 @@ _PyPathConfig_ComputeArgv0(int argc, wchar_t **argv) ...@@ -334,7 +346,7 @@ _PyPathConfig_ComputeArgv0(int argc, wchar_t **argv)
} }
} }
#else /* All other filename syntaxes */ #else /* All other filename syntaxes */
if (_HAVE_SCRIPT_ARGUMENT(argc, argv)) { if (have_script_arg) {
#if defined(HAVE_REALPATH) #if defined(HAVE_REALPATH)
if (_Py_wrealpath(argv0, fullpath, Py_ARRAY_LENGTH(fullpath))) { if (_Py_wrealpath(argv0, fullpath, Py_ARRAY_LENGTH(fullpath))) {
argv0 = fullpath; argv0 = fullpath;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment