Skip to content

Commit

Permalink
MacOS: Implement XDG for MacOS (tox-dev#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
0az committed Aug 3, 2021
1 parent df07a00 commit f1b0e68
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 16 deletions.
9 changes: 9 additions & 0 deletions src/platformdirs/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def __init__(
version: Optional[str] = None,
roaming: bool = False,
multipath: bool = False,
force_xdg: bool = None,
opinion: bool = True,
):
"""
Expand Down Expand Up @@ -54,6 +55,14 @@ def __init__(
An optional parameter only applicable to Unix/Linux which indicates that the entire list of data dirs should be
returned. By default, the first item would only be returned.
"""
self.xdg_fallback = opinion if force_xdg is None else force_xdg
"""
Whether to use XDG's fallback behavior on all platforms for
consistency. Defaults to the value of `opinion`.
This has no effect on the interpretation of `XDG_*_HOME` environment
variables, which are always used if set.
"""
self.opinion = opinion #: A flag to indicating to use opinionated values.

def _append_app_name_and_version(self, *base: str) -> str:
Expand Down
30 changes: 29 additions & 1 deletion src/platformdirs/macos.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
import os
from functools import wraps
from typing import Callable

from .api import PlatformDirsABC
from .unix import SUPPORTS_XDG, Unix


class MacOS(PlatformDirsABC):
def _xdg_fallback(func: Callable[[], str]) -> Callable[[], str]:
if func.__name__ not in SUPPORTS_XDG:
return func

@wraps(func)
def wrapper(self: PlatformDirsABC) -> str:
if SUPPORTS_XDG.get(func.__name__) in os.environ:
return getattr(super(MacOS, self), func.__name__)
path = func.__get__(self, MacOS)()
if os.path.exists(path):
return path
if self.xdg_fallback:
return getattr(super(MacOS, self), func.__name__)
return path

return wrapper


class MacOS(Unix, PlatformDirsABC):
"""
Platform directories for the macOS operating system. Follows the guidance from `Apple documentation
<https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html>`_.
Expand All @@ -12,36 +33,43 @@ class MacOS(PlatformDirsABC):
"""

@property
@_xdg_fallback
def user_data_dir(self) -> str:
""":return: data directory tied to the user, e.g. ``~/Library/Application Support/$appname/$version``"""
return self._append_app_name_and_version(os.path.expanduser("~/Library/Application Support/"))

@property
@_xdg_fallback
def site_data_dir(self) -> str:
""":return: data directory shared by users, e.g. ``/Library/Application Support/$appname/$version``"""
return self._append_app_name_and_version("/Library/Application Support")

@property
@_xdg_fallback
def user_config_dir(self) -> str:
""":return: config directory tied to the user, e.g. ``~/Library/Preferences/$appname/$version``"""
return self._append_app_name_and_version(os.path.expanduser("~/Library/Preferences/"))

@property
@_xdg_fallback
def site_config_dir(self) -> str:
""":return: config directory shared by the users, e.g. ``/Library/Preferences/$appname``"""
return self._append_app_name_and_version("/Library/Preferences")

@property
@_xdg_fallback
def user_cache_dir(self) -> str:
""":return: cache directory tied to the user, e.g. ``~/Library/Caches/$appname/$version``"""
return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches"))

@property
@_xdg_fallback
def user_state_dir(self) -> str:
""":return: state directory tied to the user, same as `user_data_dir`"""
return self.user_data_dir

@property
@_xdg_fallback
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"))
Expand Down
12 changes: 12 additions & 0 deletions src/platformdirs/unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@

from .api import PlatformDirsABC

# Mapping between function name and relevant XDG var
SUPPORTS_XDG = {
"user_data_dir": "XDG_DATA_HOME",
# "site_data_dir": "",
"user_config_dir": "XDG_CONFIG_HOME",
# "site_config_dir": "",
"user_cache_dir": "XDG_CACHE_HOME",
"user_state_dir": "XDG_STATE_HOME",
"user_log_dir": "XDG_CACHE_HOME",
}


class Unix(PlatformDirsABC):
"""
Expand Down Expand Up @@ -99,6 +110,7 @@ def user_state_dir(self) -> str:
path = os.path.expanduser("~/.local/state")
return self._append_app_name_and_version(path)

# TODO: As per XDG spec, logs should be placed under XDG_STATE_HOME
@property
def user_log_dir(self) -> str:
"""
Expand Down
Empty file added tests/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from platformdirs.macos import MacOS
from platformdirs.unix import Unix

PARAMS = {
"no_args": {},
"app_name": {"appname": "foo"},
"app_name_with_app_author": {"appname": "foo", "appauthor": "bar"},
"app_name_author_version": {
"appname": "foo",
"appauthor": "bar",
"version": "v1.0",
},
"app_name_author_version_false_opinion": {
"appname": "foo",
"appauthor": "bar",
"version": "v1.0",
"opinion": False,
},
}

OS = {
"darwin": MacOS,
"unix": Unix,
}
29 changes: 28 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from typing import Tuple, cast
import os
from pathlib import Path
from typing import Any, Dict, Tuple, cast

import pytest
from _pytest.fixtures import SubRequest
from pytest_mock import MockerFixture

PROPS = (
"user_data_dir",
Expand Down Expand Up @@ -29,3 +32,27 @@ def func_path(request: SubRequest) -> str:
@pytest.fixture()
def props() -> Tuple[str, ...]:
return PROPS


@pytest.fixture
def mock_environ(mocker: MockerFixture) -> Dict[str, Any]:
mocker.patch("os.environ", {})
return os.environ


@pytest.fixture
def mock_homedir(
mocker: MockerFixture,
mock_environ: dict,
tmp_path: Path,
) -> Path:
def _expanduser(s: str) -> str:
if s == "~":
return str(tmp_path)
if s.startswith("~/"):
return str(tmp_path / s[2:])
return s

mocker.patch("os.path.expanduser", _expanduser)
mock_environ["HOME"] = str(tmp_path)
return tmp_path
18 changes: 4 additions & 14 deletions tests/test_android.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,13 @@

from platformdirs.android import Android

from .common import PARAMS


@pytest.mark.parametrize(
"params",
[
{},
{"appname": "foo"},
{"appname": "foo", "appauthor": "bar"},
{"appname": "foo", "appauthor": "bar", "version": "v1.0"},
{"appname": "foo", "appauthor": "bar", "version": "v1.0", "opinion": False},
],
ids=[
"no_args",
"app_name",
"app_name_with_app_author",
"app_name_author_version",
"app_name_author_version_false_opinion",
],
PARAMS.values(),
ids=PARAMS.keys(),
)
def test_android(mocker: MockerFixture, params: Dict[str, Any], func: str) -> None:
mocker.patch("platformdirs.android._android_folder", return_value="/data/data/com.example", autospec=True)
Expand Down
35 changes: 35 additions & 0 deletions tests/test_xdg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os
from pathlib import Path
from typing import Any, Dict, Tuple, Union

import pytest
from _pytest.fixtures import SubRequest
from pytest_mock import MockerFixture

from platformdirs.macos import MacOS
from platformdirs.unix import SUPPORTS_XDG, Unix

from .common import OS, PARAMS


@pytest.mark.parametrize("params", PARAMS.values(), ids=PARAMS.keys())
@pytest.mark.parametrize("klass", OS.values(), ids=OS.keys())
@pytest.mark.parametrize(
"case",
SUPPORTS_XDG.items(),
ids=(s.lower() for s in SUPPORTS_XDG.keys()),
)
def test_xdg_compliance_on_unix(
mocker: MockerFixture,
params: Dict[str, Any],
klass: Union[MacOS, Unix],
case: Tuple[str, str],
mock_environ: Dict[str, str],
mock_homedir: Path,
):
func, key = case
prefix = mock_environ[key] = os.path.expanduser(f"~/{func}")

result: str = getattr(klass(**params), func)

assert result.startswith(prefix)

0 comments on commit f1b0e68

Please sign in to comment.