-
-
Notifications
You must be signed in to change notification settings - Fork 57
Use SHGetKnownFolderPath()
instead of SHGetFolderPathW()
#350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,15 @@ | |
if TYPE_CHECKING: | ||
from collections.abc import Callable | ||
|
||
try: # noqa: SIM105 | ||
import ctypes | ||
except ImportError: | ||
pass | ||
try: # noqa: SIM105 | ||
import winreg | ||
except ImportError: | ||
pass | ||
|
||
|
||
class Windows(PlatformDirsABC): | ||
""" | ||
|
@@ -31,7 +40,7 @@ def user_data_dir(self) -> str: | |
``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname`` (not roaming) or | ||
``%USERPROFILE%\\AppData\\Roaming\\$appauthor\\$appname`` (roaming) | ||
""" | ||
const = "CSIDL_APPDATA" if self.roaming else "CSIDL_LOCAL_APPDATA" | ||
const = "FOLDERID_RoamingAppData" if self.roaming else "FOLDERID_LocalAppData" | ||
path = os.path.normpath(get_win_folder(const)) | ||
return self._append_parts(path) | ||
|
||
|
@@ -53,7 +62,7 @@ def _append_parts(self, path: str, *, opinion_value: str | None = None) -> str: | |
@property | ||
def site_data_dir(self) -> str: | ||
""":return: data directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname``""" | ||
path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA")) | ||
path = os.path.normpath(get_win_folder("FOLDERID_ProgramData")) | ||
return self._append_parts(path) | ||
|
||
@property | ||
|
@@ -72,13 +81,13 @@ def user_cache_dir(self) -> str: | |
:return: cache directory tied to the user (if opinionated with ``Cache`` folder within ``$appname``) e.g. | ||
``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname\\Cache\\$version`` | ||
""" | ||
path = os.path.normpath(get_win_folder("CSIDL_LOCAL_APPDATA")) | ||
path = os.path.normpath(get_win_folder("FOLDERID_LocalAppData")) | ||
return self._append_parts(path, opinion_value="Cache") | ||
|
||
@property | ||
def site_cache_dir(self) -> str: | ||
""":return: cache directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname\\Cache\\$version``""" | ||
path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA")) | ||
path = os.path.normpath(get_win_folder("FOLDERID_ProgramData")) | ||
return self._append_parts(path, opinion_value="Cache") | ||
|
||
@property | ||
|
@@ -98,40 +107,40 @@ def user_log_dir(self) -> str: | |
@property | ||
def user_documents_dir(self) -> str: | ||
""":return: documents directory tied to the user e.g. ``%USERPROFILE%\\Documents``""" | ||
return os.path.normpath(get_win_folder("CSIDL_PERSONAL")) | ||
return os.path.normpath(get_win_folder("FOLDERID_Documents")) | ||
|
||
@property | ||
def user_downloads_dir(self) -> str: | ||
""":return: downloads directory tied to the user e.g. ``%USERPROFILE%\\Downloads``""" | ||
return os.path.normpath(get_win_folder("CSIDL_DOWNLOADS")) | ||
return os.path.normpath(get_win_folder("FOLDERID_Downloads")) | ||
|
||
@property | ||
def user_pictures_dir(self) -> str: | ||
""":return: pictures directory tied to the user e.g. ``%USERPROFILE%\\Pictures``""" | ||
return os.path.normpath(get_win_folder("CSIDL_MYPICTURES")) | ||
return os.path.normpath(get_win_folder("FOLDERID_Pictures")) | ||
|
||
@property | ||
def user_videos_dir(self) -> str: | ||
""":return: videos directory tied to the user e.g. ``%USERPROFILE%\\Videos``""" | ||
return os.path.normpath(get_win_folder("CSIDL_MYVIDEO")) | ||
return os.path.normpath(get_win_folder("FOLDERID_Videos")) | ||
|
||
@property | ||
def user_music_dir(self) -> str: | ||
""":return: music directory tied to the user e.g. ``%USERPROFILE%\\Music``""" | ||
return os.path.normpath(get_win_folder("CSIDL_MYMUSIC")) | ||
return os.path.normpath(get_win_folder("FOLDERID_Music")) | ||
|
||
@property | ||
def user_desktop_dir(self) -> str: | ||
""":return: desktop directory tied to the user, e.g. ``%USERPROFILE%\\Desktop``""" | ||
return os.path.normpath(get_win_folder("CSIDL_DESKTOPDIRECTORY")) | ||
return os.path.normpath(get_win_folder("FOLDERID_Desktop")) | ||
|
||
@property | ||
def user_runtime_dir(self) -> str: | ||
""" | ||
:return: runtime directory tied to the user, e.g. | ||
``%USERPROFILE%\\AppData\\Local\\Temp\\$appauthor\\$appname`` | ||
""" | ||
path = os.path.normpath(os.path.join(get_win_folder("CSIDL_LOCAL_APPDATA"), "Temp")) # noqa: PTH118 | ||
path = os.path.normpath(os.path.join(get_win_folder("FOLDERID_LocalAppData"), "Temp")) # noqa: PTH118 | ||
return self._append_parts(path) | ||
|
||
@property | ||
|
@@ -140,19 +149,19 @@ def site_runtime_dir(self) -> str: | |
return self.user_runtime_dir | ||
|
||
|
||
def get_win_folder_from_env_vars(csidl_name: str) -> str: | ||
def get_win_folder_from_env_vars(folderid_name: str) -> str: | ||
"""Get folder from environment variables.""" | ||
result = get_win_folder_if_csidl_name_not_env_var(csidl_name) | ||
result = get_win_folder_if_folderid_name_not_env_var(folderid_name) | ||
if result is not None: | ||
return result | ||
|
||
env_var_name = { | ||
"CSIDL_APPDATA": "APPDATA", | ||
"CSIDL_COMMON_APPDATA": "ALLUSERSPROFILE", | ||
"CSIDL_LOCAL_APPDATA": "LOCALAPPDATA", | ||
}.get(csidl_name) | ||
"FOLDERID_RoamingAppData": "APPDATA", | ||
"FOLDERID_ProgramData": "ALLUSERSPROFILE", | ||
"FOLDERID_LocalAppData": "LOCALAPPDATA", | ||
}.get(folderid_name) | ||
if env_var_name is None: | ||
msg = f"Unknown CSIDL name: {csidl_name}" | ||
msg = f"Unknown FOLDERID name: {folderid_name}" | ||
raise ValueError(msg) | ||
result = os.environ.get(env_var_name) | ||
if result is None: | ||
|
@@ -161,108 +170,172 @@ def get_win_folder_from_env_vars(csidl_name: str) -> str: | |
return result | ||
|
||
|
||
def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None: | ||
"""Get a folder for a CSIDL name that does not exist as an environment variable.""" | ||
if csidl_name == "CSIDL_PERSONAL": | ||
def get_win_folder_if_folderid_name_not_env_var(folderid_name: str) -> str | None: | ||
"""Get a folder for a FOLDERID name that does not exist as an environment variable.""" | ||
if folderid_name == "FOLDERID_Documents": | ||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Documents") # noqa: PTH118 | ||
|
||
if csidl_name == "CSIDL_DOWNLOADS": | ||
if folderid_name == "FOLDERID_Downloads": | ||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Downloads") # noqa: PTH118 | ||
|
||
if csidl_name == "CSIDL_MYPICTURES": | ||
if folderid_name == "FOLDERID_Pictures": | ||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Pictures") # noqa: PTH118 | ||
|
||
if csidl_name == "CSIDL_MYVIDEO": | ||
if folderid_name == "FOLDERID_Videos": | ||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Videos") # noqa: PTH118 | ||
|
||
if csidl_name == "CSIDL_MYMUSIC": | ||
if folderid_name == "FOLDERID_Music": | ||
return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Music") # noqa: PTH118 | ||
return None | ||
|
||
|
||
def get_win_folder_from_registry(csidl_name: str) -> str: | ||
""" | ||
Get folder from the registry. | ||
|
||
This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct answer | ||
for all CSIDL_* names. | ||
FOLDERID_Downloads_guid_string = "374DE290-123F-4565-9164-39C4925E467B" | ||
|
||
""" | ||
shell_folder_name = { | ||
"CSIDL_APPDATA": "AppData", | ||
"CSIDL_COMMON_APPDATA": "Common AppData", | ||
"CSIDL_LOCAL_APPDATA": "Local AppData", | ||
"CSIDL_PERSONAL": "Personal", | ||
"CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}", | ||
"CSIDL_MYPICTURES": "My Pictures", | ||
"CSIDL_MYVIDEO": "My Video", | ||
"CSIDL_MYMUSIC": "My Music", | ||
}.get(csidl_name) | ||
if shell_folder_name is None: | ||
msg = f"Unknown CSIDL name: {csidl_name}" | ||
raise ValueError(msg) | ||
if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows | ||
raise NotImplementedError | ||
import winreg # noqa: PLC0415 | ||
|
||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") | ||
directory, _ = winreg.QueryValueEx(key, shell_folder_name) | ||
return str(directory) | ||
|
||
|
||
def get_win_folder_via_ctypes(csidl_name: str) -> str: | ||
"""Get folder with ctypes.""" | ||
# There is no 'CSIDL_DOWNLOADS'. | ||
# Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead. | ||
# https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid | ||
|
||
import ctypes # noqa: PLC0415 | ||
|
||
csidl_const = { | ||
"CSIDL_APPDATA": 26, | ||
"CSIDL_COMMON_APPDATA": 35, | ||
"CSIDL_LOCAL_APPDATA": 28, | ||
"CSIDL_PERSONAL": 5, | ||
"CSIDL_MYPICTURES": 39, | ||
"CSIDL_MYVIDEO": 14, | ||
"CSIDL_MYMUSIC": 13, | ||
"CSIDL_DOWNLOADS": 40, | ||
"CSIDL_DESKTOPDIRECTORY": 16, | ||
}.get(csidl_name) | ||
if csidl_const is None: | ||
msg = f"Unknown CSIDL name: {csidl_name}" | ||
raise ValueError(msg) | ||
if "winreg" in globals(): | ||
|
||
buf = ctypes.create_unicode_buffer(1024) | ||
windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker | ||
windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) | ||
def get_win_folder_from_registry(folderid_name: str) -> str: | ||
""" | ||
Get folder from the registry. | ||
|
||
# Downgrade to short path name if it has high-bit chars. | ||
if any(ord(c) > 255 for c in buf): # noqa: PLR2004 | ||
buf2 = ctypes.create_unicode_buffer(1024) | ||
if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): | ||
buf = buf2 | ||
This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct | ||
answer for all FOLDERID_* names. | ||
|
||
if csidl_name == "CSIDL_DOWNLOADS": | ||
return os.path.join(buf.value, "Downloads") # noqa: PTH118 | ||
""" | ||
shell_folder_name = { | ||
"FOLDERID_RoamingAppData": "AppData", | ||
"FOLDERID_ProgramData": "Common AppData", | ||
"FOLDERID_LocalAppData": "Local AppData", | ||
"FOLDERID_Documents": "Personal", | ||
"FOLDERID_Downloads": "{" + FOLDERID_Downloads_guid_string + "}", | ||
"FOLDERID_Pictures": "My Pictures", | ||
"FOLDERID_Videos": "My Video", | ||
"FOLDERID_Music": "My Music", | ||
}.get(folderid_name) | ||
if shell_folder_name is None: | ||
msg = f"Unknown FOLDERID name: {folderid_name}" | ||
raise ValueError(msg) | ||
if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows | ||
raise NotImplementedError | ||
|
||
key = winreg.OpenKey( | ||
winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" | ||
) | ||
directory, _ = winreg.QueryValueEx(key, shell_folder_name) | ||
return str(directory) | ||
|
||
|
||
if "ctypes" in globals() and hasattr(ctypes, "windll"): | ||
|
||
class GUID(ctypes.Structure): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we avoid defining this at import time and postpone the logic until necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, like, by defining the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I don't think we need this class as I mentioned elsewhere the checks aren't necessary nor do we require a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on my research, I don’t think that it’s possible to make this pull request work without having a SidenoteIf you want to take a look at these header files yourself, then you need to install the Windows SDK. Here’s how I made sure that the Windows SDK was installed:
The prototype for the STDAPI SHGetKnownFolderPath(_In_ REFKNOWNFOLDERID rfid,
_In_ DWORD /* KNOWN_FOLDER_FLAG */ dwFlags,
_In_opt_ HANDLE hToken,
_Outptr_ PWSTR *ppszPath); // free *ppszPath with CoTaskMemFree The #ifdef __cplusplus
#define REFKNOWNFOLDERID const KNOWNFOLDERID &
#else // !__cplusplus
#define REFKNOWNFOLDERID const KNOWNFOLDERID * __MIDL_CONST
#endif // __cplusplus This means that typedef GUID KNOWNFOLDERID; This means that a #if defined(__midl)
typedef struct {
unsigned long Data1;
unsigned short Data2;
unsigned short Data3;
byte Data4[ 8 ];
} GUID;
#else
typedef struct _GUID {
unsigned long Data1;
unsigned short Data2;
unsigned short Data3;
unsigned char Data4[ 8 ];
} GUID;
#endif This means that
The
Based on that research, it seems like I need to create a structure value in order to call the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't done extensive testing but the following seems to work. Save it somewhere then try running import ctypes
def guid_str_to_bytes(guid_str):
parts = guid_str.split('-')
data1 = int(parts[0], 16).to_bytes(4, 'little')
data2 = int(parts[1], 16).to_bytes(2, 'little')
data3 = int(parts[2], 16).to_bytes(2, 'little')
data4 = bytes.fromhex(parts[3] + parts[4])
return data1 + data2 + data3 + data4
def get_win_folder_via_ctypes(folderid_name: str) -> str:
GUID_STRINGS = {
"FOLDERID_RoamingAppData": "3EB685DB-65F9-4CF6-A03A-E3EF65729F3D",
"FOLDERID_ProgramData": "62AB5D82-FDC1-4DC3-A9DD-070D1D495D97",
"FOLDERID_LocalAppData": "F1B32785-6FBA-4FCF-9D55-7B8E7F157091",
"FOLDERID_Documents": "FDD39AD0-238F-46AF-ADB4-6C85480369C7",
"FOLDERID_Pictures": "33E28130-4E1E-4676-835A-98395C3BC3BB",
"FOLDERID_Videos": "18989B1D-99B5-455B-841C-AB7C74E4DDFC",
"FOLDERID_Music": "4BD8D571-6D19-48D3-BE97-422220080E43",
"FOLDERID_Downloads": "374DE290-123F-4565-9164-39C4925E467B",
"FOLDERID_Desktop": "B4BFCC3A-DB2C-424C-B029-7FE99A87C641",
}
if folderid_name not in GUID_STRINGS:
raise ValueError(f"Unknown FOLDERID name: {folderid_name}")
guid_str = GUID_STRINGS[folderid_name]
folderid_bytes = guid_str_to_bytes(guid_str)
folderid_array = (ctypes.c_char * 16).from_buffer_copy(folderid_bytes)
kf_flag_default = 0
s_ok = 0
pointer_to_pointer_to_wchars = ctypes.pointer(ctypes.c_wchar_p())
windll = ctypes.windll
error_code = windll.shell32.SHGetKnownFolderPath(
folderid_array,
kf_flag_default,
None,
pointer_to_pointer_to_wchars
)
return_value = pointer_to_pointer_to_wchars.contents.value
windll.ole32.CoTaskMemFree(pointer_to_pointer_to_wchars.contents)
del pointer_to_pointer_to_wchars
if error_code != s_ok:
raise RuntimeError(f"SHGetKnownFolderPath() failed with this error code: 0x{error_code:08X}")
if return_value is None:
raise RuntimeError("SHGetKnownFolderPath() succeeded, but it gave us a null pointer. This should never happen.")
if any(ord(c) > 255 for c in return_value):
buf = ctypes.create_unicode_buffer(len(return_value))
if windll.kernel32.GetShortPathNameW(return_value, buf, len(buf)):
return_value = buf.value
return return_value
if __name__ == "__main__":
print(get_win_folder_via_ctypes("FOLDERID_Downloads")) I asked Gemini 2.5 Pro (experimental), Grok 3 + thinking, ChatGPT o4-mini-high and Claude 3.7 + thinking. This was from Grok which in my opinion had the best answer. Claude had something very similar but stored the GUID strings as precomputed byte strings which I think might be an overoptimization for the trade-off of reduced readability (although in my own code I would do that actually). Edit: If you want to see this is the relevant part of what Claude had. Note that if you want to do this don't copy exactly because this is still a computation, so save as raw bytes folderid_guids = {
"FOLDERID_RoamingAppData": bytes([0xDB, 0x85, 0xB6, 0x3E, 0xF9, 0x65, 0xF6, 0x4C, 0xA0, 0x3A, 0xE3, 0xEF, 0x65, 0x72, 0x9F, 0x3D]),
"FOLDERID_ProgramData": bytes([0x82, 0x5D, 0xAB, 0x62, 0xC1, 0xFD, 0xC3, 0x4D, 0xA9, 0xDD, 0x07, 0x0D, 0x1D, 0x49, 0x5D, 0x97]),
"FOLDERID_LocalAppData": bytes([0x85, 0x27, 0xB3, 0xF1, 0xBA, 0x6F, 0xCF, 0x4F, 0x9D, 0x55, 0x7B, 0x8E, 0x7F, 0x15, 0x70, 0x91]),
"FOLDERID_Documents": bytes([0xD0, 0x9A, 0xD3, 0xFD, 0x8F, 0x23, 0xAF, 0x46, 0xAD, 0xB4, 0x6C, 0x85, 0x48, 0x03, 0x69, 0xC7]),
"FOLDERID_Pictures": bytes([0x30, 0x81, 0xE2, 0x33, 0x1E, 0x4E, 0x76, 0x46, 0x83, 0x5A, 0x98, 0x39, 0x5C, 0x3B, 0xC3, 0xBB]),
"FOLDERID_Videos": bytes([0x1D, 0x9B, 0x98, 0x18, 0xB5, 0x99, 0x5B, 0x45, 0x84, 0x1C, 0xAB, 0x7C, 0x74, 0xE4, 0xDD, 0xFC]),
"FOLDERID_Music": bytes([0x71, 0xD5, 0xD8, 0x4B, 0x19, 0x6D, 0xD3, 0x48, 0xBE, 0x97, 0x42, 0x22, 0x20, 0x08, 0x0E, 0x43]),
"FOLDERID_Downloads": bytes([0x90, 0xE2, 0x4D, 0x37, 0x3F, 0x12, 0x65, 0x45, 0x91, 0x64, 0x39, 0xC4, 0x92, 0x5E, 0x46, 0x7B]),
"FOLDERID_Desktop": bytes([0x3A, 0xCC, 0xBF, 0xB4, 0x2C, 0xDB, 0x4C, 0x42, 0xB0, 0x29, 0x7F, 0xE9, 0x9A, 0x87, 0xC6, 0x41]),
}
if folderid_name not in folderid_guids:
msg = f"Unknown FOLDERID name: {folderid_name}"
raise ValueError(msg)
# Create GUID buffer
guid_buffer = ctypes.create_string_buffer(folderid_guids[folderid_name]) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’m a little bit skeptical of those solutions. According to the relevant header files, Do you think that it would be better to set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a struct. It's one manually created, but it is a struct. You could do exactly the same thing with the struct module, though I assume that would be more expensive than building it manually like this. If you did that, it would end up as a sequence of bytes as well. That's all structs are. I wouldn't worry about static type checkers. Externally, it doesn't matter - users don't care about what the internals do when type checking. For internals, type checkers can't really do much with CTypes, as it is dynamic. A few things that do matter, though: I noticed "in native byte order" in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have actual measurements of the costs of these methods? |
||
""" | ||
` | ||
The GUID structure from Windows's guiddef.h header | ||
<https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid>`_. | ||
""" | ||
|
||
return buf.value | ||
Data4Type = ctypes.c_ubyte * 8 | ||
|
||
_fields_ = ( | ||
("Data1", ctypes.c_ulong), | ||
("Data2", ctypes.c_ushort), | ||
("Data3", ctypes.c_ushort), | ||
("Data4", Data4Type), | ||
) | ||
|
||
def __init__(self, guid_string: str) -> None: | ||
digit_groups = guid_string.split("-") | ||
expected_digit_groups = 5 | ||
if len(digit_groups) != expected_digit_groups: | ||
msg = f"The guid_string, {guid_string!r}, does not contain {expected_digit_groups} groups of digits." | ||
raise ValueError(msg) | ||
for digit_group, expected_length in zip(digit_groups, (8, 4, 4, 4, 12)): | ||
if len(digit_group) != expected_length: | ||
msg = ( | ||
f"The digit group, {digit_group!r}, in the guid_string, {guid_string!r}, was the wrong length. " | ||
f"It should have been {expected_length} digits long." | ||
) | ||
raise ValueError(msg) | ||
Comment on lines
+247
to
+257
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although I appreciate the thoroughness of the edge case handling, these checks are superfluous because we control the inputs. Let's remove these for better performance. |
||
data_4_as_bytes = bytes.fromhex(digit_groups[3]) + bytes.fromhex(digit_groups[4]) | ||
|
||
super().__init__( | ||
int(digit_groups[0], base=16), | ||
int(digit_groups[1], base=16), | ||
int(digit_groups[2], base=16), | ||
self.Data4Type(*(eight_bit_int for eight_bit_int in data_4_as_bytes)), | ||
) | ||
|
||
def __repr__(self) -> str: | ||
guid_string = f"{self.Data1:08X}-{self.Data2:04X}-{self.Data3:04X}-" | ||
for i in range(len(self.Data4)): | ||
guid_string += f"{self.Data4[i]:02X}" | ||
if i == 1: | ||
guid_string += "-" | ||
return f"{type(self).__qualname__}({guid_string!r})" | ||
Comment on lines
+267
to
+273
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would only be used during debugging so I don't see the use (we should remove this class as I'm about to mention). |
||
|
||
def get_win_folder_via_ctypes(folderid_name: str) -> str: # noqa: C901, PLR0912 | ||
"""Get folder with ctypes.""" | ||
# https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid | ||
if folderid_name == "FOLDERID_RoamingAppData": | ||
folderid_const = GUID("3EB685DB-65F9-4CF6-A03A-E3EF65729F3D") | ||
elif folderid_name == "FOLDERID_ProgramData": | ||
folderid_const = GUID("62AB5D82-FDC1-4DC3-A9DD-070D1D495D97") | ||
elif folderid_name == "FOLDERID_LocalAppData": | ||
folderid_const = GUID("F1B32785-6FBA-4FCF-9D55-7B8E7F157091") | ||
elif folderid_name == "FOLDERID_Documents": | ||
folderid_const = GUID("FDD39AD0-238F-46AF-ADB4-6C85480369C7") | ||
elif folderid_name == "FOLDERID_Pictures": | ||
folderid_const = GUID("33E28130-4E1E-4676-835A-98395C3BC3BB") | ||
elif folderid_name == "FOLDERID_Videos": | ||
folderid_const = GUID("18989B1D-99B5-455B-841C-AB7C74E4DDFC") | ||
elif folderid_name == "FOLDERID_Music": | ||
folderid_const = GUID("4BD8D571-6D19-48D3-BE97-422220080E43") | ||
elif folderid_name == "FOLDERID_Downloads": | ||
folderid_const = GUID(FOLDERID_Downloads_guid_string) | ||
elif folderid_name == "FOLDERID_Desktop": | ||
folderid_const = GUID("B4BFCC3A-DB2C-424C-B029-7FE99A87C641") | ||
else: | ||
msg = f"Unknown FOLDERID name: {folderid_name}" | ||
raise ValueError(msg) | ||
# https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/ne-shlobj_core-known_folder_flag | ||
kf_flag_default = 0 | ||
# https://learn.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values | ||
s_ok = 0 | ||
|
||
pointer_to_pointer_to_wchars = ctypes.pointer(ctypes.c_wchar_p()) | ||
windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker | ||
error_code = windll.shell32.SHGetKnownFolderPath( | ||
ctypes.pointer(folderid_const), kf_flag_default, None, pointer_to_pointer_to_wchars | ||
) | ||
return_value = pointer_to_pointer_to_wchars.contents.value | ||
# The documentation for SHGetKnownFolderPath() says that this needs to be freed using CoTaskMemFree(): | ||
# https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath#parameters | ||
windll.ole32.CoTaskMemFree(pointer_to_pointer_to_wchars.contents) | ||
# Make sure that we don't accidentally use the memory now that we've freed it. | ||
del pointer_to_pointer_to_wchars | ||
if error_code != s_ok: | ||
# I'm using :08X as the format here because that's the format that the official documentation for HRESULT | ||
# uses: https://learn.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values | ||
msg = f"SHGetKnownFolderPath() failed with this error code: 0x{error_code:08X}" | ||
raise RuntimeError(msg) | ||
if return_value is None: | ||
msg = "SHGetKnownFolderPath() succeeded, but it gave us a null pointer. This should never happen." | ||
raise RuntimeError(msg) | ||
|
||
# Downgrade to short path name if it has high-bit chars. | ||
if any(ord(c) > 255 for c in return_value): # noqa: PLR2004 | ||
buf = ctypes.create_unicode_buffer(len(return_value)) | ||
if windll.kernel32.GetShortPathNameW(return_value, buf, len(buf)): | ||
return_value = buf.value | ||
|
||
return return_value | ||
|
||
|
||
def _pick_get_win_folder() -> Callable[[str], str]: | ||
try: | ||
import ctypes # noqa: PLC0415 | ||
except ImportError: | ||
pass | ||
else: | ||
if hasattr(ctypes, "windll"): | ||
return get_win_folder_via_ctypes | ||
try: | ||
import winreg # noqa: PLC0415, F401 | ||
except ImportError: | ||
return get_win_folder_from_env_vars | ||
else: | ||
if "get_win_folder_via_ctypes" in globals(): | ||
return get_win_folder_via_ctypes | ||
if "get_win_folder_from_registry" in globals(): | ||
return get_win_folder_from_registry | ||
return get_win_folder_from_env_vars | ||
|
||
|
||
get_win_folder = lru_cache(maxsize=None)(_pick_get_win_folder()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upon taking a closer look, let's do what the rest of the file does and lazily import when necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rest of the file does not do lazy importing at the moment because this pull request removes all lazy importing from
windows.py
. Are you saying that I should drop the “Simplify logic for choosingget_win_folder
’s value” commit?