diff --git a/conda/meta.yaml b/conda/meta.yaml index 6129ab0e43fd24da6d36fa8d45c93fa1f308c716..daebcd6195cbd7731719ec5de346a90066b832be 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -27,11 +27,13 @@ requirements: - click {{ click }} - tomli {{ tomli }} - tomli-w {{ tomli_w }} + - xdg {{ xdg }} run: - python >=3.9 - {{ pin_compatible('click') }} - {{ pin_compatible('tomli') }} - {{ pin_compatible('tomli-w') }} + - {{ pin_compatible('xdg') }} test: source_files: diff --git a/doc/links.rst b/doc/links.rst index 2f7e0e2013993d80c52fd1519683eb195ee013ff..2eb4ffab0e6deda36ee592e524958623fa954132 100644 --- a/doc/links.rst +++ b/doc/links.rst @@ -11,3 +11,4 @@ .. _click: http://click.pocoo.org/ .. _click-plugins: https://github.com/click-contrib/click-plugins .. _logging-tree module: https://pypi.org/project/logging_tree/ +.. _xdg-defaults: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html diff --git a/doc/rc.rst b/doc/rc.rst index 7b9b94d92a0144fadd7fac26b46316cb3e0576bf..50c4e7e89afd134bcf6b879c20bcb6529a9bddf0 100644 --- a/doc/rc.rst +++ b/doc/rc.rst @@ -28,28 +28,16 @@ load such a file and provide access to values set therein: .. code-block:: python >>> from exposed import rc - >>> defaults = rc.UserDefaults("~/.myapprc.toml") + >>> defaults = rc.UserDefaults("myapprc.toml") -The ``defaults`` object in this example is a subtype of :py:class:`dict`, with -a few extra methods implemented. Values can optionally be organized in -sections. +.. note:: -You may also pass the name of an environment variable to be used to override -the standard configuration file. If that environment variable is set by the -user, and is non-empty, then the object will load the file path pointed by the -value stored on that variable instead of the default. For example: - -.. code-block:: python - - >>> import os - >>> os.environ["MYAPPRC"] = "~/.myapprc2.toml" - >>> from exposed import rc - >>> defaults = rc.UserDefaults("~/.myapprc.toml", "MYAPPRC") - -Because the environment variable is set to a non-empty value, then the -``defaults`` object will consider ``~/.myapprc2.toml`` as the source -configuration file instead of ``~/.myapprc.toml``. All the following -operations will be based on that setting. + If the input filename given upon the construction of + :py:class:`exposed.rc.UserDefaults` is not absolute, it is considered + relative to the value of the environment variable ``$XDG_CONFIG_HOME``. In + UNIX-style operating systems, the above example would typically resolve to + ``${HOME}/.config/myapprc.toml``. Check the `XDG defaults <xdg-defaults_>`_ + for specifics. Reading and writing values diff --git a/pyproject.toml b/pyproject.toml index f7959b3448f0bbb46e07c64e1a3094d1a2b4c9b5..11a3083da63f3b2729bcbee3f9556a686d5fae3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "click>=8", "tomli", "tomli-w", + "xdg", ] [project.urls] diff --git a/src/exposed/rc.py b/src/exposed/rc.py index 923a7f01f757ff2f1e06fc85237bbfbf899780da..09163086f5cdd5bdcac7687985b01b0d8d10f542 100644 --- a/src/exposed/rc.py +++ b/src/exposed/rc.py @@ -10,21 +10,22 @@ import collections.abc import io import json import logging -import os import pathlib import typing import tomli import tomli_w +import xdg class UserDefaults(collections.abc.MutableMapping): """Contains user defaults read from the user TOML configuration file. Upon intialisation, an instance of this class will read the user - configuration file defined by the first argument. If ``envname`` is - provided, and it is set, it overrides the location of ``filename`` and the - configuration from that location is loaded instead. + configuration file defined by the first argument. If the input file is + specified as a relative path, then it is considered relative to the + environment variable ``${XDG_CONFIG_HOME}``, or its default setting (which is + operating system dependent, c.f. `XDG defaults`_). This object may be used (with limited functionality) like a dictionary. In this mode, objects of this class read and write values to the ``DEFAULT`` @@ -34,15 +35,13 @@ class UserDefaults(collections.abc.MutableMapping): Arguments: - path: The path to the file typically containing the user defaults. - The use of ``~`` (tilde) is supported to refer to one's home - directory. - - - envname: If this value is provided, and it is set by the user, and is - non-empty, then loads the file path pointed by the value stored on - that environment variable instead of the default. The use of ``~`` - (tilde) is supported to refer to one's home directory. + path: The path, absolute or relative, to the file containing the user + defaults to read. If `path` is a relative path, then it is + considered relative to the directory defined by the environment + variable ``${XDG_CONFIG_HOME}`` (read `XDG defaults`_ for details on + the default location of this directory in the various operating + systems). The tilde (`~`) character may be used to represent the + user home, and is automatically expanded. logger: A logger to use for messaging operations. If not set, use this module's logger. @@ -51,29 +50,24 @@ class UserDefaults(collections.abc.MutableMapping): Attributes: path: The current path to the user defaults base file. + + + .. _XDG defaults: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html """ def __init__( self, path: str | pathlib.Path, - envname: str | None = None, logger: logging.Logger | None = None, ) -> None: self.logger = logger or logging.getLogger(__name__) - if ( - envname is not None - and envname in os.environ - and os.environ[envname] - ): - self.path = pathlib.Path(os.environ[envname]) - self.logger.debug( - f"User configuration file set by environment variable ${envname}" - ) - else: - self.path = pathlib.Path(path) - self.path = self.path.expanduser() + self.path = pathlib.Path(path).expanduser() + + if not self.path.is_absolute(): + self.path = xdg.xdg_config_home() / self.path + self.logger.info(f"User configuration file set to `{str(self.path)}'") self.data: dict[str, typing.Any] = {} self.read() diff --git a/tests/test_rc.py b/tests/test_rc.py index bb1384a7776b08dd90b1c0445a8d1a01b5934fc9..8f34d957b1500e82384eba96501f1e82f91db2da 100644 --- a/tests/test_rc.py +++ b/tests/test_rc.py @@ -36,13 +36,19 @@ def test_rc_basic_loading(datadir): assert rc["float.error"] == 3.14 -def test_rc_env_loading(datadir): - # tests if we can simply read an RC file - envname = "_TEST_RC_ENV_LOADING" - os.environ[envname] = str(datadir / "userdefaults_ex1.cfg") - rc = UserDefaults("does-not-exist", envname) - _check_userdefaults_ex1_contents(rc) - assert not os.path.exists("does-not-exist") +def test_rc_loading_from_xdg_config_home(datadir): + # tests if we can simply read an RC file from the XDG_CONFIG_HOME + _environ = dict(os.environ) # or os.environ.copy() + try: + os.environ["XDG_CONFIG_HOME"] = str(datadir) + rc = UserDefaults("userdefaults_ex1.cfg") + _check_userdefaults_ex1_contents(rc) + + with pytest.raises(KeyError): + assert rc["float.error"] == 3.14 + finally: + os.environ.clear() + os.environ.update(_environ) def test_rc_init_empty(tmp_path):