Skip to content

Commit d28a9c1

Browse files
committed
style: improve repr
1 parent fc708ba commit d28a9c1

File tree

10 files changed

+235
-68
lines changed

10 files changed

+235
-68
lines changed

execution_engine/omop/cohort/population_intervention_pair.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ def __init__(
6060
self.set_criteria(CohortCategory.POPULATION, population)
6161
self.set_criteria(CohortCategory.INTERVENTION, intervention)
6262

63+
def __repr__(self) -> str:
64+
"""
65+
Get the string representation of the population/intervention pair.
66+
"""
67+
return (
68+
f"{self.__class__.__name__}(\n"
69+
f" name={self._name},\n"
70+
f" url={self._url},\n"
71+
f" base_criterion={repr(self._base_criterion)},\n"
72+
f" population={self._population._repr_pretty(level=1).strip()},\n"
73+
f" intervention={self._intervention._repr_pretty(level=1).strip()}\n"
74+
f")"
75+
)
76+
6377
def set_criteria(
6478
self, category: CohortCategory, criteria: CriterionCombination | None
6579
) -> None:

execution_engine/omop/cohort/recommendation.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,30 @@ def __init__(
6565
# The id is used in the recommendation_id field in the result tables.
6666
self._id = recommendation_id
6767

68+
def __repr__(self) -> str:
69+
"""
70+
Get the string representation of the recommendation.
71+
"""
72+
pi_repr = "\n".join(
73+
[(" " + line) for line in repr(self._pi_pairs).split("\n")]
74+
).strip()
75+
pi_repr = (
76+
pi_repr[0] + "\n " + pi_repr[1:-2] + pi_repr[-2] + "\n " + pi_repr[-1]
77+
)
78+
return (
79+
f"{self.__class__.__name__}(\n"
80+
f" pi_pairs={pi_repr},\n"
81+
f" base_criterion={self._base_criterion},\n"
82+
f" name='{self._name}',\n"
83+
f" title='{self._title}',\n"
84+
f" url='{self._url}',\n"
85+
f" version='{self._version}',\n"
86+
f" description='{self._description}',\n"
87+
f" recommendation_id={self._id}\n"
88+
f" package_version='{self._package_version}',\n"
89+
f")"
90+
)
91+
6892
@property
6993
def name(self) -> str:
7094
"""
@@ -179,6 +203,12 @@ def population_intervention_pairs(
179203
"""
180204
yield from self._pi_pairs
181205

206+
def __str__(self) -> str:
207+
"""
208+
Get the string representation of the recommendation.
209+
"""
210+
return f"Recommendation(name='{self._name}', description='{self.description}')"
211+
182212
def __len__(self) -> int:
183213
"""
184214
Get the number of population/intervention pairs.

execution_engine/omop/criterion/combination/combination.py

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
__all__ = ["CriterionCombination"]
88

99

10+
def snake_to_camel(s: str) -> str:
11+
return "".join(word.capitalize() for word in s.lower().split("_"))
12+
13+
1014
class CriterionCombination(AbstractCriterion, metaclass=ABCMeta):
1115
"""
1216
Base class for a combination of criteria (temporal or logical).
@@ -35,9 +39,9 @@ def __repr__(self) -> str:
3539
Get the string representation of the operator.
3640
"""
3741
if self.operator in ["AT_LEAST", "AT_MOST", "EXACTLY"]:
38-
return f'{self.__class__.__name__}("{self.operator}", threshold={self.threshold})'
42+
return f'{self.__class__.__name__}(operator="{self.operator}", threshold={self.threshold})'
3943
else:
40-
return f'{self.__class__.__name__}("{self.operator}")'
44+
return f'{self.__class__.__name__}(operator="{self.operator}")'
4145

4246
def __eq__(self, other: object) -> bool:
4347
"""
@@ -128,12 +132,6 @@ def __getitem__(self, index: int) -> Union[Criterion, "CriterionCombination"]:
128132
"""
129133
return self._criteria[index]
130134

131-
def __repr__(self) -> str:
132-
"""
133-
Get the string representation of the criterion combination.
134-
"""
135-
return str(self)
136-
137135
def description(self) -> str:
138136
"""
139137
Description of this combination.
@@ -209,3 +207,89 @@ def from_dict(cls, data: Dict[str, Any]) -> "CriterionCombination":
209207
combination.add(criterion_factory(**criterion))
210208

211209
return combination
210+
211+
def _build_repr(
212+
self,
213+
children: Sequence[tuple[str | None, Any]],
214+
params: list[tuple[str, Any]],
215+
level: int = 0,
216+
) -> str:
217+
"""
218+
Builds a multi-line string for this criterion combination,
219+
properly indenting each level but avoiding double-indenting if
220+
a child already handles indentation in its own _repr_pretty.
221+
"""
222+
indent = " " * (2 * level)
223+
child_indent = " " * (2 * (level + 1))
224+
225+
op = snake_to_camel(self.operator.operator)
226+
227+
lines: list[str] = []
228+
kw_lines: list[str] = []
229+
criteria_lines: list[str] = []
230+
231+
method_defined = hasattr(self, op) and callable(getattr(self, op))
232+
233+
if self._root:
234+
params.append(("root_combination", self._root))
235+
236+
# if there is a specific method for this operator, use it
237+
if method_defined:
238+
lines.append(f"{indent}{self.__class__.__name__}.{op}(")
239+
if self.operator.threshold is not None:
240+
params.append(("threshold", self.operator.threshold))
241+
else:
242+
lines.append(f"{indent}{self.__class__.__name__}(")
243+
params.append(("operator", self.operator))
244+
245+
# Each child on its own line
246+
for key, child in children:
247+
if hasattr(child, "_repr_pretty"):
248+
# The child already handles its own indentation and multi-line formatting.
249+
child_repr = child._repr_pretty(level + 1)
250+
# We'll put a comma on the last line that the child produces.
251+
child_lines = child_repr.split("\n")
252+
child_lines[-1] += "," # add trailing comma to the last line
253+
if key is None:
254+
criteria_lines.extend(child_lines)
255+
else:
256+
# If you have a key like "left=" or "right=", prepend that to the first line
257+
# or handle it similarly. One approach is:
258+
child_lines[0] = f"{child_indent}{key}={child_lines[0].lstrip()}"
259+
criteria_lines.extend(child_lines)
260+
else:
261+
# Fallback to normal repr, which we indent at this level
262+
child_repr = repr(child)
263+
if key is None:
264+
criteria_lines.append(child_indent + child_repr + ",")
265+
else:
266+
criteria_lines.append(f"{child_indent}{key}={child_repr},")
267+
268+
params.append(("category", self.category))
269+
270+
for key, value in params:
271+
kw_lines.append(f"{child_indent}{key}={repr(value)},")
272+
273+
if method_defined:
274+
lines.extend(criteria_lines)
275+
lines.extend(kw_lines)
276+
else:
277+
lines.extend(kw_lines)
278+
lines.append(f"{child_indent}criteria=[")
279+
lines.extend(criteria_lines)
280+
lines.append(f"{child_indent}],")
281+
282+
lines.append(f"{indent})")
283+
284+
return "\n".join(lines)
285+
286+
def _repr_pretty(self, level: int = 0) -> str:
287+
children = [(None, c) for c in self._criteria]
288+
289+
return self._build_repr(children, params=[], level=level)
290+
291+
def __repr__(self) -> str:
292+
"""
293+
Get the string representation of the criterion combination.
294+
"""
295+
return self._repr_pretty(0)

execution_engine/omop/criterion/combination/logical.py

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -147,39 +147,6 @@ def AllOrNone(
147147
criteria=criteria,
148148
)
149149

150-
def _repr_pretty(self, level: int = 0) -> str:
151-
def snake_to_camel(s: str) -> str:
152-
return "".join(word.capitalize() for word in s.lower().split("_"))
153-
154-
indent = " " * (2 * level)
155-
op = snake_to_camel(self.operator.operator)
156-
157-
lines = []
158-
# Opening line
159-
lines.append(f"{indent}{self.__class__.__name__}.{op}(")
160-
161-
# Each child on its own line, indented by level+1
162-
for c in self._criteria:
163-
if hasattr(c, "_repr_pretty"):
164-
# Assume c is another Combination or something that implements _repr_pretty
165-
lines.append(c._repr_pretty(level + 1) + ",")
166-
else:
167-
# If it's a leaf object (e.g. a ConceptCriterion), just call normal repr
168-
child_indent = " " * (2 * (level + 1))
169-
lines.append(f"{child_indent}{repr(c)},")
170-
171-
# Closing parenthesis at the current indent level
172-
lines.append(f"{indent})")
173-
174-
# Join all lines with newlines
175-
return "\n".join(lines)
176-
177-
def __repr__(self) -> str:
178-
"""
179-
Get the string representation of the criterion combination.
180-
"""
181-
return self._repr_pretty(0)
182-
183150

184151
class NonCommutativeLogicalCriterionCombination(LogicalCriterionCombination):
185152
"""
@@ -243,12 +210,6 @@ def __str__(self) -> str:
243210
"""
244211
return f"{self.operator}({', '.join(str(c) for c in self._criteria)})"
245212

246-
def __repr__(self) -> str:
247-
"""
248-
Get the string representation of the criterion combination.
249-
"""
250-
return f'{self.__class__.__name__}("{self.operator}", {self._criteria})'
251-
252213
def __eq__(self, other: object) -> bool:
253214
"""
254215
Check if the criterion combination is equal to another criterion combination.
@@ -282,6 +243,13 @@ def dict(self) -> dict:
282243
"right": {"class_name": right.__class__.__name__, "data": right.dict()},
283244
}
284245

246+
def _repr_pretty(self, level: int = 0) -> str:
247+
children = [
248+
("left", self._left),
249+
("right", self._right),
250+
]
251+
return self._build_repr(children, params=[], level=level)
252+
285253
@classmethod
286254
def from_dict(
287255
cls, data: Dict[str, Any]

execution_engine/omop/criterion/combination/temporal.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __repr__(self) -> str:
2424
"""
2525
Get the string representation of the time interval type.
2626
"""
27-
return f'{self.__class__.__name__}("{self.value}")'
27+
return f"{self.__class__.__name__}.{self.name}"
2828

2929

3030
class TemporalIndicatorCombination(CriterionCombination):
@@ -103,6 +103,15 @@ def __str__(self) -> str:
103103
else:
104104
return super().__str__()
105105

106+
def _repr_pretty(self, level: int = 0) -> str:
107+
children = [(None, c) for c in self._criteria]
108+
params = [
109+
("interval_type", self.interval_type),
110+
("start_time", self.start_time),
111+
("end_time", self.end_time),
112+
]
113+
return self._build_repr(children, params, level)
114+
106115
def dict(self) -> Dict:
107116
"""
108117
Get the dictionary representation of the criterion combination.

execution_engine/omop/criterion/custom/tidal_volume.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import numbers
2-
from typing import Any
32

43
import sqlalchemy
54
from sqlalchemy import (
@@ -17,13 +16,15 @@
1716
from sqlalchemy.sql import Select
1817

1918
import execution_engine.omop.db.omop.tables as omop_tables
20-
from execution_engine.constants import OMOPConcepts
19+
from execution_engine.constants import CohortCategory, OMOPConcepts
20+
from execution_engine.omop.concepts import Concept
2121
from execution_engine.omop.criterion.abstract import (
2222
observation_end_datetime,
2323
observation_start_datetime,
2424
)
2525
from execution_engine.omop.criterion.point_in_time import PointInTimeCriterion
26-
from execution_engine.util.value import ValueNumber
26+
from execution_engine.util.types import Timing
27+
from execution_engine.util.value import Value, ValueNumber
2728

2829
__all__ = ["TidalVolumePerIdealBodyWeight"]
2930

@@ -47,9 +48,24 @@ class TidalVolumePerIdealBodyWeight(PointInTimeCriterion):
4748

4849
__GENDER_TO_INT = {"female": 0, "male": 1, "unknown": 0.5}
4950

50-
def __init__(self, *args: Any, **kwargs: Any) -> None:
51-
super().__init__(*args, **kwargs)
52-
51+
def __init__(
52+
self,
53+
category: CohortCategory,
54+
concept: Concept,
55+
value: Value | None = None,
56+
static: bool | None = None,
57+
timing: Timing | None = None,
58+
override_value_required: bool | None = None,
59+
forward_fill: bool = True,
60+
):
61+
super().__init__(
62+
category=category,
63+
concept=concept,
64+
value=value,
65+
static=static,
66+
timing=timing,
67+
override_value_required=override_value_required,
68+
)
5369
self._table = self._cte()
5470

5571
def _cte(self) -> CTE:

execution_engine/omop/criterion/meta.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,19 @@ def __init__(self: object, *args: Any, **kwargs: Any) -> None:
7676
init_sig = inspect.signature(original_init)
7777
__init__.__signature__ = init_sig # type: ignore[attr-defined]
7878

79+
original_repr = namespace.get("__repr__")
80+
81+
if not original_repr:
82+
# Try to find an __init__ in the bases
83+
for base in reversed(bases):
84+
if base.__repr__ is not object.__repr__:
85+
original_repr = base.__repr__
86+
break
87+
7988
# If no user-defined __repr__, attach a default
80-
if "__repr__" not in namespace:
89+
if not original_repr or getattr(
90+
original_repr, "__signature_repr_generated__", False
91+
):
8192

8293
def __repr__(self: object) -> str:
8394
if not hasattr(self, "_init_args"):
@@ -99,6 +110,9 @@ def __repr__(self: object) -> str:
99110
)
100111
return f"{name}({', '.join(parts)})"
101112

113+
# Tag it so children know it's auto-generated
114+
__repr__.__signature_repr_generated__ = True # type: ignore[attr-defined]
115+
102116
setattr(cls, "__repr__", __repr__)
103117

104118
return cls

0 commit comments

Comments
 (0)