From 1ec42612834b6032e11efe136fd641a576cbbb9a Mon Sep 17 00:00:00 2001 From: Jason Yundt Date: Thu, 17 Apr 2025 07:30:12 -0400 Subject: [PATCH 1/2] =?UTF-8?q?Simplify=20logic=20for=20choosing=20`get=5F?= =?UTF-8?q?win=5Ffolder`=E2=80=99s=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this change, `windows.py` had multiple different `import ctypes` and `import winreg` lines. `windows.py` did this for a specific reason. The `get_win_folder_via_ctypes()` function depends on the `ctypes` module, and the `get_win_folder_from_registry()` functions depends on the `winreg` modules, but `windows.py` as a whole is supposed to continue to work even if `ctypes` and `winreg` are not available. Before this change, `windows.py` would try to avoid errors by only calling either of those two functions if the module that that function requires is not available. This meant that there had to be two `import ctypes` lines and two `import winreg` lines. The first one would be inside the body of the function that that depends on the module, and the second one would be in a test that determines if the function should be called. The idea was to only call `get_win_folder_via_ctypes()` and `get_win_folder_from_registry()` if their respective modules are available. This change simplifies `windows.py`. Previously, those two functions were always defined but not always called. Now, those two fuctions are not always defined and not always called. This means that there’s now only one `import ctypes` line and only one `import winreg` line. As an added bonus, those two `import` statements are now at the top of the file (like the rest of the `import` statements) and the `_pick_get_win_folder()` function is now simpler. The main motivation behind this change is to make it easier to create a future change. That future change will add additional code to `windows.py` that depends on the `ctypes` module. This change will allow me to add that additional code without adding an additional `import ctypes` line. --- src/platformdirs/windows.py | 152 ++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/src/platformdirs/windows.py b/src/platformdirs/windows.py index d7bc960..3abf054 100644 --- a/src/platformdirs/windows.py +++ b/src/platformdirs/windows.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib import os import sys from functools import lru_cache @@ -12,6 +13,11 @@ if TYPE_CHECKING: from collections.abc import Callable +with contextlib.suppress(ImportError): + import ctypes +with contextlib.suppress(ImportError): + import winreg + class Windows(PlatformDirsABC): """ @@ -180,89 +186,83 @@ def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None: return None -def get_win_folder_from_registry(csidl_name: str) -> str: - """ - Get folder from the registry. - - This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct answer - for all CSIDL_* names. +if "winreg" in globals(): - """ - shell_folder_name = { - "CSIDL_APPDATA": "AppData", - "CSIDL_COMMON_APPDATA": "Common AppData", - "CSIDL_LOCAL_APPDATA": "Local AppData", - "CSIDL_PERSONAL": "Personal", - "CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}", - "CSIDL_MYPICTURES": "My Pictures", - "CSIDL_MYVIDEO": "My Video", - "CSIDL_MYMUSIC": "My Music", - }.get(csidl_name) - if shell_folder_name is None: - msg = f"Unknown CSIDL name: {csidl_name}" - raise ValueError(msg) - if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows - raise NotImplementedError - import winreg # noqa: PLC0415 - - key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") - directory, _ = winreg.QueryValueEx(key, shell_folder_name) - return str(directory) - - -def get_win_folder_via_ctypes(csidl_name: str) -> str: - """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 - - 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, - }.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) - - # 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 + def get_win_folder_from_registry(csidl_name: str) -> str: + """ + Get folder from the registry. - if csidl_name == "CSIDL_DOWNLOADS": - return os.path.join(buf.value, "Downloads") # noqa: PTH118 + This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct + answer for all CSIDL_* names. - return buf.value + """ + shell_folder_name = { + "CSIDL_APPDATA": "AppData", + "CSIDL_COMMON_APPDATA": "Common AppData", + "CSIDL_LOCAL_APPDATA": "Local AppData", + "CSIDL_PERSONAL": "Personal", + "CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}", + "CSIDL_MYPICTURES": "My Pictures", + "CSIDL_MYVIDEO": "My Video", + "CSIDL_MYMUSIC": "My Music", + }.get(csidl_name) + if shell_folder_name is None: + msg = f"Unknown CSIDL name: {csidl_name}" + raise ValueError(msg) + if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows + raise NotImplementedError + + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" + ) + directory, _ = winreg.QueryValueEx(key, shell_folder_name) + return str(directory) + + +if "ctypes" in globals() and hasattr(ctypes, "windll"): + + def get_win_folder_via_ctypes(csidl_name: str) -> str: + """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 + 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, + }.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) + + # 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 + + if csidl_name == "CSIDL_DOWNLOADS": + return os.path.join(buf.value, "Downloads") # noqa: PTH118 + + return buf.value def _pick_get_win_folder() -> Callable[[str], str]: - try: - import ctypes # noqa: PLC0415 - except ImportError: - pass - else: - if hasattr(ctypes, "windll"): - return get_win_folder_via_ctypes - try: - import winreg # noqa: PLC0415, F401 - except ImportError: - return get_win_folder_from_env_vars - else: + if "get_win_folder_via_ctypes" in globals(): + return get_win_folder_via_ctypes + if "get_win_folder_from_registry" in globals(): return get_win_folder_from_registry + return get_win_folder_from_env_vars get_win_folder = lru_cache(maxsize=None)(_pick_get_win_folder()) From 2d2187030770ff9325985cab8d8586f8307b63e3 Mon Sep 17 00:00:00 2001 From: Jason Yundt Date: Thu, 17 Apr 2025 13:23:33 -0400 Subject: [PATCH 2/2] Use `SHGetKnownFolderPath()` instead of `SHGetFolderPathW()` The official documentation for `SHGetFolderPathW()` says: > Note As of Windows Vista, this function is merely a wrapper for > SHGetKnownFolderPath. The CSIDL value is translated to its associated > KNOWNFOLDERID and then SHGetKnownFolderPath is called. New > applications should use the known folder system rather than the older > CSIDL system, which is supported only for backward compatibility. Source: This change makes it so that platformdirs uses `SHGetKnownFolderPath()` instead of `SHGetFolderPathW()`. This change also removes references to the old CSIDL system and replaces them with references wo the FOLDERID system. Closes #348. --- src/platformdirs/windows.py | 205 ++++++++++++++++++++++++------------ 1 file changed, 139 insertions(+), 66 deletions(-) diff --git a/src/platformdirs/windows.py b/src/platformdirs/windows.py index 3abf054..8bc7dfb 100644 --- a/src/platformdirs/windows.py +++ b/src/platformdirs/windows.py @@ -2,7 +2,6 @@ from __future__ import annotations -import contextlib import os import sys from functools import lru_cache @@ -13,10 +12,14 @@ if TYPE_CHECKING: from collections.abc import Callable -with contextlib.suppress(ImportError): +try: # noqa: SIM105 import ctypes -with contextlib.suppress(ImportError): +except ImportError: + pass +try: # noqa: SIM105 import winreg +except ImportError: + pass class Windows(PlatformDirsABC): @@ -37,7 +40,7 @@ def user_data_dir(self) -> str: ``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname`` (not roaming) or ``%USERPROFILE%\\AppData\\Roaming\\$appauthor\\$appname`` (roaming) """ - const = "CSIDL_APPDATA" if self.roaming else "CSIDL_LOCAL_APPDATA" + const = "FOLDERID_RoamingAppData" if self.roaming else "FOLDERID_LocalAppData" path = os.path.normpath(get_win_folder(const)) return self._append_parts(path) @@ -59,7 +62,7 @@ def _append_parts(self, path: str, *, opinion_value: str | None = None) -> str: @property def site_data_dir(self) -> str: """:return: data directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname``""" - path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA")) + path = os.path.normpath(get_win_folder("FOLDERID_ProgramData")) return self._append_parts(path) @property @@ -78,13 +81,13 @@ def user_cache_dir(self) -> str: :return: cache directory tied to the user (if opinionated with ``Cache`` folder within ``$appname``) e.g. ``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname\\Cache\\$version`` """ - path = os.path.normpath(get_win_folder("CSIDL_LOCAL_APPDATA")) + path = os.path.normpath(get_win_folder("FOLDERID_LocalAppData")) return self._append_parts(path, opinion_value="Cache") @property def site_cache_dir(self) -> str: """:return: cache directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname\\Cache\\$version``""" - path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA")) + path = os.path.normpath(get_win_folder("FOLDERID_ProgramData")) return self._append_parts(path, opinion_value="Cache") @property @@ -104,32 +107,32 @@ def user_log_dir(self) -> str: @property def user_documents_dir(self) -> str: """:return: documents directory tied to the user e.g. ``%USERPROFILE%\\Documents``""" - return os.path.normpath(get_win_folder("CSIDL_PERSONAL")) + return os.path.normpath(get_win_folder("FOLDERID_Documents")) @property def user_downloads_dir(self) -> str: """:return: downloads directory tied to the user e.g. ``%USERPROFILE%\\Downloads``""" - return os.path.normpath(get_win_folder("CSIDL_DOWNLOADS")) + return os.path.normpath(get_win_folder("FOLDERID_Downloads")) @property def user_pictures_dir(self) -> str: """:return: pictures directory tied to the user e.g. ``%USERPROFILE%\\Pictures``""" - return os.path.normpath(get_win_folder("CSIDL_MYPICTURES")) + return os.path.normpath(get_win_folder("FOLDERID_Pictures")) @property def user_videos_dir(self) -> str: """:return: videos directory tied to the user e.g. ``%USERPROFILE%\\Videos``""" - return os.path.normpath(get_win_folder("CSIDL_MYVIDEO")) + return os.path.normpath(get_win_folder("FOLDERID_Videos")) @property def user_music_dir(self) -> str: """:return: music directory tied to the user e.g. ``%USERPROFILE%\\Music``""" - return os.path.normpath(get_win_folder("CSIDL_MYMUSIC")) + return os.path.normpath(get_win_folder("FOLDERID_Music")) @property def user_desktop_dir(self) -> str: """:return: desktop directory tied to the user, e.g. ``%USERPROFILE%\\Desktop``""" - return os.path.normpath(get_win_folder("CSIDL_DESKTOPDIRECTORY")) + return os.path.normpath(get_win_folder("FOLDERID_Desktop")) @property def user_runtime_dir(self) -> str: @@ -137,7 +140,7 @@ def user_runtime_dir(self) -> str: :return: runtime directory tied to the user, e.g. ``%USERPROFILE%\\AppData\\Local\\Temp\\$appauthor\\$appname`` """ - path = os.path.normpath(os.path.join(get_win_folder("CSIDL_LOCAL_APPDATA"), "Temp")) # noqa: PTH118 + path = os.path.normpath(os.path.join(get_win_folder("FOLDERID_LocalAppData"), "Temp")) # noqa: PTH118 return self._append_parts(path) @property @@ -146,19 +149,19 @@ def site_runtime_dir(self) -> str: return self.user_runtime_dir -def get_win_folder_from_env_vars(csidl_name: str) -> str: +def get_win_folder_from_env_vars(folderid_name: str) -> str: """Get folder from environment variables.""" - result = get_win_folder_if_csidl_name_not_env_var(csidl_name) + result = get_win_folder_if_folderid_name_not_env_var(folderid_name) if result is not None: return result env_var_name = { - "CSIDL_APPDATA": "APPDATA", - "CSIDL_COMMON_APPDATA": "ALLUSERSPROFILE", - "CSIDL_LOCAL_APPDATA": "LOCALAPPDATA", - }.get(csidl_name) + "FOLDERID_RoamingAppData": "APPDATA", + "FOLDERID_ProgramData": "ALLUSERSPROFILE", + "FOLDERID_LocalAppData": "LOCALAPPDATA", + }.get(folderid_name) if env_var_name is None: - msg = f"Unknown CSIDL name: {csidl_name}" + msg = f"Unknown FOLDERID name: {folderid_name}" raise ValueError(msg) result = os.environ.get(env_var_name) if result is None: @@ -167,47 +170,49 @@ def get_win_folder_from_env_vars(csidl_name: str) -> str: return result -def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None: - """Get a folder for a CSIDL name that does not exist as an environment variable.""" - if csidl_name == "CSIDL_PERSONAL": +def get_win_folder_if_folderid_name_not_env_var(folderid_name: str) -> str | None: + """Get a folder for a FOLDERID name that does not exist as an environment variable.""" + if folderid_name == "FOLDERID_Documents": return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Documents") # noqa: PTH118 - if csidl_name == "CSIDL_DOWNLOADS": + if folderid_name == "FOLDERID_Downloads": return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Downloads") # noqa: PTH118 - if csidl_name == "CSIDL_MYPICTURES": + if folderid_name == "FOLDERID_Pictures": return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Pictures") # noqa: PTH118 - if csidl_name == "CSIDL_MYVIDEO": + if folderid_name == "FOLDERID_Videos": return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Videos") # noqa: PTH118 - if csidl_name == "CSIDL_MYMUSIC": + if folderid_name == "FOLDERID_Music": return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Music") # noqa: PTH118 return None +FOLDERID_Downloads_guid_string = "374DE290-123F-4565-9164-39C4925E467B" + if "winreg" in globals(): - def get_win_folder_from_registry(csidl_name: str) -> str: + def get_win_folder_from_registry(folderid_name: str) -> str: """ Get folder from the registry. This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct - answer for all CSIDL_* names. + answer for all FOLDERID_* names. """ shell_folder_name = { - "CSIDL_APPDATA": "AppData", - "CSIDL_COMMON_APPDATA": "Common AppData", - "CSIDL_LOCAL_APPDATA": "Local AppData", - "CSIDL_PERSONAL": "Personal", - "CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}", - "CSIDL_MYPICTURES": "My Pictures", - "CSIDL_MYVIDEO": "My Video", - "CSIDL_MYMUSIC": "My Music", - }.get(csidl_name) + "FOLDERID_RoamingAppData": "AppData", + "FOLDERID_ProgramData": "Common AppData", + "FOLDERID_LocalAppData": "Local AppData", + "FOLDERID_Documents": "Personal", + "FOLDERID_Downloads": "{" + FOLDERID_Downloads_guid_string + "}", + "FOLDERID_Pictures": "My Pictures", + "FOLDERID_Videos": "My Video", + "FOLDERID_Music": "My Music", + }.get(folderid_name) if shell_folder_name is None: - msg = f"Unknown CSIDL name: {csidl_name}" + msg = f"Unknown FOLDERID name: {folderid_name}" raise ValueError(msg) if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows raise NotImplementedError @@ -221,40 +226,108 @@ def get_win_folder_from_registry(csidl_name: str) -> str: if "ctypes" in globals() and hasattr(ctypes, "windll"): - def get_win_folder_via_ctypes(csidl_name: str) -> str: + class GUID(ctypes.Structure): + """ + ` + The GUID structure from Windows's guiddef.h header + `_. + """ + + Data4Type = ctypes.c_ubyte * 8 + + _fields_ = ( + ("Data1", ctypes.c_ulong), + ("Data2", ctypes.c_ushort), + ("Data3", ctypes.c_ushort), + ("Data4", Data4Type), + ) + + def __init__(self, guid_string: str) -> None: + digit_groups = guid_string.split("-") + expected_digit_groups = 5 + if len(digit_groups) != expected_digit_groups: + msg = f"The guid_string, {guid_string!r}, does not contain {expected_digit_groups} groups of digits." + raise ValueError(msg) + for digit_group, expected_length in zip(digit_groups, (8, 4, 4, 4, 12)): + if len(digit_group) != expected_length: + msg = ( + f"The digit group, {digit_group!r}, in the guid_string, {guid_string!r}, was the wrong length. " + f"It should have been {expected_length} digits long." + ) + raise ValueError(msg) + data_4_as_bytes = bytes.fromhex(digit_groups[3]) + bytes.fromhex(digit_groups[4]) + + super().__init__( + int(digit_groups[0], base=16), + int(digit_groups[1], base=16), + int(digit_groups[2], base=16), + self.Data4Type(*(eight_bit_int for eight_bit_int in data_4_as_bytes)), + ) + + def __repr__(self) -> str: + guid_string = f"{self.Data1:08X}-{self.Data2:04X}-{self.Data3:04X}-" + for i in range(len(self.Data4)): + guid_string += f"{self.Data4[i]:02X}" + if i == 1: + guid_string += "-" + return f"{type(self).__qualname__}({guid_string!r})" + + def get_win_folder_via_ctypes(folderid_name: str) -> str: # noqa: C901, PLR0912 """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 - 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, - }.get(csidl_name) - if csidl_const is None: - msg = f"Unknown CSIDL name: {csidl_name}" + if folderid_name == "FOLDERID_RoamingAppData": + folderid_const = GUID("3EB685DB-65F9-4CF6-A03A-E3EF65729F3D") + elif folderid_name == "FOLDERID_ProgramData": + folderid_const = GUID("62AB5D82-FDC1-4DC3-A9DD-070D1D495D97") + elif folderid_name == "FOLDERID_LocalAppData": + folderid_const = GUID("F1B32785-6FBA-4FCF-9D55-7B8E7F157091") + elif folderid_name == "FOLDERID_Documents": + folderid_const = GUID("FDD39AD0-238F-46AF-ADB4-6C85480369C7") + elif folderid_name == "FOLDERID_Pictures": + folderid_const = GUID("33E28130-4E1E-4676-835A-98395C3BC3BB") + elif folderid_name == "FOLDERID_Videos": + folderid_const = GUID("18989B1D-99B5-455B-841C-AB7C74E4DDFC") + elif folderid_name == "FOLDERID_Music": + folderid_const = GUID("4BD8D571-6D19-48D3-BE97-422220080E43") + elif folderid_name == "FOLDERID_Downloads": + folderid_const = GUID(FOLDERID_Downloads_guid_string) + elif folderid_name == "FOLDERID_Desktop": + folderid_const = GUID("B4BFCC3A-DB2C-424C-B029-7FE99A87C641") + else: + msg = f"Unknown FOLDERID name: {folderid_name}" raise ValueError(msg) + # https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ne-shlobj_core-known_folder_flag + kf_flag_default = 0 + # https://learn.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values + s_ok = 0 - buf = ctypes.create_unicode_buffer(1024) + pointer_to_pointer_to_wchars = ctypes.pointer(ctypes.c_wchar_p()) 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) + error_code = windll.shell32.SHGetKnownFolderPath( + ctypes.pointer(folderid_const), kf_flag_default, None, pointer_to_pointer_to_wchars + ) + return_value = pointer_to_pointer_to_wchars.contents.value + # The documentation for SHGetKnownFolderPath() says that this needs to be freed using CoTaskMemFree(): + # https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath#parameters + windll.ole32.CoTaskMemFree(pointer_to_pointer_to_wchars.contents) + # Make sure that we don't accidentally use the memory now that we've freed it. + del pointer_to_pointer_to_wchars + if error_code != s_ok: + # I'm using :08X as the format here because that's the format that the official documentation for HRESULT + # uses: https://learn.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values + msg = f"SHGetKnownFolderPath() failed with this error code: 0x{error_code:08X}" + raise RuntimeError(msg) + if return_value is None: + msg = "SHGetKnownFolderPath() succeeded, but it gave us a null pointer. This should never happen." + raise RuntimeError(msg) # 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 - - if csidl_name == "CSIDL_DOWNLOADS": - return os.path.join(buf.value, "Downloads") # noqa: PTH118 + if any(ord(c) > 255 for c in return_value): # noqa: PLR2004 + buf = ctypes.create_unicode_buffer(len(return_value)) + if windll.kernel32.GetShortPathNameW(return_value, buf, len(buf)): + return_value = buf.value - return buf.value + return return_value def _pick_get_win_folder() -> Callable[[str], str]: