diff --git a/sphinx/user-guide/configuration.rst b/sphinx/user-guide/configuration.rst index d0e2221a..219abf21 100644 --- a/sphinx/user-guide/configuration.rst +++ b/sphinx/user-guide/configuration.rst @@ -11,14 +11,52 @@ The configuration can be modified using two functions: - :py:func:`config.load_config `: Set multiple keys from a TOML file. The value of a specific key can be queried using -:py:func:`config.from_confi `. It is also +:py:func:`config.from_config `. It is also possible to print the entire configuration using -:py:func:`config.print_config `. +:py:func:`config.print_config `. If the +current configuration was read from a file, the corresponding path can +be read from the value of ``config.file``. + +.. code:: toml + + [storage] + address = "ftp.domain.com" + + +.. code:: python + + >>> from vortex import config + >>> config.load_config("~/.vortex.d/vortex.toml") # The default + >>> config.file + PosixPath('/home/user/.vortex.d/vortex.toml') + >>> config.from_config("storage", "address") + "ftp.domain.com" + >>> from_config("storage", "protocol") + ConfigurationError: Missing configuration key protocol in section storage + >>> config.set_config("storage", "protocol", value="ftp") + >>> config.print_config() + Section: storage + ADDRESS: hendrix.meteo.fr + PROTOCOL: ftp .. seealso:: :doc:`../reference/configuration` + +Default configuration +^^^^^^^^^^^^^^^^^^^^^ + +At import time, ``vortex`` tries to read configuration from a file +``vortex.toml`` in the current working directory. + +If not found, ``vortex`` reads configuration from +``~/.vortex.d/vortex.toml``. + +If the default configuration is not found, *vortex* is left +unconfigured. + + ``data-tree`` ^^^^^^^^^^^^^ diff --git a/src/vortex/config.py b/src/vortex/config.py index dcdfb7b1..03bb83d6 100644 --- a/src/vortex/config.py +++ b/src/vortex/config.py @@ -3,6 +3,8 @@ """ +import sys +import types from pathlib import Path import tomli @@ -17,6 +19,7 @@ ] VORTEX_CONFIG = {} +_PATH = None logger = loggers.getLogger(__name__) @@ -42,11 +45,12 @@ def load_config(configpath=Path("vortex.toml")): # ... """ global VORTEX_CONFIG + global _PATH configpath = Path(configpath) try: with configpath.open(mode="rb") as f: VORTEX_CONFIG = tomli.load(f) - print(f"Successfully read configuration file {configpath.absolute()}") + _PATH = configpath.absolute() except FileNotFoundError: print( f"Could not read configuration file {configpath.absolute()} (not found)." @@ -56,9 +60,12 @@ def load_config(configpath=Path("vortex.toml")): def print_config(): """Print configuration (key, value) pairs""" - if VORTEX_CONFIG: - for k, v in VORTEX_CONFIG.items(): - print(k.upper(), v) + if not VORTEX_CONFIG: + return None + for section_name, section in VORTEX_CONFIG.items(): + print(f"Section: {section_name.upper()}") + for k, v in section.items(): + print(f" {k.upper()}: {v}") def from_config(section, key=None): @@ -114,3 +121,16 @@ def get_from_config_w_default(section, key, default): return from_config(section, key) except ConfigurationError: return default + + +class _ConfigModule(types.ModuleType): + @property + def file(self): + return _PATH + + @file.setter + def file(self, value): + raise AttributeError("config.file is read-only") + + +sys.modules[__name__].__class__ = _ConfigModule diff --git a/tests/test_config.py b/tests/test_config.py index 6c20463c..30d842e4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -13,6 +13,26 @@ }, } + +@pytest.fixture +def clean_config(): + """Save and restore VORTEX_CONFIG and _PATH around a test.""" + saved_cfg = config.VORTEX_CONFIG.copy() + saved_path = config._PATH + config.VORTEX_CONFIG = {} + config._PATH = None + yield + config.VORTEX_CONFIG = saved_cfg + config._PATH = saved_path + + +@pytest.fixture +def toml_config_file(tmp_path): + p = tmp_path / "vortex.toml" + p.write_text('[data-tree]\nrootdir = "/tmp/data"\n') + return p + + def test_section_from_config(): with pytest.raises(config.ConfigurationError): config_section = config.from_config("nonexist") @@ -45,14 +65,50 @@ def test_is_defined(): def test_get_from_config_w_default(): + assert ( + config.get_from_config_w_default( + "data-tree", + "nonexist", + "default", + ) + == "default" + ) assert config.get_from_config_w_default( - "data-tree", "nonexist", "default", - ) == "default" - - assert config.get_from_config_w_default( - "data-tree", "op-rootdir", "default", + "data-tree", + "op-rootdir", + "default", ) == config.from_config("data-tree", "op-rootdir") - - - + + +def test_load_config_populates_config(clean_config, toml_config_file): + config.load_config(toml_config_file) + + assert config.VORTEX_CONFIG == {"data-tree": {"rootdir": "/tmp/data"}} + + +def test_load_config_sets_file_property(clean_config, toml_config_file): + config.load_config(toml_config_file) + + assert config.file == toml_config_file.absolute() + + +def test_load_config_overrides_existing(clean_config, tmp_path): + first = tmp_path / "first.toml" + first.write_text('[section-a]\nkey = "value-a"\n') + second = tmp_path / "second.toml" + second.write_text('[section-b]\nkey = "value-b"\n') + + config.load_config(first) + config.load_config(second) + + assert "section-a" not in config.VORTEX_CONFIG + assert config.VORTEX_CONFIG == {"section-b": {"key": "value-b"}} + + +def test_load_config_file_not_found(clean_config, tmp_path, capsys): + config.load_config(tmp_path / "nonexistent.toml") + + captured = capsys.readouterr() + assert "not found" in captured.out + assert config.VORTEX_CONFIG == {}