Source code for repobee_plug.config

"""Helpers related to configuration."""
import configparser
import pathlib
import os

from typing import Any, Optional, List

from typing_extensions import Protocol

from repobee_plug import exceptions

__all__ = ["Config", "ConfigSection"]


[docs]class ConfigSection(Protocol): """Protocol defining how a section of the config behaves.""" def __getitem__(self, key: str) -> Any: ... def __setitem__(self, key: str, value: Any) -> None: ... def __contains__(self, key: str) -> bool: ...
[docs]class Config: """Object representing RepoBee's config. This class defines read-only inheritance. This means that when you read a value from the config, for example with :py:meth:`get`, it will do a recursive lookup in parent configs. Writing to a config object, e.g. ``config[section][option] = value`` does *not* respect inheritance, and unconditionally writes to *this* config, and not any of its parents. Similarly, writing to disk with :py:meth:`store` only writes to the most local config, and not to any of the parent configs. .. important:: Changes to the config are only persisted if the :py:meth:`Config.store` method is called. .. warning:: The behavior of this class is currently not stable. Any minor release of RepoBee might bring breaking changes. """ CORE_SECTION_NAME = "repobee" PARENT_CONFIG_KEY = "parent_config" def __init__(self, config_path: pathlib.Path): super().__init__() self._config_path = config_path self._config_parser = configparser.ConfigParser() self._parent: Optional[Config] = None self.create_section(self.CORE_SECTION_NAME) self._check_for_cycle(paths=[]) self.refresh()
[docs] def refresh(self) -> None: """Refresh the parser by reading from the config file. Does nothing if the config file does not exist. """ if self._config_path.exists(): self._config_parser.read(self._config_path) raw_parent_path = self.get( self.CORE_SECTION_NAME, self.PARENT_CONFIG_KEY ) if raw_parent_path: parent_path = self._resolve_absolute_parent_path( raw_parent_path ) self._parent = Config(parent_path)
def _resolve_absolute_parent_path( self, raw_parent_path: str ) -> pathlib.Path: parent_path = pathlib.Path(raw_parent_path) return ( parent_path if parent_path.is_absolute() else (self.path.parent / parent_path).resolve(strict=False) )
[docs] def store(self) -> None: """Write the current state of the config to the config file. If the directory does not exist, it is created. """ if not self._config_path.exists(): os.makedirs(self._config_path.parent, mode=0o700, exist_ok=True) with open(self._config_path, encoding="utf8", mode="w") as f: self._config_parser.write(f)
[docs] def create_section(self, section_name: str) -> None: """Add a section to the config. Args: section_name: Name of the section. """ return self._config_parser.add_section(section_name)
[docs] def get( self, section_name: str, key: str, fallback: Optional[Any] = None ) -> Optional[Any]: """Get a value from the given section. Args: section_name: Name of the section. key: Key to get the value for. fallback: An optional fallback value to use if the section or key do not exist. Returns: The value for the section and key, or the fallback value if neither exist. """ return self._config_parser.get( section_name, key, fallback=self.parent.get(section_name, key, fallback) if self.parent else fallback, )
@property def path(self) -> pathlib.Path: """Path to the config file.""" return self._config_path @property def parent(self) -> Optional["Config"]: """Returns the parent config if defined, otherwise None.""" return self._parent @parent.setter def parent(self, value: "Config") -> None: self._parent = value self[self.CORE_SECTION_NAME][self.PARENT_CONFIG_KEY] = str(value.path) self._check_for_cycle([]) def __getitem__(self, section_key: str) -> ConfigSection: return _ParentAwareConfigSection(self, section_key) def __contains__(self, section_name: str) -> bool: return section_name in self._config_parser def _check_for_cycle(self, paths: List[pathlib.Path]) -> None: """Check if there's a cycle in the inheritance.""" if self.path in paths: cycle = " -> ".join(map(str, paths + [self.path])) raise exceptions.PlugError( f"Cyclic inheritance detected in config: {cycle}" ) elif self.parent is not None: self.parent._check_for_cycle(paths + [self.path])
class _ParentAwareConfigSection: """A section of the config that respects sections from parent configs.""" def __init__(self, config: Config, section_key: str): self._config = config self._section_key = section_key def __getitem__(self, key: str): value = self._config.get(self._section_key, key) if value is None: raise KeyError(key) else: return value def __setitem__(self, key: str, value: Any): self._config._config_parser.set(self._section_key, key, value) def __contains__(self, key: str) -> bool: return self._config.get(self._section_key, key) is not None