Skip to content

Commit 5968313

Browse files
committed
[WIP] Use SHGetKnownFolderPath() instead of SHGetFolderPathW()
The official documentation for `SHGetFolderPathW()` says: > Note As of Windows Vista, this function is merely a wrapper for > SHGetKnownFolderPath. The CSIDL value is translated to its associated > KNOWNFOLDERID and then SHGetKnownFolderPath is called. New > applications should use the known folder system rather than the older > CSIDL system, which is supported only for backward compatibility. Source: <https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetfolderpathw> This change makes it so that platformdirs uses `SHGetKnownFolderPath()` instead of `SHGetFolderPathW()`. This change also removes references to the old CSIDL system and replaces them with references wo the FOLDERID system. Closes tox-dev#348. TODO: Test in CI before submitting.
1 parent 1ec4261 commit 5968313

File tree

1 file changed

+126
-63
lines changed

1 file changed

+126
-63
lines changed

src/platformdirs/windows.py

+126-63
Original file line numberDiff line numberDiff line change
@@ -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+
189191
if "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

222224
if "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

260323
def _pick_get_win_folder() -> Callable[[str], str]:

0 commit comments

Comments
 (0)