From ecc1a48dd80b2fbf5731b1046a3e6733139b18bd Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 27 Jan 2026 17:15:26 +0530 Subject: [PATCH 1/6] ENH: Add global config manager - Implement global config system with set_config, get_config, reset_config, config_context - Integrate global config into BaseObject.get_config with priority: local > global > defaults - Add extension point __skbase_get_config__ for downstream packages - Add comprehensive tests --- skbase/base/_base.py | 35 +++++++++--- skbase/config/__init__.py | 18 ++++++ skbase/config/_config.py | 88 ++++++++++++++++++++++++++++++ skbase/config/tests/__init__.py | 1 + skbase/config/tests/test_config.py | 79 +++++++++++++++++++++++++++ skbase/tests/test_base.py | 36 +++++++++++- 6 files changed, 246 insertions(+), 11 deletions(-) create mode 100644 skbase/config/__init__.py create mode 100644 skbase/config/_config.py create mode 100644 skbase/config/tests/__init__.py create mode 100644 skbase/config/tests/test_config.py diff --git a/skbase/base/_base.py b/skbase/base/_base.py index dd385372..e182961e 100644 --- a/skbase/base/_base.py +++ b/skbase/base/_base.py @@ -64,6 +64,7 @@ class name: BaseEstimator from skbase.base._clone_base import _check_clone, _clone from skbase.base._pretty_printing._object_html_repr import _object_html_repr from skbase.base._tagmanager import _FlagManager +from skbase.config import get_config as get_global_config __author__: List[str] = ["fkiraly", "mloning", "RNKuhns", "tpvasconcelos"] __all__: List[str] = ["BaseEstimator", "BaseObject"] @@ -736,22 +737,38 @@ def get_config(self): Configs are key-value pairs of ``self``, typically used as transient flags for controlling behaviour. - ``get_config`` returns dynamic configs, which override the default configs. - - Default configs are set in the class attribute ``_config`` of - the class or its parent classes, - and are overridden by dynamic configs set via ``set_config``. + ``get_config`` returns configs with the following priority order: + 1. Local configs set via ``set_config`` (highest priority) + 2. Global configs set via ``skbase.config.set_config`` + 3. Extension configs from ``__skbase_get_config__`` if defined + 4. Default configs from class ``_config`` attributes (lowest priority) Configs are retained under ``clone`` or ``reset`` calls. Returns ------- config_dict : dict - Dictionary of config name : config value pairs. Collected from _config - class attribute via nested inheritance and then any overrides - and new tags from _onfig_dynamic object attribute. + Dictionary of config name : config value pairs. """ - return self._get_flags(flag_attr_name="_config") + # Start with class defaults + config = self._get_class_flags(flag_attr_name="_config") + + # Update with global config + global_config = get_global_config() + config.update(global_config) + + # Update with extension config if available + if hasattr(self, "__skbase_get_config__"): + extension_config = self.__skbase_get_config__() + if isinstance(extension_config, dict): + config.update(extension_config) + + # Update with local config overrides (highest priority) + if hasattr(self, "_config_dynamic"): + local_overrides = getattr(self, "_config_dynamic") + config.update(local_overrides) + + return config def set_config(self, **config_dict): """Set config flags to given values. diff --git a/skbase/config/__init__.py b/skbase/config/__init__.py new file mode 100644 index 00000000..8eb21590 --- /dev/null +++ b/skbase/config/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +"""Global configuration management for skbase.""" + +from skbase.config._config import ( + config_context, + get_config, + get_default_config, + reset_config, + set_config, +) + +__all__ = [ + "config_context", + "get_config", + "get_default_config", + "reset_config", + "set_config", +] \ No newline at end of file diff --git a/skbase/config/_config.py b/skbase/config/_config.py new file mode 100644 index 00000000..9971be71 --- /dev/null +++ b/skbase/config/_config.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +"""Global configuration management for skbase.""" + +from contextlib import contextmanager +from copy import deepcopy + +__author__ = ["RNKuhns"] +__all__ = [ + "config_context", + "get_config", + "get_default_config", + "reset_config", + "set_config", +] + +# Global configuration defaults +_DEFAULT_CONFIG = { + "display": "diagram", + "print_changed_only": True, + "check_clone": False, + "clone_config": True, +} + +# Global config storage +_global_config = deepcopy(_DEFAULT_CONFIG) + + +def _get_global_config(): + """Get the global config dict.""" + return _global_config + + +def get_default_config(): + """Get the default global config. + + Returns + ------- + dict + Default global config. + """ + return deepcopy(_DEFAULT_CONFIG) + + +def get_config(): + """Get current global config. + + Returns + ------- + dict + Current global config. + """ + return deepcopy(_get_global_config()) + + +def set_config(**config_dict): + """Set global config values. + + Parameters + ---------- + **config_dict : dict + Config key-value pairs to set globally. + """ + global_config = _get_global_config() + global_config.update(config_dict) + + +def reset_config(): + """Reset global config to defaults.""" + global _global_config + _global_config = deepcopy(_DEFAULT_CONFIG) + + +@contextmanager +def config_context(**config_dict): + """Context manager for temporary config changes. + + Parameters + ---------- + **config_dict : dict + Config key-value pairs to set temporarily. + """ + old_config = get_config() + set_config(**config_dict) + try: + yield + finally: + global _global_config + _global_config = old_config \ No newline at end of file diff --git a/skbase/config/tests/__init__.py b/skbase/config/tests/__init__.py new file mode 100644 index 00000000..7c68785e --- /dev/null +++ b/skbase/config/tests/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/skbase/config/tests/test_config.py b/skbase/config/tests/test_config.py new file mode 100644 index 00000000..0e250c5b --- /dev/null +++ b/skbase/config/tests/test_config.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +"""Tests for global config functionality.""" + +import pytest + +from skbase.config import ( + config_context, + get_config, + get_default_config, + reset_config, + set_config, +) + + +def test_get_default_config(): + """Test get_default_config returns correct defaults.""" + defaults = get_default_config() + expected = { + "display": "diagram", + "print_changed_only": True, + "check_clone": False, + "clone_config": True, + } + assert defaults == expected + + +def test_get_config(): + """Test get_config returns current global config.""" + reset_config() + config = get_config() + assert config == get_default_config() + + +def test_set_config(): + """Test set_config updates global config.""" + reset_config() + set_config(display="text", check_clone=True) + config = get_config() + assert config["display"] == "text" + assert config["check_clone"] is True + assert config["print_changed_only"] is True # unchanged + + +def test_reset_config(): + """Test reset_config resets to defaults.""" + set_config(display="text") + reset_config() + config = get_config() + assert config == get_default_config() + + +def test_config_context(): + """Test config_context temporarily changes config.""" + reset_config() + original_config = get_config() + + with config_context(display="text", check_clone=True): + inner_config = get_config() + assert inner_config["display"] == "text" + assert inner_config["check_clone"] is True + + # Should be back to original + final_config = get_config() + assert final_config == original_config + + +def test_config_context_nested(): + """Test nested config_context.""" + reset_config() + + with config_context(display="text"): + assert get_config()["display"] == "text" + + with config_context(print_changed_only=False): + assert get_config()["display"] == "text" + assert get_config()["print_changed_only"] is False + + assert get_config()["display"] == "text" + assert get_config()["print_changed_only"] is True # back to default \ No newline at end of file diff --git a/skbase/tests/test_base.py b/skbase/tests/test_base.py index a924ff07..555b9c30 100644 --- a/skbase/tests/test_base.py +++ b/skbase/tests/test_base.py @@ -76,6 +76,7 @@ import scipy.sparse as sp from skbase.base import BaseEstimator, BaseObject +from skbase.config import get_default_config, reset_config as reset_global_config from skbase.tests.conftest import Child, Parent from skbase.tests.mock_package.test_mock_package import CompositionDummy from skbase.utils.dependencies import _check_soft_dependencies @@ -1361,6 +1362,7 @@ def test_eq_dunder(): def test_get_set_config(): """Tests get_config and set_config methods.""" + reset_global_config() # Reset global config to defaults class _TestConfig(BaseObject): _config = {"foo_config": 42, "bar": "a"} @@ -1375,13 +1377,15 @@ def __init__(self, a, b=42): test_obj = _TestConfig(7) expected_config_orig = BaseObject._config.copy() - expected_config_orig.update({"foo_config": 42, "bar": "a"}) + expected_config_orig.update(get_default_config()) # global defaults + expected_config_orig.update({"foo_config": 42, "bar": "a"}) # class # Test get_config assert test_obj.get_config() == expected_config_orig expected_config = BaseObject._config.copy() - expected_config.update({"foo_config": 37, "bar": "a"}) + expected_config.update(get_default_config()) # global + expected_config.update({"foo_config": 37, "bar": "a"}) # local override # Test set_config test_obj.set_config(foo_config=37) @@ -1393,6 +1397,34 @@ def __init__(self, a, b=42): assert test_obj.get_config() == expected_config +def test_global_config_integration(): + """Test that global config is integrated into BaseObject.get_config.""" + from skbase.config import set_config as set_global_config, reset_config as reset_global_config + + reset_global_config() + + class _TestGlobalConfig(BaseObject): + _config = {"local_config": "class_value"} + + test_obj = _TestGlobalConfig() + + # Initially, should have class + global defaults + config = test_obj.get_config() + assert config["local_config"] == "class_value" + assert config["display"] == "diagram" # global default + + # Set global config + set_global_config(display="text") + + config = test_obj.get_config() + assert config["display"] == "text" # global override + + # Local override should take precedence + test_obj.set_config(display="diagram") + config = test_obj.get_config() + assert config["display"] == "diagram" # local override + + def test_clone_with_custom_plugins(): """Test that cloning works when custom clone_plugins are provided. From 936cc1dd90af0da8ef12d7dd0452afabad563fee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:47:49 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- skbase/base/_base.py | 8 ++++---- skbase/config/__init__.py | 2 +- skbase/config/_config.py | 2 +- skbase/config/tests/__init__.py | 2 +- skbase/config/tests/test_config.py | 12 ++++++------ skbase/tests/test_base.py | 20 +++++++++++--------- 6 files changed, 24 insertions(+), 22 deletions(-) diff --git a/skbase/base/_base.py b/skbase/base/_base.py index e182961e..542e2a3a 100644 --- a/skbase/base/_base.py +++ b/skbase/base/_base.py @@ -752,22 +752,22 @@ def get_config(self): """ # Start with class defaults config = self._get_class_flags(flag_attr_name="_config") - + # Update with global config global_config = get_global_config() config.update(global_config) - + # Update with extension config if available if hasattr(self, "__skbase_get_config__"): extension_config = self.__skbase_get_config__() if isinstance(extension_config, dict): config.update(extension_config) - + # Update with local config overrides (highest priority) if hasattr(self, "_config_dynamic"): local_overrides = getattr(self, "_config_dynamic") config.update(local_overrides) - + return config def set_config(self, **config_dict): diff --git a/skbase/config/__init__.py b/skbase/config/__init__.py index 8eb21590..10d397d5 100644 --- a/skbase/config/__init__.py +++ b/skbase/config/__init__.py @@ -15,4 +15,4 @@ "get_default_config", "reset_config", "set_config", -] \ No newline at end of file +] diff --git a/skbase/config/_config.py b/skbase/config/_config.py index 9971be71..c4a7bb87 100644 --- a/skbase/config/_config.py +++ b/skbase/config/_config.py @@ -85,4 +85,4 @@ def config_context(**config_dict): yield finally: global _global_config - _global_config = old_config \ No newline at end of file + _global_config = old_config diff --git a/skbase/config/tests/__init__.py b/skbase/config/tests/__init__.py index 7c68785e..40a96afc 100644 --- a/skbase/config/tests/__init__.py +++ b/skbase/config/tests/__init__.py @@ -1 +1 @@ -# -*- coding: utf-8 -*- \ No newline at end of file +# -*- coding: utf-8 -*- diff --git a/skbase/config/tests/test_config.py b/skbase/config/tests/test_config.py index 0e250c5b..296dc854 100644 --- a/skbase/config/tests/test_config.py +++ b/skbase/config/tests/test_config.py @@ -53,12 +53,12 @@ def test_config_context(): """Test config_context temporarily changes config.""" reset_config() original_config = get_config() - + with config_context(display="text", check_clone=True): inner_config = get_config() assert inner_config["display"] == "text" assert inner_config["check_clone"] is True - + # Should be back to original final_config = get_config() assert final_config == original_config @@ -67,13 +67,13 @@ def test_config_context(): def test_config_context_nested(): """Test nested config_context.""" reset_config() - + with config_context(display="text"): assert get_config()["display"] == "text" - + with config_context(print_changed_only=False): assert get_config()["display"] == "text" assert get_config()["print_changed_only"] is False - + assert get_config()["display"] == "text" - assert get_config()["print_changed_only"] is True # back to default \ No newline at end of file + assert get_config()["print_changed_only"] is True # back to default diff --git a/skbase/tests/test_base.py b/skbase/tests/test_base.py index 555b9c30..47f1504b 100644 --- a/skbase/tests/test_base.py +++ b/skbase/tests/test_base.py @@ -76,7 +76,8 @@ import scipy.sparse as sp from skbase.base import BaseEstimator, BaseObject -from skbase.config import get_default_config, reset_config as reset_global_config +from skbase.config import get_default_config +from skbase.config import reset_config as reset_global_config from skbase.tests.conftest import Child, Parent from skbase.tests.mock_package.test_mock_package import CompositionDummy from skbase.utils.dependencies import _check_soft_dependencies @@ -1399,26 +1400,27 @@ def __init__(self, a, b=42): def test_global_config_integration(): """Test that global config is integrated into BaseObject.get_config.""" - from skbase.config import set_config as set_global_config, reset_config as reset_global_config - + from skbase.config import reset_config as reset_global_config + from skbase.config import set_config as set_global_config + reset_global_config() - + class _TestGlobalConfig(BaseObject): _config = {"local_config": "class_value"} - + test_obj = _TestGlobalConfig() - + # Initially, should have class + global defaults config = test_obj.get_config() assert config["local_config"] == "class_value" assert config["display"] == "diagram" # global default - + # Set global config set_global_config(display="text") - + config = test_obj.get_config() assert config["display"] == "text" # global override - + # Local override should take precedence test_obj.set_config(display="diagram") config = test_obj.get_config() From 848d022c76386f43a2864890cdf1267ac0b5b155 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 27 Jan 2026 17:23:06 +0530 Subject: [PATCH 3/6] MNT: Apply pre-commit formatting fixes - Add missing newlines at end of files - Remove trailing whitespace - Sort imports --- skbase/base/_base.py | 3 +-- skbase/config/tests/__init__.py | 1 + skbase/config/tests/test_config.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/skbase/base/_base.py b/skbase/base/_base.py index 542e2a3a..16ffc06e 100644 --- a/skbase/base/_base.py +++ b/skbase/base/_base.py @@ -765,8 +765,7 @@ def get_config(self): # Update with local config overrides (highest priority) if hasattr(self, "_config_dynamic"): - local_overrides = getattr(self, "_config_dynamic") - config.update(local_overrides) + config.update(self._config_dynamic) return config diff --git a/skbase/config/tests/__init__.py b/skbase/config/tests/__init__.py index 40a96afc..ea4f9ee6 100644 --- a/skbase/config/tests/__init__.py +++ b/skbase/config/tests/__init__.py @@ -1 +1,2 @@ # -*- coding: utf-8 -*- +"""Tests for skbase global config functionality.""" diff --git a/skbase/config/tests/test_config.py b/skbase/config/tests/test_config.py index 296dc854..06e8f0ca 100644 --- a/skbase/config/tests/test_config.py +++ b/skbase/config/tests/test_config.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- """Tests for global config functionality.""" -import pytest - from skbase.config import ( config_context, get_config, From a67ab9af302641afe58242fd44a300cf23fbb782 Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Tue, 27 Jan 2026 17:31:12 +0530 Subject: [PATCH 4/6] MNT: Update conftest.py for new config module - Add skbase.config modules to SKBASE_MODULES and SKBASE_PUBLIC_MODULES - Add config functions to SKBASE_PUBLIC_FUNCTIONS_BY_MODULE and SKBASE_FUNCTIONS_BY_MODULE --- skbase/tests/conftest.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/skbase/tests/conftest.py b/skbase/tests/conftest.py index dec1b79b..56b3deef 100644 --- a/skbase/tests/conftest.py +++ b/skbase/tests/conftest.py @@ -33,6 +33,10 @@ "skbase.base._pretty_printing._object_html_repr", "skbase.base._pretty_printing._pprint", "skbase.base._tagmanager", + "skbase.config", + "skbase.config._config", + "skbase.config.tests", + "skbase.config.tests.test_config", "skbase.lookup", "skbase.lookup.tests", "skbase.lookup.tests.test_lookup", @@ -72,6 +76,9 @@ SKBASE_PUBLIC_MODULES = ( "skbase", "skbase.base", + "skbase.config", + "skbase.config.tests", + "skbase.config.tests.test_config", "skbase.lookup", "skbase.lookup.tests", "skbase.lookup.tests.test_lookup", @@ -157,6 +164,20 @@ } ) SKBASE_PUBLIC_FUNCTIONS_BY_MODULE = { + "skbase.config": ( + "config_context", + "get_config", + "get_default_config", + "reset_config", + "set_config", + ), + "skbase.config._config": ( + "config_context", + "get_config", + "get_default_config", + "reset_config", + "set_config", + ), "skbase.lookup": ("all_objects", "get_package_metadata"), "skbase.lookup._lookup": ("all_objects", "get_package_metadata"), "skbase.testing.utils._conditional_fixtures": ( @@ -223,6 +244,14 @@ "_write_label_html", ), "skbase.base._pretty_printing._pprint": ("_changed_params", "_safe_repr"), + "skbase.config._config": ( + "_get_global_config", + "config_context", + "get_config", + "get_default_config", + "reset_config", + "set_config", + ), "skbase.lookup._lookup": ( "all_objects", "get_package_metadata", From 128935013eec3930b7df0dfe9da9117bc7d1b3cd Mon Sep 17 00:00:00 2001 From: Arnav Kapoor Date: Fri, 6 Mar 2026 08:58:02 +0530 Subject: [PATCH 5/6] Update config documentation in _base.py Clarify the priority order of config retrieval and enhance the return value description. --- skbase/base/_base.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/skbase/base/_base.py b/skbase/base/_base.py index 16ffc06e..f3e6df2d 100644 --- a/skbase/base/_base.py +++ b/skbase/base/_base.py @@ -736,21 +736,22 @@ def get_config(self): Configs are key-value pairs of ``self``, typically used as transient flags for controlling behaviour. + + ``get_config`` returns dynamic configs, which override the default configs. - ``get_config`` returns configs with the following priority order: - 1. Local configs set via ``set_config`` (highest priority) - 2. Global configs set via ``skbase.config.set_config`` - 3. Extension configs from ``__skbase_get_config__`` if defined - 4. Default configs from class ``_config`` attributes (lowest priority) + Default configs are set in the class attribute ``_config`` of + the class or its parent classes, + and are overridden by dynamic configs set via ``set_config``. Configs are retained under ``clone`` or ``reset`` calls. Returns ------- config_dict : dict - Dictionary of config name : config value pairs. + Dictionary of config name : config value pairs. Collected from _config + class attribute via nested inheritance and then any overrides + and new tags from _onfig_dynamic object attribute. """ - # Start with class defaults config = self._get_class_flags(flag_attr_name="_config") # Update with global config From 9d7c97ffd614bddb109cf6786ebdfcfb2eaab4a6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:28:23 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- skbase/base/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skbase/base/_base.py b/skbase/base/_base.py index f3e6df2d..c61c1350 100644 --- a/skbase/base/_base.py +++ b/skbase/base/_base.py @@ -736,7 +736,7 @@ def get_config(self): Configs are key-value pairs of ``self``, typically used as transient flags for controlling behaviour. - + ``get_config`` returns dynamic configs, which override the default configs. Default configs are set in the class attribute ``_config`` of