Skip to content

Commit 185999b

Browse files
authored
gh-90329: Add _winapi.GetLongPathName and GetShortPathName and use in venv to reduce warnings (GH-117817)
1 parent 64cd6fc commit 185999b

File tree

6 files changed

+328
-3
lines changed

6 files changed

+328
-3
lines changed

Lib/test/test_venv.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
is_emscripten, is_wasi,
2424
requires_venv_with_pip, TEST_HOME_DIR,
2525
requires_resource, copy_python_src_ignore)
26-
from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree)
26+
from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree,
27+
TESTFN)
2728
import unittest
2829
import venv
2930
from unittest.mock import patch, Mock
@@ -744,6 +745,36 @@ def test_cli_without_scm_ignore_files(self):
744745
with self.assertRaises(FileNotFoundError):
745746
self.get_text_file_contents('.gitignore')
746747

748+
def test_venv_same_path(self):
749+
same_path = venv.EnvBuilder._same_path
750+
if sys.platform == 'win32':
751+
# Case-insensitive, and handles short/long names
752+
tests = [
753+
(True, TESTFN, TESTFN),
754+
(True, TESTFN.lower(), TESTFN.upper()),
755+
]
756+
import _winapi
757+
# ProgramFiles is the most reliable path that will have short/long
758+
progfiles = os.getenv('ProgramFiles')
759+
if progfiles:
760+
tests = [
761+
*tests,
762+
(True, progfiles, progfiles),
763+
(True, _winapi.GetShortPathName(progfiles), _winapi.GetLongPathName(progfiles)),
764+
]
765+
else:
766+
# Just a simple case-sensitive comparison
767+
tests = [
768+
(True, TESTFN, TESTFN),
769+
(False, TESTFN.lower(), TESTFN.upper()),
770+
]
771+
for r, path1, path2 in tests:
772+
with self.subTest(f"{path1}-{path2}"):
773+
if r:
774+
self.assertTrue(same_path(path1, path2))
775+
else:
776+
self.assertFalse(same_path(path1, path2))
777+
747778
@requireVenvCreate
748779
class EnsurePipTest(BaseTest):
749780
"""Test venv module installation of pip."""

Lib/test/test_winapi.py

+35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Test the Windows-only _winapi module
22

3+
import os
4+
import pathlib
35
import random
6+
import re
47
import threading
58
import time
69
import unittest
@@ -92,3 +95,35 @@ def test_many_events_waitany(self):
9295

9396
def test_max_events_waitany(self):
9497
self._events_waitany_test(MAXIMUM_BATCHED_WAIT_OBJECTS)
98+
99+
100+
class WinAPITests(unittest.TestCase):
101+
def test_getlongpathname(self):
102+
testfn = pathlib.Path(os.getenv("ProgramFiles")).parents[-1] / "PROGRA~1"
103+
if not os.path.isdir(testfn):
104+
raise unittest.SkipTest("require x:\\PROGRA~1 to test")
105+
106+
# pathlib.Path will be rejected - only str is accepted
107+
with self.assertRaises(TypeError):
108+
_winapi.GetLongPathName(testfn)
109+
110+
actual = _winapi.GetLongPathName(os.fsdecode(testfn))
111+
112+
# Can't assume that PROGRA~1 expands to any particular variation, so
113+
# ensure it matches any one of them.
114+
candidates = set(testfn.parent.glob("Progra*"))
115+
self.assertIn(pathlib.Path(actual), candidates)
116+
117+
def test_getshortpathname(self):
118+
testfn = pathlib.Path(os.getenv("ProgramFiles"))
119+
if not os.path.isdir(testfn):
120+
raise unittest.SkipTest("require '%ProgramFiles%' to test")
121+
122+
# pathlib.Path will be rejected - only str is accepted
123+
with self.assertRaises(TypeError):
124+
_winapi.GetShortPathName(testfn)
125+
126+
actual = _winapi.GetShortPathName(os.fsdecode(testfn))
127+
128+
# Should contain "PROGRA~" but we can't predict the number
129+
self.assertIsNotNone(re.match(r".\:\\PROGRA~\d", actual.upper()), actual)

Lib/venv/__init__.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,33 @@ def _venv_path(self, env_dir, name):
107107
}
108108
return sysconfig.get_path(name, scheme='venv', vars=vars)
109109

110+
@classmethod
111+
def _same_path(cls, path1, path2):
112+
"""Check whether two paths appear the same.
113+
114+
Whether they refer to the same file is irrelevant; we're testing for
115+
whether a human reader would look at the path string and easily tell
116+
that they're the same file.
117+
"""
118+
if sys.platform == 'win32':
119+
if os.path.normcase(path1) == os.path.normcase(path2):
120+
return True
121+
# gh-90329: Don't display a warning for short/long names
122+
import _winapi
123+
try:
124+
path1 = _winapi.GetLongPathName(os.fsdecode(path1))
125+
except OSError:
126+
pass
127+
try:
128+
path2 = _winapi.GetLongPathName(os.fsdecode(path2))
129+
except OSError:
130+
pass
131+
if os.path.normcase(path1) == os.path.normcase(path2):
132+
return True
133+
return False
134+
else:
135+
return path1 == path2
136+
110137
def ensure_directories(self, env_dir):
111138
"""
112139
Create the directories for the environment.
@@ -171,7 +198,7 @@ def create_if_needed(d):
171198
# bpo-45337: Fix up env_exec_cmd to account for file system redirections.
172199
# Some redirects only apply to CreateFile and not CreateProcess
173200
real_env_exe = os.path.realpath(context.env_exe)
174-
if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe):
201+
if not self._same_path(real_env_exe, context.env_exe):
175202
logger.warning('Actual environment location may have moved due to '
176203
'redirects, links or junctions.\n'
177204
' Requested location: "%s"\n'
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

+87
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,49 @@ _winapi_GetLastError_impl(PyObject *module)
15171517
return GetLastError();
15181518
}
15191519

1520+
1521+
/*[clinic input]
1522+
_winapi.GetLongPathName
1523+
1524+
path: LPCWSTR
1525+
1526+
Return the long version of the provided path.
1527+
1528+
If the path is already in its long form, returns the same value.
1529+
1530+
The path must already be a 'str'. If the type is not known, use
1531+
os.fsdecode before calling this function.
1532+
[clinic start generated code]*/
1533+
1534+
static PyObject *
1535+
_winapi_GetLongPathName_impl(PyObject *module, LPCWSTR path)
1536+
/*[clinic end generated code: output=c4774b080275a2d0 input=9872e211e3a4a88f]*/
1537+
{
1538+
DWORD cchBuffer;
1539+
PyObject *result = NULL;
1540+
1541+
Py_BEGIN_ALLOW_THREADS
1542+
cchBuffer = GetLongPathNameW(path, NULL, 0);
1543+
Py_END_ALLOW_THREADS
1544+
if (cchBuffer) {
1545+
WCHAR *buffer = (WCHAR *)PyMem_Malloc(cchBuffer * sizeof(WCHAR));
1546+
if (buffer) {
1547+
Py_BEGIN_ALLOW_THREADS
1548+
cchBuffer = GetLongPathNameW(path, buffer, cchBuffer);
1549+
Py_END_ALLOW_THREADS
1550+
if (cchBuffer) {
1551+
result = PyUnicode_FromWideChar(buffer, cchBuffer);
1552+
} else {
1553+
PyErr_SetFromWindowsErr(0);
1554+
}
1555+
PyMem_Free((void *)buffer);
1556+
}
1557+
} else {
1558+
PyErr_SetFromWindowsErr(0);
1559+
}
1560+
return result;
1561+
}
1562+
15201563
/*[clinic input]
15211564
_winapi.GetModuleFileName
15221565
@@ -1551,6 +1594,48 @@ _winapi_GetModuleFileName_impl(PyObject *module, HMODULE module_handle)
15511594
return PyUnicode_FromWideChar(filename, wcslen(filename));
15521595
}
15531596

1597+
/*[clinic input]
1598+
_winapi.GetShortPathName
1599+
1600+
path: LPCWSTR
1601+
1602+
Return the short version of the provided path.
1603+
1604+
If the path is already in its short form, returns the same value.
1605+
1606+
The path must already be a 'str'. If the type is not known, use
1607+
os.fsdecode before calling this function.
1608+
[clinic start generated code]*/
1609+
1610+
static PyObject *
1611+
_winapi_GetShortPathName_impl(PyObject *module, LPCWSTR path)
1612+
/*[clinic end generated code: output=dab6ae494c621e81 input=43fa349aaf2ac718]*/
1613+
{
1614+
DWORD cchBuffer;
1615+
PyObject *result = NULL;
1616+
1617+
Py_BEGIN_ALLOW_THREADS
1618+
cchBuffer = GetShortPathNameW(path, NULL, 0);
1619+
Py_END_ALLOW_THREADS
1620+
if (cchBuffer) {
1621+
WCHAR *buffer = (WCHAR *)PyMem_Malloc(cchBuffer * sizeof(WCHAR));
1622+
if (buffer) {
1623+
Py_BEGIN_ALLOW_THREADS
1624+
cchBuffer = GetShortPathNameW(path, buffer, cchBuffer);
1625+
Py_END_ALLOW_THREADS
1626+
if (cchBuffer) {
1627+
result = PyUnicode_FromWideChar(buffer, cchBuffer);
1628+
} else {
1629+
PyErr_SetFromWindowsErr(0);
1630+
}
1631+
PyMem_Free((void *)buffer);
1632+
}
1633+
} else {
1634+
PyErr_SetFromWindowsErr(0);
1635+
}
1636+
return result;
1637+
}
1638+
15541639
/*[clinic input]
15551640
_winapi.GetStdHandle -> HANDLE
15561641
@@ -2846,7 +2931,9 @@ static PyMethodDef winapi_functions[] = {
28462931
_WINAPI_GETCURRENTPROCESS_METHODDEF
28472932
_WINAPI_GETEXITCODEPROCESS_METHODDEF
28482933
_WINAPI_GETLASTERROR_METHODDEF
2934+
_WINAPI_GETLONGPATHNAME_METHODDEF
28492935
_WINAPI_GETMODULEFILENAME_METHODDEF
2936+
_WINAPI_GETSHORTPATHNAME_METHODDEF
28502937
_WINAPI_GETSTDHANDLE_METHODDEF
28512938
_WINAPI_GETVERSION_METHODDEF
28522939
_WINAPI_MAPVIEWOFFILE_METHODDEF

0 commit comments

Comments
 (0)