Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/platforms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,27 @@ Default paths
applications directory where ``.desktop`` files (Linux), app bundles (macOS), or
Start Menu shortcuts (Windows) are placed.

``site_applications_dir``
~~~~~~~~~~~~~~~~~~~~~~~~~~

.. list-table::
:widths: 20 80

* - Linux
- ``/usr/share/applications``
* - macOS
- ``/Applications``
* - Windows
- ``C:\ProgramData\Microsoft\Windows\Start Menu\Programs``
* - Android
- same as ``user_applications_dir``

.. note::

This property does not append ``appname`` or ``version``. It returns the system-wide
applications directory where ``.desktop`` files (Linux), app bundles (macOS), or
Start Menu shortcuts (Windows) are installed for all users.

``user_bin_dir``
~~~~~~~~~~~~~~~~

Expand Down
32 changes: 32 additions & 0 deletions src/platformdirs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,21 @@ def user_applications_dir() -> str:
return PlatformDirs().user_applications_dir


def site_applications_dir(
multipath: bool = False, # noqa: FBT001, FBT002
ensure_exists: bool = False, # noqa: FBT001, FBT002
) -> str:
"""
:param multipath: See `multipath <platformdirs.api.PlatformDirsABC.multipath>`.
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
:returns: applications directory shared by users
"""
return PlatformDirs(
multipath=multipath,
ensure_exists=ensure_exists,
).site_applications_dir


def user_runtime_dir( # noqa: PLR0913, PLR0917
appname: str | None = None,
appauthor: str | Literal[False] | None = None,
Expand Down Expand Up @@ -688,6 +703,21 @@ def user_applications_path() -> Path:
return PlatformDirs().user_applications_path


def site_applications_path(
multipath: bool = False, # noqa: FBT001, FBT002
ensure_exists: bool = False, # noqa: FBT001, FBT002
) -> Path:
"""
:param multipath: See `multipath <platformdirs.api.PlatformDirsABC.multipath>`.
:param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
:returns: applications path shared by users
"""
return PlatformDirs(
multipath=multipath,
ensure_exists=ensure_exists,
).site_applications_path


def user_runtime_path( # noqa: PLR0913, PLR0917
appname: str | None = None,
appauthor: str | Literal[False] | None = None,
Expand Down Expand Up @@ -745,6 +775,8 @@ def site_runtime_path(
"PlatformDirsABC",
"__version__",
"__version_info__",
"site_applications_dir",
"site_applications_path",
"site_cache_dir",
"site_cache_path",
"site_config_dir",
Expand Down
1 change: 1 addition & 0 deletions src/platformdirs/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"site_cache_dir",
"site_state_dir",
"site_log_dir",
"site_applications_dir",
"site_runtime_dir",
)

Expand Down
12 changes: 12 additions & 0 deletions src/platformdirs/_xdg.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ def user_applications_dir(self) -> str:
return os.path.join(os.path.expanduser(path), "applications") # noqa: PTH111, PTH118
return super().user_applications_dir

@property
def _site_applications_dirs(self) -> list[str]:
if xdg_dirs := os.environ.get("XDG_DATA_DIRS", "").strip():
return [os.path.join(p, "applications") for p in xdg_dirs.split(os.pathsep) if p.strip()] # noqa: PTH118
return super()._site_applications_dirs # type: ignore[misc]

@property
def site_applications_dir(self) -> str:
""":return: applications directories shared by users, from ``$XDG_DATA_DIRS`` if set, else platform default"""
dirs = self._site_applications_dirs
return os.pathsep.join(dirs) if self.multipath else dirs[0]


__all__ = [
"XDGMixin",
Expand Down
7 changes: 6 additions & 1 deletion src/platformdirs/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .api import PlatformDirsABC


class Android(PlatformDirsABC):
class Android(PlatformDirsABC): # noqa: PLR0904
"""
Platform directories for Android.

Expand Down Expand Up @@ -123,6 +123,11 @@ def user_applications_dir(self) -> str:
""":return: applications directory tied to the user, same as `user_data_dir`"""
return self.user_data_dir

@property
def site_applications_dir(self) -> str:
""":return: applications directory shared by users, same as `user_applications_dir`"""
return self.user_applications_dir

@property
def user_runtime_dir(self) -> str:
"""
Expand Down
10 changes: 10 additions & 0 deletions src/platformdirs/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ def user_bin_dir(self) -> str:
def user_applications_dir(self) -> str:
""":return: applications directory tied to the user"""

@property
@abstractmethod
def site_applications_dir(self) -> str:
""":return: applications directory shared by users"""

@property
@abstractmethod
def user_runtime_dir(self) -> str:
Expand Down Expand Up @@ -314,6 +319,11 @@ def user_applications_path(self) -> Path:
""":return: applications path tied to the user"""
return Path(self.user_applications_dir)

@property
def site_applications_path(self) -> Path:
""":return: applications path shared by users"""
return Path(self.site_applications_dir)

@property
def user_runtime_path(self) -> Path:
""":return: runtime path tied to the user"""
Expand Down
10 changes: 10 additions & 0 deletions src/platformdirs/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ def user_applications_dir(self) -> str:
""":return: applications directory tied to the user, e.g. ``~/Applications``"""
return os.path.expanduser("~/Applications") # noqa: PTH111

@property
def _site_applications_dirs(self) -> list[str]:
return ["/Applications"]

@property
def site_applications_dir(self) -> str:
""":return: applications directory shared by users, e.g. ``/Applications``"""
dirs = self._site_applications_dirs
return os.pathsep.join(dirs) if self.multipath else dirs[0]

@property
def user_runtime_dir(self) -> str:
""":return: runtime directory tied to the user, e.g. ``~/Library/Caches/TemporaryItems/$appname/$version``"""
Expand Down
15 changes: 15 additions & 0 deletions src/platformdirs/unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ def user_applications_dir(self) -> str:
""":return: applications directory tied to the user, e.g. ``~/.local/share/applications``"""
return os.path.join(os.path.expanduser("~/.local/share"), "applications") # noqa: PTH111, PTH118

@property
def _site_applications_dirs(self) -> list[str]:
return [os.path.join(p, "applications") for p in ["/usr/local/share", "/usr/share"]] # noqa: PTH118

@property
def site_applications_dir(self) -> str:
""":return: applications directory shared by users, e.g. ``/usr/share/applications``"""
dirs = self._site_applications_dirs
return os.pathsep.join(dirs) if self.multipath else dirs[0]

@property
def user_runtime_dir(self) -> str:
"""
Expand Down Expand Up @@ -244,6 +254,11 @@ def user_log_dir(self) -> str:
""":return: log directory tied to the user, or site equivalent when root with ``use_site_for_root``"""
return self.site_log_dir if self._use_site else super().user_log_dir

@property
def user_applications_dir(self) -> str:
""":return: applications directory tied to the user, or site equivalent when root with ``use_site_for_root``"""
return self.site_applications_dir if self._use_site else super().user_applications_dir

@property
def user_runtime_dir(self) -> str:
""":return: runtime directory tied to the user, or site equivalent when root with ``use_site_for_root``"""
Expand Down
22 changes: 21 additions & 1 deletion src/platformdirs/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
_KF_FLAG_DONT_VERIFY: Final[int] = 0x00004000


class Windows(PlatformDirsABC):
class Windows(PlatformDirsABC): # noqa: PLR0904
"""
`MSDN on where to store app data files <https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid>`_.

Expand Down Expand Up @@ -151,6 +151,14 @@ def user_applications_dir(self) -> str:
""":return: applications directory tied to the user, e.g. ``Start Menu\\Programs``"""
return os.path.normpath(get_win_folder("CSIDL_PROGRAMS"))

@property
def site_applications_dir(self) -> str:
"""
:return: applications directory shared by users, e.g. \
``C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs``
"""
return os.path.normpath(get_win_folder("CSIDL_COMMON_PROGRAMS"))

@property
def user_runtime_dir(self) -> str:
"""
Expand Down Expand Up @@ -212,6 +220,15 @@ def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None: #
"Start Menu",
"Programs",
)

if csidl_name == "CSIDL_COMMON_PROGRAMS":
return os.path.join( # noqa: PTH118
os.path.normpath(os.environ.get("PROGRAMDATA", os.environ.get("ALLUSERSPROFILE", "C:\\ProgramData"))),
"Microsoft",
"Windows",
"Start Menu",
"Programs",
)
return None


Expand All @@ -225,6 +242,7 @@ def get_win_folder_from_registry(csidl_name: str) -> str:
"""
machine_names = {
"CSIDL_COMMON_APPDATA",
"CSIDL_COMMON_PROGRAMS",
}
shell_folder_name = {
"CSIDL_APPDATA": "AppData",
Expand All @@ -236,6 +254,7 @@ def get_win_folder_from_registry(csidl_name: str) -> str:
"CSIDL_MYVIDEO": "My Video",
"CSIDL_MYMUSIC": "My Music",
"CSIDL_PROGRAMS": "Programs",
"CSIDL_COMMON_PROGRAMS": "Common Programs",
}.get(csidl_name)
if shell_folder_name is None:
msg = f"Unknown CSIDL name: {csidl_name}"
Expand Down Expand Up @@ -263,6 +282,7 @@ def get_win_folder_from_registry(csidl_name: str) -> str:
"CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}",
"CSIDL_DESKTOPDIRECTORY": "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}",
"CSIDL_PROGRAMS": "{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}",
"CSIDL_COMMON_PROGRAMS": "{0139D44E-6AFE-49F2-8690-3DAFCAE6FFB8}",
}


Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"site_cache_dir",
"site_state_dir",
"site_log_dir",
"site_applications_dir",
"site_runtime_dir",
)

Expand Down
1 change: 1 addition & 0 deletions tests/test_android.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def test_android(mocker: MockerFixture, params: dict[str, Any], func: str) -> No
"user_desktop_dir": "/storage/emulated/0/Desktop",
"user_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}",
"site_runtime_dir": f"/data/data/com.example/cache{suffix}{'' if not params.get('opinion', True) else val}",
}
Expand Down
1 change: 1 addition & 0 deletions tests/test_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def test_macos(mocker: MockerFixture, params: dict[str, Any], func: str) -> None
"user_desktop_dir": f"{home}/Desktop",
"user_bin_dir": f"{home}/.local/bin",
"user_applications_dir": f"{home}/Applications",
"site_applications_dir": "/Applications",
"user_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}",
"site_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}",
}
Expand Down
1 change: 1 addition & 0 deletions tests/test_unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def _func_to_path(func: str) -> XDGVariable | None:
"user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", f"{gettempdir()}/runtime-1234"),
"user_bin_dir": None,
"user_applications_dir": None,
"site_applications_dir": None,
"site_log_dir": None,
"site_state_dir": None,
"site_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run"),
Expand Down
3 changes: 3 additions & 0 deletions tests/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"CSIDL_MYMUSIC": r"C:\Users\Test\Music",
"CSIDL_DESKTOPDIRECTORY": r"C:\Users\Test\Desktop",
"CSIDL_PROGRAMS": r"C:\Users\Test\AppData\Roaming\Microsoft\Windows\Start Menu\Programs",
"CSIDL_COMMON_PROGRAMS": r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs",
}

_LOCAL = os.path.normpath(_WIN_FOLDERS["CSIDL_LOCAL_APPDATA"])
Expand Down Expand Up @@ -91,6 +92,7 @@ def test_windows(params: dict[str, Any], func: str) -> None:
"user_desktop_dir": os.path.normpath(_WIN_FOLDERS["CSIDL_DESKTOPDIRECTORY"]),
"user_bin_dir": os.path.join(_LOCAL, "Programs"), # 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,
"site_runtime_dir": temp,
}
Expand Down Expand Up @@ -302,6 +304,7 @@ def test_known_folder_guids_has_all_csidl_names() -> None:
"CSIDL_DOWNLOADS",
"CSIDL_DESKTOPDIRECTORY",
"CSIDL_PROGRAMS",
"CSIDL_COMMON_PROGRAMS",
}
assert set(_KNOWN_FOLDER_GUIDS.keys()) == expected

Expand Down