Source code for clapper.rc

# SPDX-FileCopyrightText: Copyright © 2022 Idiap Research Institute <contact@idiap.ch>
# SPDX-FileContributor: Amir Mohammadi  <amir.mohammadi@idiap.ch>
#
# SPDX-License-Identifier: BSD-3-Clause

"""Implements a global configuration system setup and readout."""

import collections.abc
import io
import json
import logging
import pathlib
import typing

import tomli
import tomli_w
import xdg


[docs] 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 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`` section. The ``len()`` method will also return the number of variables set at the ``DEFAULT`` section as well. Parameters ---------- 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 <https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html>`_ 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. Attributes ---------- path The current path to the user defaults base file. """ def __init__( self, path: str | pathlib.Path, logger: logging.Logger | None = None, ) -> None: self.logger = logger or logging.getLogger(__name__) 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()
[docs] def read(self) -> None: """Read configuration file, replaces any internal values.""" if self.path.exists(): self.logger.debug("User configuration file exists, reading contents...") self.data.clear() with self.path.open("rb") as f: contents = f.read() # Support for legacy JSON file format. Remove after sometime # FYI: today is September 16, 2022 try: data = json.loads(contents) self.logger.warning( f"Converting `{str(self.path)}' from (legacy) JSON " f"to (new) TOML format" ) self.update(data) self.write() self.clear() # reload contents with self.path.open("rb") as f: contents = f.read() except ValueError: pass self.data.update(tomli.load(io.BytesIO(contents))) else: self.logger.debug("Initializing empty user configuration...")
[docs] def write(self) -> None: """Store any modifications done on the user configuration.""" if self.path.exists(): backup = pathlib.Path(str(self.path) + "~") self.logger.debug(f"Backing-up {str(self.path)} -> {str(backup)}") self.path.rename(backup) with self.path.open("wb") as f: tomli_w.dump(self.data, f) self.logger.info(f"Wrote configuration at {str(self.path)}")
def __str__(self) -> str: t = io.BytesIO() tomli_w.dump(self.data, t) return t.getvalue().decode(encoding="utf-8") def __getitem__(self, k: str) -> typing.Any: if k in self.data: return self.data[k] if "." in k: # search for a key with a matching name after the "." parts = k.split(".") base = self.data for n in range(len(parts)): if parts[n] in base: base = base[parts[n]] if (not isinstance(base, dict)) and (n < (len(parts) - 1)): # this is an actual value, not another dict whereas it # should not as we have more parts to go break else: break subkey = ".".join(parts[(n + 1) :]) if subkey in base: return base[subkey] # otherwise, defaults to the default behaviour return self.data.__getitem__(k) def __setitem__(self, k: str, v: typing.Any) -> None: assert isinstance(k, str) if "." in k: # sets nested subsections parts = k.split(".") base = self.data for n in range(len(parts) - 1): base = base.setdefault(parts[n], {}) if not isinstance(base, dict): raise KeyError( f"You are trying to set configuration key " f"{k}, but {'.'.join(parts[: (n + 1)])} is already a " f"variable set in the file, and not a section" ) base[parts[-1]] = v return v # otherwise, defaults to the default behaviour return self.data.__setitem__(k, v) def __delitem__(self, k: str) -> None: assert isinstance(k, str) if "." in k: # search for a key with a matching name after the "." parts = k.split(".") base = self.data for n in range(len(parts) - 1): if parts[n] in base: base = base[parts[n]] if not isinstance(base, dict): # this is an actual value, not another dict whereas it # should not as we have more parts to go break else: break subkey = ".".join(parts[(n + 1) :]) if subkey in base: del base[subkey] return None # otherwise, defaults to the default behaviour return self.data.__delitem__(k) def __iter__(self) -> typing.Iterator[str]: return self.data.__iter__() def __len__(self) -> int: return self.data.__len__()