[ENH]: Introduce ConfigManager #401

Merged
fraimondo merged 10 commits from enh/config_manager into main 2024-11-26 14:50:30 +00:00
19 changed files with 204 additions and 27 deletions

View file

@ -0,0 +1 @@
Introduce :class:`junifer.utils.ConfigManager` singleton class to manage global configuration by `Fede Raimondo`_

View file

@ -13,8 +13,8 @@ from ..typing import DataGrabberLike, MarkerLike, PreprocessorLike, StorageLike
__all__ = [
"register_datagrabber",
"register_datareader",
"register_preprocessor",
"register_marker",
"register_preprocessor",
"register_storage",
]

View file

@ -24,7 +24,7 @@ from ..typing import DataGrabberLike, MarkerLike, PreprocessorLike, StorageLike
from ..utils import logger, raise_error, yaml
__all__ = ["run", "collect", "queue", "reset", "list_elements"]
__all__ = ["collect", "list_elements", "queue", "reset", "run"]
def _get_datagrabber(datagrabber_config: dict) -> DataGrabberLike:

View file

@ -29,19 +29,19 @@ from .utils import (
__all__ = [
"cli",
"run",
"collect",
"queue",
"wtf",
"selftest",
"reset",
"list_elements",
"setup",
"afni_docker",
"fsl_docker",
"ants_docker",
"cli",
"collect",
"freesurfer_docker",
"fsl_docker",
"list_elements",
"queue",
"reset",
"run",
"selftest",
"setup",
"wtf",
]

View file

@ -15,7 +15,7 @@ import pandas as pd
from ..utils import logger, raise_error, warn_with_log, yaml
__all__ = ["parse_yaml", "parse_elements"]
__all__ = ["parse_elements", "parse_yaml"]
def parse_yaml(filepath: Union[str, Path]) -> dict: # noqa: C901

View file

@ -25,11 +25,11 @@ if TYPE_CHECKING:
__all__ = [
"deregister_data",
"get_data",
"list_data",
"load_data",
"register_data",
"deregister_data",
]

View file

@ -35,7 +35,7 @@ if TYPE_CHECKING:
from nibabel.nifti1 import Nifti1Image
__all__ = ["compute_brain_mask", "MaskRegistry"]
__all__ = ["MaskRegistry", "compute_brain_mask"]
# Path to the masks

View file

@ -16,7 +16,7 @@ from ..utils import logger, raise_error
from .utils import closest_resolution
__all__ = ["get_xfm", "get_template"]
__all__ = ["get_template", "get_xfm"]
def get_xfm(

View file

@ -13,7 +13,7 @@ from scipy.stats.mstats import winsorize
from .utils import logger, raise_error
__all__ = ["get_aggfunc_by_name", "count", "winsorized_mean", "select"]
__all__ = ["count", "get_aggfunc_by_name", "select", "winsorized_mean"]
def get_aggfunc_by_name(

View file

@ -15,11 +15,11 @@ from ..utils.logging import logger, raise_error
__all__ = [
"get_dependency_version",
"process_meta",
"element_to_prefix",
"store_matrix_checks",
"get_dependency_version",
"matrix_to_vector",
"process_meta",
"store_matrix_checks",
]

View file

@ -15,8 +15,8 @@ from ..datagrabber.base import BaseDataGrabber
__all__ = [
"OasisVBMTestingDataGrabber",
"SPMAuditoryTestingDataGrabber",
"PartlyCloudyTestingDataGrabber",
"SPMAuditoryTestingDataGrabber",
]

View file

@ -9,6 +9,7 @@ __all__ = [
"ExternalDependencies",
"MarkerInOutMappings",
"DataGrabberPatterns",
"ConfigVal",
]
from ._typing import (
@ -22,4 +23,5 @@ from ._typing import (
ExternalDependencies,
MarkerInOutMappings,
DataGrabberPatterns,
ConfigVal,
)

View file

@ -19,16 +19,17 @@ if TYPE_CHECKING:
__all__ = [
"DataGrabberLike",
"PreprocessorLike",
"MarkerLike",
"StorageLike",
"PipelineComponent",
"Dependencies",
"ConditionalDependencies",
"ConfigVal",
"DataGrabberLike",
"DataGrabberPatterns",
"Dependencies",
"ExternalDependencies",
"MarkerInOutMappings",
"DataGrabberPatterns",
"MarkerLike",
"PipelineComponent",
"PreprocessorLike",
"StorageLike",
]
@ -60,3 +61,4 @@ MarkerInOutMappings = MutableMapping[str, MutableMapping[str, str]]
DataGrabberPatterns = dict[
str, Union[dict[str, str], Sequence[dict[str, str]]]
]
ConfigVal = Union[bool, int, float]

View file

@ -1,15 +1,18 @@
__all__ = [
"make_executable",
"configure_logging",
"config",
"logger",
"raise_error",
"warn_with_log",
"run_ext_cmd",
"deep_update",
"yaml",
"ConfigManager",
]
from .fs import make_executable
from .logging import configure_logging, logger, raise_error, warn_with_log
from ._config import config, ConfigManager
from .helpers import run_ext_cmd, deep_update
from ._yaml import yaml

110
junifer/utils/_config.py Normal file
View file

@ -0,0 +1,110 @@
"""Provide junifer global configuration."""
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
# Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL
import os
from typing import Optional
from ..typing import ConfigVal
from .logging import logger
from .singleton import Singleton
__all__ = ["ConfigManager", "config"]
class ConfigManager(metaclass=Singleton):
"""Manage configuration parameters.
Attributes
----------
_config : dict
Configuration parameters.
"""
def __init__(self) -> None:
"""Initialize the class."""
self._config = {}
# Initial setup from process env
self._reload()
def _reload(self) -> None:
"""Reload env vars."""
for t_var in os.environ:
if t_var.startswith("JUNIFER_"):
# Set correct type
var_value = os.environ[t_var]
# bool
if var_value.lower() == "true":
var_value = True
elif var_value.lower() == "false":
var_value = False
# numeric
else:
try:
var_value = int(var_value)
except ValueError:
try:
var_value = float(var_value)
except ValueError:
pass
# Set value
var_name = (
t_var.replace("JUNIFER_", "").lower().replace("_", ".")
)
logger.debug(
f"Setting `{var_name}` from environment to "
f"`{var_value}` (type: {type(var_value)})"
)
self._config[var_name] = var_value
def get(self, key: str, default: Optional[ConfigVal] = None) -> ConfigVal:
"""Get configuration parameter.
Parameters
----------
key : str
The configuration key to get.
default : bool or int or float or None, optional
The default value to return if the key is not found (default None).
Returns
-------
bool or int or float
The configuration value.
"""
return self._config.get(key, default)
def set(self, key: str, val: ConfigVal) -> None:
"""Set configuration parameter.
Parameters
----------
key : str
The configuration key to set.
val : bool or int or float
The value to set ``key`` to.
"""
logger.debug(f"Setting `{key}` to `{val}` (type: {type(val)})")
self._config[key] = val
def delete(self, key: str) -> None:
"""Delete configuration parameter.
Parameters
----------
key : str
The configuration key to delete.
"""
logger.debug(f"Deleting `{key}` from config")
_ = self._config.pop(key)
# Initialize here to access from anywhere
config = ConfigManager()

View file

@ -10,7 +10,7 @@ import sys
from .logging import logger, raise_error
__all__ = ["run_ext_cmd", "deep_update"]
__all__ = ["deep_update", "run_ext_cmd"]
def run_ext_cmd(name: str, cmd: list[str]) -> None:

View file

@ -24,9 +24,9 @@ import datalad
__all__ = [
"WrapStdOut",
"configure_logging",
"get_versions",
"log_versions",
"configure_logging",
"raise_error",
"warn_with_log",
]

View file

@ -8,7 +8,7 @@ from abc import ABCMeta
from typing import Any, ClassVar
__all__ = ["Singleton", "ABCSingleton"]
__all__ = ["ABCSingleton", "Singleton"]
class Singleton(type):

View file

@ -0,0 +1,59 @@
"""Provide tests for ConfigManager."""
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
# Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL
import os
import pytest
from junifer.typing import ConfigVal
from junifer.utils import config
from junifer.utils._config import ConfigManager
def test_config_manager_singleton() -> None:
"""Test that ConfigManager is a singleton."""
config_mgr_1 = ConfigManager()
config_mgr_2 = ConfigManager()
assert id(config_mgr_1) == id(config_mgr_2)
def test_config_manager() -> None:
"""Test config operations for ConfigManager."""
# Get non-existing with default
assert config.get(key="scooby") is None
# Set
config.set(key="scooby", val=True)
# Get existing
assert config.get("scooby")
# Delete
config.delete("scooby")
# Get non-existing with default
assert config.get(key="scooby") is None
@pytest.mark.parametrize(
"val, expected_val",
[("TRUE", True), ("FALSE", False), ("1", 1), ("0.0", 0.0)],
)
def test_config_manager_env_reload(val: str, expected_val: ConfigVal) -> None:
"""Test config parsing from env reload.
Parameters
----------
val : str
The parametrized values.
expected_val : bool or int or float
The parametrized expected value.
"""
# Set env var
os.environ["JUNIFER_TESTME"] = val
# Check
config._reload()
assert config.get("testme") == expected_val
# Cleanup
del os.environ["JUNIFER_TESTME"]
config.delete("testme")