Skip to content

Commit

Permalink
refactor: re-implement the XDG Base Directory spec ourselves
Browse files Browse the repository at this point in the history
  • Loading branch information
xen0n committed May 13, 2024
1 parent 1ca4f36 commit 9700e36
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 39 deletions.
71 changes: 32 additions & 39 deletions ruyi/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
import tomllib
from typing import Any, Iterable, NotRequired, Self, TypedDict

from xdg import BaseDirectory

from .. import log, argv0
from ..utils.xdg_basedir import XDGBaseDir
from .news import NewsReadStatusStore


DEFAULT_APP_NAME = "ruyi"
DEFAULT_REPO_URL = "https://github.com/ruyisdk/packages-index.git"
DEFAULT_REPO_BRANCH = "main"

Expand Down Expand Up @@ -45,8 +44,6 @@ class GlobalConfigRootType(TypedDict):


class GlobalConfig:
resource_name = "ruyi"

def __init__(self) -> None:
# all defaults
self.override_repo_dir: str | None = None
Expand All @@ -58,6 +55,8 @@ def __init__(self) -> None:

self._lang_code = _get_lang_code()

self._dirs = XDGBaseDir(DEFAULT_APP_NAME)

def apply_config(self, config_data: GlobalConfigRootType) -> None:
if pkgs_cfg := config_data.get("packages"):
self.include_prereleases = pkgs_cfg.get("prereleases", False)
Expand All @@ -79,16 +78,16 @@ def lang_code(self) -> str:
return self._lang_code

@property
def cache_root(self) -> str:
return os.path.join(BaseDirectory.xdg_cache_home, self.resource_name)
def cache_root(self) -> os.PathLike[Any]:
return self._dirs.app_cache

@property
def data_root(self) -> str:
return os.path.join(BaseDirectory.xdg_data_home, self.resource_name)
def data_root(self) -> os.PathLike[Any]:
return self._dirs.app_data

@property
def state_root(self) -> str:
return os.path.join(BaseDirectory.xdg_state_home, self.resource_name)
def state_root(self) -> os.PathLike[Any]:
return self._dirs.app_state

@property
def news_read_status(self) -> NewsReadStatusStore:
Expand Down Expand Up @@ -124,52 +123,46 @@ def global_blob_install_root(self, slug: str) -> str:

def lookup_binary_install_dir(self, host: str, slug: str) -> PathLike[Any] | None:
host_path = get_host_path_fragment_for_binary_install_dir(host)
for data_dir in BaseDirectory.load_data_paths(self.resource_name):
p = pathlib.Path(data_dir) / "binaries" / host_path / slug
for data_dir in self._dirs.data_dirs:
p = data_dir / "binaries" / host_path / slug
if p.exists():
return p
return None

@classmethod
def ensure_data_dir(cls) -> str:
return BaseDirectory.save_data_path(cls.resource_name)

@classmethod
def ensure_config_dir(cls) -> str:
return BaseDirectory.save_config_path(cls.resource_name)
def ensure_data_dir(self) -> os.PathLike[Any]:
p = self._dirs.app_data
p.mkdir(parents=True, exist_ok=True)
return p

@classmethod
def ensure_cache_dir(cls) -> str:
return BaseDirectory.save_cache_path(cls.resource_name)
def ensure_cache_dir(self) -> os.PathLike[Any]:
p = self._dirs.app_cache
p.mkdir(parents=True, exist_ok=True)
return p

@classmethod
def ensure_state_dir(cls) -> str:
return BaseDirectory.save_state_path(cls.resource_name)
def ensure_config_dir(self) -> os.PathLike[Any]:
p = self._dirs.app_config
p.mkdir(parents=True, exist_ok=True)
return p

@classmethod
def get_config_file(cls) -> str | None:
# TODO: maybe allow customization of config root
config_dir = BaseDirectory.load_first_config(cls.resource_name)
if config_dir is None:
return None
return os.path.join(config_dir, "config.toml")
def ensure_state_dir(self) -> os.PathLike[Any]:
p = self._dirs.app_state
p.mkdir(parents=True, exist_ok=True)
return p

@classmethod
def iter_xdg_configs(cls) -> Iterable[os.PathLike[Any]]:
def iter_xdg_configs(self) -> Iterable[os.PathLike[Any]]:
"""
Yields possible Ruyi config files in all XDG config paths, sorted by precedence
from lowest to highest (so that each file may be simply applied consecutively).
"""

all_config_dirs = list(BaseDirectory.load_config_paths(cls.resource_name))
for config_dir in reversed(all_config_dirs):
yield pathlib.Path(config_dir) / "config.toml"
for config_dir in reversed(list(self._dirs.app_config_dirs)):
yield config_dir / "config.toml"

@classmethod
def load_from_config(cls) -> Self:
obj = cls()

for config_path in cls.iter_xdg_configs():
for config_path in obj.iter_xdg_configs():
log.D(f"trying config file: {config_path}")
try:
with open(config_path, "rb") as fp:
Expand Down
78 changes: 78 additions & 0 deletions ruyi/utils/xdg_basedir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Re-implementation of necessary XDG Base Directory Specification semantics
# without pyxdg, which is under LGPL and not updated for the latest spec
# revision (0.6 vs 0.8 released in 2021).

import os
import pathlib
from typing import Iterable


def _paths_from_env(env: str, default: str) -> Iterable[pathlib.Path]:
v = os.environ.get(env, default)
for p in v.split(":"):
yield pathlib.Path(p)


class XDGBaseDir:
def __init__(self, app_name: str) -> None:
self.app_name = app_name

@property
def cache_home(self) -> pathlib.Path:
v = os.environ.get("XDG_CACHE_HOME", "")
return pathlib.Path(v) if v else pathlib.Path.home() / ".cache"

@property
def config_home(self) -> pathlib.Path:
v = os.environ.get("XDG_CONFIG_HOME", "")
return pathlib.Path(v) if v else pathlib.Path.home() / ".config"

@property
def data_home(self) -> pathlib.Path:
v = os.environ.get("XDG_DATA_HOME", "")
return pathlib.Path(v) if v else pathlib.Path.home() / ".local" / "share"

@property
def state_home(self) -> pathlib.Path:
v = os.environ.get("XDG_DATA_HOME", "")
return pathlib.Path(v) if v else pathlib.Path.home() / ".local" / "state"

@property
def config_dirs(self) -> Iterable[pathlib.Path]:
# from highest precedence to lowest
yield from _paths_from_env("XDG_CONFIG_DIRS", "/etc/xdg")

@property
def data_dirs(self) -> Iterable[pathlib.Path]:
# from highest precedence to lowest
yield from _paths_from_env("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")

# derived info

@property
def app_cache(self) -> pathlib.Path:
return self.cache_home / self.app_name

@property
def app_config(self) -> pathlib.Path:
return self.config_home / self.app_name

@property
def app_data(self) -> pathlib.Path:
return self.data_home / self.app_name

@property
def app_state(self) -> pathlib.Path:
return self.state_home / self.app_name

@property
def app_config_dirs(self) -> Iterable[pathlib.Path]:
# from highest precedence to lowest
yield self.app_config
yield from self.config_dirs

@property
def app_data_dirs(self) -> Iterable[pathlib.Path]:
# from highest precedence to lowest
yield self.app_data
yield from self.data_dirs

0 comments on commit 9700e36

Please sign in to comment.