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