|
2 | 2 |
|
3 | 3 | from __future__ import annotations
|
4 | 4 |
|
| 5 | +import contextlib |
5 | 6 | import os
|
6 | 7 | import sys
|
7 | 8 | from functools import lru_cache
|
|
12 | 13 | if TYPE_CHECKING:
|
13 | 14 | from collections.abc import Callable
|
14 | 15 |
|
| 16 | +with contextlib.suppress(ImportError): |
| 17 | + import ctypes |
| 18 | +with contextlib.suppress(ImportError): |
| 19 | + import winreg |
| 20 | + |
15 | 21 |
|
16 | 22 | class Windows(PlatformDirsABC):
|
17 | 23 | """
|
@@ -180,89 +186,83 @@ def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None:
|
180 | 186 | return None
|
181 | 187 |
|
182 | 188 |
|
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(): |
189 | 190 |
|
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. |
245 | 194 |
|
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. |
248 | 197 |
|
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 |
250 | 258 |
|
251 | 259 |
|
252 | 260 | 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(): |
265 | 264 | return get_win_folder_from_registry
|
| 265 | + return get_win_folder_from_env_vars |
266 | 266 |
|
267 | 267 |
|
268 | 268 | get_win_folder = lru_cache(maxsize=None)(_pick_get_win_folder())
|
|
0 commit comments