diff --git a/src/platformdirs/windows.py b/src/platformdirs/windows.py index c11c21a6..37e5f5d2 100644 --- a/src/platformdirs/windows.py +++ b/src/platformdirs/windows.py @@ -4,6 +4,7 @@ import os import sys +from functools import cache from pathlib import Path from typing import TYPE_CHECKING, Final @@ -11,6 +12,7 @@ if TYPE_CHECKING: from collections.abc import Callable + from ctypes import Structure # Not exposed by CPython; defined in the Windows SDK (shlobj_core.h) _KF_FLAG_DONT_VERIFY: Final[int] = 0x00004000 @@ -302,6 +304,21 @@ def get_win_folder_from_registry(csidl_name: str) -> str: } +@cache +def _get_guid_type() -> type[Structure]: + from ctypes import Structure, wintypes # noqa: PLC0415 + + class _Guid(Structure): + _fields_ = [ + ("Data1", wintypes.DWORD), + ("Data2", wintypes.WORD), + ("Data3", wintypes.WORD), + ("Data4", wintypes.BYTE * 8), + ] + + return _Guid + + def get_win_folder_via_ctypes(csidl_name: str) -> str: """Get folder via :func:`SHGetKnownFolderPath`. @@ -310,25 +327,23 @@ def get_win_folder_via_ctypes(csidl_name: str) -> str: """ if sys.platform != "win32": # only needed for type checker to know that this code runs only on Windows raise NotImplementedError - from ctypes import HRESULT, POINTER, Structure, WinDLL, byref, create_unicode_buffer, wintypes # noqa: PLC0415 - - class _GUID(Structure): - _fields_ = [ - ("Data1", wintypes.DWORD), - ("Data2", wintypes.WORD), - ("Data3", wintypes.WORD), - ("Data4", wintypes.BYTE * 8), - ] + from ctypes import HRESULT, POINTER, WinDLL, byref, create_unicode_buffer, wintypes # noqa: PLC0415 + guid_type = _get_guid_type() ole32 = WinDLL("ole32") ole32.CLSIDFromString.restype = HRESULT - ole32.CLSIDFromString.argtypes = [wintypes.LPCOLESTR, POINTER(_GUID)] + ole32.CLSIDFromString.argtypes = [wintypes.LPCOLESTR, POINTER(guid_type)] ole32.CoTaskMemFree.restype = None ole32.CoTaskMemFree.argtypes = [wintypes.LPVOID] shell32 = WinDLL("shell32") shell32.SHGetKnownFolderPath.restype = HRESULT - shell32.SHGetKnownFolderPath.argtypes = [POINTER(_GUID), wintypes.DWORD, wintypes.HANDLE, POINTER(wintypes.LPWSTR)] + shell32.SHGetKnownFolderPath.argtypes = [ + POINTER(guid_type), + wintypes.DWORD, + wintypes.HANDLE, + POINTER(wintypes.LPWSTR), + ] kernel32 = WinDLL("kernel32") kernel32.GetShortPathNameW.restype = wintypes.DWORD @@ -339,7 +354,7 @@ class _GUID(Structure): msg = f"Unknown CSIDL name: {csidl_name}" raise ValueError(msg) - guid = _GUID() + guid = guid_type() ole32.CLSIDFromString(folder_guid, byref(guid)) path_ptr = wintypes.LPWSTR() diff --git a/tests/test_windows.py b/tests/test_windows.py index c7f99f31..cc5f7f0a 100644 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -266,6 +266,42 @@ def test_get_win_folder_via_ctypes_passes_dont_verify_flag(mocker: MockerFixture assert flags_arg == _KF_FLAG_DONT_VERIFY +@pytest.mark.skipif(sys.platform == "win32", reason="mock-based type inspection only runs on non-Windows") +def test_get_win_folder_via_ctypes_reuses_guid_type(mocker: MockerFixture) -> None: + _setup_ctypes_mocks(mocker) + mock_pointer = mocker.patch("ctypes.POINTER", return_value=MagicMock()) + + mock_ole32 = MagicMock() + mock_shell32 = MagicMock() + mock_kernel32 = MagicMock() + mocker.patch.object( + ctypes, + "WinDLL", + MagicMock( + side_effect=lambda name: {"ole32": mock_ole32, "shell32": mock_shell32, "kernel32": mock_kernel32}[name], + ), + ) + mocker.patch("ctypes.byref", side_effect=lambda value: value) + + mock_path_ptr = MagicMock() + mock_path_ptr.value = r"C:\Users\Test\AppData\Local" + mocker.patch("ctypes.wintypes.LPWSTR", return_value=mock_path_ptr) + + try: + importlib.reload(windows) + from platformdirs.windows import get_win_folder_via_ctypes as fresh_fn # noqa: PLC0415 + + fresh_fn("CSIDL_LOCAL_APPDATA") + guid_type_from_first_call = mock_pointer.call_args_list[-2].args[0] + mock_pointer.reset_mock() + fresh_fn("CSIDL_LOCAL_APPDATA") + guid_type_from_second_call = mock_pointer.call_args_list[-2].args[0] + finally: + _cleanup_ctypes_mocks() + + assert guid_type_from_first_call is guid_type_from_second_call + + def test_get_win_folder_via_ctypes_unknown_csidl(mocker: MockerFixture) -> None: if sys.platform != "win32": _setup_ctypes_mocks(mocker, win_dll=MagicMock(side_effect=lambda _name: MagicMock()))