Skip to content

Commit 3939c32

Browse files
authored
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.
1 parent 080b6b4 commit 3939c32

File tree

10 files changed

+211
-42
lines changed

10 files changed

+211
-42
lines changed

Diff for: Doc/whatsnew/3.9.rst

+8
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ New Features
7575
Other Language Changes
7676
======================
7777

78+
* Python now gets the absolute path of the script filename specified on
79+
the command line (ex: ``python3 script.py``): the ``__file__`` attribute of
80+
the ``__main__`` module, ``sys.argv[0]`` and ``sys.path[0]`` become an
81+
absolute path, rather than a relative path. These paths now remain valid
82+
after the current directory is changed by :func:`os.chdir`. As a side effect,
83+
a traceback also displays the absolute path for ``__main__`` module frames in
84+
this case.
85+
(Contributed by Victor Stinner in :issue:`20443`.)
7886

7987

8088
New Modules

Diff for: Include/fileutils.h

+6
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ PyAPI_FUNC(wchar_t*) _Py_wrealpath(
154154
size_t resolved_path_len);
155155
#endif
156156

157+
#ifndef MS_WINDOWS
158+
PyAPI_FUNC(int) _Py_isabs(const wchar_t *path);
159+
#endif
160+
161+
PyAPI_FUNC(int) _Py_abspath(const wchar_t *path, wchar_t **abspath_p);
162+
157163
PyAPI_FUNC(wchar_t*) _Py_wgetcwd(
158164
wchar_t *buf,
159165
/* Number of characters of 'buf' buffer

Diff for: Lib/test/test_cmd_line_script.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,18 @@ def test_basic_script(self):
217217
with support.temp_dir() as script_dir:
218218
script_name = _make_test_script(script_dir, 'script')
219219
self._check_script(script_name, script_name, script_name,
220+
script_dir, None,
221+
importlib.machinery.SourceFileLoader,
222+
expected_cwd=script_dir)
223+
224+
def test_script_abspath(self):
225+
# pass the script using the relative path, expect the absolute path
226+
# in __file__ and sys.argv[0]
227+
with support.temp_cwd() as script_dir:
228+
self.assertTrue(os.path.isabs(script_dir), script_dir)
229+
230+
script_name = _make_test_script(script_dir, 'script')
231+
self._check_script(os.path.basename(script_name), script_name, script_name,
220232
script_dir, None,
221233
importlib.machinery.SourceFileLoader)
222234

@@ -542,7 +554,7 @@ def test_non_ascii(self):
542554

543555
# Issue #16218
544556
source = 'print(ascii(__file__))\n'
545-
script_name = _make_test_script(os.curdir, name, source)
557+
script_name = _make_test_script(os.getcwd(), name, source)
546558
self.addCleanup(support.unlink, script_name)
547559
rc, stdout, stderr = assert_python_ok(script_name)
548560
self.assertEqual(

Diff for: Lib/test/test_embed.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -805,9 +805,10 @@ def test_preinit_parse_argv(self):
805805
preconfig = {
806806
'allocator': PYMEM_ALLOCATOR_DEBUG,
807807
}
808+
script_abspath = os.path.abspath('script.py')
808809
config = {
809-
'argv': ['script.py'],
810-
'run_filename': 'script.py',
810+
'argv': [script_abspath],
811+
'run_filename': script_abspath,
811812
'dev_mode': 1,
812813
'faulthandler': 1,
813814
'warnoptions': ['default'],

Diff for: Lib/test/test_warnings/__init__.py

+9-10
Original file line numberDiff line numberDiff line change
@@ -926,27 +926,26 @@ def run(*args):
926926
return stderr
927927

928928
# tracemalloc disabled
929+
filename = os.path.abspath(support.TESTFN)
929930
stderr = run('-Wd', support.TESTFN)
930-
expected = textwrap.dedent('''
931-
{fname}:5: ResourceWarning: unclosed file <...>
931+
expected = textwrap.dedent(f'''
932+
{filename}:5: ResourceWarning: unclosed file <...>
932933
f = None
933934
ResourceWarning: Enable tracemalloc to get the object allocation traceback
934-
''')
935-
expected = expected.format(fname=support.TESTFN).strip()
935+
''').strip()
936936
self.assertEqual(stderr, expected)
937937

938938
# tracemalloc enabled
939939
stderr = run('-Wd', '-X', 'tracemalloc=2', support.TESTFN)
940-
expected = textwrap.dedent('''
941-
{fname}:5: ResourceWarning: unclosed file <...>
940+
expected = textwrap.dedent(f'''
941+
{filename}:5: ResourceWarning: unclosed file <...>
942942
f = None
943943
Object allocated at (most recent call last):
944-
File "{fname}", lineno 7
944+
File "{filename}", lineno 7
945945
func()
946-
File "{fname}", lineno 3
946+
File "{filename}", lineno 3
947947
f = open(__file__)
948-
''')
949-
expected = expected.format(fname=support.TESTFN).strip()
948+
''').strip()
950949
self.assertEqual(stderr, expected)
951950

952951

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

Diff for: Modules/getpath.c

+8-8
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ static PyStatus
240240
joinpath(wchar_t *buffer, const wchar_t *stuff, size_t buflen)
241241
{
242242
size_t n, k;
243-
if (stuff[0] != SEP) {
243+
if (!_Py_isabs(stuff)) {
244244
n = wcslen(buffer);
245245
if (n >= buflen) {
246246
return PATHLEN_ERR();
@@ -283,7 +283,7 @@ safe_wcscpy(wchar_t *dst, const wchar_t *src, size_t n)
283283
static PyStatus
284284
copy_absolute(wchar_t *path, const wchar_t *p, size_t pathlen)
285285
{
286-
if (p[0] == SEP) {
286+
if (_Py_isabs(p)) {
287287
if (safe_wcscpy(path, p, pathlen) < 0) {
288288
return PATHLEN_ERR();
289289
}
@@ -312,7 +312,7 @@ copy_absolute(wchar_t *path, const wchar_t *p, size_t pathlen)
312312
static PyStatus
313313
absolutize(wchar_t *path, size_t path_len)
314314
{
315-
if (path[0] == SEP) {
315+
if (_Py_isabs(path)) {
316316
return _PyStatus_OK();
317317
}
318318

@@ -761,7 +761,7 @@ calculate_program_full_path(const PyConfig *config,
761761
* absolutize() should help us out below
762762
*/
763763
else if(0 == _NSGetExecutablePath(execpath, &nsexeclength) &&
764-
execpath[0] == SEP)
764+
_Py_isabs(execpath))
765765
{
766766
size_t len;
767767
wchar_t *path = Py_DecodeLocale(execpath, &len);
@@ -815,7 +815,7 @@ calculate_program_full_path(const PyConfig *config,
815815
else {
816816
program_full_path[0] = '\0';
817817
}
818-
if (program_full_path[0] != SEP && program_full_path[0] != '\0') {
818+
if (!_Py_isabs(program_full_path) && program_full_path[0] != '\0') {
819819
status = absolutize(program_full_path, program_full_path_len);
820820
if (_PyStatus_EXCEPTION(status)) {
821821
return status;
@@ -916,7 +916,7 @@ calculate_argv0_path(PyCalculatePath *calculate, const wchar_t *program_full_pat
916916
const size_t buflen = Py_ARRAY_LENGTH(tmpbuffer);
917917
int linklen = _Py_wreadlink(program_full_path, tmpbuffer, buflen);
918918
while (linklen != -1) {
919-
if (tmpbuffer[0] == SEP) {
919+
if (_Py_isabs(tmpbuffer)) {
920920
/* tmpbuffer should never be longer than MAXPATHLEN,
921921
but extra check does not hurt */
922922
if (safe_wcscpy(calculate->argv0_path, tmpbuffer, argv0_path_len) < 0) {
@@ -1046,7 +1046,7 @@ calculate_module_search_path(const PyConfig *config,
10461046
while (1) {
10471047
wchar_t *delim = wcschr(defpath, DELIM);
10481048

1049-
if (defpath[0] != SEP) {
1049+
if (!_Py_isabs(defpath)) {
10501050
/* Paths are relative to prefix */
10511051
bufsz += prefixsz;
10521052
}
@@ -1088,7 +1088,7 @@ calculate_module_search_path(const PyConfig *config,
10881088
while (1) {
10891089
wchar_t *delim = wcschr(defpath, DELIM);
10901090

1091-
if (defpath[0] != SEP) {
1091+
if (!_Py_isabs(defpath)) {
10921092
wcscat(buf, prefix);
10931093
if (prefixsz >= 2 && prefix[prefixsz - 2] != SEP &&
10941094
defpath[0] != (delim ? DELIM : L'\0'))

Diff for: Modules/posixmodule.c

+17-21
Original file line numberDiff line numberDiff line change
@@ -3784,29 +3784,25 @@ static PyObject *
37843784
os__getfullpathname_impl(PyObject *module, path_t *path)
37853785
/*[clinic end generated code: output=bb8679d56845bc9b input=332ed537c29d0a3e]*/
37863786
{
3787-
wchar_t woutbuf[MAX_PATH], *woutbufp = woutbuf;
3788-
wchar_t *wtemp;
3789-
DWORD result;
3790-
PyObject *v;
3787+
wchar_t *abspath;
37913788

3792-
result = GetFullPathNameW(path->wide,
3793-
Py_ARRAY_LENGTH(woutbuf),
3794-
woutbuf, &wtemp);
3795-
if (result > Py_ARRAY_LENGTH(woutbuf)) {
3796-
woutbufp = PyMem_New(wchar_t, result);
3797-
if (!woutbufp)
3798-
return PyErr_NoMemory();
3799-
result = GetFullPathNameW(path->wide, result, woutbufp, &wtemp);
3789+
/* _Py_abspath() is implemented with GetFullPathNameW() on Windows */
3790+
if (_Py_abspath(path->wide, &abspath) < 0) {
3791+
return win32_error_object("GetFullPathNameW", path->object);
38003792
}
3801-
if (result) {
3802-
v = PyUnicode_FromWideChar(woutbufp, wcslen(woutbufp));
3803-
if (path->narrow)
3804-
Py_SETREF(v, PyUnicode_EncodeFSDefault(v));
3805-
} else
3806-
v = win32_error_object("GetFullPathNameW", path->object);
3807-
if (woutbufp != woutbuf)
3808-
PyMem_Free(woutbufp);
3809-
return v;
3793+
if (abspath == NULL) {
3794+
return PyErr_NoMemory();
3795+
}
3796+
3797+
PyObject *str = PyUnicode_FromWideChar(abspath, wcslen(abspath));
3798+
PyMem_RawFree(abspath);
3799+
if (str == NULL) {
3800+
return NULL;
3801+
}
3802+
if (path->narrow) {
3803+
Py_SETREF(str, PyUnicode_EncodeFSDefault(str));
3804+
}
3805+
return str;
38103806
}
38113807

38123808

Diff for: Python/fileutils.c

+97
Original file line numberDiff line numberDiff line change
@@ -1734,6 +1734,103 @@ _Py_wrealpath(const wchar_t *path,
17341734
}
17351735
#endif
17361736

1737+
1738+
#ifndef MS_WINDOWS
1739+
int
1740+
_Py_isabs(const wchar_t *path)
1741+
{
1742+
return (path[0] == SEP);
1743+
}
1744+
#endif
1745+
1746+
1747+
/* Get an absolute path.
1748+
On error (ex: fail to get the current directory), return -1.
1749+
On memory allocation failure, set *abspath_p to NULL and return 0.
1750+
On success, return a newly allocated to *abspath_p to and return 0.
1751+
The string must be freed by PyMem_RawFree(). */
1752+
int
1753+
_Py_abspath(const wchar_t *path, wchar_t **abspath_p)
1754+
{
1755+
#ifdef MS_WINDOWS
1756+
wchar_t woutbuf[MAX_PATH], *woutbufp = woutbuf;
1757+
DWORD result;
1758+
1759+
result = GetFullPathNameW(path,
1760+
Py_ARRAY_LENGTH(woutbuf), woutbuf,
1761+
NULL);
1762+
if (!result) {
1763+
return -1;
1764+
}
1765+
1766+
if (result > Py_ARRAY_LENGTH(woutbuf)) {
1767+
if ((size_t)result <= (size_t)PY_SSIZE_T_MAX / sizeof(wchar_t)) {
1768+
woutbufp = PyMem_RawMalloc((size_t)result * sizeof(wchar_t));
1769+
}
1770+
else {
1771+
woutbufp = NULL;
1772+
}
1773+
if (!woutbufp) {
1774+
*abspath_p = NULL;
1775+
return 0;
1776+
}
1777+
1778+
result = GetFullPathNameW(path, result, woutbufp, NULL);
1779+
if (!result) {
1780+
PyMem_RawFree(woutbufp);
1781+
return -1;
1782+
}
1783+
}
1784+
1785+
if (woutbufp != woutbuf) {
1786+
*abspath_p = woutbufp;
1787+
return 0;
1788+
}
1789+
1790+
*abspath_p = _PyMem_RawWcsdup(woutbufp);
1791+
return 0;
1792+
#else
1793+
if (_Py_isabs(path)) {
1794+
*abspath_p = _PyMem_RawWcsdup(path);
1795+
return 0;
1796+
}
1797+
1798+
wchar_t cwd[MAXPATHLEN + 1];
1799+
cwd[Py_ARRAY_LENGTH(cwd) - 1] = 0;
1800+
if (!_Py_wgetcwd(cwd, Py_ARRAY_LENGTH(cwd) - 1)) {
1801+
/* unable to get the current directory */
1802+
return -1;
1803+
}
1804+
1805+
size_t cwd_len = wcslen(cwd);
1806+
size_t path_len = wcslen(path);
1807+
size_t len = cwd_len + 1 + path_len + 1;
1808+
if (len <= (size_t)PY_SSIZE_T_MAX / sizeof(wchar_t)) {
1809+
*abspath_p = PyMem_RawMalloc(len * sizeof(wchar_t));
1810+
}
1811+
else {
1812+
*abspath_p = NULL;
1813+
}
1814+
if (*abspath_p == NULL) {
1815+
return 0;
1816+
}
1817+
1818+
wchar_t *abspath = *abspath_p;
1819+
memcpy(abspath, cwd, cwd_len * sizeof(wchar_t));
1820+
abspath += cwd_len;
1821+
1822+
*abspath = (wchar_t)SEP;
1823+
abspath++;
1824+
1825+
memcpy(abspath, path, path_len * sizeof(wchar_t));
1826+
abspath += path_len;
1827+
1828+
*abspath = 0;
1829+
return 0;
1830+
#endif
1831+
}
1832+
1833+
17371834
/* Get the current directory. buflen is the buffer size in wide characters
17381835
including the null character. Decode the path from the locale encoding.
17391836

0 commit comments

Comments
 (0)