Skip to content

Commit

Permalink
Add user_runtime_dir for $XDG_RUNTIME_DIR (#37)
Browse files Browse the repository at this point in the history
Co-authored-by: Bernát Gábor <[email protected]>
  • Loading branch information
whonore and gaborbernat authored Aug 30, 2021
1 parent 87ae394 commit 36f17d6
Show file tree
Hide file tree
Showing 14 changed files with 143 additions and 0 deletions.
13 changes: 13 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ This kind of thing is what the ``platformdirs`` module is for.
- site data dir (``site_data_dir``)
- site config dir (``site_config_dir``)
- user log dir (``user_log_dir``)
- user runtime dir (``user_runtime_dir``)

And also:

Expand All @@ -65,6 +66,8 @@ On macOS:
'/Users/trentm/Library/Caches/SuperApp'
>>> user_log_dir(appname, appauthor)
'/Users/trentm/Library/Logs/SuperApp'
>>> user_runtime_dir(appname, appauthor)
'/Users/trentm/Library/Caches/TemporaryItems/SuperApp'
On Windows 7:

Expand All @@ -81,6 +84,8 @@ On Windows 7:
'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Cache'
>>> user_log_dir(appname, appauthor)
'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Logs'
>>> user_runtime_dir(appname, appauthor)
'C:\\Users\\trentm\\AppData\\Local\\Temp\\Acme\\SuperApp'
On Linux:

Expand All @@ -101,6 +106,8 @@ On Linux:
'/home/trentm/.cache/SuperApp/log'
>>> user_config_dir(appname)
'/home/trentm/.config/SuperApp'
>>> user_runtime_dir(appname, appauthor)
'/run/user/{os.getuid()}/SuperApp'
>>> site_config_dir(appname)
'/etc/xdg/SuperApp'
>>> os.environ['XDG_CONFIG_DIRS'] = '/etc:/usr/local/etc'
Expand All @@ -120,6 +127,8 @@ On Android::
'/data/data/com.termux/cache/SuperApp/log'
>>> user_config_dir(appname)
'/data/data/com.termux/shared_prefs/SuperApp'
>>> user_runtime_dir(appname, appauthor)
'/data/data/com.termux/cache/SuperApp/tmp'


``PlatformDirs`` for convenience
Expand All @@ -137,6 +146,8 @@ On Android::
'/Users/trentm/Library/Caches/SuperApp'
>>> dirs.user_log_dir
'/Users/trentm/Library/Logs/SuperApp'
>>> dirs.user_runtime_dir
'/Users/trentm/Library/Caches/TemporaryItems/SuperApp'
Per-version isolation
=====================
Expand All @@ -155,6 +166,8 @@ dirs::
'/Users/trentm/Library/Caches/SuperApp/1.0'
>>> dirs.user_log_dir
'/Users/trentm/Library/Logs/SuperApp/1.0'
>>> dirs.user_runtime_dir
'/Users/trentm/Library/Caches/TemporaryItems/SuperApp/1.0'

Be wary of using this for configuration files though; you'll need to handle
migrating configuration files manually.
Expand Down
6 changes: 6 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ Logs directory
.. autofunction:: platformdirs.user_log_dir
.. autofunction:: platformdirs.user_log_path

Runtime directory
-------------------

.. autofunction:: platformdirs.user_runtime_dir
.. autofunction:: platformdirs.user_runtime_path

Shared directories
~~~~~~~~~~~~~~~~~~

Expand Down
34 changes: 34 additions & 0 deletions src/platformdirs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,22 @@ def user_log_dir(
return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_log_dir


def user_runtime_dir(
appname: Optional[str] = None,
appauthor: Union[str, None, "Literal[False]"] = None,
version: Optional[str] = None,
opinion: bool = True,
) -> 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>`.
:returns: runtime directory tied to the user
"""
return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_runtime_dir


def user_data_path(
appname: Optional[str] = None,
appauthor: Union[str, None, "Literal[False]"] = None,
Expand Down Expand Up @@ -256,6 +272,22 @@ def user_log_path(
return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_log_path


def user_runtime_path(
appname: Optional[str] = None,
appauthor: Union[str, None, "Literal[False]"] = None,
version: Optional[str] = None,
opinion: bool = True,
) -> 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>`.
:returns: runtime path tied to the user
"""
return PlatformDirs(appname=appname, appauthor=appauthor, version=version, opinion=opinion).user_runtime_path


__all__ = [
"__version__",
"__version_info__",
Expand All @@ -267,13 +299,15 @@ def user_log_path(
"user_cache_dir",
"user_state_dir",
"user_log_dir",
"user_runtime_dir",
"site_data_dir",
"site_config_dir",
"user_data_path",
"user_config_path",
"user_cache_path",
"user_state_path",
"user_log_path",
"user_runtime_path",
"site_data_path",
"site_config_path",
]
1 change: 1 addition & 0 deletions src/platformdirs/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"user_cache_dir",
"user_state_dir",
"user_log_dir",
"user_runtime_dir",
"site_data_dir",
"site_config_dir",
)
Expand Down
11 changes: 11 additions & 0 deletions src/platformdirs/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ def user_log_dir(self) -> str:
path = os.path.join(path, "log")
return path

@property
def user_runtime_dir(self) -> str:
"""
:return: runtime directory tied to the user, same as `user_cache_dir` if not opinionated else ``tmp`` in it,
e.g. ``/data/user/<userid>/<packagename>/cache/<AppName>/tmp``
"""
path = self.user_cache_dir
if self.opinion:
path = os.path.join(path, "tmp")
return path


@lru_cache(maxsize=1)
def _android_folder() -> 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 @@ -99,6 +99,11 @@ def user_state_dir(self) -> str:
def user_log_dir(self) -> str:
""":return: log directory tied to the user"""

@property
@abstractmethod
def user_runtime_dir(self) -> str:
""":return: runtime directory tied to the user"""

@property
def user_data_path(self) -> Path:
""":return: data path tied to the user"""
Expand Down Expand Up @@ -133,3 +138,8 @@ def user_state_path(self) -> Path:
def user_log_path(self) -> Path:
""":return: log path tied to the user"""
return Path(self.user_log_dir)

@property
def user_runtime_path(self) -> Path:
""":return: runtime path tied to the user"""
return Path(self.user_runtime_dir)
5 changes: 5 additions & 0 deletions src/platformdirs/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ def user_log_dir(self) -> str:
""":return: log directory tied to the user, e.g. ``~/Library/Logs/$appname/$version``"""
return self._append_app_name_and_version(os.path.expanduser("~/Library/Logs"))

@property
def user_runtime_dir(self) -> str:
""":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"))


__all__ = [
"MacOS",
Expand Down
19 changes: 19 additions & 0 deletions src/platformdirs/unix.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import os
import sys
from pathlib import Path

from .api import PlatformDirsABC

if sys.platform.startswith("linux"): # pragma: no branch # no op check, only to please the type checker
from os import getuid
else:

def getuid() -> int:
raise RuntimeError("should only be used on Linux")


class Unix(PlatformDirsABC):
"""
Expand Down Expand Up @@ -103,6 +111,17 @@ def user_log_dir(self) -> str:
path = os.path.join(path, "log")
return path

@property
def user_runtime_dir(self) -> str:
"""
:return: runtime directory tied to the user, e.g. ``/run/user/$(id -u)/$appname/$version`` or
``$XDG_RUNTIME_DIR/$appname/$version``
"""
path = os.environ.get("XDG_RUNTIME_DIR", "")
if not path.strip():
path = f"/run/user/{getuid()}"
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``"""
Expand Down
9 changes: 9 additions & 0 deletions src/platformdirs/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ def user_log_dir(self) -> str:
path = os.path.join(path, "Logs")
return path

@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"))
return self._append_parts(path)


def get_win_folder_from_env_vars(csidl_name: str) -> str:
"""Get folder from environment variables."""
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"user_cache_dir",
"user_state_dir",
"user_log_dir",
"user_runtime_dir",
"site_data_dir",
"site_config_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 @@ -48,6 +48,7 @@ def test_android(mocker: MockerFixture, params: Dict[str, Any], func: str) -> No
"user_cache_dir": f"/data/data/com.example/cache{suffix}",
"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_runtime_dir": f"/data/data/com.example/cache{suffix}{'' if not params.get('opinion', True) else '/tmp'}",
}
expected = expected_map[func]

Expand Down
4 changes: 4 additions & 0 deletions tests/test_comp_with_appdirs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import platformdirs

NEW_IN_PLATFORMDIRS = {"user_runtime_dir"}


def test_has_backward_compatible_class() -> None:
from platformdirs import AppDirs
Expand All @@ -29,6 +31,8 @@ def test_has_backward_compatible_class() -> None:
],
)
def test_compatibility(params: Dict[str, Any], func: str) -> None:
if func in NEW_IN_PLATFORMDIRS:
pytest.skip(f"`{func}` does not exist in `appdirs`")
if sys.platform == "darwin":
msg = { # pragma: no cover
"user_log_dir": "without appname produces NoneType error",
Expand Down
26 changes: 26 additions & 0 deletions tests/test_unix.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import importlib
import os
import sys
import typing

import pytest
from _pytest.monkeypatch import MonkeyPatch
from pytest_mock import MockerFixture

from platformdirs.unix import Unix

Expand All @@ -21,6 +24,7 @@ def _func_to_path(func: str) -> XDGVariable:
"user_cache_dir": XDGVariable("XDG_CACHE_HOME", "~/.cache"),
"user_state_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"),
"user_log_dir": XDGVariable("XDG_CACHE_HOME", "~/.cache"),
"user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run/user/1234"),
}
return mapping[func]

Expand All @@ -30,22 +34,44 @@ def dirs_instance() -> Unix:
return Unix(multipath=True, opinion=False)


@pytest.fixture()
def _getuid(mocker: MockerFixture) -> None:
mocker.patch("platformdirs.unix.getuid", return_value=1234)


@pytest.mark.usefixtures("_getuid")
def test_xdg_variable_not_set(monkeypatch: MonkeyPatch, dirs_instance: Unix, func: str) -> None:
xdg_variable = _func_to_path(func)
monkeypatch.delenv(xdg_variable.name, raising=False)
result = getattr(dirs_instance, func)
assert result == os.path.expanduser(xdg_variable.default_value)


@pytest.mark.usefixtures("_getuid")
def test_xdg_variable_empty_value(monkeypatch: MonkeyPatch, dirs_instance: Unix, func: str) -> None:
xdg_variable = _func_to_path(func)
monkeypatch.setenv(xdg_variable.name, "")
result = getattr(dirs_instance, func)
assert result == os.path.expanduser(xdg_variable.default_value)


@pytest.mark.usefixtures("_getuid")
def test_xdg_variable_custom_value(monkeypatch: MonkeyPatch, dirs_instance: Unix, func: str) -> None:
xdg_variable = _func_to_path(func)
monkeypatch.setenv(xdg_variable.name, "/tmp/custom-dir")
result = getattr(dirs_instance, func)
assert result == "/tmp/custom-dir"


def test_platform_non_linux(monkeypatch: MonkeyPatch) -> None:
from platformdirs import unix

try:
with monkeypatch.context() as context:
context.setattr(sys, "platform", "magic")
monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
importlib.reload(unix)
with pytest.raises(RuntimeError, match="should only be used on Linux"):
unix.Unix().user_runtime_dir
finally:
importlib.reload(unix)
3 changes: 3 additions & 0 deletions whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ csidl
delenv
dirs
func
getuid
highbit
hkey
intersphinx
Expand All @@ -26,9 +27,11 @@ pathlib
pathsep
platformdirs
pyjnius
runtime
setenv
shell32
typehints
unittest
usefixtures
winreg
xdg

0 comments on commit 36f17d6

Please sign in to comment.