From 06425ad4b5e29ddae3c9f114bf072b0cb3fd2d03 Mon Sep 17 00:00:00 2001 From: Omkar Kabde Date: Wed, 18 Feb 2026 15:07:55 +0530 Subject: [PATCH 1/9] [ENH] make `OpenMLSetup` inherit `OpenMLBase` --- openml/setups/setup.py | 57 ++++++++++++++++++++++++++++------------- openml/utils/_openml.py | 1 + 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/openml/setups/setup.py b/openml/setups/setup.py index 170838138..61dac3b1d 100644 --- a/openml/setups/setup.py +++ b/openml/setups/setup.py @@ -1,25 +1,28 @@ # License: BSD 3-Clause from __future__ import annotations +from collections.abc import Sequence from dataclasses import asdict, dataclass from typing import Any -import openml.config import openml.flows +from openml.base import OpenMLBase -@dataclass -class OpenMLSetup: +@dataclass(repr=False) +class OpenMLSetup(OpenMLBase): """Setup object (a.k.a. Configuration). + A setup is the combination of a flow with all its hyperparameters set. + Parameters ---------- setup_id : int - The OpenML setup id + The OpenML setup id. flow_id : int - The flow that it is build upon - parameters : dict - The setting of the parameters + The id of the flow that this setup is built upon. + parameters : dict[int, Any] or None + The hyperparameter settings, keyed by parameter input id. """ setup_id: int @@ -36,6 +39,12 @@ def __post_init__(self) -> None: if self.parameters is not None and not isinstance(self.parameters, dict): raise ValueError("parameters should be dict") + @property + def id(self) -> int | None: + """The id of the setup.""" + return self.setup_id + + def _to_dict(self) -> dict[str, Any]: return { "setup_id": self.setup_id, @@ -45,10 +54,9 @@ def _to_dict(self) -> dict[str, Any]: else None, } - def __repr__(self) -> str: - header = "OpenML Setup" - header = f"{header}\n{'=' * len(header)}\n" - + def _get_repr_body_fields(self) -> Sequence[tuple[str, str | int | None]]: + """Collect all information to display in the __repr__ body. + """ fields = { "Setup ID": self.setup_id, "Flow ID": self.flow_id, @@ -58,14 +66,27 @@ def __repr__(self) -> str: ), } - # determines the order in which the information will be printed order = ["Setup ID", "Flow ID", "Flow URL", "# of Parameters"] - _fields = [(key, fields[key]) for key in order if key in fields] - - longest_field_name_length = max(len(name) for name, _ in _fields) - field_line_format = f"{{:.<{longest_field_name_length}}}: {{}}" - body = "\n".join(field_line_format.format(name, value) for name, value in _fields) - return header + body + return [(key, fields[key]) for key in order if key in fields] + + def _parse_publish_response(self, xml_response: dict[str, str]) -> None: + """Not supported for setups. + + Setups are created implicitly when a run is published and cannot be + published directly. + """ + raise NotImplementedError("Setups cannot be published directly.") + + def publish(self) -> OpenMLBase: + """Not supported for setups. + + Setups are created implicitly when a run is published to the server + and cannot be published directly. + """ + raise NotImplementedError( + "Setups cannot be published directly. " + "A setup is created automatically when a run is published." + ) @dataclass diff --git a/openml/utils/_openml.py b/openml/utils/_openml.py index f18dbe3e0..cec3f1b01 100644 --- a/openml/utils/_openml.py +++ b/openml/utils/_openml.py @@ -101,6 +101,7 @@ def _get_rest_api_type_alias(oml_object: OpenMLBase) -> str: (openml.flows.OpenMLFlow, "flow"), (openml.tasks.OpenMLTask, "task"), (openml.runs.OpenMLRun, "run"), + (openml.setups.OpenMLSetup, "setup"), ((openml.study.OpenMLStudy, openml.study.OpenMLBenchmarkSuite), "study"), ] _, api_type_alias = next( From beb08911047e0c65880dd9b81f4c9a15a37ab0f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:41:07 +0000 Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- openml/setups/setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openml/setups/setup.py b/openml/setups/setup.py index 61dac3b1d..96a54fc1b 100644 --- a/openml/setups/setup.py +++ b/openml/setups/setup.py @@ -44,7 +44,6 @@ def id(self) -> int | None: """The id of the setup.""" return self.setup_id - def _to_dict(self) -> dict[str, Any]: return { "setup_id": self.setup_id, @@ -55,8 +54,7 @@ def _to_dict(self) -> dict[str, Any]: } def _get_repr_body_fields(self) -> Sequence[tuple[str, str | int | None]]: - """Collect all information to display in the __repr__ body. - """ + """Collect all information to display in the __repr__ body.""" fields = { "Setup ID": self.setup_id, "Flow ID": self.flow_id, From 93807ef107cd934d57c563158852317d87d00f7f Mon Sep 17 00:00:00 2001 From: Omkar Kabde Date: Wed, 18 Feb 2026 15:12:13 +0530 Subject: [PATCH 3/9] quality fixes --- openml/setups/setup.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/openml/setups/setup.py b/openml/setups/setup.py index 61dac3b1d..187fecc75 100644 --- a/openml/setups/setup.py +++ b/openml/setups/setup.py @@ -44,7 +44,6 @@ def id(self) -> int | None: """The id of the setup.""" return self.setup_id - def _to_dict(self) -> dict[str, Any]: return { "setup_id": self.setup_id, @@ -54,9 +53,8 @@ def _to_dict(self) -> dict[str, Any]: else None, } - def _get_repr_body_fields(self) -> Sequence[tuple[str, str | int | None]]: - """Collect all information to display in the __repr__ body. - """ + def _get_repr_body_fields(self) -> Sequence[tuple]: + """Collect all information to display in the __repr__ body.""" fields = { "Setup ID": self.setup_id, "Flow ID": self.flow_id, @@ -167,9 +165,9 @@ def __repr__(self) -> str: parameter_default, parameter_value, ] - _fields = [(key, fields[key]) for key in order if key in fields] + fields_ = [(key, fields[key]) for key in order if key in fields] - longest_field_name_length = max(len(name) for name, _ in _fields) + longest_field_name_length = max(len(name) for name, _ in fields_) field_line_format = f"{{:.<{longest_field_name_length}}}: {{}}" - body = "\n".join(field_line_format.format(name, value) for name, value in _fields) + body = "\n".join(field_line_format.format(name, value) for name, value in fields_) return header + body From 2174551bacb88b874634945f07de708cc08077f0 Mon Sep 17 00:00:00 2001 From: Omkar Kabde Date: Wed, 18 Feb 2026 15:22:29 +0530 Subject: [PATCH 4/9] add tests for `OpenMLSetup` --- tests/test_setups/test_setup.py | 90 +++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/test_setups/test_setup.py diff --git a/tests/test_setups/test_setup.py b/tests/test_setups/test_setup.py new file mode 100644 index 000000000..3a1765f55 --- /dev/null +++ b/tests/test_setups/test_setup.py @@ -0,0 +1,90 @@ +# License: BSD 3-Clause +from __future__ import annotations + +import pytest + +import openml +from openml.setups import OpenMLParameter, OpenMLSetup + + +@pytest.fixture +def sample_parameters(): + param1 = OpenMLParameter( + input_id=1, + flow_id=100, + flow_name="sklearn.tree.DecisionTreeClassifier", + full_name="sklearn.tree.DecisionTreeClassifier(1)", + parameter_name="max_depth", + data_type="int", + default_value="None", + value="5", + ) + param2 = OpenMLParameter( + input_id=2, + flow_id=100, + flow_name="sklearn.tree.DecisionTreeClassifier", + full_name="sklearn.tree.DecisionTreeClassifier(2)", + parameter_name="min_samples_split", + data_type="int", + default_value="2", + value="3", + ) + return {1: param1, 2: param2} + + +@pytest.fixture +def setup(sample_parameters): + return OpenMLSetup(setup_id=42, flow_id=100, parameters=sample_parameters) + + +def test_id_property(setup): + assert setup.id == 42 + + +def test_openml_url(setup): + assert setup.openml_url == f"{openml.config.get_server_base_url()}/s/42" + + +def test_repr(setup): + repr_str = repr(setup) + assert "OpenML Setup" in repr_str + assert "Setup ID" in repr_str + assert "42" in repr_str + assert "Flow ID" in repr_str + assert "# of Parameters" in repr_str + + +def test_repr_none_parameters(): + s = OpenMLSetup(setup_id=7, flow_id=200, parameters=None) + assert "# of Parameters" in repr(s) + + +def test_to_dict(setup): + result = setup._to_dict() + assert result["setup_id"] == 42 + assert result["flow_id"] == 100 + assert len(result["parameters"]) == 2 + + +def test_to_dict_none_parameters(): + s = OpenMLSetup(setup_id=7, flow_id=200, parameters=None) + assert s._to_dict()["parameters"] is None + + +def test_publish_raises(setup): + with pytest.raises(NotImplementedError, match="Setups cannot be published directly"): + setup.publish() + + +@pytest.mark.parametrize( + ("field", "value", "match"), + [ + ("setup_id", "bad", "setup id should be int"), + ("flow_id", "bad", "flow id should be int"), + ("parameters", "bad", "parameters should be dict"), + ], +) +def test_invalid_init(field, value, match): + kwargs = {"setup_id": 1, "flow_id": 1, "parameters": None, field: value} + with pytest.raises(ValueError, match=match): + OpenMLSetup(**kwargs) From 5f8d9390dc250443cae19692803f3f79d5c64455 Mon Sep 17 00:00:00 2001 From: Omkar Kabde Date: Wed, 18 Feb 2026 16:18:15 +0530 Subject: [PATCH 5/9] add `ReprMixin` inheritance --- openml/setups/setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openml/setups/setup.py b/openml/setups/setup.py index 187fecc75..23f6bf0ea 100644 --- a/openml/setups/setup.py +++ b/openml/setups/setup.py @@ -7,10 +7,11 @@ import openml.flows from openml.base import OpenMLBase +from openml.utils import ReprMixin @dataclass(repr=False) -class OpenMLSetup(OpenMLBase): +class OpenMLSetup(OpenMLBase, ReprMixin): """Setup object (a.k.a. Configuration). A setup is the combination of a flow with all its hyperparameters set. From a5b5262b6b86a25db87229ef2b90a2853883eaa1 Mon Sep 17 00:00:00 2001 From: Omkar Kabde Date: Wed, 18 Feb 2026 17:13:32 +0530 Subject: [PATCH 6/9] add tests for tagging and xml parsing --- tests/test_setups/test_setup.py | 36 +++++++++++++++++++++++ tests/test_setups/test_setup_functions.py | 32 ++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/tests/test_setups/test_setup.py b/tests/test_setups/test_setup.py index 3a1765f55..5f228736e 100644 --- a/tests/test_setups/test_setup.py +++ b/tests/test_setups/test_setup.py @@ -1,10 +1,13 @@ # License: BSD 3-Clause from __future__ import annotations +from unittest import mock + import pytest import openml from openml.setups import OpenMLParameter, OpenMLSetup +from openml.utils import _get_rest_api_type_alias, _tag_entity @pytest.fixture @@ -88,3 +91,36 @@ def test_invalid_init(field, value, match): kwargs = {"setup_id": 1, "flow_id": 1, "parameters": None, field: value} with pytest.raises(ValueError, match=match): OpenMLSetup(**kwargs) + + +def test_get_rest_api_type_alias_returns_setup(sample_parameters): + setup = OpenMLSetup(setup_id=42, flow_id=100, parameters=sample_parameters) + assert _get_rest_api_type_alias(setup) == "setup" + + +def test_tag_entity_setup_tag_response_parsing(): + """Verify _tag_entity correctly parses setup tag API response.""" + tag_response = ( + '' + "mytagother" + ) + with mock.patch( + "openml._api_calls._perform_api_call", + return_value=tag_response, + ): + tags = _tag_entity("setup", 1, "mytag") + assert tags == ["mytag", "other"] + + +def test_tag_entity_setup_untag_response_parsing(): + """Verify _tag_entity correctly parses setup untag API response (empty).""" + untag_response = ( + '' + "" + ) + with mock.patch( + "openml._api_calls._perform_api_call", + return_value=untag_response, + ): + tags = _tag_entity("setup", 1, "mytag", untag=True) + assert tags == [] diff --git a/tests/test_setups/test_setup_functions.py b/tests/test_setups/test_setup_functions.py index a0469f9a5..69546175c 100644 --- a/tests/test_setups/test_setup_functions.py +++ b/tests/test_setups/test_setup_functions.py @@ -2,6 +2,7 @@ from __future__ import annotations import hashlib +import random import time import unittest.mock @@ -15,6 +16,7 @@ import openml import openml.exceptions from openml.testing import TestBase +from openml.utils import _tag_entity def get_sentinel(): @@ -189,3 +191,33 @@ def test_get_uncached_setup(self): openml.config.set_root_cache_directory(self.static_cache_dir) with pytest.raises(openml.exceptions.OpenMLCacheException): openml.setups.functions._get_cached_setup(10) + + @pytest.mark.uses_test_server() + def test_tag_untag_setup(self): + setups = openml.setups.list_setups(size=1) + if not setups: + pytest.skip("Test server has no setups") + setup_id = next(iter(setups.keys())) + tag = f"test_tag_{random.randint(1, 1000000)}" + all_tags = _tag_entity("setup", setup_id, tag) + assert tag in all_tags + all_tags = _tag_entity("setup", setup_id, tag, untag=True) + assert tag not in all_tags + + @pytest.mark.uses_test_server() + def test_tagging(self): + setups = openml.setups.list_setups(size=1) + if not setups: + pytest.skip("Test server has no setups") + setup_id = next(iter(setups.keys())) + setup = setups[setup_id] + tag = f"test_tag_TestSetupFunctions_{str(time.time()).replace('.', '')}" + tagged = openml.setups.list_setups(tag=tag) + assert len(tagged) == 0 + setup.push_tag(tag) + tagged = openml.setups.list_setups(tag=tag) + assert len(tagged) == 1 + assert setup_id in tagged + setup.remove_tag(tag) + tagged = openml.setups.list_setups(tag=tag) + assert len(tagged) == 0 From 30ab989e17672f07e3f1f22cf99a56014a99ef36 Mon Sep 17 00:00:00 2001 From: Omkar Kabde Date: Wed, 18 Feb 2026 17:18:33 +0530 Subject: [PATCH 7/9] follow #1567 convention for `_get_repr_body_fields` --- openml/setups/setup.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openml/setups/setup.py b/openml/setups/setup.py index 23f6bf0ea..aedb6a43c 100644 --- a/openml/setups/setup.py +++ b/openml/setups/setup.py @@ -54,17 +54,16 @@ def _to_dict(self) -> dict[str, Any]: else None, } - def _get_repr_body_fields(self) -> Sequence[tuple]: + def _get_repr_body_fields(self) -> Sequence[tuple[str, str | int | list[str] | None]]: """Collect all information to display in the __repr__ body.""" - fields = { + fields: dict[str, int | str | None] = { "Setup ID": self.setup_id, "Flow ID": self.flow_id, "Flow URL": openml.flows.OpenMLFlow.url_for_id(self.flow_id), - "# of Parameters": ( - len(self.parameters) if self.parameters is not None else float("nan") - ), + "# of Parameters": (len(self.parameters) if self.parameters is not None else "nan"), } + # determines the order in which the information will be printed order = ["Setup ID", "Flow ID", "Flow URL", "# of Parameters"] return [(key, fields[key]) for key in order if key in fields] From b002ab17ffb92c2e91243d92543972023d71037a Mon Sep 17 00:00:00 2001 From: Omkar Kabde Date: Wed, 18 Feb 2026 17:20:08 +0530 Subject: [PATCH 8/9] rename test markers --- tests/test_setups/test_setup_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_setups/test_setup_functions.py b/tests/test_setups/test_setup_functions.py index 9bccde871..97c77a71e 100644 --- a/tests/test_setups/test_setup_functions.py +++ b/tests/test_setups/test_setup_functions.py @@ -192,7 +192,7 @@ def test_get_uncached_setup(self): with pytest.raises(openml.exceptions.OpenMLCacheException): openml.setups.functions._get_cached_setup(10) - @pytest.mark.uses_test_server() + @pytest.mark.test_server() def test_tag_untag_setup(self): setups = openml.setups.list_setups(size=1) if not setups: @@ -204,7 +204,7 @@ def test_tag_untag_setup(self): all_tags = _tag_entity("setup", setup_id, tag, untag=True) assert tag not in all_tags - @pytest.mark.uses_test_server() + @pytest.mark.test_server() def test_tagging(self): setups = openml.setups.list_setups(size=1) if not setups: From 25221c5330be6bb7d576b1c839aeea05e007f6c8 Mon Sep 17 00:00:00 2001 From: Omkar Kabde Date: Wed, 18 Feb 2026 17:24:06 +0530 Subject: [PATCH 9/9] Update openml/setups/setup.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- openml/setups/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openml/setups/setup.py b/openml/setups/setup.py index aedb6a43c..8cf7bc6ee 100644 --- a/openml/setups/setup.py +++ b/openml/setups/setup.py @@ -41,7 +41,7 @@ def __post_init__(self) -> None: raise ValueError("parameters should be dict") @property - def id(self) -> int | None: + def id(self) -> int: """The id of the setup.""" return self.setup_id