platformdirs: introduce `site_runtime_dir` (#212)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
diff --git a/src/platformdirs/__init__.py b/src/platformdirs/__init__.py
index 2d61c87..3d5a5bd 100644
--- a/src/platformdirs/__init__.py
+++ b/src/platformdirs/__init__.py
@@ -293,6 +293,30 @@
     ).user_runtime_dir
 
 
+def site_runtime_dir(
+    appname: str | None = None,
+    appauthor: str | None | Literal[False] = None,
+    version: str | None = None,
+    opinion: bool = True,  # noqa: FBT001, FBT002
+    ensure_exists: bool = False,  # noqa: FBT001, FBT002
+) -> str:
+    """
+    :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+    :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+    :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+    :param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
+    :param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
+    :returns: runtime directory shared by users
+    """
+    return PlatformDirs(
+        appname=appname,
+        appauthor=appauthor,
+        version=version,
+        opinion=opinion,
+        ensure_exists=ensure_exists,
+    ).site_runtime_dir
+
+
 def user_data_path(
     appname: str | None = None,
     appauthor: str | None | Literal[False] = None,
@@ -539,6 +563,30 @@
     ).user_runtime_path
 
 
+def site_runtime_path(
+    appname: str | None = None,
+    appauthor: str | None | Literal[False] = None,
+    version: str | None = None,
+    opinion: bool = True,  # noqa: FBT001, FBT002
+    ensure_exists: bool = False,  # noqa: FBT001, FBT002
+) -> Path:
+    """
+    :param appname: See `appname <platformdirs.api.PlatformDirsABC.appname>`.
+    :param appauthor: See `appauthor <platformdirs.api.PlatformDirsABC.appauthor>`.
+    :param version: See `version <platformdirs.api.PlatformDirsABC.version>`.
+    :param opinion: See `opinion <platformdirs.api.PlatformDirsABC.opinion>`.
+    :param ensure_exists: See `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`.
+    :returns: runtime path shared by users
+    """
+    return PlatformDirs(
+        appname=appname,
+        appauthor=appauthor,
+        version=version,
+        opinion=opinion,
+        ensure_exists=ensure_exists,
+    ).site_runtime_path
+
+
 __all__ = [
     "__version__",
     "__version_info__",
@@ -560,6 +608,7 @@
     "site_data_dir",
     "site_config_dir",
     "site_cache_dir",
+    "site_runtime_dir",
     "user_data_path",
     "user_config_path",
     "user_cache_path",
@@ -575,4 +624,5 @@
     "site_data_path",
     "site_config_path",
     "site_cache_path",
+    "site_runtime_path",
 ]
diff --git a/src/platformdirs/__main__.py b/src/platformdirs/__main__.py
index d91204d..3cefedb 100644
--- a/src/platformdirs/__main__.py
+++ b/src/platformdirs/__main__.py
@@ -18,6 +18,7 @@
     "site_data_dir",
     "site_config_dir",
     "site_cache_dir",
+    "site_runtime_dir",
 )
 
 
diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py
index 29aa995..572559f 100644
--- a/src/platformdirs/android.py
+++ b/src/platformdirs/android.py
@@ -108,6 +108,11 @@
             path = os.path.join(path, "tmp")  # noqa: PTH118
         return path
 
+    @property
+    def site_runtime_dir(self) -> str:
+        """:return: runtime directory shared by users, same as `user_runtime_dir`"""
+        return self.user_runtime_dir
+
 
 @lru_cache(maxsize=1)
 def _android_folder() -> str | None:
diff --git a/src/platformdirs/api.py b/src/platformdirs/api.py
index ea327b2..1315799 100644
--- a/src/platformdirs/api.py
+++ b/src/platformdirs/api.py
@@ -158,6 +158,11 @@
         """:return: runtime directory tied to the user"""
 
     @property
+    @abstractmethod
+    def site_runtime_dir(self) -> str:
+        """:return: runtime directory shared by users"""
+
+    @property
     def user_data_path(self) -> Path:
         """:return: data path tied to the user"""
         return Path(self.user_data_dir)
@@ -231,3 +236,8 @@
     def user_runtime_path(self) -> Path:
         """:return: runtime path tied to the user"""
         return Path(self.user_runtime_dir)
+
+    @property
+    def site_runtime_path(self) -> Path:
+        """:return: runtime path shared by users"""
+        return Path(self.site_runtime_dir)
diff --git a/src/platformdirs/macos.py b/src/platformdirs/macos.py
index 3a10dcb..7800fe1 100644
--- a/src/platformdirs/macos.py
+++ b/src/platformdirs/macos.py
@@ -90,6 +90,11 @@
         """:return: runtime directory tied to the user, e.g. ``~/Library/Caches/TemporaryItems/$appname/$version``"""
         return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches/TemporaryItems"))  # noqa: PTH111
 
+    @property
+    def site_runtime_dir(self) -> str:
+        """:return: runtime directory shared by users, same as `user_runtime_dir`"""
+        return self.user_runtime_dir
+
 
 __all__ = [
     "MacOS",
diff --git a/src/platformdirs/unix.py b/src/platformdirs/unix.py
index c355f17..de4573e 100644
--- a/src/platformdirs/unix.py
+++ b/src/platformdirs/unix.py
@@ -173,6 +173,28 @@
         return self._append_app_name_and_version(path)
 
     @property
+    def site_runtime_dir(self) -> str:
+        """
+        :return: runtime directory shared by users, e.g. ``/run/$appname/$version`` or
+        ``$XDG_RUNTIME_DIR/$appname/$version``.
+
+        Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will
+        fallback to paths associated to the root user instead of a regular logged-in user if it's not set.
+
+        If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir`
+        instead.
+
+        For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set.
+        """
+        path = os.environ.get("XDG_RUNTIME_DIR", "")
+        if not path.strip():
+            if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
+                path = "/var/run"
+            else:
+                path = "/run"
+        return self._append_app_name_and_version(path)
+
+    @property
     def site_data_path(self) -> Path:
         """:return: data path shared by users. Only return first item, even if ``multipath`` is set to ``True``"""
         return self._first_item_as_path_if_multipath(self.site_data_dir)
diff --git a/src/platformdirs/windows.py b/src/platformdirs/windows.py
index 54c2f24..0ba3d27 100644
--- a/src/platformdirs/windows.py
+++ b/src/platformdirs/windows.py
@@ -136,6 +136,11 @@
         path = os.path.normpath(os.path.join(get_win_folder("CSIDL_LOCAL_APPDATA"), "Temp"))  # noqa: PTH118
         return self._append_parts(path)
 
+    @property
+    def site_runtime_dir(self) -> str:
+        """:return: runtime directory shared by users, same as `user_runtime_dir`"""
+        return self.user_runtime_dir
+
 
 def get_win_folder_from_env_vars(csidl_name: str) -> str:
     """Get folder from environment variables."""
diff --git a/tests/conftest.py b/tests/conftest.py
index ff759d7..06c6802 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -22,6 +22,7 @@
     "site_data_dir",
     "site_config_dir",
     "site_cache_dir",
+    "site_runtime_dir",
 )
 
 
diff --git a/tests/test_android.py b/tests/test_android.py
index 281d7aa..87fa98c 100644
--- a/tests/test_android.py
+++ b/tests/test_android.py
@@ -60,6 +60,7 @@
         "user_music_dir": "/storage/emulated/0/Music",
         "user_desktop_dir": "/storage/emulated/0/Desktop",
         "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}",
     }
     expected = expected_map[func]
 
diff --git a/tests/test_macos.py b/tests/test_macos.py
index ae1c4f5..decbec5 100644
--- a/tests/test_macos.py
+++ b/tests/test_macos.py
@@ -40,6 +40,7 @@
         "user_music_dir": f"{home}/Music",
         "user_desktop_dir": f"{home}/Desktop",
         "user_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}",
+        "site_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 3a52fd8..3c096af 100644
--- a/tests/test_unix.py
+++ b/tests/test_unix.py
@@ -98,6 +98,7 @@
         "user_state_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"),
         "user_log_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"),
         "user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run/user/1234"),
+        "site_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run"),
     }
     return mapping.get(func)
 
@@ -151,6 +152,8 @@
     monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
     mocker.patch("sys.platform", platform)
 
+    assert Unix().site_runtime_dir == "/var/run"
+
     mocker.patch("pathlib.Path.exists", return_value=True)
     assert Unix().user_runtime_dir == "/var/run/user/1234"