]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-90329: Add _winapi.GetLongPathName and GetShortPathName and use in venv to reduce...
authorSteve Dower <steve.dower@python.org>
Mon, 15 Apr 2024 14:36:06 +0000 (15:36 +0100)
committerGitHub <noreply@github.com>
Mon, 15 Apr 2024 14:36:06 +0000 (15:36 +0100)
Lib/test/test_venv.py
Lib/test/test_winapi.py
Lib/venv/__init__.py
Misc/NEWS.d/next/Windows/2024-04-12-14-02-58.gh-issue-90329.YpEeaO.rst [new file with mode: 0644]
Modules/_winapi.c
Modules/clinic/_winapi.c.h

index f410ce7198dc86b42a4be37a72718f7ff64e51ba..668642f73e8d9f9c989828954332d9e783a3fd8f 100644 (file)
@@ -23,7 +23,8 @@ from test.support import (captured_stdout, captured_stderr,
                           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
@@ -744,6 +745,36 @@ class BasicTest(BaseTest):
         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."""
index 014aeea7239e2b1db6772f7a98738ad2e2ec8b44..2ac6f3621710cd13bc4052f14a25e0dc17c541b2 100644 (file)
@@ -1,6 +1,9 @@
 # Test the Windows-only _winapi module
 
+import os
+import pathlib
 import random
+import re
 import threading
 import time
 import unittest
@@ -92,3 +95,35 @@ class WinAPIBatchedWaitForMultipleObjectsTests(unittest.TestCase):
 
     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)
index 4856594755ae577aaea06e7c848ca14be6d55871..fa69d5846f2fa795353b0268c03aa2e313dcf4c1 100644 (file)
@@ -107,6 +107,33 @@ class EnvBuilder:
         }
         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.
@@ -171,7 +198,7 @@ class EnvBuilder:
             # 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'
diff --git a/Misc/NEWS.d/next/Windows/2024-04-12-14-02-58.gh-issue-90329.YpEeaO.rst b/Misc/NEWS.d/next/Windows/2024-04-12-14-02-58.gh-issue-90329.YpEeaO.rst
new file mode 100644 (file)
index 0000000..7242428
--- /dev/null
@@ -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.
index 8f9b8520bb3f349b838fdae53c9af69e59776e61..57b8bdc7ea2448440674924c4f21da7a863c0d9b 100644 (file)
@@ -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
 
@@ -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
 
@@ -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
index 468457e624c6916e1008ae65182df50e92f77fdd..9acb2dc4fe7eba09de392a21159aee8638518328 100644 (file)
@@ -741,6 +741,76 @@ exit:
     return return_value;
 }
 
+PyDoc_STRVAR(_winapi_GetLongPathName__doc__,
+"GetLongPathName($module, /, path)\n"
+"--\n"
+"\n"
+"Return the long version of the provided path.\n"
+"\n"
+"If the path is already in its long form, returns the same value.\n"
+"\n"
+"The path must already be a \'str\'. If the type is not known, use\n"
+"os.fsdecode before calling this function.");
+
+#define _WINAPI_GETLONGPATHNAME_METHODDEF    \
+    {"GetLongPathName", _PyCFunction_CAST(_winapi_GetLongPathName), METH_FASTCALL|METH_KEYWORDS, _winapi_GetLongPathName__doc__},
+
+static PyObject *
+_winapi_GetLongPathName_impl(PyObject *module, LPCWSTR path);
+
+static PyObject *
+_winapi_GetLongPathName(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 1
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_item = { &_Py_ID(path), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"path", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "GetLongPathName",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[1];
+    LPCWSTR path = NULL;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!PyUnicode_Check(args[0])) {
+        _PyArg_BadArgument("GetLongPathName", "argument 'path'", "str", args[0]);
+        goto exit;
+    }
+    path = PyUnicode_AsWideCharString(args[0], NULL);
+    if (path == NULL) {
+        goto exit;
+    }
+    return_value = _winapi_GetLongPathName_impl(module, path);
+
+exit:
+    /* Cleanup for path */
+    PyMem_Free((void *)path);
+
+    return return_value;
+}
+
 PyDoc_STRVAR(_winapi_GetModuleFileName__doc__,
 "GetModuleFileName($module, module_handle, /)\n"
 "--\n"
@@ -775,6 +845,76 @@ exit:
     return return_value;
 }
 
+PyDoc_STRVAR(_winapi_GetShortPathName__doc__,
+"GetShortPathName($module, /, path)\n"
+"--\n"
+"\n"
+"Return the short version of the provided path.\n"
+"\n"
+"If the path is already in its short form, returns the same value.\n"
+"\n"
+"The path must already be a \'str\'. If the type is not known, use\n"
+"os.fsdecode before calling this function.");
+
+#define _WINAPI_GETSHORTPATHNAME_METHODDEF    \
+    {"GetShortPathName", _PyCFunction_CAST(_winapi_GetShortPathName), METH_FASTCALL|METH_KEYWORDS, _winapi_GetShortPathName__doc__},
+
+static PyObject *
+_winapi_GetShortPathName_impl(PyObject *module, LPCWSTR path);
+
+static PyObject *
+_winapi_GetShortPathName(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 1
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_item = { &_Py_ID(path), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"path", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "GetShortPathName",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[1];
+    LPCWSTR path = NULL;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!PyUnicode_Check(args[0])) {
+        _PyArg_BadArgument("GetShortPathName", "argument 'path'", "str", args[0]);
+        goto exit;
+    }
+    path = PyUnicode_AsWideCharString(args[0], NULL);
+    if (path == NULL) {
+        goto exit;
+    }
+    return_value = _winapi_GetShortPathName_impl(module, path);
+
+exit:
+    /* Cleanup for path */
+    PyMem_Free((void *)path);
+
+    return return_value;
+}
+
 PyDoc_STRVAR(_winapi_GetStdHandle__doc__,
 "GetStdHandle($module, std_handle, /)\n"
 "--\n"
@@ -1978,4 +2118,4 @@ exit:
 
     return return_value;
 }
-/*[clinic end generated code: output=1f5bbcfa8d1847c5 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=ed94a2482ede3744 input=a9049054013a1b77]*/