diff --git a/docs/changes/newsfragments/401.feature b/docs/changes/newsfragments/401.feature new file mode 100644 index 000000000..24e89567e --- /dev/null +++ b/docs/changes/newsfragments/401.feature @@ -0,0 +1 @@ +Introduce :class:`junifer.utils.ConfigManager` singleton class to manage global configuration by `Fede Raimondo`_ diff --git a/junifer/api/decorators.py b/junifer/api/decorators.py index b063dc24a..bf39c6c5c 100644 --- a/junifer/api/decorators.py +++ b/junifer/api/decorators.py @@ -13,8 +13,8 @@ from ..typing import DataGrabberLike, MarkerLike, PreprocessorLike, StorageLike __all__ = [ "register_datagrabber", "register_datareader", - "register_preprocessor", "register_marker", + "register_preprocessor", "register_storage", ] diff --git a/junifer/api/functions.py b/junifer/api/functions.py index a67a22b62..d721c9dff 100644 --- a/junifer/api/functions.py +++ b/junifer/api/functions.py @@ -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: diff --git a/junifer/cli/cli.py b/junifer/cli/cli.py index da0448c9e..2ac97b3e1 100644 --- a/junifer/cli/cli.py +++ b/junifer/cli/cli.py @@ -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", ] diff --git a/junifer/cli/parser.py b/junifer/cli/parser.py index 7d17c9246..99577c102 100644 --- a/junifer/cli/parser.py +++ b/junifer/cli/parser.py @@ -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 diff --git a/junifer/data/_dispatch.py b/junifer/data/_dispatch.py index b504fbe6a..c8be8cdb2 100644 --- a/junifer/data/_dispatch.py +++ b/junifer/data/_dispatch.py @@ -25,11 +25,11 @@ if TYPE_CHECKING: __all__ = [ + "deregister_data", "get_data", "list_data", "load_data", "register_data", - "deregister_data", ] diff --git a/junifer/data/masks/_masks.py b/junifer/data/masks/_masks.py index e7cd2bda0..779aa3eab 100644 --- a/junifer/data/masks/_masks.py +++ b/junifer/data/masks/_masks.py @@ -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 diff --git a/junifer/data/template_spaces.py b/junifer/data/template_spaces.py index 76475b49a..1ec253580 100644 --- a/junifer/data/template_spaces.py +++ b/junifer/data/template_spaces.py @@ -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( diff --git a/junifer/stats.py b/junifer/stats.py index 63678efef..61a35bdca 100644 --- a/junifer/stats.py +++ b/junifer/stats.py @@ -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( diff --git a/junifer/storage/utils.py b/junifer/storage/utils.py index 21615e740..1d4bb7fe4 100644 --- a/junifer/storage/utils.py +++ b/junifer/storage/utils.py @@ -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", ] diff --git a/junifer/testing/datagrabbers.py b/junifer/testing/datagrabbers.py index e305937a1..63703a9ab 100644 --- a/junifer/testing/datagrabbers.py +++ b/junifer/testing/datagrabbers.py @@ -15,8 +15,8 @@ from ..datagrabber.base import BaseDataGrabber __all__ = [ "OasisVBMTestingDataGrabber", - "SPMAuditoryTestingDataGrabber", "PartlyCloudyTestingDataGrabber", + "SPMAuditoryTestingDataGrabber", ] diff --git a/junifer/typing/__init__.pyi b/junifer/typing/__init__.pyi index 43ab84c02..bfe026fda 100644 --- a/junifer/typing/__init__.pyi +++ b/junifer/typing/__init__.pyi @@ -9,6 +9,7 @@ __all__ = [ "ExternalDependencies", "MarkerInOutMappings", "DataGrabberPatterns", + "ConfigVal", ] from ._typing import ( @@ -22,4 +23,5 @@ from ._typing import ( ExternalDependencies, MarkerInOutMappings, DataGrabberPatterns, + ConfigVal, ) diff --git a/junifer/typing/_typing.py b/junifer/typing/_typing.py index b7be28415..d7275b597 100644 --- a/junifer/typing/_typing.py +++ b/junifer/typing/_typing.py @@ -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] diff --git a/junifer/utils/__init__.pyi b/junifer/utils/__init__.pyi index b9db68f18..c840252d3 100644 --- a/junifer/utils/__init__.pyi +++ b/junifer/utils/__init__.pyi @@ -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 diff --git a/junifer/utils/_config.py b/junifer/utils/_config.py new file mode 100644 index 000000000..b2b8e4643 --- /dev/null +++ b/junifer/utils/_config.py @@ -0,0 +1,110 @@ +"""Provide junifer global configuration.""" + +# Authors: Federico Raimondo +# Synchon Mandal +# 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() diff --git a/junifer/utils/helpers.py b/junifer/utils/helpers.py index 229b75042..f4f67dbe0 100644 --- a/junifer/utils/helpers.py +++ b/junifer/utils/helpers.py @@ -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: diff --git a/junifer/utils/logging.py b/junifer/utils/logging.py index 0fb26bdbe..93ff8618f 100644 --- a/junifer/utils/logging.py +++ b/junifer/utils/logging.py @@ -24,9 +24,9 @@ import datalad __all__ = [ "WrapStdOut", + "configure_logging", "get_versions", "log_versions", - "configure_logging", "raise_error", "warn_with_log", ] diff --git a/junifer/utils/singleton.py b/junifer/utils/singleton.py index 1503fc9c7..e6e31baa2 100644 --- a/junifer/utils/singleton.py +++ b/junifer/utils/singleton.py @@ -8,7 +8,7 @@ from abc import ABCMeta from typing import Any, ClassVar -__all__ = ["Singleton", "ABCSingleton"] +__all__ = ["ABCSingleton", "Singleton"] class Singleton(type): diff --git a/junifer/utils/tests/test_config.py b/junifer/utils/tests/test_config.py new file mode 100644 index 000000000..209acfa64 --- /dev/null +++ b/junifer/utils/tests/test_config.py @@ -0,0 +1,59 @@ +"""Provide tests for ConfigManager.""" + +# Authors: Federico Raimondo +# Synchon Mandal +# 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")