Skip to content
Draft
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
98 changes: 74 additions & 24 deletions src/platformdirs/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
if TYPE_CHECKING:
from collections.abc import Callable

assert sys.platform == "win32"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should do this.

Copy link
Author

@moi15moi moi15moi Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just that mypy is kinda shit for file with platform specific code.
Anyways, it breaks the doc generation.

To be honest, i don't know how to fix those kinds of mypy error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you need to solve it differently.



class Windows(PlatformDirsABC):
"""
Expand Down Expand Up @@ -210,52 +212,100 @@ def get_win_folder_from_registry(csidl_name: str) -> str:
return str(directory)


def get_win_folder_via_ctypes(csidl_name: str) -> str:
def get_win_folder_via_ctypes(csidl_name: str) -> str: # noqa: PLR0914
"""Get folder with ctypes."""
# There is no 'CSIDL_DOWNLOADS'.
# Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead.
# https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid

import ctypes # noqa: PLC0415
from ctypes import HRESULT, POINTER, Structure, WinDLL, byref, create_unicode_buffer, wintypes # noqa: PLC0415

class GUID(Structure):
_fields_ = [ # noqa: RUF012
("Data1", wintypes.DWORD),
("Data2", wintypes.WORD),
("Data3", wintypes.WORD),
("Data4", wintypes.BYTE * 8),
]

@classmethod
def from_uuid(cls, clsid: str) -> GUID:
guid = GUID()
CLSIDFromString(clsid, byref(guid))
return guid

LPCLSID = POINTER(GUID) # noqa: N806

ole32_dll = WinDLL("ole32")

CLSIDFromString = ole32_dll.CLSIDFromString # noqa: N806
CLSIDFromString.restype = HRESULT
CLSIDFromString.argtypes = [wintypes.LPCOLESTR, LPCLSID]

CoTaskMemFree = ole32_dll.CoTaskMemFree # noqa: N806
CoTaskMemFree.restype = None
CoTaskMemFree.argtypes = [wintypes.LPVOID]

KNOWNFOLDERID = GUID # noqa: N806
REFKNOWNFOLDERID = POINTER(KNOWNFOLDERID) # noqa: N806

shell32_dll = WinDLL("shell32")
SHGetKnownFolderPath = shell32_dll.SHGetKnownFolderPath # noqa: N806
SHGetKnownFolderPath.restype = HRESULT
SHGetKnownFolderPath.argtypes = [REFKNOWNFOLDERID, wintypes.DWORD, wintypes.HANDLE, POINTER(wintypes.LPWSTR)]

kernel32_dll = WinDLL("kernel32")
GetShortPathNameW = kernel32_dll.GetShortPathNameW # noqa: N806
GetShortPathNameW.restype = wintypes.DWORD
GetShortPathNameW.argtypes = [wintypes.LPWSTR, wintypes.LPWSTR, wintypes.DWORD]

csidl_const = {
"CSIDL_APPDATA": 26,
"CSIDL_COMMON_APPDATA": 35,
"CSIDL_LOCAL_APPDATA": 28,
"CSIDL_PERSONAL": 5,
"CSIDL_MYPICTURES": 39,
"CSIDL_MYVIDEO": 14,
"CSIDL_MYMUSIC": 13,
"CSIDL_DOWNLOADS": 40,
"CSIDL_DESKTOPDIRECTORY": 16,
"CSIDL_APPDATA": "{3EB685DB-65F9-4CF6-A03A-E3EF65729F3D}",
"CSIDL_COMMON_APPDATA": "{62AB5D82-FDC1-4DC3-A9DD-070D1D495D97}",
"CSIDL_LOCAL_APPDATA": "{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}",
"CSIDL_PERSONAL": "{FDD39AD0-238F-46AF-ADB4-6C85480369C7}",
"CSIDL_MYPICTURES": "{33E28130-4E1E-4676-835A-98395C3BC3BB}",
"CSIDL_MYVIDEO": "{18989B1D-99B5-455B-841C-AB7C74E4DDFC}",
"CSIDL_MYMUSIC": "{4BD8D571-6D19-48D3-BE97-422220080E43}",
"CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}",
"CSIDL_DESKTOPDIRECTORY": "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}",
}.get(csidl_name)
if csidl_const is None:
msg = f"Unknown CSIDL name: {csidl_name}"
raise ValueError(msg)

buf = ctypes.create_unicode_buffer(1024)
windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker
windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
guid = GUID.from_uuid(csidl_const)
path_buf = wintypes.LPWSTR()

# Downgrade to short path name if it has high-bit chars.
if any(ord(c) > 255 for c in buf): # noqa: PLR2004
buf2 = ctypes.create_unicode_buffer(1024)
if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
buf = buf2
SHGetKnownFolderPath(byref(guid), 0, None, byref(path_buf))
win_folder = path_buf.value

if csidl_name == "CSIDL_DOWNLOADS":
return os.path.join(buf.value, "Downloads") # noqa: PTH118
CoTaskMemFree(path_buf)

if win_folder is None:
msg = "Unexpected, win_folder is None. This should never happen."
raise ValueError(msg)

# Downgrade to short path name if it has high-bit chars.
if any(ord(c) > 255 for c in win_folder): # noqa: PLR2004
buf2 = create_unicode_buffer(1024)
if GetShortPathNameW(win_folder, buf2, 1024):
win_folder = buf2.value

return buf.value
return win_folder


def _pick_get_win_folder() -> Callable[[str], str]:
try:
import ctypes # noqa: PLC0415
import ctypes # noqa: PLC0415, F401
except ImportError:
pass
else:
if hasattr(ctypes, "windll"):
try:
from ctypes import WinDLL # noqa: PLC0415, F401
except ImportError:
pass
else:
return get_win_folder_via_ctypes
try:
import winreg # noqa: PLC0415, F401
Expand Down