bpo-20443: _PyConfig_Read() gets the absolute path of run_filename (GH-14053)

Python now gets the absolute path of the script filename specified on
the command line (ex: "python3 script.py"): the __file__ attribute of
the __main__ module, sys.argv[0] and sys.path[0] become an absolute
path, rather than a relative path.

* Add _Py_isabs() and _Py_abspath() functions.
* _PyConfig_Read() now tries to get the absolute path of
  run_filename, but keeps the relative path if _Py_abspath() fails.
* Reimplement os._getfullpathname() using _Py_abspath().
* Use _Py_isabs() in getpath.c.
This commit is contained in:
Victor Stinner 2019-06-25 15:02:43 +02:00 committed by GitHub
parent 080b6b40fa
commit 3939c321c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 211 additions and 42 deletions

View file

@ -75,6 +75,14 @@ New Features
Other Language Changes Other Language Changes
====================== ======================
* Python now gets the absolute path of the script filename specified on
the command line (ex: ``python3 script.py``): the ``__file__`` attribute of
the ``__main__`` module, ``sys.argv[0]`` and ``sys.path[0]`` become an
absolute path, rather than a relative path. These paths now remain valid
after the current directory is changed by :func:`os.chdir`. As a side effect,
a traceback also displays the absolute path for ``__main__`` module frames in
this case.
(Contributed by Victor Stinner in :issue:`20443`.)
New Modules New Modules

View file

@ -154,6 +154,12 @@ PyAPI_FUNC(wchar_t*) _Py_wrealpath(
size_t resolved_path_len); size_t resolved_path_len);
#endif #endif
#ifndef MS_WINDOWS
PyAPI_FUNC(int) _Py_isabs(const wchar_t *path);
#endif
PyAPI_FUNC(int) _Py_abspath(const wchar_t *path, wchar_t **abspath_p);
PyAPI_FUNC(wchar_t*) _Py_wgetcwd( PyAPI_FUNC(wchar_t*) _Py_wgetcwd(
wchar_t *buf, wchar_t *buf,
/* Number of characters of 'buf' buffer /* Number of characters of 'buf' buffer

View file

@ -217,6 +217,18 @@ class CmdLineTest(unittest.TestCase):
with support.temp_dir() as script_dir: with support.temp_dir() as script_dir:
script_name = _make_test_script(script_dir, 'script') script_name = _make_test_script(script_dir, 'script')
self._check_script(script_name, script_name, script_name, self._check_script(script_name, script_name, script_name,
script_dir, None,
importlib.machinery.SourceFileLoader,
expected_cwd=script_dir)
def test_script_abspath(self):
# pass the script using the relative path, expect the absolute path
# in __file__ and sys.argv[0]
with support.temp_cwd() as script_dir:
self.assertTrue(os.path.isabs(script_dir), script_dir)
script_name = _make_test_script(script_dir, 'script')
self._check_script(os.path.basename(script_name), script_name, script_name,
script_dir, None, script_dir, None,
importlib.machinery.SourceFileLoader) importlib.machinery.SourceFileLoader)
@ -542,7 +554,7 @@ class CmdLineTest(unittest.TestCase):
# Issue #16218 # Issue #16218
source = 'print(ascii(__file__))\n' source = 'print(ascii(__file__))\n'
script_name = _make_test_script(os.curdir, name, source) script_name = _make_test_script(os.getcwd(), name, source)
self.addCleanup(support.unlink, script_name) self.addCleanup(support.unlink, script_name)
rc, stdout, stderr = assert_python_ok(script_name) rc, stdout, stderr = assert_python_ok(script_name)
self.assertEqual( self.assertEqual(

View file

@ -805,9 +805,10 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
preconfig = { preconfig = {
'allocator': PYMEM_ALLOCATOR_DEBUG, 'allocator': PYMEM_ALLOCATOR_DEBUG,
} }
script_abspath = os.path.abspath('script.py')
config = { config = {
'argv': ['script.py'], 'argv': [script_abspath],
'run_filename': 'script.py', 'run_filename': script_abspath,
'dev_mode': 1, 'dev_mode': 1,
'faulthandler': 1, 'faulthandler': 1,
'warnoptions': ['default'], 'warnoptions': ['default'],

View file

@ -926,27 +926,26 @@ class PyWarningsDisplayTests(WarningsDisplayTests, unittest.TestCase):
return stderr return stderr
# tracemalloc disabled # tracemalloc disabled
filename = os.path.abspath(support.TESTFN)
stderr = run('-Wd', support.TESTFN) stderr = run('-Wd', support.TESTFN)
expected = textwrap.dedent(''' expected = textwrap.dedent(f'''
{fname}:5: ResourceWarning: unclosed file <...> {filename}:5: ResourceWarning: unclosed file <...>
f = None f = None
ResourceWarning: Enable tracemalloc to get the object allocation traceback ResourceWarning: Enable tracemalloc to get the object allocation traceback
''') ''').strip()
expected = expected.format(fname=support.TESTFN).strip()
self.assertEqual(stderr, expected) self.assertEqual(stderr, expected)
# tracemalloc enabled # tracemalloc enabled
stderr = run('-Wd', '-X', 'tracemalloc=2', support.TESTFN) stderr = run('-Wd', '-X', 'tracemalloc=2', support.TESTFN)
expected = textwrap.dedent(''' expected = textwrap.dedent(f'''
{fname}:5: ResourceWarning: unclosed file <...> {filename}:5: ResourceWarning: unclosed file <...>
f = None f = None
Object allocated at (most recent call last): Object allocated at (most recent call last):
File "{fname}", lineno 7 File "{filename}", lineno 7
func() func()
File "{fname}", lineno 3 File "{filename}", lineno 3
f = open(__file__) f = open(__file__)
''') ''').strip()
expected = expected.format(fname=support.TESTFN).strip()
self.assertEqual(stderr, expected) self.assertEqual(stderr, expected)

View file

@ -0,0 +1,3 @@
Python now gets the absolute path of the script filename specified on the
command line (ex: "python3 script.py"): the __file__ attribute of the __main__
module and sys.path[0] become an absolute path, rather than a relative path.

View file

@ -240,7 +240,7 @@ static PyStatus
joinpath(wchar_t *buffer, const wchar_t *stuff, size_t buflen) joinpath(wchar_t *buffer, const wchar_t *stuff, size_t buflen)
{ {
size_t n, k; size_t n, k;
if (stuff[0] != SEP) { if (!_Py_isabs(stuff)) {
n = wcslen(buffer); n = wcslen(buffer);
if (n >= buflen) { if (n >= buflen) {
return PATHLEN_ERR(); return PATHLEN_ERR();
@ -283,7 +283,7 @@ safe_wcscpy(wchar_t *dst, const wchar_t *src, size_t n)
static PyStatus static PyStatus
copy_absolute(wchar_t *path, const wchar_t *p, size_t pathlen) copy_absolute(wchar_t *path, const wchar_t *p, size_t pathlen)
{ {
if (p[0] == SEP) { if (_Py_isabs(p)) {
if (safe_wcscpy(path, p, pathlen) < 0) { if (safe_wcscpy(path, p, pathlen) < 0) {
return PATHLEN_ERR(); return PATHLEN_ERR();
} }
@ -312,7 +312,7 @@ copy_absolute(wchar_t *path, const wchar_t *p, size_t pathlen)
static PyStatus static PyStatus
absolutize(wchar_t *path, size_t path_len) absolutize(wchar_t *path, size_t path_len)
{ {
if (path[0] == SEP) { if (_Py_isabs(path)) {
return _PyStatus_OK(); return _PyStatus_OK();
} }
@ -761,7 +761,7 @@ calculate_program_full_path(const PyConfig *config,
* absolutize() should help us out below * absolutize() should help us out below
*/ */
else if(0 == _NSGetExecutablePath(execpath, &nsexeclength) && else if(0 == _NSGetExecutablePath(execpath, &nsexeclength) &&
execpath[0] == SEP) _Py_isabs(execpath))
{ {
size_t len; size_t len;
wchar_t *path = Py_DecodeLocale(execpath, &len); wchar_t *path = Py_DecodeLocale(execpath, &len);
@ -815,7 +815,7 @@ calculate_program_full_path(const PyConfig *config,
else { else {
program_full_path[0] = '\0'; program_full_path[0] = '\0';
} }
if (program_full_path[0] != SEP && program_full_path[0] != '\0') { if (!_Py_isabs(program_full_path) && program_full_path[0] != '\0') {
status = absolutize(program_full_path, program_full_path_len); status = absolutize(program_full_path, program_full_path_len);
if (_PyStatus_EXCEPTION(status)) { if (_PyStatus_EXCEPTION(status)) {
return status; return status;
@ -916,7 +916,7 @@ calculate_argv0_path(PyCalculatePath *calculate, const wchar_t *program_full_pat
const size_t buflen = Py_ARRAY_LENGTH(tmpbuffer); const size_t buflen = Py_ARRAY_LENGTH(tmpbuffer);
int linklen = _Py_wreadlink(program_full_path, tmpbuffer, buflen); int linklen = _Py_wreadlink(program_full_path, tmpbuffer, buflen);
while (linklen != -1) { while (linklen != -1) {
if (tmpbuffer[0] == SEP) { if (_Py_isabs(tmpbuffer)) {
/* tmpbuffer should never be longer than MAXPATHLEN, /* tmpbuffer should never be longer than MAXPATHLEN,
but extra check does not hurt */ but extra check does not hurt */
if (safe_wcscpy(calculate->argv0_path, tmpbuffer, argv0_path_len) < 0) { if (safe_wcscpy(calculate->argv0_path, tmpbuffer, argv0_path_len) < 0) {
@ -1046,7 +1046,7 @@ calculate_module_search_path(const PyConfig *config,
while (1) { while (1) {
wchar_t *delim = wcschr(defpath, DELIM); wchar_t *delim = wcschr(defpath, DELIM);
if (defpath[0] != SEP) { if (!_Py_isabs(defpath)) {
/* Paths are relative to prefix */ /* Paths are relative to prefix */
bufsz += prefixsz; bufsz += prefixsz;
} }
@ -1088,7 +1088,7 @@ calculate_module_search_path(const PyConfig *config,
while (1) { while (1) {
wchar_t *delim = wcschr(defpath, DELIM); wchar_t *delim = wcschr(defpath, DELIM);
if (defpath[0] != SEP) { if (!_Py_isabs(defpath)) {
wcscat(buf, prefix); wcscat(buf, prefix);
if (prefixsz >= 2 && prefix[prefixsz - 2] != SEP && if (prefixsz >= 2 && prefix[prefixsz - 2] != SEP &&
defpath[0] != (delim ? DELIM : L'\0')) defpath[0] != (delim ? DELIM : L'\0'))

View file

@ -3784,29 +3784,25 @@ static PyObject *
os__getfullpathname_impl(PyObject *module, path_t *path) os__getfullpathname_impl(PyObject *module, path_t *path)
/*[clinic end generated code: output=bb8679d56845bc9b input=332ed537c29d0a3e]*/ /*[clinic end generated code: output=bb8679d56845bc9b input=332ed537c29d0a3e]*/
{ {
wchar_t woutbuf[MAX_PATH], *woutbufp = woutbuf; wchar_t *abspath;
wchar_t *wtemp;
DWORD result;
PyObject *v;
result = GetFullPathNameW(path->wide, /* _Py_abspath() is implemented with GetFullPathNameW() on Windows */
Py_ARRAY_LENGTH(woutbuf), if (_Py_abspath(path->wide, &abspath) < 0) {
woutbuf, &wtemp); return win32_error_object("GetFullPathNameW", path->object);
if (result > Py_ARRAY_LENGTH(woutbuf)) {
woutbufp = PyMem_New(wchar_t, result);
if (!woutbufp)
return PyErr_NoMemory();
result = GetFullPathNameW(path->wide, result, woutbufp, &wtemp);
} }
if (result) { if (abspath == NULL) {
v = PyUnicode_FromWideChar(woutbufp, wcslen(woutbufp)); return PyErr_NoMemory();
if (path->narrow) }
Py_SETREF(v, PyUnicode_EncodeFSDefault(v));
} else PyObject *str = PyUnicode_FromWideChar(abspath, wcslen(abspath));
v = win32_error_object("GetFullPathNameW", path->object); PyMem_RawFree(abspath);
if (woutbufp != woutbuf) if (str == NULL) {
PyMem_Free(woutbufp); return NULL;
return v; }
if (path->narrow) {
Py_SETREF(str, PyUnicode_EncodeFSDefault(str));
}
return str;
} }

View file

@ -1734,6 +1734,103 @@ _Py_wrealpath(const wchar_t *path,
} }
#endif #endif
#ifndef MS_WINDOWS
int
_Py_isabs(const wchar_t *path)
{
return (path[0] == SEP);
}
#endif
/* Get an absolute path.
On error (ex: fail to get the current directory), return -1.
On memory allocation failure, set *abspath_p to NULL and return 0.
On success, return a newly allocated to *abspath_p to and return 0.
The string must be freed by PyMem_RawFree(). */
int
_Py_abspath(const wchar_t *path, wchar_t **abspath_p)
{
#ifdef MS_WINDOWS
wchar_t woutbuf[MAX_PATH], *woutbufp = woutbuf;
DWORD result;
result = GetFullPathNameW(path,
Py_ARRAY_LENGTH(woutbuf), woutbuf,
NULL);
if (!result) {
return -1;
}
if (result > Py_ARRAY_LENGTH(woutbuf)) {
if ((size_t)result <= (size_t)PY_SSIZE_T_MAX / sizeof(wchar_t)) {
woutbufp = PyMem_RawMalloc((size_t)result * sizeof(wchar_t));
}
else {
woutbufp = NULL;
}
if (!woutbufp) {
*abspath_p = NULL;
return 0;
}
result = GetFullPathNameW(path, result, woutbufp, NULL);
if (!result) {
PyMem_RawFree(woutbufp);
return -1;
}
}
if (woutbufp != woutbuf) {
*abspath_p = woutbufp;
return 0;
}
*abspath_p = _PyMem_RawWcsdup(woutbufp);
return 0;
#else
if (_Py_isabs(path)) {
*abspath_p = _PyMem_RawWcsdup(path);
return 0;
}
wchar_t cwd[MAXPATHLEN + 1];
cwd[Py_ARRAY_LENGTH(cwd) - 1] = 0;
if (!_Py_wgetcwd(cwd, Py_ARRAY_LENGTH(cwd) - 1)) {
/* unable to get the current directory */
return -1;
}
size_t cwd_len = wcslen(cwd);
size_t path_len = wcslen(path);
size_t len = cwd_len + 1 + path_len + 1;
if (len <= (size_t)PY_SSIZE_T_MAX / sizeof(wchar_t)) {
*abspath_p = PyMem_RawMalloc(len * sizeof(wchar_t));
}
else {
*abspath_p = NULL;
}
if (*abspath_p == NULL) {
return 0;
}
wchar_t *abspath = *abspath_p;
memcpy(abspath, cwd, cwd_len * sizeof(wchar_t));
abspath += cwd_len;
*abspath = (wchar_t)SEP;
abspath++;
memcpy(abspath, path, path_len * sizeof(wchar_t));
abspath += path_len;
*abspath = 0;
return 0;
#endif
}
/* Get the current directory. buflen is the buffer size in wide characters /* Get the current directory. buflen is the buffer size in wide characters
including the null character. Decode the path from the locale encoding. including the null character. Decode the path from the locale encoding.

View file

@ -2137,6 +2137,11 @@ config_update_argv(PyConfig *config, Py_ssize_t opt_index)
/* Force sys.argv[0] = '-m'*/ /* Force sys.argv[0] = '-m'*/
arg0 = L"-m"; arg0 = L"-m";
} }
else if (config->run_filename != NULL) {
/* run_filename is converted to an absolute path: update argv */
arg0 = config->run_filename;
}
if (arg0 != NULL) { if (arg0 != NULL) {
arg0 = _PyMem_RawWcsdup(arg0); arg0 = _PyMem_RawWcsdup(arg0);
if (arg0 == NULL) { if (arg0 == NULL) {
@ -2183,6 +2188,37 @@ core_read_precmdline(PyConfig *config, _PyPreCmdline *precmdline)
} }
/* Get run_filename absolute path */
static PyStatus
config_run_filename_abspath(PyConfig *config)
{
if (!config->run_filename) {
return _PyStatus_OK();
}
#ifndef MS_WINDOWS
if (_Py_isabs(config->run_filename)) {
/* path is already absolute */
return _PyStatus_OK();
}
#endif
wchar_t *abs_filename;
if (_Py_abspath(config->run_filename, &abs_filename) < 0) {
/* failed to get the absolute path of the command line filename:
ignore the error, keep the relative path */
return _PyStatus_OK();
}
if (abs_filename == NULL) {
return _PyStatus_NO_MEMORY();
}
PyMem_RawFree(config->run_filename);
config->run_filename = abs_filename;
return _PyStatus_OK();
}
static PyStatus static PyStatus
config_read_cmdline(PyConfig *config) config_read_cmdline(PyConfig *config)
{ {
@ -2208,11 +2244,22 @@ config_read_cmdline(PyConfig *config)
goto done; goto done;
} }
status = config_run_filename_abspath(config);
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
status = config_update_argv(config, opt_index); status = config_update_argv(config, opt_index);
if (_PyStatus_EXCEPTION(status)) { if (_PyStatus_EXCEPTION(status)) {
goto done; goto done;
} }
} }
else {
status = config_run_filename_abspath(config);
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
}
if (config->use_environment) { if (config->use_environment) {
status = config_init_env_warnoptions(config, &env_warnoptions); status = config_init_env_warnoptions(config, &env_warnoptions);