Skip to content

Commit 1ec4261

Browse files
committed
Simplify logic for choosing get_win_folder’s value
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.
1 parent 5519a4f commit 1ec4261

File tree

1 file changed

+76
-76
lines changed

1 file changed

+76
-76
lines changed

src/platformdirs/windows.py

+76-76
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import contextlib
56
import os
67
import sys
78
from functools import lru_cache
@@ -12,6 +13,11 @@
1213
if TYPE_CHECKING:
1314
from collections.abc import Callable
1415

16+
with contextlib.suppress(ImportError):
17+
import ctypes
18+
with contextlib.suppress(ImportError):
19+
import winreg
20+
1521

1622
class Windows(PlatformDirsABC):
1723
"""
@@ -180,89 +186,83 @@ def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None:
180186
return None
181187

182188

183-
def get_win_folder_from_registry(csidl_name: str) -> str:
184-
"""
185-
Get folder from the registry.
186-
187-
This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct answer
188-
for all CSIDL_* names.
189+
if "winreg" in globals():
189190

190-
"""
191-
shell_folder_name = {
192-
"CSIDL_APPDATA": "AppData",
193-
"CSIDL_COMMON_APPDATA": "Common AppData",
194-
"CSIDL_LOCAL_APPDATA": "Local AppData",
195-
"CSIDL_PERSONAL": "Personal",
196-
"CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}",
197-
"CSIDL_MYPICTURES": "My Pictures",
198-
"CSIDL_MYVIDEO": "My Video",
199-
"CSIDL_MYMUSIC": "My Music",
200-
}.get(csidl_name)
201-
if shell_folder_name is None:
202-
msg = f"Unknown CSIDL name: {csidl_name}"
203-
raise ValueError(msg)
204-
if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows
205-
raise NotImplementedError
206-
import winreg # noqa: PLC0415
207-
208-
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
209-
directory, _ = winreg.QueryValueEx(key, shell_folder_name)
210-
return str(directory)
211-
212-
213-
def get_win_folder_via_ctypes(csidl_name: str) -> str:
214-
"""Get folder with ctypes."""
215-
# There is no 'CSIDL_DOWNLOADS'.
216-
# Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead.
217-
# https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
218-
219-
import ctypes # noqa: PLC0415
220-
221-
csidl_const = {
222-
"CSIDL_APPDATA": 26,
223-
"CSIDL_COMMON_APPDATA": 35,
224-
"CSIDL_LOCAL_APPDATA": 28,
225-
"CSIDL_PERSONAL": 5,
226-
"CSIDL_MYPICTURES": 39,
227-
"CSIDL_MYVIDEO": 14,
228-
"CSIDL_MYMUSIC": 13,
229-
"CSIDL_DOWNLOADS": 40,
230-
"CSIDL_DESKTOPDIRECTORY": 16,
231-
}.get(csidl_name)
232-
if csidl_const is None:
233-
msg = f"Unknown CSIDL name: {csidl_name}"
234-
raise ValueError(msg)
235-
236-
buf = ctypes.create_unicode_buffer(1024)
237-
windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker
238-
windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
239-
240-
# Downgrade to short path name if it has high-bit chars.
241-
if any(ord(c) > 255 for c in buf): # noqa: PLR2004
242-
buf2 = ctypes.create_unicode_buffer(1024)
243-
if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
244-
buf = buf2
191+
def get_win_folder_from_registry(csidl_name: str) -> str:
192+
"""
193+
Get folder from the registry.
245194
246-
if csidl_name == "CSIDL_DOWNLOADS":
247-
return os.path.join(buf.value, "Downloads") # noqa: PTH118
195+
This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct
196+
answer for all CSIDL_* names.
248197
249-
return buf.value
198+
"""
199+
shell_folder_name = {
200+
"CSIDL_APPDATA": "AppData",
201+
"CSIDL_COMMON_APPDATA": "Common AppData",
202+
"CSIDL_LOCAL_APPDATA": "Local AppData",
203+
"CSIDL_PERSONAL": "Personal",
204+
"CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}",
205+
"CSIDL_MYPICTURES": "My Pictures",
206+
"CSIDL_MYVIDEO": "My Video",
207+
"CSIDL_MYMUSIC": "My Music",
208+
}.get(csidl_name)
209+
if shell_folder_name is None:
210+
msg = f"Unknown CSIDL name: {csidl_name}"
211+
raise ValueError(msg)
212+
if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows
213+
raise NotImplementedError
214+
215+
key = winreg.OpenKey(
216+
winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
217+
)
218+
directory, _ = winreg.QueryValueEx(key, shell_folder_name)
219+
return str(directory)
220+
221+
222+
if "ctypes" in globals() and hasattr(ctypes, "windll"):
223+
224+
def get_win_folder_via_ctypes(csidl_name: str) -> str:
225+
"""Get folder with ctypes."""
226+
# There is no 'CSIDL_DOWNLOADS'.
227+
# Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead.
228+
# https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
229+
csidl_const = {
230+
"CSIDL_APPDATA": 26,
231+
"CSIDL_COMMON_APPDATA": 35,
232+
"CSIDL_LOCAL_APPDATA": 28,
233+
"CSIDL_PERSONAL": 5,
234+
"CSIDL_MYPICTURES": 39,
235+
"CSIDL_MYVIDEO": 14,
236+
"CSIDL_MYMUSIC": 13,
237+
"CSIDL_DOWNLOADS": 40,
238+
"CSIDL_DESKTOPDIRECTORY": 16,
239+
}.get(csidl_name)
240+
if csidl_const is None:
241+
msg = f"Unknown CSIDL name: {csidl_name}"
242+
raise ValueError(msg)
243+
244+
buf = ctypes.create_unicode_buffer(1024)
245+
windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker
246+
windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
247+
248+
# Downgrade to short path name if it has high-bit chars.
249+
if any(ord(c) > 255 for c in buf): # noqa: PLR2004
250+
buf2 = ctypes.create_unicode_buffer(1024)
251+
if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
252+
buf = buf2
253+
254+
if csidl_name == "CSIDL_DOWNLOADS":
255+
return os.path.join(buf.value, "Downloads") # noqa: PTH118
256+
257+
return buf.value
250258

251259

252260
def _pick_get_win_folder() -> Callable[[str], str]:
253-
try:
254-
import ctypes # noqa: PLC0415
255-
except ImportError:
256-
pass
257-
else:
258-
if hasattr(ctypes, "windll"):
259-
return get_win_folder_via_ctypes
260-
try:
261-
import winreg # noqa: PLC0415, F401
262-
except ImportError:
263-
return get_win_folder_from_env_vars
264-
else:
261+
if "get_win_folder_via_ctypes" in globals():
262+
return get_win_folder_via_ctypes
263+
if "get_win_folder_from_registry" in globals():
265264
return get_win_folder_from_registry
265+
return get_win_folder_from_env_vars
266266

267267

268268
get_win_folder = lru_cache(maxsize=None)(_pick_get_win_folder())

0 commit comments

Comments
 (0)