Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@

* `frequenz.client.common.metrics.Bounds.__str__` now renders as `[lower,upper]` (no space after the comma) to match the compact format used by `Lifetime` and to compose cleanly with the `<invalid:...>` marker on `InvalidBounds`.

* Several `__str__` representations were standardized around the `<invalid:VALUE>` marker, so a `grep '<invalid:'` over logs finds every invariant violation regardless of which type produced it:

* `frequenz.client.common.metrics.MetricConnection.__str__` renders as `{name}:{category}` (with `name` possibly empty); known categories render as their member name, the unspecified category renders as `cat=<invalid:0>`, and unknown non-zero categories render as `cat=<int>`.
* `frequenz.client.common.metrics.MetricSample` gained a compact `__str__` (`metric=value`, plus `@connection` when a connection is set) instead of falling back to the dataclass `repr`.
* `UnrecognizedElectricalComponent`, `MismatchedCategoryElectricalComponent`, `UnrecognizedBattery`, `UnrecognizedEvCharger` and `UnrecognizedInverter` now expose their raw wire `category` / `type` in `__str__` (e.g. `CID1:comp1:Inverter:type=99`), instead of hiding it behind the class name alone. These values are merely unrecognized (forward-compatible), not invariant violations, so they use a plain `:field=value` detail rather than the `<invalid:...>` marker.

* `frequenz.client.common.metrics.MetricSample.bounds` is now deprecated; use `bounds_set` instead.

The field type changed from `list[Bounds]` to `BoundsSet | InvalidBoundsSet` (see New Features). Reads and construction remain backward compatible: passing the `bounds=` keyword argument still works (it builds a `BoundsSet` and emits a `DeprecationWarning`), and reading `MetricSample.bounds` still returns the valid `Bounds` as a `list` (also emitting a `DeprecationWarning`). The compatibility property returns only the valid, normalized bounds, so it may differ from the raw wire list when bounds overlapped or touched.
Expand Down
2 changes: 1 addition & 1 deletion src/frequenz/client/common/_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def __init__(
(
message
if message is not None
else f"unrecognized enum value {value!r} for attribute {attr_name!r} in {instance}"
else f"unrecognized enum value {value} for attribute {attr_name!r} in {instance}"
),
)

Expand Down
2 changes: 1 addition & 1 deletion src/frequenz/client/common/grid/_delivery_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def __init__(
"""The invalid delivery area instance that caused this error."""

message = (
f"invalid delivery area {delivery_area!r} for attribute {attr_name!r} in {instance}"
f"invalid delivery area {delivery_area} for attribute {attr_name!r} in {instance}"
if message is None
else message
)
Expand Down
4 changes: 2 additions & 2 deletions src/frequenz/client/common/metrics/_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def __init__(
(
message
if message is not None
else f"invalid bounds {bounds!r} for attribute {attr_name!r} in {instance}"
else f"invalid bounds {bounds} for attribute {attr_name!r} in {instance}"
),
)

Expand Down Expand Up @@ -419,6 +419,6 @@ def __init__(
(
message
if message is not None
else f"invalid bounds set {bounds_set!r} for attribute {attr_name!r} in {instance}"
else f"invalid bounds set {bounds_set} for attribute {attr_name!r} in {instance}"
),
)
38 changes: 30 additions & 8 deletions src/frequenz/client/common/metrics/_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,18 @@ class MetricConnection:

def __str__(self) -> str:
"""Return a string representation of this connection."""
category_name = (
str(self.category)
if isinstance(self.category, int)
else f"<CATEGORY={self.category.name}>"
)
if self.name:
return f"{category_name}({self.name})"
return category_name
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
match self.category:
case 0 | MetricConnectionCategory.UNSPECIFIED:
category_name = "cat=<invalid:0>"
case MetricConnectionCategory() as category:
category_name = category.name
case int() as category:
category_name = f"cat={category}"
case unexpected:
assert_never(unexpected)
return f"{self.name}:{category_name}"

def get_category(self) -> MetricConnectionCategory:
"""Return the connection category as a known enum member.
Expand Down Expand Up @@ -286,6 +290,24 @@ def __init__(
object.__setattr__(self, "bounds_set", bounds_set)
object.__setattr__(self, "connection", connection)

def __str__(self) -> str:
"""Return a compact string representation of this sample."""
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
match self.metric:
case 0 | Metric.UNSPECIFIED:
metric = "<invalid:0>"
case Metric() as known:
metric = known.name
case int() as unknown:
metric = str(unknown)
case unexpected:
assert_never(unexpected)
sample = f"{metric}={self.value}"
if self.connection is not None:
sample = f"{sample}@{self.connection}"
return sample

@property
@deprecated("`MetricSample.bounds` is deprecated; use `bounds_set` instead.")
def bounds(self) -> list[Bounds]:
Expand Down
2 changes: 1 addition & 1 deletion src/frequenz/client/common/microgrid/_lifetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,6 @@ def __init__(
(
message
if message is not None
else f"invalid lifetime {lifetime!r} for attribute {attr_name!r} in {instance}"
else f"invalid lifetime {lifetime} for attribute {attr_name!r} in {instance}"
),
)
2 changes: 1 addition & 1 deletion src/frequenz/client/common/microgrid/_microgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def is_active(self) -> bool:
self,
"_active",
value,
f"unrecognized status of microgrid {self}: {value!r}",
f"unrecognized status of microgrid {self}: {value}",
)
case unknown:
assert_never(unknown)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def provides_telemetry(self) -> bool:
self,
"_provides_telemetry",
value,
f"operational mode {value!r} of {self} is not a recognized "
f"operational mode {value} of {self} is not a recognized "
"ElectricalComponentOperationalMode; telemetry availability "
"is unknown",
)
Expand Down Expand Up @@ -195,7 +195,7 @@ def accepts_control(self) -> bool:
self,
"_accepts_control",
value,
f"operational mode {value!r} of {self} is not a recognized "
f"operational mode {value} of {self} is not a recognized "
"ElectricalComponentOperationalMode; control availability "
"is unknown",
)
Expand Down Expand Up @@ -257,7 +257,7 @@ def get_metric_config_bounds(
self,
"metric_config_bounds",
invalid,
f"invalid bounds {invalid!r} for metric {metric} in {self}",
f"invalid bounds {invalid} for metric {metric} in {self}",
)
case Bounds() as valid:
return valid
Expand Down
4 changes: 2 additions & 2 deletions src/frequenz/client/common/types/_location.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(
(
message
if message is not None
else f"invalid latitude {value!r} for attribute {attr_name!r} in "
else f"invalid latitude {value} for attribute {attr_name!r} in "
f"{instance}; must be in [-90, 90]"
),
)
Expand Down Expand Up @@ -84,7 +84,7 @@ def __init__(
(
message
if message is not None
else f"invalid longitude {value!r} for attribute {attr_name!r} in "
else f"invalid longitude {value} for attribute {attr_name!r} in "
f"{instance}; must be in [-180, 180]"
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_default_message() -> None:
error = InvalidDeliveryAreaError("some-instance", "delivery_area", invalid)
assert error.delivery_area is invalid
assert (
"invalid delivery area InvalidDeliveryArea(code='', code_type=0) for "
f"invalid delivery area {invalid} for "
"attribute 'delivery_area' in some-instance" == str(error)
)

Expand Down
2 changes: 1 addition & 1 deletion tests/metrics/_bounds/test_invalid_bounds_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test_default_message() -> None:

assert error.bounds is invalid
assert (
str(error) == f"invalid bounds {invalid!r} for attribute 'config_bounds' "
str(error) == f"invalid bounds {invalid} for attribute 'config_bounds' "
"in some-instance"
)

Expand Down
2 changes: 1 addition & 1 deletion tests/metrics/_bounds/test_invalid_bounds_set_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_default_message() -> None:

assert error.bounds_set is invalid
assert (
str(error) == f"invalid bounds set {invalid!r} for attribute 'bounds_set' "
str(error) == f"invalid bounds set {invalid} for attribute 'bounds_set' "
"in some-instance"
)

Expand Down
20 changes: 16 additions & 4 deletions tests/metrics/test_sample_metric_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,39 @@
pytest.param(
MetricConnectionCategory.BATTERY,
"",
"<CATEGORY=BATTERY>",
":BATTERY",
id="enum_category_empty_name",
),
pytest.param(
MetricConnectionCategory.PV,
"dc_pv_0",
"<CATEGORY=PV>(dc_pv_0)",
"dc_pv_0:PV",
id="enum_category_with_name",
),
pytest.param(
999,
"",
"999",
":cat=999",
id="int_category_empty_name",
),
pytest.param(
999,
"unknown_connection",
"999(unknown_connection)",
"unknown_connection:cat=999",
id="int_category_with_name",
),
pytest.param(
0,
"",
":cat=<invalid:0>",
id="unspecified_int_empty_name",
),
pytest.param(
0,
"conn",
"conn:cat=<invalid:0>",
id="unspecified_int_with_name",
),
],
)
def test_str_representation(
Expand Down
61 changes: 61 additions & 0 deletions tests/metrics/test_sample_metric_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
InvalidBoundsSetError,
Metric,
MetricConnection,
MetricConnectionCategory,
MetricSample,
)

Expand Down Expand Up @@ -78,6 +79,66 @@ def test_creation(
assert sample.connection == connection


@pytest.mark.parametrize(
"metric, value, connection, expected",
[
pytest.param(
Metric.AC_POWER_ACTIVE,
5.0,
None,
"AC_POWER_ACTIVE=5.0",
id="known_metric",
),
pytest.param(
Metric.AC_POWER_ACTIVE,
5.0,
MetricConnection(
category=MetricConnectionCategory.BATTERY, name="dc_battery_0"
),
"AC_POWER_ACTIVE=5.0@dc_battery_0:BATTERY",
id="with_connection",
),
pytest.param(
0,
None,
None,
"<invalid:0>=None",
id="unspecified_metric",
),
pytest.param(
99999,
42,
None,
"99999=42",
id="unrecognized_metric",
),
pytest.param(
Metric.AC_POWER_ACTIVE,
AggregatedMetricValue(avg=5.0, min=1.0, max=10.0, raw=[1.0, 5.0, 10.0]),
None,
"AC_POWER_ACTIVE=avg:5.0<min:1.0 max:10.0 num_raw:3>",
id="aggregated_value",
),
],
)
def test_str(
now: datetime,
metric: Metric | int,
value: FloatInt | AggregatedMetricValue | None,
connection: MetricConnection | None,
expected: str,
) -> None:
"""`MetricSample.__str__` renders a compact `metric=value` summary."""
sample = MetricSample(
sample_time=now,
metric=metric,
value=value,
bounds_set=BoundsSet(),
connection=connection,
)
assert str(sample) == expected


@pytest.mark.parametrize(
"value, method_results",
[
Expand Down
2 changes: 1 addition & 1 deletion tests/microgrid/_lifetime/test_invalid_lifetime_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def test_default_message(present: datetime, future: datetime) -> None:
assert error.lifetime is invalid
assert (
str(error)
== f"invalid lifetime {invalid!r} for attribute 'operational_lifetime' "
== f"invalid lifetime {invalid} for attribute 'operational_lifetime' "
"in some-instance"
)

Expand Down