Skip to content

Commit

Permalink
gh-90329: Add _winapi.GetLongPathName and GetShortPathName and use in…
Browse files Browse the repository at this point in the history
… venv to reduce warnings (GH-117817)
  • Loading branch information
zooba committed Apr 15, 2024
1 parent 667a574 commit 1a45659
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 3 deletions.
33 changes: 32 additions & 1 deletion Lib/test/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
requires_subprocess, is_emscripten, is_wasi,
requires_venv_with_pip, TEST_HOME_DIR,
requires_resource, copy_python_src_ignore)
from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree)
from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree,
TESTFN)
import unittest
import venv
from unittest.mock import patch, Mock
Expand Down Expand Up @@ -639,6 +640,36 @@ def test_activate_shell_script_has_no_dos_newlines(self):
error_message = f"CR LF found in line {i}"
self.assertFalse(line.endswith(b'\r\n'), error_message)

def test_venv_same_path(self):
same_path = venv.EnvBuilder._same_path
if sys.platform == 'win32':
# Case-insensitive, and handles short/long names
tests = [
(True, TESTFN, TESTFN),
(True, TESTFN.lower(), TESTFN.upper()),
]
import _winapi
# ProgramFiles is the most reliable path that will have short/long
progfiles = os.getenv('ProgramFiles')
if progfiles:
tests = [
*tests,
(True, progfiles, progfiles),
(True, _winapi.GetShortPathName(progfiles), _winapi.GetLongPathName(progfiles)),
]
else:
# Just a simple case-sensitive comparison
tests = [
(True, TESTFN, TESTFN),
(False, TESTFN.lower(), TESTFN.upper()),
]
for r, path1, path2 in tests:
with self.subTest(f"{path1}-{path2}"):
if r:
self.assertTrue(same_path(path1, path2))
else:
self.assertFalse(same_path(path1, path2))

@requireVenvCreate
class EnsurePipTest(BaseTest):
"""Test venv module installation of pip."""
Expand Down
40 changes: 40 additions & 0 deletions Lib/test/test_winapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Test the Windows-only _winapi module

import os
import pathlib
import re
import unittest
from test.support import import_helper

_winapi = import_helper.import_module('_winapi', required_on=['win'])

class WinAPITests(unittest.TestCase):
def test_getlongpathname(self):
testfn = pathlib.Path(os.getenv("ProgramFiles")).parents[-1] / "PROGRA~1"
if not os.path.isdir(testfn):
raise unittest.SkipTest("require x:\\PROGRA~1 to test")

# pathlib.Path will be rejected - only str is accepted
with self.assertRaises(TypeError):
_winapi.GetLongPathName(testfn)

actual = _winapi.GetLongPathName(os.fsdecode(testfn))

# Can't assume that PROGRA~1 expands to any particular variation, so
# ensure it matches any one of them.
candidates = set(testfn.parent.glob("Progra*"))
self.assertIn(pathlib.Path(actual), candidates)

def test_getshortpathname(self):
testfn = pathlib.Path(os.getenv("ProgramFiles"))
if not os.path.isdir(testfn):
raise unittest.SkipTest("require '%ProgramFiles%' to test")

# pathlib.Path will be rejected - only str is accepted
with self.assertRaises(TypeError):
_winapi.GetShortPathName(testfn)

actual = _winapi.GetShortPathName(os.fsdecode(testfn))

# Should contain "PROGRA~" but we can't predict the number
self.assertIsNotNone(re.match(r".\:\\PROGRA~\d", actual.upper()), actual)
29 changes: 28 additions & 1 deletion Lib/venv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ def _venv_path(self, env_dir, name):
}
return sysconfig.get_path(name, scheme='venv', vars=vars)

@classmethod
def _same_path(cls, path1, path2):
"""Check whether two paths appear the same.
Whether they refer to the same file is irrelevant; we're testing for
whether a human reader would look at the path string and easily tell
that they're the same file.
"""
if sys.platform == 'win32':
if os.path.normcase(path1) == os.path.normcase(path2):
return True
# gh-90329: Don't display a warning for short/long names
import _winapi
try:
path1 = _winapi.GetLongPathName(os.fsdecode(path1))
except OSError:
pass
try:
path2 = _winapi.GetLongPathName(os.fsdecode(path2))
except OSError:
pass
if os.path.normcase(path1) == os.path.normcase(path2):
return True
return False
else:
return path1 == path2

def ensure_directories(self, env_dir):
"""
Create the directories for the environment.
Expand Down Expand Up @@ -162,7 +189,7 @@ def create_if_needed(d):
# bpo-45337: Fix up env_exec_cmd to account for file system redirections.
# Some redirects only apply to CreateFile and not CreateProcess
real_env_exe = os.path.realpath(context.env_exe)
if os.path.normcase(real_env_exe) != os.path.normcase(context.env_exe):
if not self._same_path(real_env_exe, context.env_exe):
logger.warning('Actual environment location may have moved due to '
'redirects, links or junctions.\n'
' Requested location: "%s"\n'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Suppress the warning displayed on virtual environment creation when the
requested and created paths differ only by a short (8.3 style) name.
Warnings will continue to be shown if a junction or symlink in the path
caused the venv to be created in a different location than originally
requested.
87 changes: 87 additions & 0 deletions Modules/_winapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,49 @@ _winapi_GetLastError_impl(PyObject *module)
return GetLastError();
}


/*[clinic input]
_winapi.GetLongPathName
path: LPCWSTR
Return the long version of the provided path.
If the path is already in its long form, returns the same value.
The path must already be a 'str'. If the type is not known, use
os.fsdecode before calling this function.
[clinic start generated code]*/

static PyObject *
_winapi_GetLongPathName_impl(PyObject *module, LPCWSTR path)
/*[clinic end generated code: output=c4774b080275a2d0 input=9872e211e3a4a88f]*/
{
DWORD cchBuffer;
PyObject *result = NULL;

Py_BEGIN_ALLOW_THREADS
cchBuffer = GetLongPathNameW(path, NULL, 0);
Py_END_ALLOW_THREADS
if (cchBuffer) {
WCHAR *buffer = (WCHAR *)PyMem_Malloc(cchBuffer * sizeof(WCHAR));
if (buffer) {
Py_BEGIN_ALLOW_THREADS
cchBuffer = GetLongPathNameW(path, buffer, cchBuffer);
Py_END_ALLOW_THREADS
if (cchBuffer) {
result = PyUnicode_FromWideChar(buffer, cchBuffer);
} else {
PyErr_SetFromWindowsErr(0);
}
PyMem_Free((void *)buffer);
}
} else {
PyErr_SetFromWindowsErr(0);
}
return result;
}

/*[clinic input]
_winapi.GetModuleFileName
Expand Down Expand Up @@ -1486,6 +1529,48 @@ _winapi_GetModuleFileName_impl(PyObject *module, HMODULE module_handle)
return PyUnicode_FromWideChar(filename, wcslen(filename));
}

/*[clinic input]
_winapi.GetShortPathName
path: LPCWSTR
Return the short version of the provided path.
If the path is already in its short form, returns the same value.
The path must already be a 'str'. If the type is not known, use
os.fsdecode before calling this function.
[clinic start generated code]*/

static PyObject *
_winapi_GetShortPathName_impl(PyObject *module, LPCWSTR path)
/*[clinic end generated code: output=dab6ae494c621e81 input=43fa349aaf2ac718]*/
{
DWORD cchBuffer;
PyObject *result = NULL;

Py_BEGIN_ALLOW_THREADS
cchBuffer = GetShortPathNameW(path, NULL, 0);
Py_END_ALLOW_THREADS
if (cchBuffer) {
WCHAR *buffer = (WCHAR *)PyMem_Malloc(cchBuffer * sizeof(WCHAR));
if (buffer) {
Py_BEGIN_ALLOW_THREADS
cchBuffer = GetShortPathNameW(path, buffer, cchBuffer);
Py_END_ALLOW_THREADS
if (cchBuffer) {
result = PyUnicode_FromWideChar(buffer, cchBuffer);
} else {
PyErr_SetFromWindowsErr(0);
}
PyMem_Free((void *)buffer);
}
} else {
PyErr_SetFromWindowsErr(0);
}
return result;
}

/*[clinic input]
_winapi.GetStdHandle -> HANDLE
Expand Down Expand Up @@ -2345,7 +2430,9 @@ static PyMethodDef winapi_functions[] = {
_WINAPI_GETCURRENTPROCESS_METHODDEF
_WINAPI_GETEXITCODEPROCESS_METHODDEF
_WINAPI_GETLASTERROR_METHODDEF
_WINAPI_GETLONGPATHNAME_METHODDEF
_WINAPI_GETMODULEFILENAME_METHODDEF
_WINAPI_GETSHORTPATHNAME_METHODDEF
_WINAPI_GETSTDHANDLE_METHODDEF
_WINAPI_GETVERSION_METHODDEF
_WINAPI_MAPVIEWOFFILE_METHODDEF
Expand Down
Loading

0 comments on commit 1a45659

Please sign in to comment.