From 0213ba0a47f3a591211a912d67e6127a34260f3b Mon Sep 17 00:00:00 2001 From: Kemal Zebari Date: Mon, 24 Apr 2023 18:07:58 -0700 Subject: [PATCH] platformdirs: introduce `user_pictures_dir()` Introduces the means to acquire the user's pictures directory path. Feature suggested from #141. --- src/platformdirs/__init__.py | 16 ++++++++++++++++ src/platformdirs/__main__.py | 1 + src/platformdirs/android.py | 23 +++++++++++++++++++++++ src/platformdirs/api.py | 10 ++++++++++ src/platformdirs/macos.py | 5 +++++ src/platformdirs/unix.py | 23 +++++++++++++++++------ src/platformdirs/windows.py | 12 ++++++++++++ tests/conftest.py | 1 + tests/test_android.py | 1 + tests/test_macos.py | 1 + tests/test_unix.py | 34 ++++++++++++++++++++++++++++++++++ 11 files changed, 121 insertions(+), 6 deletions(-) diff --git a/src/platformdirs/__init__.py b/src/platformdirs/__init__.py index 4f4f9f86..0bdaa462 100644 --- a/src/platformdirs/__init__.py +++ b/src/platformdirs/__init__.py @@ -243,6 +243,13 @@ def user_documents_dir() -> str: return PlatformDirs().user_documents_dir +def user_pictures_dir() -> str: + """ + :returns: pictures directory tied to the user + """ + return PlatformDirs().user_pictures_dir + + def user_runtime_dir( appname: str | None = None, appauthor: str | None | Literal[False] = None, @@ -466,6 +473,13 @@ def user_documents_path() -> Path: return PlatformDirs().user_documents_path +def user_pictures_path() -> Path: + """ + :returns: pictures path tied to the user + """ + return PlatformDirs().user_pictures_path + + def user_runtime_path( appname: str | None = None, appauthor: str | None | Literal[False] = None, @@ -502,6 +516,7 @@ def user_runtime_path( "user_state_dir", "user_log_dir", "user_documents_dir", + "user_pictures_dir", "user_runtime_dir", "site_data_dir", "site_config_dir", @@ -512,6 +527,7 @@ def user_runtime_path( "user_state_path", "user_log_path", "user_documents_path", + "user_pictures_path", "user_runtime_path", "site_data_path", "site_config_path", diff --git a/src/platformdirs/__main__.py b/src/platformdirs/__main__.py index b740787b..72a91bbd 100644 --- a/src/platformdirs/__main__.py +++ b/src/platformdirs/__main__.py @@ -9,6 +9,7 @@ "user_state_dir", "user_log_dir", "user_documents_dir", + "user_pictures_dir", "user_runtime_dir", "site_data_dir", "site_config_dir", diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py index f6de7451..50395e8a 100644 --- a/src/platformdirs/android.py +++ b/src/platformdirs/android.py @@ -72,6 +72,13 @@ def user_documents_dir(self) -> str: """ return _android_documents_folder() + @property + def user_pictures_dir(self) -> str: + """ + :return: pictures directory tied to the user e.g. ``/storage/emulated/0/Pictures`` + """ + return _android_pictures_folder() + @property def user_runtime_dir(self) -> str: """ @@ -121,6 +128,22 @@ def _android_documents_folder() -> str: return documents_dir +@lru_cache(maxsize=1) +def _android_pictures_folder() -> str: + """:return: pictures folder for the Android OS""" + # Get directories with pyjnius + try: + from jnius import autoclass + + Context = autoclass("android.content.Context") # noqa: N806 + Environment = autoclass("android.os.Environment") # noqa: N806 + pictures_dir: str = Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + except Exception: + pictures_dir = "/storage/emulated/0/Pictures" + + return pictures_dir + + __all__ = [ "Android", ] diff --git a/src/platformdirs/api.py b/src/platformdirs/api.py index f140e8b6..3bab4686 100644 --- a/src/platformdirs/api.py +++ b/src/platformdirs/api.py @@ -123,6 +123,11 @@ def user_log_dir(self) -> str: def user_documents_dir(self) -> str: """:return: documents directory tied to the user""" + @property + @abstractmethod + def user_pictures_dir(self) -> str: + """:return: pictures directory tied to the user""" + @property @abstractmethod def user_runtime_dir(self) -> str: @@ -173,6 +178,11 @@ def user_documents_path(self) -> Path: """:return: documents path tied to the user""" return Path(self.user_documents_dir) + @property + def user_pictures_path(self) -> Path: + """:return: pictures path tied to the user""" + return Path(self.user_pictures_dir) + @property def user_runtime_path(self) -> Path: """:return: runtime path tied to the user""" diff --git a/src/platformdirs/macos.py b/src/platformdirs/macos.py index ec975112..ac0dfb56 100644 --- a/src/platformdirs/macos.py +++ b/src/platformdirs/macos.py @@ -59,6 +59,11 @@ def user_documents_dir(self) -> str: """:return: documents directory tied to the user, e.g. ``~/Documents``""" return os.path.expanduser("~/Documents") + @property + def user_pictures_dir(self) -> str: + """:return: pictures directory tied to the user, e.g. ``~/Pictures``""" + return os.path.expanduser("~/Pictures") + @property def user_runtime_dir(self) -> str: """:return: runtime directory tied to the user, e.g. ``~/Library/Caches/TemporaryItems/$appname/$version``""" diff --git a/src/platformdirs/unix.py b/src/platformdirs/unix.py index 17d355da..6c7b353f 100644 --- a/src/platformdirs/unix.py +++ b/src/platformdirs/unix.py @@ -127,13 +127,14 @@ def user_documents_dir(self) -> str: """ :return: documents directory tied to the user, e.g. ``~/Documents`` """ - documents_dir = _get_user_dirs_folder("XDG_DOCUMENTS_DIR") - if documents_dir is None: - documents_dir = os.environ.get("XDG_DOCUMENTS_DIR", "").strip() - if not documents_dir: - documents_dir = os.path.expanduser("~/Documents") + return _get_user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents") - return documents_dir + @property + def user_pictures_dir(self) -> str: + """ + :return: pictures directory tied to the user, e.g. ``~/Pictures`` + """ + return _get_user_media_dir("XDG_PICTURES_DIR", "~/Pictures") @property def user_runtime_dir(self) -> str: @@ -168,6 +169,16 @@ def _first_item_as_path_if_multipath(self, directory: str) -> Path: return Path(directory) +def _get_user_media_dir(env_var: str, fallback_tilde_path: str) -> str: + media_dir = _get_user_dirs_folder(env_var) + if media_dir is None: + media_dir = os.environ.get(env_var, "").strip() + if not media_dir: + media_dir = os.path.expanduser(fallback_tilde_path) + + return media_dir + + def _get_user_dirs_folder(key: str) -> str | None: """Return directory from user-dirs.dirs config file. See https://freedesktop.org/wiki/Software/xdg-user-dirs/""" user_dirs_config_path = os.path.join(Unix().user_config_dir, "user-dirs.dirs") diff --git a/src/platformdirs/windows.py b/src/platformdirs/windows.py index e7573c3d..e549ad69 100644 --- a/src/platformdirs/windows.py +++ b/src/platformdirs/windows.py @@ -101,6 +101,13 @@ def user_documents_dir(self) -> str: """ return os.path.normpath(get_win_folder("CSIDL_PERSONAL")) + @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")) + @property def user_runtime_dir(self) -> str: """ @@ -116,6 +123,9 @@ def get_win_folder_from_env_vars(csidl_name: str) -> str: if csidl_name == "CSIDL_PERSONAL": # does not have an environment name return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Documents") + if csidl_name == "CSIDL_MYPICTURES": # does not have an environment name + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Pictures") + env_var_name = { "CSIDL_APPDATA": "APPDATA", "CSIDL_COMMON_APPDATA": "ALLUSERSPROFILE", @@ -141,6 +151,7 @@ def get_win_folder_from_registry(csidl_name: str) -> str: "CSIDL_COMMON_APPDATA": "Common AppData", "CSIDL_LOCAL_APPDATA": "Local AppData", "CSIDL_PERSONAL": "Personal", + "CSIDL_MYPICTURES": "My Pictures", }.get(csidl_name) if shell_folder_name is None: raise ValueError(f"Unknown CSIDL name: {csidl_name}") @@ -160,6 +171,7 @@ def get_win_folder_via_ctypes(csidl_name: str) -> str: "CSIDL_COMMON_APPDATA": 35, "CSIDL_LOCAL_APPDATA": 28, "CSIDL_PERSONAL": 5, + "CSIDL_MYPICTURES": 39, }.get(csidl_name) if csidl_const is None: raise ValueError(f"Unknown CSIDL name: {csidl_name}") diff --git a/tests/conftest.py b/tests/conftest.py index f205c7cc..ebaa0b11 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ "user_state_dir", "user_log_dir", "user_documents_dir", + "user_pictures_dir", "user_runtime_dir", "site_data_dir", "site_config_dir", diff --git a/tests/test_android.py b/tests/test_android.py index 6e1d7fc3..558e5f86 100644 --- a/tests/test_android.py +++ b/tests/test_android.py @@ -52,6 +52,7 @@ def test_android(mocker: MockerFixture, params: dict[str, Any], func: str) -> No "user_state_dir": f"/data/data/com.example/files{suffix}", "user_log_dir": f"/data/data/com.example/cache{suffix}{'' if params.get('opinion', True) is False else '/log'}", "user_documents_dir": "/storage/emulated/0/Documents", + "user_pictures_dir": "/storage/emulated/0/Pictures", "user_runtime_dir": f"/data/data/com.example/cache{suffix}{'' if not params.get('opinion', True) else '/tmp'}", } expected = expected_map[func] diff --git a/tests/test_macos.py b/tests/test_macos.py index f17fc4a9..2d31f11c 100644 --- a/tests/test_macos.py +++ b/tests/test_macos.py @@ -33,6 +33,7 @@ def test_macos(params: dict[str, Any], func: str) -> None: "user_state_dir": f"{home}/Library/Application Support{suffix}", "user_log_dir": f"{home}/Library/Logs{suffix}", "user_documents_dir": f"{home}/Documents", + "user_pictures_dir": f"{home}/Pictures", "user_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}", } expected = expected_map[func] diff --git a/tests/test_unix.py b/tests/test_unix.py index 51ffae3e..da82513c 100644 --- a/tests/test_unix.py +++ b/tests/test_unix.py @@ -47,6 +47,40 @@ def test_user_documents_dir_default(mocker: MockerFixture) -> None: assert Unix().user_documents_dir == "/home/example/Documents" +def test_user_pictures_dir(mocker: MockerFixture) -> None: + example_path = "/home/example/ExamplePicturesFolder" + mock = mocker.patch("platformdirs.unix._get_user_dirs_folder") + mock.return_value = example_path + assert Unix().user_pictures_dir == example_path + + +def test_user_pictures_dir_env_var(mocker: MockerFixture) -> None: + # Mock pictures dir not being in user-dirs.dirs file + mock = mocker.patch("platformdirs.unix._get_user_dirs_folder") + mock.return_value = None + + example_path = "/home/example/ExamplePicturesFolder" + mocker.patch.dict(os.environ, {"XDG_PICTURES_DIR": example_path}) + + assert Unix().user_pictures_dir == example_path + + +def test_user_pictures_dir_default(mocker: MockerFixture) -> None: + # Mock pictures dir not being in user-dirs.dirs file + mock = mocker.patch("platformdirs.unix._get_user_dirs_folder") + mock.return_value = None + + # Mock no XDG_PICTURES_DIR env variable being set + mocker.patch.dict(os.environ, {"XDG_PICTURES_DIR": ""}) + + # Mock home directory + mocker.patch.dict(os.environ, {"HOME": "/home/example"}) + # Mock home directory for running the test on Windows + mocker.patch.dict(os.environ, {"USERPROFILE": "/home/example"}) + + assert Unix().user_pictures_dir == "/home/example/Pictures" + + class XDGVariable(typing.NamedTuple): name: str default_value: str