Skip to content

Commit 23703a9

Browse files
committed
pythongh-86179: Implement realpath() on Windows for getpath.py calculations (pythonGH-113033)
1 parent c6c559a commit 23703a9

File tree

3 files changed

+80
-6
lines changed

3 files changed

+80
-6
lines changed

Lib/test/test_venv.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,18 @@ def check_output(cmd, encoding=None):
4747
p = subprocess.Popen(cmd,
4848
stdout=subprocess.PIPE,
4949
stderr=subprocess.PIPE,
50-
encoding=encoding)
50+
env={**os.environ, "PYTHONHOME": ""})
5151
out, err = p.communicate()
5252
if p.returncode:
5353
if verbose and err:
54-
print(err.decode('utf-8', 'backslashreplace'))
54+
print(err.decode(encoding or 'utf-8', 'backslashreplace'))
5555
raise subprocess.CalledProcessError(
5656
p.returncode, cmd, out, err)
57+
if encoding:
58+
return (
59+
out.decode(encoding, 'backslashreplace'),
60+
err.decode(encoding, 'backslashreplace'),
61+
)
5762
return out, err
5863

5964
class BaseTest(unittest.TestCase):
@@ -273,8 +278,18 @@ def test_sysconfig(self):
273278
('get_config_h_filename()', sysconfig.get_config_h_filename())):
274279
with self.subTest(call):
275280
cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
276-
out, err = check_output(cmd)
277-
self.assertEqual(out.strip(), expected.encode(), err)
281+
out, err = check_output(cmd, encoding='utf-8')
282+
self.assertEqual(out.strip(), expected, err)
283+
for attr, expected in (
284+
('executable', self.envpy()),
285+
# Usually compare to sys.executable, but if we're running in our own
286+
# venv then we really need to compare to our base executable
287+
('_base_executable', sys._base_executable),
288+
):
289+
with self.subTest(attr):
290+
cmd[2] = f'import sys; print(sys.{attr})'
291+
out, err = check_output(cmd, encoding='utf-8')
292+
self.assertEqual(out.strip(), expected, err)
278293

279294
@requireVenvCreate
280295
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
@@ -296,8 +311,18 @@ def test_sysconfig_symlinks(self):
296311
('get_config_h_filename()', sysconfig.get_config_h_filename())):
297312
with self.subTest(call):
298313
cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
299-
out, err = check_output(cmd)
300-
self.assertEqual(out.strip(), expected.encode(), err)
314+
out, err = check_output(cmd, encoding='utf-8')
315+
self.assertEqual(out.strip(), expected, err)
316+
for attr, expected in (
317+
('executable', self.envpy()),
318+
# Usually compare to sys.executable, but if we're running in our own
319+
# venv then we really need to compare to our base executable
320+
('_base_executable', sys._base_executable),
321+
):
322+
with self.subTest(attr):
323+
cmd[2] = f'import sys; print(sys.{attr})'
324+
out, err = check_output(cmd, encoding='utf-8')
325+
self.assertEqual(out.strip(), expected, err)
301326

302327
if sys.platform == 'win32':
303328
ENV_SUBDIRS = (
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes path calculations when launching Python on Windows through a symlink.

Modules/getpath.c

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,54 @@ getpath_realpath(PyObject *Py_UNUSED(self) , PyObject *args)
499499
PyMem_Free((void *)path);
500500
PyMem_Free((void *)narrow);
501501
return r;
502+
#elif defined(MS_WINDOWS)
503+
HANDLE hFile;
504+
wchar_t resolved[MAXPATHLEN+1];
505+
int len = 0, err;
506+
Py_ssize_t pathlen;
507+
PyObject *result;
508+
509+
wchar_t *path = PyUnicode_AsWideCharString(pathobj, &pathlen);
510+
if (!path) {
511+
return NULL;
512+
}
513+
if (wcslen(path) != pathlen) {
514+
PyErr_SetString(PyExc_ValueError, "path contains embedded nulls");
515+
return NULL;
516+
}
517+
518+
Py_BEGIN_ALLOW_THREADS
519+
hFile = CreateFileW(path, 0, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
520+
if (hFile != INVALID_HANDLE_VALUE) {
521+
len = GetFinalPathNameByHandleW(hFile, resolved, MAXPATHLEN, VOLUME_NAME_DOS);
522+
err = len ? 0 : GetLastError();
523+
CloseHandle(hFile);
524+
} else {
525+
err = GetLastError();
526+
}
527+
Py_END_ALLOW_THREADS
528+
529+
if (err) {
530+
PyErr_SetFromWindowsErr(err);
531+
result = NULL;
532+
} else if (len <= MAXPATHLEN) {
533+
const wchar_t *p = resolved;
534+
if (0 == wcsncmp(p, L"\\\\?\\", 4)) {
535+
if (GetFileAttributesW(&p[4]) != INVALID_FILE_ATTRIBUTES) {
536+
p += 4;
537+
len -= 4;
538+
}
539+
}
540+
if (CompareStringOrdinal(path, (int)pathlen, p, len, TRUE) == CSTR_EQUAL) {
541+
result = Py_NewRef(pathobj);
542+
} else {
543+
result = PyUnicode_FromWideChar(p, len);
544+
}
545+
} else {
546+
result = Py_NewRef(pathobj);
547+
}
548+
PyMem_Free(path);
549+
return result;
502550
#endif
503551

504552
return Py_NewRef(pathobj);

0 commit comments

Comments
 (0)