Skip to content

Commit 05a58ee

Browse files
JATAYU000fkiralyEmanAbdelhaleemrohansen856DDiyash
authored
[ENH] refactor OpenML classes to inherit ReprMixin for default __repr__, instead of copy-paste (#1567)
<!-- Thanks for contributing a pull request to the OpenML python connector! Please ensure you have taken a look at the contribution guidelines: https://github.com/openml/openml-python/blob/main/CONTRIBUTING.md#Contributing-Pull-Requests Please make sure that: * the title of the pull request is descriptive * this pull requests is against the `develop` branch * for any new functionality, consider adding a relevant example * add unit tests for new functionalities * collect files uploaded to test server using _mark_entity_for_removal() * add the BSD 3-Clause license to any new file created --> #### Metadata * Reference Issue: Fixes: #1563 * New Tests Added: No * Documentation Updated: No * Change Log Entry: Inherit `ReprMixin` for common `__repr__` formatting across all `OpenML` classes * Dependent on #1595 --------- Co-authored-by: Franz Király <fkiraly@gcos.ai> Co-authored-by: Eman Abdelhaleem <101830347+EmanAbdelhaleem@users.noreply.github.com> Co-authored-by: Rohan Sen <rohansen856@gmail.com> Co-authored-by: DDiyash <149958769+DDiyash@users.noreply.github.com> Co-authored-by: Armaghan Shakir <raoarmaghanshakir040@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pieter Gijsbers <p.gijsbers@tue.nl>
1 parent 3bdf3d2 commit 05a58ee

4 files changed

Lines changed: 67 additions & 69 deletions

File tree

‎openml/base.py‎

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
# License: BSD 3-Clause
22
from __future__ import annotations
33

4-
import re
54
import webbrowser
65
from abc import ABC, abstractmethod
76
from typing import TYPE_CHECKING
87

98
import xmltodict
109

10+
import openml
1111
import openml._api_calls
12+
from openml.utils import ReprMixin
1213

1314
from .utils import _get_rest_api_type_alias, _tag_openml_base
1415

1516
if TYPE_CHECKING:
16-
from collections.abc import Iterable, Sequence
17+
from collections.abc import Sequence
1718

1819

19-
class OpenMLBase(ABC):
20+
class OpenMLBase(ReprMixin, ABC):
2021
"""Base object for functionality that is shared across entities."""
2122

22-
def __repr__(self) -> str:
23-
body_fields = self._get_repr_body_fields()
24-
return self._apply_repr_template(body_fields)
25-
2623
@property
2724
@abstractmethod
2825
def id(self) -> int | None:
@@ -62,34 +59,6 @@ def _get_repr_body_fields(self) -> Sequence[tuple[str, str | int | list[str] | N
6259
"""
6360
# Should be implemented in the base class.
6461

65-
def _apply_repr_template(
66-
self,
67-
body_fields: Iterable[tuple[str, str | int | list[str] | None]],
68-
) -> str:
69-
"""Generates the header and formats the body for string representation of the object.
70-
71-
Parameters
72-
----------
73-
body_fields: List[Tuple[str, str]]
74-
A list of (name, value) pairs to display in the body of the __repr__.
75-
"""
76-
# We add spaces between capitals, e.g. ClassificationTask -> Classification Task
77-
name_with_spaces = re.sub(
78-
r"(\w)([A-Z])",
79-
r"\1 \2",
80-
self.__class__.__name__[len("OpenML") :],
81-
)
82-
header_text = f"OpenML {name_with_spaces}"
83-
header = f"{header_text}\n{'=' * len(header_text)}\n"
84-
85-
_body_fields: list[tuple[str, str | int | list[str]]] = [
86-
(k, "None" if v is None else v) for k, v in body_fields
87-
]
88-
longest_field_name_length = max(len(name) for name, _ in _body_fields)
89-
field_line_format = f"{{:.<{longest_field_name_length}}}: {{}}"
90-
body = "\n".join(field_line_format.format(name, value) for name, value in _body_fields)
91-
return header + body
92-
9362
@abstractmethod
9463
def _to_dict(self) -> dict[str, dict]:
9564
"""Creates a dictionary representation of self.

‎openml/datasets/data_feature.py‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
from IPython.lib import pretty
1010

11+
from openml.utils import ReprMixin
1112

12-
class OpenMLDataFeature: # noqa: PLW1641, RUF100
13+
14+
class OpenMLDataFeature(ReprMixin):
1315
"""
1416
Data Feature (a.k.a. Attribute) object.
1517
@@ -75,11 +77,29 @@ def __init__( # noqa: PLR0913,PLR0917
7577
self.number_missing_values = number_missing_values
7678
self.ontologies = ontologies
7779

78-
def __repr__(self) -> str:
79-
return f"[{self.index} - {self.name} ({self.data_type})]"
80+
def _get_repr_body_fields(self) -> Sequence[tuple[str, str | int | list[str] | None]]:
81+
"""Collect all information to display in the __repr__ body."""
82+
fields: dict[str, int | str | None] = {
83+
"Index": self.index,
84+
"Name": self.name,
85+
"Data Type": self.data_type,
86+
}
87+
return list(fields.items())
8088

8189
def __eq__(self, other: Any) -> bool:
8290
return isinstance(other, OpenMLDataFeature) and self.__dict__ == other.__dict__
8391

92+
def __hash__(self) -> int:
93+
return hash(
94+
(
95+
self.index,
96+
self.name,
97+
self.data_type,
98+
tuple(self.nominal_values) if self.nominal_values is not None else None,
99+
self.number_missing_values,
100+
tuple(self.ontologies) if self.ontologies is not None else None,
101+
)
102+
)
103+
84104
def _repr_pretty_(self, pp: pretty.PrettyPrinter, cycle: bool) -> None: # noqa: ARG002
85105
pp.text(str(self))

‎openml/setups/setup.py‎

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
from __future__ import annotations
33

44
from dataclasses import asdict, dataclass
5-
from typing import Any
5+
from typing import TYPE_CHECKING, Any
66

77
import openml.flows
8+
from openml.utils import ReprMixin
89

10+
if TYPE_CHECKING:
11+
from collections.abc import Sequence
912

10-
@dataclass
11-
class OpenMLSetup:
13+
14+
@dataclass(repr=False)
15+
class OpenMLSetup(ReprMixin):
1216
"""Setup object (a.k.a. Configuration).
1317
1418
Parameters
@@ -44,31 +48,22 @@ def _to_dict(self) -> dict[str, Any]:
4448
else None,
4549
}
4650

47-
def __repr__(self) -> str:
48-
header = "OpenML Setup"
49-
header = f"{header}\n{'=' * len(header)}\n"
50-
51-
fields = {
51+
def _get_repr_body_fields(self) -> Sequence[tuple[str, str | int | list[str] | None]]:
52+
"""Collect all information to display in the __repr__ body."""
53+
fields: dict[str, int | str | None] = {
5254
"Setup ID": self.setup_id,
5355
"Flow ID": self.flow_id,
5456
"Flow URL": openml.flows.OpenMLFlow.url_for_id(self.flow_id),
55-
"# of Parameters": (
56-
len(self.parameters) if self.parameters is not None else float("nan")
57-
),
57+
"# of Parameters": (len(self.parameters) if self.parameters is not None else "nan"),
5858
}
5959

6060
# determines the order in which the information will be printed
6161
order = ["Setup ID", "Flow ID", "Flow URL", "# of Parameters"]
62-
_fields = [(key, fields[key]) for key in order if key in fields]
63-
64-
longest_field_name_length = max(len(name) for name, _ in _fields)
65-
field_line_format = f"{{:.<{longest_field_name_length}}}: {{}}"
66-
body = "\n".join(field_line_format.format(name, value) for name, value in _fields)
67-
return header + body
62+
return [(key, fields[key]) for key in order if key in fields]
6863

6964

70-
@dataclass
71-
class OpenMLParameter:
65+
@dataclass(repr=False)
66+
class OpenMLParameter(ReprMixin):
7267
"""Parameter object (used in setup).
7368
7469
Parameters
@@ -112,11 +107,9 @@ def _to_dict(self) -> dict[str, Any]:
112107
result["id"] = result.pop("input_id")
113108
return result
114109

115-
def __repr__(self) -> str:
116-
header = "OpenML Parameter"
117-
header = f"{header}\n{'=' * len(header)}\n"
118-
119-
fields = {
110+
def _get_repr_body_fields(self) -> Sequence[tuple[str, str | int | list[str] | None]]:
111+
"""Collect all information to display in the __repr__ body."""
112+
fields: dict[str, int | str | None] = {
120113
"ID": self.id,
121114
"Flow ID": self.flow_id,
122115
# "Flow Name": self.flow_name,
@@ -145,9 +138,4 @@ def __repr__(self) -> str:
145138
parameter_default,
146139
parameter_value,
147140
]
148-
_fields = [(key, fields[key]) for key in order if key in fields]
149-
150-
longest_field_name_length = max(len(name) for name, _ in _fields)
151-
field_line_format = f"{{:.<{longest_field_name_length}}}: {{}}"
152-
body = "\n".join(field_line_format.format(name, value) for name, value in _fields)
153-
return header + body
141+
return [(key, fields[key]) for key in order if key in fields]

‎openml/tasks/split.py‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
import arff # type: ignore
1010
import numpy as np
1111

12+
from openml.utils import ReprMixin
13+
1214
if TYPE_CHECKING:
15+
from collections.abc import Sequence
1316
from pathlib import Path
1417

1518

@@ -20,7 +23,7 @@ class Split(NamedTuple):
2023
test: np.ndarray
2124

2225

23-
class OpenMLSplit: # noqa: PLW1641, RUF100
26+
class OpenMLSplit(ReprMixin):
2427
"""OpenML Split object.
2528
2629
This class manages train-test splits for a dataset across multiple
@@ -65,6 +68,22 @@ def __init__(
6568
self.folds = len(self.split[0])
6669
self.samples = len(self.split[0][0])
6770

71+
def _get_repr_body_fields(self) -> Sequence[tuple[str, str | int | list[str] | None]]:
72+
"""Collect all information to display in the __repr__ body."""
73+
fields: dict[str, int | str | None] = {
74+
"Name": self.name,
75+
"Description": (
76+
self.description if len(self.description) <= 80 else self.description[:77] + "..."
77+
),
78+
"Repeats": self.repeats,
79+
"Folds": self.folds,
80+
"Samples": self.samples,
81+
}
82+
83+
order = ["Name", "Description", "Repeats", "Folds", "Samples"]
84+
85+
return [(key, fields[key]) for key in order if key in fields]
86+
6887
def __eq__(self, other: Any) -> bool:
6988
if (
7089
(not isinstance(self, type(other)))
@@ -92,6 +111,8 @@ def __eq__(self, other: Any) -> bool:
92111
return False
93112
return True
94113

114+
__hash__ = None # type: ignore
115+
95116
@classmethod
96117
def _from_arff_file(cls, filename: Path) -> OpenMLSplit: # noqa: C901, PLR0912
97118
repetitions = None

0 commit comments

Comments
 (0)