Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions src/platformdirs/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

import os
import sys
from functools import cache
from pathlib import Path
from typing import TYPE_CHECKING, Final

from .api import PlatformDirsABC

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
Expand Down Expand Up @@ -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`.

Expand All @@ -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
Expand All @@ -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()
Expand Down
36 changes: 36 additions & 0 deletions tests/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down