2
2
3
3
from __future__ import annotations
4
4
5
- import contextlib
6
5
import os
7
6
import sys
8
7
from functools import lru_cache
13
12
if TYPE_CHECKING :
14
13
from collections .abc import Callable
15
14
16
- with contextlib . suppress ( ImportError ):
15
+ try : # noqa: SIM105
17
16
import ctypes
18
- with contextlib .suppress (ImportError ):
17
+ except ImportError :
18
+ pass
19
+ try : # noqa: SIM105
19
20
import winreg
21
+ except ImportError :
22
+ pass
20
23
21
24
22
25
class Windows (PlatformDirsABC ):
@@ -37,7 +40,7 @@ def user_data_dir(self) -> str:
37
40
``%USERPROFILE%\\ AppData\\ Local\\ $appauthor\\ $appname`` (not roaming) or
38
41
``%USERPROFILE%\\ AppData\\ Roaming\\ $appauthor\\ $appname`` (roaming)
39
42
"""
40
- const = "CSIDL_APPDATA " if self .roaming else "CSIDL_LOCAL_APPDATA "
43
+ const = "FOLDERID_RoamingAppData " if self .roaming else "FOLDERID_LocalAppData "
41
44
path = os .path .normpath (get_win_folder (const ))
42
45
return self ._append_parts (path )
43
46
@@ -59,7 +62,7 @@ def _append_parts(self, path: str, *, opinion_value: str | None = None) -> str:
59
62
@property
60
63
def site_data_dir (self ) -> str :
61
64
""":return: data directory shared by users, e.g. ``C:\\ ProgramData\\ $appauthor\\ $appname``"""
62
- path = os .path .normpath (get_win_folder ("CSIDL_COMMON_APPDATA " ))
65
+ path = os .path .normpath (get_win_folder ("FOLDERID_ProgramData " ))
63
66
return self ._append_parts (path )
64
67
65
68
@property
@@ -78,13 +81,13 @@ def user_cache_dir(self) -> str:
78
81
:return: cache directory tied to the user (if opinionated with ``Cache`` folder within ``$appname``) e.g.
79
82
``%USERPROFILE%\\ AppData\\ Local\\ $appauthor\\ $appname\\ Cache\\ $version``
80
83
"""
81
- path = os .path .normpath (get_win_folder ("CSIDL_LOCAL_APPDATA " ))
84
+ path = os .path .normpath (get_win_folder ("FOLDERID_LocalAppData " ))
82
85
return self ._append_parts (path , opinion_value = "Cache" )
83
86
84
87
@property
85
88
def site_cache_dir (self ) -> str :
86
89
""":return: cache directory shared by users, e.g. ``C:\\ ProgramData\\ $appauthor\\ $appname\\ Cache\\ $version``"""
87
- path = os .path .normpath (get_win_folder ("CSIDL_COMMON_APPDATA " ))
90
+ path = os .path .normpath (get_win_folder ("FOLDERID_ProgramData " ))
88
91
return self ._append_parts (path , opinion_value = "Cache" )
89
92
90
93
@property
@@ -104,40 +107,40 @@ def user_log_dir(self) -> str:
104
107
@property
105
108
def user_documents_dir (self ) -> str :
106
109
""":return: documents directory tied to the user e.g. ``%USERPROFILE%\\ Documents``"""
107
- return os .path .normpath (get_win_folder ("CSIDL_PERSONAL " ))
110
+ return os .path .normpath (get_win_folder ("FOLDERID_Documents " ))
108
111
109
112
@property
110
113
def user_downloads_dir (self ) -> str :
111
114
""":return: downloads directory tied to the user e.g. ``%USERPROFILE%\\ Downloads``"""
112
- return os .path .normpath (get_win_folder ("CSIDL_DOWNLOADS " ))
115
+ return os .path .normpath (get_win_folder ("FOLDERID_Downloads " ))
113
116
114
117
@property
115
118
def user_pictures_dir (self ) -> str :
116
119
""":return: pictures directory tied to the user e.g. ``%USERPROFILE%\\ Pictures``"""
117
- return os .path .normpath (get_win_folder ("CSIDL_MYPICTURES " ))
120
+ return os .path .normpath (get_win_folder ("FOLDERID_Pictures " ))
118
121
119
122
@property
120
123
def user_videos_dir (self ) -> str :
121
124
""":return: videos directory tied to the user e.g. ``%USERPROFILE%\\ Videos``"""
122
- return os .path .normpath (get_win_folder ("CSIDL_MYVIDEO " ))
125
+ return os .path .normpath (get_win_folder ("FOLDERID_Videos " ))
123
126
124
127
@property
125
128
def user_music_dir (self ) -> str :
126
129
""":return: music directory tied to the user e.g. ``%USERPROFILE%\\ Music``"""
127
- return os .path .normpath (get_win_folder ("CSIDL_MYMUSIC " ))
130
+ return os .path .normpath (get_win_folder ("FOLDERID_Music " ))
128
131
129
132
@property
130
133
def user_desktop_dir (self ) -> str :
131
134
""":return: desktop directory tied to the user, e.g. ``%USERPROFILE%\\ Desktop``"""
132
- return os .path .normpath (get_win_folder ("CSIDL_DESKTOPDIRECTORY " ))
135
+ return os .path .normpath (get_win_folder ("FOLDERID_Desktop " ))
133
136
134
137
@property
135
138
def user_runtime_dir (self ) -> str :
136
139
"""
137
140
:return: runtime directory tied to the user, e.g.
138
141
``%USERPROFILE%\\ AppData\\ Local\\ Temp\\ $appauthor\\ $appname``
139
142
"""
140
- path = os .path .normpath (os .path .join (get_win_folder ("CSIDL_LOCAL_APPDATA " ), "Temp" )) # noqa: PTH118
143
+ path = os .path .normpath (os .path .join (get_win_folder ("FOLDERID_LocalAppData " ), "Temp" )) # noqa: PTH118
141
144
return self ._append_parts (path )
142
145
143
146
@property
@@ -146,19 +149,19 @@ def site_runtime_dir(self) -> str:
146
149
return self .user_runtime_dir
147
150
148
151
149
- def get_win_folder_from_env_vars (csidl_name : str ) -> str :
152
+ def get_win_folder_from_env_vars (folderid_name : str ) -> str :
150
153
"""Get folder from environment variables."""
151
- result = get_win_folder_if_csidl_name_not_env_var ( csidl_name )
154
+ result = get_win_folder_if_folderid_name_not_env_var ( folderid_name )
152
155
if result is not None :
153
156
return result
154
157
155
158
env_var_name = {
156
- "CSIDL_APPDATA " : "APPDATA" ,
157
- "CSIDL_COMMON_APPDATA " : "ALLUSERSPROFILE" ,
158
- "CSIDL_LOCAL_APPDATA " : "LOCALAPPDATA" ,
159
- }.get (csidl_name )
159
+ "FOLDERID_RoamingAppData " : "APPDATA" ,
160
+ "FOLDERID_ProgramData " : "ALLUSERSPROFILE" ,
161
+ "FOLDERID_LocalAppData " : "LOCALAPPDATA" ,
162
+ }.get (folderid_name )
160
163
if env_var_name is None :
161
- msg = f"Unknown CSIDL name: { csidl_name } "
164
+ msg = f"Unknown FOLDERID name: { folderid_name } "
162
165
raise ValueError (msg )
163
166
result = os .environ .get (env_var_name )
164
167
if result is None :
@@ -167,47 +170,49 @@ def get_win_folder_from_env_vars(csidl_name: str) -> str:
167
170
return result
168
171
169
172
170
- def get_win_folder_if_csidl_name_not_env_var ( csidl_name : str ) -> str | None :
171
- """Get a folder for a CSIDL name that does not exist as an environment variable."""
172
- if csidl_name == "CSIDL_PERSONAL " :
173
+ def get_win_folder_if_folderid_name_not_env_var ( folderid_name : str ) -> str | None :
174
+ """Get a folder for a FOLDERID name that does not exist as an environment variable."""
175
+ if folderid_name == "FOLDERID_Documents " :
173
176
return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Documents" ) # noqa: PTH118
174
177
175
- if csidl_name == "CSIDL_DOWNLOADS " :
178
+ if folderid_name == "FOLDERID_Downloads " :
176
179
return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Downloads" ) # noqa: PTH118
177
180
178
- if csidl_name == "CSIDL_MYPICTURES " :
181
+ if folderid_name == "FOLDERID_Pictures " :
179
182
return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Pictures" ) # noqa: PTH118
180
183
181
- if csidl_name == "CSIDL_MYVIDEO " :
184
+ if folderid_name == "FOLDERID_Videos " :
182
185
return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Videos" ) # noqa: PTH118
183
186
184
- if csidl_name == "CSIDL_MYMUSIC " :
187
+ if folderid_name == "FOLDERID_Music " :
185
188
return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Music" ) # noqa: PTH118
186
189
return None
187
190
188
191
192
+ FOLDERID_Downloads_guid_string = "374DE290-123F-4565-9164-39C4925E467B"
193
+
189
194
if "winreg" in globals ():
190
195
191
- def get_win_folder_from_registry (csidl_name : str ) -> str :
196
+ def get_win_folder_from_registry (folderid_name : str ) -> str :
192
197
"""
193
198
Get folder from the registry.
194
199
195
200
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.
201
+ answer for all FOLDERID_ * names.
197
202
198
203
"""
199
204
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 )
205
+ "FOLDERID_RoamingAppData " : "AppData" ,
206
+ "FOLDERID_ProgramData " : "Common AppData" ,
207
+ "FOLDERID_LocalAppData " : "Local AppData" ,
208
+ "FOLDERID_Documents " : "Personal" ,
209
+ "FOLDERID_Downloads " : "{" + FOLDERID_Downloads_guid_string + " }" ,
210
+ "FOLDERID_Pictures " : "My Pictures" ,
211
+ "FOLDERID_Videos " : "My Video" ,
212
+ "FOLDERID_Music " : "My Music" ,
213
+ }.get (folderid_name )
209
214
if shell_folder_name is None :
210
- msg = f"Unknown CSIDL name: { csidl_name } "
215
+ msg = f"Unknown FOLDERID name: { folderid_name } "
211
216
raise ValueError (msg )
212
217
if sys .platform != "win32" : # only needed for mypy type checker to know that this code runs only on Windows
213
218
raise NotImplementedError
@@ -221,40 +226,108 @@ def get_win_folder_from_registry(csidl_name: str) -> str:
221
226
222
227
if "ctypes" in globals () and hasattr (ctypes , "windll" ):
223
228
224
- def get_win_folder_via_ctypes (csidl_name : str ) -> str :
229
+ class GUID (ctypes .Structure ):
230
+ """
231
+ `
232
+ The GUID structure from Windows's guiddef.h header
233
+ <https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid>`_.
234
+ """
235
+
236
+ Data4Type = ctypes .c_ubyte * 8
237
+
238
+ _fields_ = (
239
+ ("Data1" , ctypes .c_ulong ),
240
+ ("Data2" , ctypes .c_ushort ),
241
+ ("Data3" , ctypes .c_ushort ),
242
+ ("Data4" , Data4Type ),
243
+ )
244
+
245
+ def __init__ (self , guid_string : str ) -> None :
246
+ digit_groups = guid_string .split ("-" )
247
+ expected_digit_groups = 5
248
+ if len (digit_groups ) != expected_digit_groups :
249
+ msg = f"The guid_string, { guid_string !r} , does not contain { expected_digit_groups } groups of digits."
250
+ raise ValueError (msg )
251
+ for digit_group , expected_length in zip (digit_groups , (8 , 4 , 4 , 4 , 12 )):
252
+ if len (digit_group ) != expected_length :
253
+ msg = (
254
+ f"The digit group, { digit_group !r} , in the guid_string, { guid_string !r} , was the wrong length. "
255
+ f"It should have been { expected_length } digits long."
256
+ )
257
+ raise ValueError (msg )
258
+ data_4_as_bytes = bytes .fromhex (digit_groups [3 ]) + bytes .fromhex (digit_groups [4 ])
259
+
260
+ super ().__init__ (
261
+ int (digit_groups [0 ], base = 16 ),
262
+ int (digit_groups [1 ], base = 16 ),
263
+ int (digit_groups [2 ], base = 16 ),
264
+ self .Data4Type (* (eight_bit_int for eight_bit_int in data_4_as_bytes )),
265
+ )
266
+
267
+ def __repr__ (self ) -> str :
268
+ guid_string = f"{ self .Data1 :08X} -{ self .Data2 :04X} -{ self .Data3 :04X} -"
269
+ for i in range (len (self .Data4 )):
270
+ guid_string += f"{ self .Data4 [i ]:02X} "
271
+ if i == 1 :
272
+ guid_string += "-"
273
+ return f"{ type (self ).__qualname__ } ({ guid_string !r} )"
274
+
275
+ def get_win_folder_via_ctypes (folderid_name : str ) -> str : # noqa: C901, PLR0912
225
276
"""Get folder with ctypes."""
226
- # There is no 'CSIDL_DOWNLOADS'.
227
- # Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead.
228
277
# 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 } "
278
+ if folderid_name == "FOLDERID_RoamingAppData" :
279
+ folderid_const = GUID ("3EB685DB-65F9-4CF6-A03A-E3EF65729F3D" )
280
+ elif folderid_name == "FOLDERID_ProgramData" :
281
+ folderid_const = GUID ("62AB5D82-FDC1-4DC3-A9DD-070D1D495D97" )
282
+ elif folderid_name == "FOLDERID_LocalAppData" :
283
+ folderid_const = GUID ("F1B32785-6FBA-4FCF-9D55-7B8E7F157091" )
284
+ elif folderid_name == "FOLDERID_Documents" :
285
+ folderid_const = GUID ("FDD39AD0-238F-46AF-ADB4-6C85480369C7" )
286
+ elif folderid_name == "FOLDERID_Pictures" :
287
+ folderid_const = GUID ("33E28130-4E1E-4676-835A-98395C3BC3BB" )
288
+ elif folderid_name == "FOLDERID_Videos" :
289
+ folderid_const = GUID ("18989B1D-99B5-455B-841C-AB7C74E4DDFC" )
290
+ elif folderid_name == "FOLDERID_Music" :
291
+ folderid_const = GUID ("4BD8D571-6D19-48D3-BE97-422220080E43" )
292
+ elif folderid_name == "FOLDERID_Downloads" :
293
+ folderid_const = GUID (FOLDERID_Downloads_guid_string )
294
+ elif folderid_name == "FOLDERID_Desktop" :
295
+ folderid_const = GUID ("B4BFCC3A-DB2C-424C-B029-7FE99A87C641" )
296
+ else :
297
+ msg = f"Unknown FOLDERID name: { folderid_name } "
242
298
raise ValueError (msg )
299
+ # https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ne-shlobj_core-known_folder_flag
300
+ kf_flag_default = 0
301
+ # https://learn.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values
302
+ s_ok = 0
243
303
244
- buf = ctypes .create_unicode_buffer ( 1024 )
304
+ pointer_to_pointer_to_wchars = ctypes .pointer ( ctypes . c_wchar_p () )
245
305
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 )
306
+ error_code = windll .shell32 .SHGetKnownFolderPath (
307
+ ctypes .pointer (folderid_const ), kf_flag_default , None , pointer_to_pointer_to_wchars
308
+ )
309
+ return_value = pointer_to_pointer_to_wchars .contents .value
310
+ # The documentation for SHGetKnownFolderPath() says that this needs to be freed using CoTaskMemFree():
311
+ # https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath#parameters
312
+ windll .ole32 .CoTaskMemFree (pointer_to_pointer_to_wchars .contents )
313
+ # Make sure that we don't accidentally use the memory now that we've freed it.
314
+ del pointer_to_pointer_to_wchars
315
+ if error_code != s_ok :
316
+ # I'm using :08X as the format here because that's the format that the official documentation for HRESULT
317
+ # uses: https://learn.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values
318
+ msg = f"SHGetKnownFolderPath() failed with this error code: 0x{ error_code :08X} "
319
+ raise RuntimeError (msg )
320
+ if return_value is None :
321
+ msg = "SHGetKnownFolderPath() succeeded, but it gave us a null pointer. This should never happen."
322
+ raise RuntimeError (msg )
247
323
248
324
# 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
325
+ if any (ord (c ) > 255 for c in return_value ): # noqa: PLR2004
326
+ buf = ctypes .create_unicode_buffer (len (return_value ))
327
+ if windll .kernel32 .GetShortPathNameW (return_value , buf , len (buf )):
328
+ return_value = buf .value
256
329
257
- return buf . value
330
+ return return_value
258
331
259
332
260
333
def _pick_get_win_folder () -> Callable [[str ], str ]:
0 commit comments