diff --git a/docs/platforms.rst b/docs/platforms.rst index d00d320..57c9a44 100644 --- a/docs/platforms.rst +++ b/docs/platforms.rst @@ -341,6 +341,30 @@ Default paths This property does not append ``appname`` or ``version``. It returns the directory where user-installed executables and scripts are placed. +``site_bin_dir`` +~~~~~~~~~~~~~~~~ + +.. list-table:: + :widths: 20 80 + + * - Linux + - ``/usr/local/bin`` + * - macOS + - ``/usr/local/bin`` + * - Windows + - ``C:\ProgramData\bin`` + * - Android + - Same as ``user_bin_dir`` + +.. note:: + + This property does not append ``appname`` or ``version``. It returns the directory + where system-wide executables and scripts are placed. On Unix/Linux, this follows + the `FHS 3.0 `_ + standard for locally-installed software. On Windows, it mirrors the ``site_data_dir`` + pattern using ``%ProgramData%``, following the precedent set by + `Chocolatey `_. + macOS ----- diff --git a/src/platformdirs/__init__.py b/src/platformdirs/__init__.py index 84c9df5..fb530c9 100644 --- a/src/platformdirs/__init__.py +++ b/src/platformdirs/__init__.py @@ -340,6 +340,11 @@ def user_bin_dir() -> str: return PlatformDirs().user_bin_dir +def site_bin_dir() -> str: + """:returns: bin directory shared by users""" + return PlatformDirs().site_bin_dir + + def user_applications_dir() -> str: """:returns: applications directory tied to the user""" return PlatformDirs().user_applications_dir @@ -698,6 +703,11 @@ def user_bin_path() -> Path: return PlatformDirs().user_bin_path +def site_bin_path() -> Path: + """:returns: bin path shared by users""" + return PlatformDirs().site_bin_path + + def user_applications_path() -> Path: """:returns: applications path tied to the user""" return PlatformDirs().user_applications_path @@ -777,6 +787,8 @@ def site_runtime_path( "__version_info__", "site_applications_dir", "site_applications_path", + "site_bin_dir", + "site_bin_path", "site_cache_dir", "site_cache_path", "site_config_dir", diff --git a/src/platformdirs/__main__.py b/src/platformdirs/__main__.py index be875b2..2490ffb 100644 --- a/src/platformdirs/__main__.py +++ b/src/platformdirs/__main__.py @@ -16,6 +16,7 @@ "user_videos_dir", "user_music_dir", "user_bin_dir", + "site_bin_dir", "user_applications_dir", "user_runtime_dir", "site_data_dir", diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py index f9c4538..708355b 100644 --- a/src/platformdirs/android.py +++ b/src/platformdirs/android.py @@ -118,6 +118,11 @@ def user_bin_dir(self) -> str: """:return: bin directory tied to the user, e.g. ``/data/user///files/bin``""" return os.path.join(cast("str", _android_folder()), "files", "bin") # noqa: PTH118 + @property + def site_bin_dir(self) -> str: + """:return: bin directory shared by users, same as `user_bin_dir`""" + return self.user_bin_dir + @property def user_applications_dir(self) -> str: """:return: applications directory tied to the user, same as `user_data_dir`""" diff --git a/src/platformdirs/api.py b/src/platformdirs/api.py index ae90557..46afe0a 100644 --- a/src/platformdirs/api.py +++ b/src/platformdirs/api.py @@ -209,6 +209,11 @@ def user_desktop_dir(self) -> str: def user_bin_dir(self) -> str: """:return: bin directory tied to the user""" + @property + @abstractmethod + def site_bin_dir(self) -> str: + """:return: bin directory shared by users""" + @property @abstractmethod def user_applications_dir(self) -> str: @@ -314,6 +319,11 @@ def user_bin_path(self) -> Path: """:return: bin path tied to the user""" return Path(self.user_bin_dir) + @property + def site_bin_path(self) -> Path: + """:return: bin path shared by users""" + return Path(self.site_bin_dir) + @property def user_applications_path(self) -> Path: """:return: applications path tied to the user""" diff --git a/src/platformdirs/macos.py b/src/platformdirs/macos.py index 4be30bf..3343a0b 100644 --- a/src/platformdirs/macos.py +++ b/src/platformdirs/macos.py @@ -135,6 +135,11 @@ def user_bin_dir(self) -> str: """:return: bin directory tied to the user, e.g. ``~/.local/bin``""" return os.path.expanduser("~/.local/bin") # noqa: PTH111 + @property + def site_bin_dir(self) -> str: + """:return: bin directory shared by users, e.g. ``/usr/local/bin``""" + return "/usr/local/bin" + @property def user_applications_dir(self) -> str: """:return: applications directory tied to the user, e.g. ``~/Applications``""" diff --git a/src/platformdirs/unix.py b/src/platformdirs/unix.py index 84f6d6f..e388fcb 100644 --- a/src/platformdirs/unix.py +++ b/src/platformdirs/unix.py @@ -140,6 +140,11 @@ def user_bin_dir(self) -> str: """:return: bin directory tied to the user, e.g. ``~/.local/bin``""" return os.path.expanduser("~/.local/bin") # noqa: PTH111 + @property + def site_bin_dir(self) -> str: + """:return: bin directory shared by users, e.g. ``/usr/local/bin``""" + return "/usr/local/bin" + @property def user_applications_dir(self) -> str: """:return: applications directory tied to the user, e.g. ``~/.local/share/applications``""" @@ -266,6 +271,11 @@ def user_runtime_dir(self) -> str: """:return: runtime directory tied to the user, or site equivalent when root with ``use_site_for_root``""" return self.site_runtime_dir if self._use_site else super().user_runtime_dir + @property + def user_bin_dir(self) -> str: + """:return: bin directory tied to the user, or site equivalent when root with ``use_site_for_root``""" + return self.site_bin_dir if self._use_site else super().user_bin_dir + def _get_user_media_dir(env_var: str, fallback_tilde_path: str) -> str: if media_dir := _get_user_dirs_folder(env_var): diff --git a/src/platformdirs/windows.py b/src/platformdirs/windows.py index ad09afb..84afaa7 100644 --- a/src/platformdirs/windows.py +++ b/src/platformdirs/windows.py @@ -146,6 +146,11 @@ def user_bin_dir(self) -> str: """:return: bin directory tied to the user, e.g. ``%LOCALAPPDATA%\\Programs``""" return os.path.normpath(os.path.join(get_win_folder("CSIDL_LOCAL_APPDATA"), "Programs")) # noqa: PTH118 + @property + def site_bin_dir(self) -> str: + """:return: bin directory shared by users, e.g. ``C:\\ProgramData\\bin``""" + return os.path.normpath(os.path.join(get_win_folder("CSIDL_COMMON_APPDATA"), "bin")) # noqa: PTH118 + @property def user_applications_dir(self) -> str: """:return: applications directory tied to the user, e.g. ``Start Menu\\Programs``""" diff --git a/tests/conftest.py b/tests/conftest.py index d262d29..5f9edb9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ "user_videos_dir", "user_music_dir", "user_bin_dir", + "site_bin_dir", "user_applications_dir", "user_runtime_dir", "site_data_dir", diff --git a/tests/test_android.py b/tests/test_android.py index 3bb7a05..829bacb 100644 --- a/tests/test_android.py +++ b/tests/test_android.py @@ -62,6 +62,7 @@ def test_android(mocker: MockerFixture, params: dict[str, Any], func: str) -> No "user_music_dir": "/storage/emulated/0/Music", "user_desktop_dir": "/storage/emulated/0/Desktop", "user_bin_dir": "/data/data/com.example/files/bin", + "site_bin_dir": "/data/data/com.example/files/bin", "user_applications_dir": f"/data/data/com.example/files{suffix}", "site_applications_dir": f"/data/data/com.example/files{suffix}", "user_runtime_dir": f"/data/data/com.example/cache{suffix}{'' if not params.get('opinion', True) else val}", diff --git a/tests/test_macos.py b/tests/test_macos.py index daf8b35..4690860 100644 --- a/tests/test_macos.py +++ b/tests/test_macos.py @@ -84,6 +84,7 @@ def test_macos(mocker: MockerFixture, params: dict[str, Any], func: str) -> None "user_music_dir": f"{home}/Music", "user_desktop_dir": f"{home}/Desktop", "user_bin_dir": f"{home}/.local/bin", + "site_bin_dir": "/usr/local/bin", "user_applications_dir": f"{home}/Applications", "site_applications_dir": "/Applications", "user_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}", @@ -270,6 +271,7 @@ def test_macos_xdg_empty_falls_back( "user_music_dir": f"{home}/Music", "user_desktop_dir": f"{home}/Desktop", "user_bin_dir": f"{home}/.local/bin", + "site_bin_dir": "/usr/local/bin", "user_applications_dir": f"{home}/Applications", } assert getattr(MacOS(), prop) == expected_map[prop] diff --git a/tests/test_unix.py b/tests/test_unix.py index 82b6d26..856ce57 100644 --- a/tests/test_unix.py +++ b/tests/test_unix.py @@ -106,6 +106,7 @@ def _func_to_path(func: str) -> XDGVariable | None: "user_log_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"), "user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", f"{gettempdir()}/runtime-1234"), "user_bin_dir": None, + "site_bin_dir": None, "user_applications_dir": None, "site_applications_dir": None, "site_log_dir": None, @@ -346,6 +347,7 @@ def test_user_media_dir_no_user_dirs_file( ("user_state_dir", os.path.join("/var/lib", "foo")), # noqa: PTH118 ("user_log_dir", os.path.join("/var/log", "foo")), # noqa: PTH118 ("user_runtime_dir", os.path.join("/run", "foo")), # noqa: PTH118 + ("user_bin_dir", "/usr/local/bin"), ] diff --git a/tests/test_windows.py b/tests/test_windows.py index 5e123a0..4a4a704 100644 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -91,6 +91,7 @@ def test_windows(params: dict[str, Any], func: str) -> None: "user_music_dir": os.path.normpath(_WIN_FOLDERS["CSIDL_MYMUSIC"]), "user_desktop_dir": os.path.normpath(_WIN_FOLDERS["CSIDL_DESKTOPDIRECTORY"]), "user_bin_dir": os.path.join(_LOCAL, "Programs"), # noqa: PTH118 + "site_bin_dir": os.path.join(_COMMON, "bin"), # noqa: PTH118 "user_applications_dir": os.path.normpath(_WIN_FOLDERS["CSIDL_PROGRAMS"]), "site_applications_dir": os.path.normpath(_WIN_FOLDERS["CSIDL_COMMON_PROGRAMS"]), "user_runtime_dir": temp,