Skip to content

Commit 1a45659

Browse files
authored
gh-90329: Add _winapi.GetLongPathName and GetShortPathName and use in venv to reduce warnings (GH-117817)
1 parent 667a574 commit 1a45659

File tree

6 files changed

+333
-3
lines changed

6 files changed

+333
-3
lines changed

Lib/test/test_venv.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
requires_subprocess, is_emscripten, is_wasi,
2323
requires_venv_with_pip, TEST_HOME_DIR,
2424
requires_resource, copy_python_src_ignore)
25-
from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree)
25+
from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree,
26+
TESTFN)
2627
import unittest
2728
import venv
2829
from unittest.mock import patch, Mock
@@ -639,6 +640,36 @@ def test_activate_shell_script_has_no_dos_newlines(self):
639640
error_message = f"CR LF found in line {i}"
640641
self.assertFalse(line.endswith(b'\r\n'), error_message)
641642

643+
def test_venv_same_path(self):
644+
same_path = venv.EnvBuilder._same_path
645+
if sys.platform == 'win32':
646+
# Case-insensitive, and handles short/long names
647+
tests = [
648+
(True, TESTFN, TESTFN),
649+
(True, TESTFN.lower(), TESTFN.upper()),
650+
]
651+
import _winapi
652+
# ProgramFiles is the most reliable path that will have short/long
653+
progfiles = os.getenv('ProgramFiles')
654+
if progfiles:
655+
tests = [
656+
*tests,
657+
(True, progfiles, progfiles),
658+
(True, _winapi.GetShortPathName(progfiles), _winapi.GetLongPathName(progfiles)),
659+
]
660+
else:
661+
# Just a simple case-sensitive comparison
662+
tests = [
663+
(True, TESTFN, TESTFN),
664+
(False, TESTFN.lower(), TESTFN.upper()),
665+
]
666+
for r, path1, path2 in tests:
667+
with self.subTest(f"{path1}-{path2}"):
668+
if r:
669+
self.assertTrue(same_path(path1, path2))
670+
else:
671+
self.assertFalse(same_path(path1, path2))
672+
642673
@requireVenvCreate
643674
class EnsurePipTest(BaseTest):
644675
"""Test venv module installation of pip."""

Lib/test/test_winapi.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Test the Windows-only _winapi module
2+
3+
import os
4+
import pathlib
5+
import re
6+
import unittest
7+
from test.support import import_helper
8+
9+
_winapi = import_helper.import_module('_winapi', required_on=['win'])
10+
11+
class WinAPITests(unittest.TestCase):
12+
def test_getlongpathname(self):
13+
testfn = pathlib.Path(os.getenv("ProgramFiles")).parents[-1] / "PROGRA~1"
14+
if not os.path.isdir(testfn):
15+
raise unittest.SkipTest("require x:\\PROGRA~1 to test")
16+
17+
# pathlib.Path will be rejected - only str is accepted
18+
with self.assertRaises(TypeError):
19+
_winapi.GetLongPathName(testfn)
20+
21+
actual = _winapi.GetLongPathName(os.fsdecode(testfn))
22+
23+
# Can't assume that PROGRA~1 expands to any particular variation, so
24+
# ensure it matches any one of them.
25+
candidates = set(testfn.parent.glob("Progra*"))
26+
self.assertIn(pathlib.Path(actual), candidates)
27+
28+
def test_getshortpathname(self):
29+
testfn = pathlib.Path(os.getenv("ProgramFiles"))
30+
if not os.path.isdir(testfn):
31+
raise unittest.SkipTest("require '%ProgramFiles%' to test")
32+
33+
# pathlib.Path will be rejected - only str is accepted
34+
with self.assertRaises(TypeError):
35+
_winapi.GetShortPathName(testfn)
36+
37+
actual = _winapi.GetShortPathName(os.fsdecode(testfn))
38+
39+
# Should contain "PROGRA~" but we can't predict the number
40+
self.assertIsNotNone(re.match(r".\:\\PROGRA~\d", actual.upper()), actual)

Lib/venv/__init__.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,33 @@ def _venv_path(self, env_dir, name):
102102
}
103103
return sysconfig.get_path(name, scheme='venv', vars=vars)
104104

105+
@classmethod
106+
def _same_path(cls, path1, path2):
107+
"""Check whether two paths appear the same.
108+
109+
Whether they refer to the same file is irrelevant; we're testing for
110+
whether a human reader would look at the path string and easily tell
111+
that they're the same file.
112+
"""
113+
if sys.platform == 'win32':
114+
if os.path.normcase(path1) == os.path.normcase(path2):
115+
return True
116+
# gh-90329: Don't display a warning for short/long names
117+
import _winapi
118+
try:
119+
path1 = _winapi.GetLongPathName(os.fsdecode(path1))
120+
except OSError:
121+
pass
122+
try:
123+
path2 = _winapi.GetLongPathName(os.fsdecode(path2))
124+
except OSError:
125+
pass
126+
if os.path.normcase(path1) == os.path.normcase(path2):
127+
return True
128+
return False
129+
else:
130+
return path1 == path2
131+
105132
def ensure_directories(self, env_dir):
106133
"""
107134
Create the directories for the environment.
@@ -162,7 +189,7 @@ def create_if_needed(d):
162189
# bpo-45337: Fix up env_exec_cmd to account for file system redirections.
163190
# Some redirects only apply to CreateFile and not CreateProcess
164191
real_env_exe = os.path.realpath(context.env_exe)
165-
if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe):
192+
if not self._same_path(real_env_exe, context.env_exe):
166193
logger.warning('Actual environment location may have moved due to '
167194
'redirects, links or junctions.\n'
168195
' Requested location: "%s"\n'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Suppress the warning displayed on virtual environment creation when the
2+
requested and created paths differ only by a short (8.3 style) name.
3+
Warnings will continue to be shown if a junction or symlink in the path
4+
caused the venv to be created in a different location than originally
5+
requested.

Modules/_winapi.c

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,49 @@ _winapi_GetLastError_impl(PyObject *module)
14521452
return GetLastError();
14531453
}
14541454

1455+
1456+
/*[clinic input]
1457+
_winapi.GetLongPathName
1458+
1459+
path: LPCWSTR
1460+
1461+
Return the long version of the provided path.
1462+
1463+
If the path is already in its long form, returns the same value.
1464+
1465+
The path must already be a 'str'. If the type is not known, use
1466+
os.fsdecode before calling this function.
1467+
[clinic start generated code]*/
1468+
1469+
static PyObject *
1470+
_winapi_GetLongPathName_impl(PyObject *module, LPCWSTR path)
1471+
/*[clinic end generated code: output=c4774b080275a2d0 input=9872e211e3a4a88f]*/
1472+
{
1473+
DWORD cchBuffer;
1474+
PyObject *result = NULL;
1475+
1476+
Py_BEGIN_ALLOW_THREADS
1477+
cchBuffer = GetLongPathNameW(path, NULL, 0);
1478+
Py_END_ALLOW_THREADS
1479+
if (cchBuffer) {
1480+
WCHAR *buffer = (WCHAR *)PyMem_Malloc(cchBuffer * sizeof(WCHAR));
1481+
if (buffer) {
1482+
Py_BEGIN_ALLOW_THREADS
1483+
cchBuffer = GetLongPathNameW(path, buffer, cchBuffer);
1484+
Py_END_ALLOW_THREADS
1485+
if (cchBuffer) {
1486+
result = PyUnicode_FromWideChar(buffer, cchBuffer);
1487+
} else {
1488+
PyErr_SetFromWindowsErr(0);
1489+
}
1490+
PyMem_Free((void *)buffer);
1491+
}
1492+
} else {
1493+
PyErr_SetFromWindowsErr(0);
1494+
}
1495+
return result;
1496+
}
1497+
14551498
/*[clinic input]
14561499
_winapi.GetModuleFileName
14571500
@@ -1486,6 +1529,48 @@ _winapi_GetModuleFileName_impl(PyObject *module, HMODULE module_handle)
14861529
return PyUnicode_FromWideChar(filename, wcslen(filename));
14871530
}
14881531

1532+
/*[clinic input]
1533+
_winapi.GetShortPathName
1534+
1535+
path: LPCWSTR
1536+
1537+
Return the short version of the provided path.
1538+
1539+
If the path is already in its short form, returns the same value.
1540+
1541+
The path must already be a 'str'. If the type is not known, use
1542+
os.fsdecode before calling this function.
1543+
[clinic start generated code]*/
1544+
1545+
static PyObject *
1546+
_winapi_GetShortPathName_impl(PyObject *module, LPCWSTR path)
1547+
/*[clinic end generated code: output=dab6ae494c621e81 input=43fa349aaf2ac718]*/
1548+
{
1549+
DWORD cchBuffer;
1550+
PyObject *result = NULL;
1551+
1552+
Py_BEGIN_ALLOW_THREADS
1553+
cchBuffer = GetShortPathNameW(path, NULL, 0);
1554+
Py_END_ALLOW_THREADS
1555+
if (cchBuffer) {
1556+
WCHAR *buffer = (WCHAR *)PyMem_Malloc(cchBuffer * sizeof(WCHAR));
1557+
if (buffer) {
1558+
Py_BEGIN_ALLOW_THREADS
1559+
cchBuffer = GetShortPathNameW(path, buffer, cchBuffer);
1560+
Py_END_ALLOW_THREADS
1561+
if (cchBuffer) {
1562+
result = PyUnicode_FromWideChar(buffer, cchBuffer);
1563+
} else {
1564+
PyErr_SetFromWindowsErr(0);
1565+
}
1566+
PyMem_Free((void *)buffer);
1567+
}
1568+
} else {
1569+
PyErr_SetFromWindowsErr(0);
1570+
}
1571+
return result;
1572+
}
1573+
14891574
/*[clinic input]
14901575
_winapi.GetStdHandle -> HANDLE
14911576
@@ -2345,7 +2430,9 @@ static PyMethodDef winapi_functions[] = {
23452430
_WINAPI_GETCURRENTPROCESS_METHODDEF
23462431
_WINAPI_GETEXITCODEPROCESS_METHODDEF
23472432
_WINAPI_GETLASTERROR_METHODDEF
2433+
_WINAPI_GETLONGPATHNAME_METHODDEF
23482434
_WINAPI_GETMODULEFILENAME_METHODDEF
2435+
_WINAPI_GETSHORTPATHNAME_METHODDEF
23492436
_WINAPI_GETSTDHANDLE_METHODDEF
23502437
_WINAPI_GETVERSION_METHODDEF
23512438
_WINAPI_MAPVIEWOFFILE_METHODDEF

0 commit comments

Comments
 (0)