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 64cd6fc commit 185999b
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 3 deletions.
33 changes: 32 additions & 1 deletion Lib/test/test_venv.py
Expand Up @@ -23,7 +23,8 @@
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 @@ -744,6 +745,36 @@ def test_cli_without_scm_ignore_files(self):
with self.assertRaises(FileNotFoundError):
self.get_text_file_contents('.gitignore')

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
35 changes: 35 additions & 0 deletions Lib/test/test_winapi.py
@@ -1,6 +1,9 @@
# Test the Windows-only _winapi module

import os
import pathlib
import random
import re
import threading
import time
import unittest
Expand Down Expand Up @@ -92,3 +95,35 @@ def test_many_events_waitany(self):

def test_max_events_waitany(self):
self._events_waitany_test(MAXIMUM_BATCHED_WAIT_OBJECTS)


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
Expand Up @@ -107,6 +107,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 @@ -171,7 +198,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
@@ -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
Expand Up @@ -1517,6 +1517,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 @@ -1551,6 +1594,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 @@ -2846,7 +2931,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

0 comments on commit 185999b

Please sign in to comment.