@@ -37,7 +37,7 @@ def user_data_dir(self) -> str:
3737 ``%USERPROFILE%\\ AppData\\ Local\\ $appauthor\\ $appname`` (not roaming) or
3838 ``%USERPROFILE%\\ AppData\\ Roaming\\ $appauthor\\ $appname`` (roaming)
3939 """
40- const = "CSIDL_APPDATA " if self .roaming else "CSIDL_LOCAL_APPDATA "
40+ const = "FOLDERID_RoamingAppData " if self .roaming else "FOLDERID_LocalAppData "
4141 path = os .path .normpath (get_win_folder (const ))
4242 return self ._append_parts (path )
4343
@@ -59,7 +59,7 @@ def _append_parts(self, path: str, *, opinion_value: str | None = None) -> str:
5959 @property
6060 def site_data_dir (self ) -> str :
6161 """: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 " ))
6363 return self ._append_parts (path )
6464
6565 @property
@@ -78,13 +78,13 @@ def user_cache_dir(self) -> str:
7878 :return: cache directory tied to the user (if opinionated with ``Cache`` folder within ``$appname``) e.g.
7979 ``%USERPROFILE%\\ AppData\\ Local\\ $appauthor\\ $appname\\ Cache\\ $version``
8080 """
81- path = os .path .normpath (get_win_folder ("CSIDL_LOCAL_APPDATA " ))
81+ path = os .path .normpath (get_win_folder ("FOLDERID_LocalAppData " ))
8282 return self ._append_parts (path , opinion_value = "Cache" )
8383
8484 @property
8585 def site_cache_dir (self ) -> str :
8686 """: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 " ))
8888 return self ._append_parts (path , opinion_value = "Cache" )
8989
9090 @property
@@ -104,40 +104,40 @@ def user_log_dir(self) -> str:
104104 @property
105105 def user_documents_dir (self ) -> str :
106106 """: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 " ))
108108
109109 @property
110110 def user_downloads_dir (self ) -> str :
111111 """: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 " ))
113113
114114 @property
115115 def user_pictures_dir (self ) -> str :
116116 """: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 " ))
118118
119119 @property
120120 def user_videos_dir (self ) -> str :
121121 """: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 " ))
123123
124124 @property
125125 def user_music_dir (self ) -> str :
126126 """: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 " ))
128128
129129 @property
130130 def user_desktop_dir (self ) -> str :
131131 """: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 " ))
133133
134134 @property
135135 def user_runtime_dir (self ) -> str :
136136 """
137137 :return: runtime directory tied to the user, e.g.
138138 ``%USERPROFILE%\\ AppData\\ Local\\ Temp\\ $appauthor\\ $appname``
139139 """
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
141141 return self ._append_parts (path )
142142
143143 @property
@@ -146,19 +146,19 @@ def site_runtime_dir(self) -> str:
146146 return self .user_runtime_dir
147147
148148
149- def get_win_folder_from_env_vars (csidl_name : str ) -> str :
149+ def get_win_folder_from_env_vars (folderid_name : str ) -> str :
150150 """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 )
152152 if result is not None :
153153 return result
154154
155155 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 )
160160 if env_var_name is None :
161- msg = f"Unknown CSIDL name: { csidl_name } "
161+ msg = f"Unknown FOLDERID name: { folderid_name } "
162162 raise ValueError (msg )
163163 result = os .environ .get (env_var_name )
164164 if result is None :
@@ -167,47 +167,49 @@ def get_win_folder_from_env_vars(csidl_name: str) -> str:
167167 return result
168168
169169
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 " :
173173 return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Documents" ) # noqa: PTH118
174174
175- if csidl_name == "CSIDL_DOWNLOADS " :
175+ if folderid_name == "FOLDERID_Downloads " :
176176 return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Downloads" ) # noqa: PTH118
177177
178- if csidl_name == "CSIDL_MYPICTURES " :
178+ if folderid_name == "FOLDERID_Pictures " :
179179 return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Pictures" ) # noqa: PTH118
180180
181- if csidl_name == "CSIDL_MYVIDEO " :
181+ if folderid_name == "FOLDERID_Videos " :
182182 return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Videos" ) # noqa: PTH118
183183
184- if csidl_name == "CSIDL_MYMUSIC " :
184+ if folderid_name == "FOLDERID_Music " :
185185 return os .path .join (os .path .normpath (os .environ ["USERPROFILE" ]), "Music" ) # noqa: PTH118
186186 return None
187187
188188
189+ FOLDERID_Downloads_guid_string = "374DE290-123F-4565-9164-39C4925E467B"
190+
189191if "winreg" in globals ():
190192
191- def get_win_folder_from_registry (csidl_name : str ) -> str :
193+ def get_win_folder_from_registry (folderid_name : str ) -> str :
192194 """
193195 Get folder from the registry.
194196
195197 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.
197199
198200 """
199201 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 )
209211 if shell_folder_name is None :
210- msg = f"Unknown CSIDL name: { csidl_name } "
212+ msg = f"Unknown FOLDERID name: { folderid_name } "
211213 raise ValueError (msg )
212214 if sys .platform != "win32" : # only needed for mypy type checker to know that this code runs only on Windows
213215 raise NotImplementedError
@@ -221,40 +223,101 @@ def get_win_folder_from_registry(csidl_name: str) -> str:
221223
222224if "ctypes" in globals () and hasattr (ctypes , "windll" ):
223225
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 :
225273 """Get folder with ctypes."""
226- # There is no 'CSIDL_DOWNLOADS'.
227- # Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead.
228274 # 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 } "
242288 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
243293
244- buf = ctypes .create_unicode_buffer ( 1024 )
294+ pointer_to_pointer_to_wchars = ctypes .pointer ( ctypes . c_wchar_p () )
245295 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 )
247313
248314 # 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
256319
257- return buf . value
320+ return return_value
258321
259322
260323def _pick_get_win_folder () -> Callable [[str ], str ]:
0 commit comments