Skip to content

Commit bca3a83

Browse files
authored
Restore ComponentId from v0.3.x (#287)
This type was removed too soon. v0.4.0 was released but not very widely adopted, so this problem was never detected. Removing the old `ComponentId` means no downstream project can have a working version that supports both v0.3.x and v0.4.x series, which complicates a clean non-breaking upgrade path.
2 parents fa45d8e + fcd21dd commit bca3a83

4 files changed

Lines changed: 192 additions & 0 deletions

File tree

‎RELEASE_NOTES.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ There are a few intentional hard breaks too, all listed in the Upgrading section
1818

1919
## Upgrading
2020

21+
* `frequenz.client.common.microgrid.components.ComponentId` is restored as a deprecated compatibility class. It keeps its historical import path and remains a distinct type from `ElectricalComponentId`; importing both classes logs the existing duplicate `CID` prefix warning. This restores only `ComponentId`, not the removed `ComponentCategory`, `ComponentStateCode` or `ComponentErrorCode` symbols. The v0.4.0 release remains incompatible with users of the removed import path. Removal will be coordinated with downstream migration rather than tied automatically to v0.5.0.
22+
2123
* The `frequenz.client.common.microgrid.electrical_components.ElectricalComponentCategory` enum is now deprecated and will be removed in a future release.
2224

2325
Accessing any member of this enum will emit a `DeprecationWarning`. Users are encouraged to switch to the `ElectricalComponent` class hierarchy (using `match` expressions or `isinstance()`) to identify components.

‎docs/wrapping-guide/deprecation-and-compatibility.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ with deprecated_call(
4040
assert thing_from_proto(3) == "3"
4141
```
4242

43+
## Check downstream adoption before removal
44+
45+
When possible, check downstream client releases to see if they import or expose
46+
a deprecated public type in public signatures before removing it. Remove the
47+
type only after those clients have migrated, or keep a compatibility class
48+
until the migration is complete. When a migration is expected to last long,
49+
choose the removal point from downstream adoption and the support policy, not
50+
from a fixed one-minor-release delay.
51+
4352
## Add a new converter when its contract changes
4453

4554
When a conversion function's arguments or return type change in an incompatible
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Provide the deprecated component ID compatibility import."""
5+
6+
from typing import final
7+
8+
from frequenz.core.id import BaseId
9+
from typing_extensions import deprecated
10+
11+
12+
@deprecated(
13+
"frequenz.client.common.microgrid.components.ComponentId is deprecated. "
14+
"Use frequenz.client.common.microgrid.electrical_components."
15+
"ElectricalComponentId instead."
16+
)
17+
@final
18+
class ComponentId(BaseId, str_prefix="CID"):
19+
"""A unique identifier for a microgrid component."""
20+
21+
22+
__all__ = ["ComponentId"]

‎tests/microgrid/test_ids.py‎

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,32 @@
33

44
"""Tests for microgrid-related IDs."""
55

6+
import base64
7+
import importlib
8+
import os
9+
import pickle
10+
import subprocess
11+
import sys
12+
from pathlib import Path
13+
614
import pytest
715
from frequenz.core.id import BaseId
816

917
from frequenz.client.common.microgrid import EnterpriseId, MicrogridId
18+
from frequenz.client.common.microgrid.components import ComponentId
1019
from frequenz.client.common.microgrid.electrical_components import ElectricalComponentId
1120
from frequenz.client.common.microgrid.sensors import SensorId
1221

22+
# Trusted fixture generated from the released v0.3.8 tag (peeled commit
23+
# 66b890388c47412006c6767c6c3fcdd69c187c5d) with:
24+
# uv run --extra dev-pytest python -c 'import base64,pickle; from
25+
# frequenz.client.common.microgrid.components import ComponentId; print(
26+
# base64.b64encode(pickle.dumps(ComponentId(123))).decode())'
27+
_V0_3_8_COMPONENT_ID_PICKLE = base64.b64decode(
28+
"gAWVTgAAAAAAAACMK2ZyZXF1ZW56LmNsaWVudC5jb21tb24ubWljcm9ncmlkLmNvbXBvbmVudHOU"
29+
"jAtDb21wb25lbnRJZJSTlCmBlH2UjANfaWSUS3tzYi4="
30+
)
31+
1332

1433
@pytest.mark.parametrize(
1534
"id_class, prefix",
@@ -26,3 +45,143 @@ def test_string_representation(id_class: type[BaseId], prefix: str) -> None:
2645

2746
assert str(_id) == f"{prefix}123"
2847
assert repr(_id) == f"{id_class.__name__}(123)"
48+
49+
50+
def test_component_id_is_a_distinct_deprecated_type() -> None:
51+
"""Test the restored component ID type remains distinct from its replacement."""
52+
with pytest.deprecated_call():
53+
component_id = ComponentId(123)
54+
electrical_component_id = ElectricalComponentId(123)
55+
with pytest.deprecated_call():
56+
same_component_id = ComponentId(123)
57+
58+
assert component_id != electrical_component_id
59+
assert not isinstance(component_id, ElectricalComponentId)
60+
assert not isinstance(electrical_component_id, ComponentId)
61+
assert {component_id, electrical_component_id} == {
62+
same_component_id,
63+
ElectricalComponentId(123),
64+
}
65+
assert {component_id: "old", electrical_component_id: "new"} == {
66+
same_component_id: "old",
67+
ElectricalComponentId(123): "new",
68+
}
69+
70+
71+
def test_component_id_equality_and_validation() -> None:
72+
"""Test equality, hashing and validation for component IDs."""
73+
with pytest.deprecated_call():
74+
first = ComponentId(123)
75+
with pytest.deprecated_call():
76+
second = ComponentId(123)
77+
78+
assert first == second
79+
assert hash(first) == hash(second)
80+
assert int(first) == 123
81+
assert str(first) == "CID123"
82+
assert repr(first) == "ComponentId(123)"
83+
84+
with pytest.deprecated_call():
85+
with pytest.raises(ValueError, match="ComponentId can't be negative"):
86+
ComponentId(-1)
87+
88+
89+
@pytest.mark.parametrize(
90+
"imports",
91+
[
92+
(
93+
"from frequenz.client.common.microgrid.components import ComponentId\n"
94+
"from frequenz.client.common.microgrid.electrical_components "
95+
"import ElectricalComponentId"
96+
),
97+
(
98+
"from frequenz.client.common.microgrid.electrical_components "
99+
"import ElectricalComponentId\n"
100+
"from frequenz.client.common.microgrid.components import ComponentId"
101+
),
102+
],
103+
)
104+
def test_component_id_import_orders_keep_current_prefix_warning(imports: str) -> None:
105+
"""Test both import orders and acknowledge the current duplicate-prefix warning."""
106+
source_root = Path(__file__).resolve().parents[2] / "src"
107+
script = f"""
108+
import logging
109+
logging.basicConfig(level=logging.WARNING)
110+
{imports}
111+
assert ComponentId is not ElectricalComponentId
112+
"""
113+
env = os.environ.copy()
114+
env["PYTHONPATH"] = os.pathsep.join(
115+
path for path in (str(source_root), env.get("PYTHONPATH")) if path
116+
)
117+
result = subprocess.run(
118+
[sys.executable, "-c", script],
119+
check=False,
120+
capture_output=True,
121+
text=True,
122+
env=env,
123+
)
124+
125+
assert result.returncode == 0, result.stderr
126+
assert "Prefix 'CID' is already registered" in result.stderr
127+
128+
129+
def test_component_id_does_not_restore_legacy_enums() -> None:
130+
"""Test that only the component ID compatibility symbol is restored."""
131+
module = importlib.import_module("frequenz.client.common.microgrid.components")
132+
133+
assert not hasattr(module, "ComponentCategory")
134+
assert not hasattr(module, "ComponentStateCode")
135+
assert not hasattr(module, "ComponentErrorCode")
136+
137+
138+
def test_component_id_loads_an_old_path_pickle() -> None:
139+
"""Test loading a trusted pickle carrying the historical import path."""
140+
source_root = Path(__file__).resolve().parents[2] / "src"
141+
script = """
142+
import base64
143+
import pickle
144+
from frequenz.client.common.microgrid.components import ComponentId
145+
146+
print(base64.b64encode(pickle.dumps(ComponentId(123))).decode())
147+
"""
148+
env = os.environ.copy()
149+
env["PYTHONPATH"] = os.pathsep.join(
150+
path for path in (str(source_root), env.get("PYTHONPATH")) if path
151+
)
152+
result = subprocess.run(
153+
[sys.executable, "-c", script],
154+
check=False,
155+
capture_output=True,
156+
text=True,
157+
env=env,
158+
)
159+
160+
assert result.returncode == 0, result.stderr
161+
payload = base64.b64decode(result.stdout.strip())
162+
assert b"frequenz.client.common.microgrid.components" in payload
163+
assert b"ComponentId" in payload
164+
165+
with pytest.deprecated_call():
166+
loaded = pickle.loads(payload)
167+
# Exact type identity is part of the restored compatibility behavior.
168+
# pylint: disable-next=unidiomatic-typecheck
169+
assert type(loaded) is ComponentId
170+
with pytest.deprecated_call():
171+
assert loaded == ComponentId(123)
172+
173+
174+
def test_component_id_loads_a_v0_3_8_pickle() -> None:
175+
"""Test loading a pickle generated by the released v0.3.8 class."""
176+
payload = _V0_3_8_COMPONENT_ID_PICKLE
177+
178+
assert b"frequenz.client.common.microgrid.components" in payload
179+
assert b"ComponentId" in payload
180+
with pytest.deprecated_call():
181+
loaded = pickle.loads(payload)
182+
183+
# Exact type identity is part of the restored compatibility behavior.
184+
# pylint: disable-next=unidiomatic-typecheck
185+
assert type(loaded) is ComponentId
186+
with pytest.deprecated_call():
187+
assert loaded == ComponentId(123)

0 commit comments

Comments
 (0)