diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 35ea0f5e..11c0385f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -122,9 +122,9 @@ There are a few intentional hard breaks too, all listed in the Upgrading section * `frequenz.client.common.proto.datetime_from_proto` is now deprecated; use `datetime_from_proto2` instead. -* `frequenz.client.common.metrics.MetricSample.sample_time` is now a deprecated read-only property; use the new `sample_time2` field instead. +* `frequenz.client.common.metrics.MetricSample.sample_time` is now a deprecated read-only property; use the new `get_sample_time()` method instead, or the new `sample_time2` field to also see malformed timestamps. - The field became `datetime | InvalidDatetime`, which the released `datetime` annotation cannot express, so it was renamed. Reading `sample_time` still returns a `datetime` and now emits a `DeprecationWarning`; for a malformed wire timestamp it raises `InvalidDatetimeError` (a `ValueError`) rather than returning a repaired value. `get_sample_time()` does the same without the warning. + The field became `datetime | InvalidDatetime`, which the released `datetime` annotation cannot express, so it was renamed. Reading `sample_time` still returns a `datetime` and now emits a `DeprecationWarning`; for a malformed wire timestamp it raises `InvalidDatetimeError` (a `ValueError`) rather than returning a repaired value. `get_sample_time()` does the same without the warning, which is why the warning recommends it. Constructing with `sample_time=` is **not** deprecated and keeps working: it accepts a well-formed `datetime` today and will accept the wider type once `sample_time2` is renamed back to `sample_time`. Use `sample_time2=` to build a sample from a malformed wire timestamp. @@ -248,3 +248,4 @@ There are a few intentional hard breaks too, all listed in the Upgrading section * Fixed `EnumParityTest` so protobuf values whose Python member name exists with a different number fail parity checks instead of being treated as unmirrored protobuf values. * Fixed potential unexpected exceptions due to type-checking accepting `int` for code annotated to only accept `float`. Fixes #250. * Exception messages reporting an invalid value now use its `str()` instead of its `repr()`, so they show the compact `` rendering instead of a verbose dataclass dump. +* Fixed warnings being shown over and over instead of once. The library silenced its own internal deprecation warnings with `warnings.catch_warnings()`, which resets the warnings deduplication history of the whole program every time it is used ([python/cpython#73858](https://github.com/python/cpython/issues/73858)), so every warning already shown, from this library or any other code, was shown again on each call to a converter, accessor or `str()`. It now uses `frequenz.core.warnings.ignoring_deprecations()`, which leaves that history alone. diff --git a/docs/_css/mkdocstrings.css b/docs/_css/mkdocstrings.css index 572abff1..851214f2 100644 --- a/docs/_css/mkdocstrings.css +++ b/docs/_css/mkdocstrings.css @@ -42,3 +42,25 @@ a.autorefs-external::after { a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); } + +/* A "Deprecated" admonition, styled like a warning but with its own icon. */ +:root { + --md-admonition-icon--deprecated: url('data:image/svg+xml;charset=utf-8,'); +} + +.md-typeset .admonition.deprecated, +.md-typeset details.deprecated { + border-color: #cc9900; +} + +.md-typeset .deprecated > .admonition-title, +.md-typeset .deprecated > summary { + background-color: #cc99001a; +} + +.md-typeset .deprecated > .admonition-title::before, +.md-typeset .deprecated > summary::before { + background-color: #cc9900; + -webkit-mask-image: var(--md-admonition-icon--deprecated); + mask-image: var(--md-admonition-icon--deprecated); +} diff --git a/docs/wrapping-guide/deprecation-and-compatibility.md b/docs/wrapping-guide/deprecation-and-compatibility.md index 4009b81a..af39c8f5 100644 --- a/docs/wrapping-guide/deprecation-and-compatibility.md +++ b/docs/wrapping-guide/deprecation-and-compatibility.md @@ -9,9 +9,20 @@ define the wider 0.x versioning process. Mark the old public symbol with [`typing_extensions.deprecated`][typing_extensions.deprecated]. Use this exact -message form: `" is deprecated. Use instead."`. Write both -fully qualified names exactly. In the old API documentation, explain any change -to the return type or behavior. A caller should know what to use from the +message form: `" is deprecated since v. Use [][] +instead."`. Write both fully qualified names exactly, the old one plain and the +replacement as a bare cross-reference so the rendered `Deprecated:` admonition +links to it. The version belongs in the sentence: a separate "since" line +cannot be expressed through the decorator, so the two would drift apart. + +The same string is printed as a runtime warning, so keep it to a sentence or +two and build it by concatenating single-line strings. A triple-quoted message +keeps its indentation, which stops the cross-reference from resolving and +prints an indented warning in the terminal. Do not put the names in backticks +either: they buy code font in the documentation at the cost of noise in the +console, where the reader cannot skip over them. Anything beyond "use X +instead", such as a change to the return type or behavior, goes in the +docstring body as prose. A caller should still know what to use from the warning alone. This example gives the replacement conversion function a numeric-suffixed name @@ -27,19 +38,39 @@ def thing_from_proto2(value: int) -> str: @deprecated( - "example.thing_from_proto is deprecated. Use example.thing_from_proto2 instead." + "example.thing_from_proto is deprecated since v0.4.1. " + "Use [example.thing_from_proto2][] instead." ) def thing_from_proto(value: int) -> str: return thing_from_proto2(value) with deprecated_call( - match="example.thing_from_proto is deprecated. " - "Use example.thing_from_proto2 instead." + match=r"^example\.thing_from_proto is deprecated since v0\.4\.1\. " + r"Use \[example\.thing_from_proto2\]\[\] instead\.$" ): assert thing_from_proto(3) == "3" ``` +`match` is a regular expression, so the brackets of the cross-reference have to +be escaped there, as do the dots of the qualified names. + +The documentation build turns the decorator's message into a `Deprecated:` +admonition at the top of the symbol's docstring, and adds a `deprecated` label +next to its name. It reads the message from the source without running it, so +write it as string literals in the decorator call: a message held in a +constant, built by an f-string or returned by a helper function renders no +admonition at all. + +Where no admonition can be generated, write the notice as a `Deprecated:` +admonition in the docstring instead. That is the case for a single argument, +construction that is being made stricter, a property (the decorator works at +runtime, but the documentation build ignores it there), and a message that is +not a literal. Put it immediately after the summary line and give it no custom +title (a title replaces the word "Deprecated" in the rendered output), and +again state the version in the text. Never hand-write one for a symbol that +already gets a generated one, or the page shows the same notice twice. + ## Check downstream adoption before removal When possible, check downstream client releases to see if they import or expose @@ -65,8 +96,115 @@ unsuffixed name while retaining a deprecated alias for the suffixed name. For an enum-member change, use [`deprecated_member`][frequenz.core.enum.deprecated_member]. It keeps the old -member temporarily and warns when code uses it. Document the representation new -code should use. +member temporarily and warns when code uses it. Its message follows the same +form as the decorator's and gets the same generated admonition and label, as +long as it is written as string literals in the call. Document the +representation new code should use. + +A renamed member keeps its old name as a deprecated alias of the same value, +which [`unique`][frequenz.core.enum.unique] allows: + +```python +from frequenz.core.enum import Enum, deprecated_member, unique + + +@unique +class Mode(Enum): + """Modes a thing can run in.""" + + NEW_NAME = 1 + """The thing runs normally.""" + + OLD_NAME = deprecated_member( + 1, + "example.Mode.OLD_NAME is deprecated since v0.5.0. " + "Use example.Mode.NEW_NAME instead.", + ) + """Old name of `NEW_NAME`.""" +``` + +## Keep a moved symbol importable + +When a public symbol moves to another module, keep its old import path working +with `frequenz.core.warnings.deprecated_aliases()` instead of writing a module +`__getattr__` by hand. The alias is the very same object, so +[`isinstance()`][isinstance] keeps working through both paths, and the +documentation build generates the admonition and label for every alias, as +long as it is a literal in the call. + +```python +from typing import TYPE_CHECKING, TypeAlias + +from frequenz.core.warnings import DeprecatedAlias, deprecated_aliases + +if TYPE_CHECKING: + from example.new import Thing as _Thing + + Thing: TypeAlias = _Thing + """A thing, now living in `example.new`.""" +else: + __getattr__ = deprecated_aliases( + __name__, + DeprecatedAlias("Thing", new_module="example.new", since="v0.5.0"), + ) +``` + +Keep that structure exactly: without the `else:`, type checkers see the +`__getattr__` and treat every name in the module as `Any`. When the symbol was +renamed too, give its new name as `new_name`; without `new_module`, the alias +points at a renamed symbol in its own module. Each alias gives +its own `since`, the version it is deprecated in, so aliases deprecated in +different releases can each say theirs; the warning and the generated +admonition both read `{old} is deprecated since {since}. Use {new} instead.` +When that standard wording is not enough, give `message` instead of `since`, +a full template taking only `{old}` and `{new}`, the two fully qualified +names; the documentation turns `{new}` into a link there too, so leave out +the cross-reference brackets. + +An alias only fits when the old name can be the same object as the new one. +When the old type has to stay distinct, as +[`ComponentId`][frequenz.client.common.microgrid.components.ComponentId] does +next to +[`ElectricalComponentId`][frequenz.client.common.microgrid.electrical_components.ElectricalComponentId], +keep a deprecated class instead. + +## Silence only the deprecations you raise yourself + +Sometimes library code has to touch a symbol it deprecated itself, such as a +deprecated converter that still has to build the deprecated type it returns. +The caller already gets the converter's own warning, so a second one from +inside it is noise. Silence it with +`frequenz.core.warnings.ignoring_deprecations()`, around the statement that +raises it and nothing more, so deprecations from anywhere else still get +through: + +```python +from frequenz.core.warnings import ignoring_deprecations +from typing_extensions import deprecated + +from example import OldThing, ThingProto + + +@deprecated( + "example.old_thing_from_proto is deprecated since v0.5.0. " + "Use example.thing_from_proto instead." +) +def old_thing_from_proto(message: ThingProto) -> OldThing: + """Convert a protobuf message to the deprecated `OldThing`.""" + with ignoring_deprecations(): + return OldThing(value=message.value) +``` + +The same applies to code that is not deprecated itself but still has to accept +or build a deprecated symbol for compatibility: the user is warned where they +use the deprecated symbol, not by the library's internals. + +Do not use [`warnings.catch_warnings()`][warnings.catch_warnings] for this. +Entering and leaving it resets the warnings deduplication history of the whole +program ([python/cpython#73858](https://github.com/python/cpython/issues/73858)), +so every warning that was already shown, by this library or any other code, is +shown again after each call. In an application converting data in a loop, that +turns a handful of warnings into tens of thousands. ## Tighten invariants in stages @@ -85,11 +223,26 @@ can test the stricter behavior before it becomes required and migrate on purpose Test every public deprecation with [`pytest.deprecated_call()`][pytest.deprecated_call]. Check the exact message -and the replacement behavior. If deprecated code correctly calls another -deprecated symbol, suppress only that expected inner -[`DeprecationWarning`][] -in a small [`warnings.catch_warnings()`][warnings.catch_warnings] block. The -outer API must still emit its one public warning. +and the replacement behavior. The outer API must still emit its one public +warning, even when it silences inner ones as described above. + +Check that the replacement doesn't go through anything deprecated with +`frequenz.core.warnings.asserting_no_deprecations()`, rather than with an +`"error"` filter. The filter turns the warning into an exception inside the +code under test, where a broad `except` can swallow it; the helper records the +warnings instead and fails when the block ends, listing each one and where it +came from: + +```python +from frequenz.core.warnings import asserting_no_deprecations + +from example import thing_from_proto2 + + +def test_thing_from_proto2_does_not_warn() -> None: + with asserting_no_deprecations(): + assert thing_from_proto2(3) == "3" +``` Add `RELEASE_NOTES.md` migration bullets that state the old behavior, the replacement, what changes, and the planned removal version. Remove the diff --git a/docs/wrapping-guide/index.md b/docs/wrapping-guide/index.md index a196faeb..4ad4aa8b 100644 --- a/docs/wrapping-guide/index.md +++ b/docs/wrapping-guide/index.md @@ -30,7 +30,7 @@ wrappers. `*_from_proto` and `*_to_proto` functions that translate protobuf messages to wrapper types. - [Deprecation and compatibility](deprecation-and-compatibility.md) — Describes - how to replace public functions and tighten validation without surprising - callers. + how to replace or move public symbols and tighten validation without + surprising callers, and how to keep the library's own deprecations quiet. - [Testing](testing.md) — Shows how to place tests, check enum parity, and make documentation examples and warnings part of the test suite. diff --git a/docs/wrapping-guide/testing.md b/docs/wrapping-guide/testing.md index d186c291..df7658b0 100644 --- a/docs/wrapping-guide/testing.md +++ b/docs/wrapping-guide/testing.md @@ -43,6 +43,10 @@ Assert each expected deprecation with [`pytest.deprecated_call()`][pytest.deprecated_call]. This records the public warning and stops an unrelated warning from being hidden. When deprecated code correctly calls another deprecated symbol, suppress only that inner -`DeprecationWarning` in a small warning block to avoid duplicate messages. In a -test, use the same small suppression only for a warning that a dedicated -assertion already checks. Never suppress warnings globally. +`DeprecationWarning` in a small `frequenz.core.warnings.ignoring_deprecations()` +block to avoid duplicate messages. In a test, use the same small suppression +only for a warning that a dedicated assertion already checks, and check that a +replacement doesn't warn with `frequenz.core.warnings.asserting_no_deprecations()`. +Never suppress warnings globally, and never with +[`warnings.catch_warnings()`][warnings.catch_warnings]; [Deprecation and +compatibility](deprecation-and-compatibility.md) explains both helpers and why. diff --git a/mkdocs.yml b/mkdocs.yml index 73a4c8d5..c059a454 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -105,6 +105,13 @@ plugins: python: paths: ["src"] options: + extensions: + - griffe_warnings_deprecated: + kind: deprecated + title: Deprecated + - griffe_frequenz_core.deprecations: + kind: deprecated + title: Deprecated docstring_section_style: spacy inherited_members: true merge_init_into_class: false diff --git a/pyproject.toml b/pyproject.toml index 6d94ac2b..70f1fe61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ requires-python = ">= 3.11, < 4" dependencies = [ "typing-extensions >= 4.13.0, < 5", "frequenz-api-common >= 0.8.4, < 1", - "frequenz-core >= 1.4.0, < 2", + "frequenz-core >= 1.5.0, < 2", "protobuf >= 6.33.6, < 8", ] dynamic = ["version"] @@ -50,6 +50,8 @@ dev-formatting = ["black == 26.5.1", "isort == 9.0.1"] dev-mkdocs = [ "Markdown == 3.11", "black == 26.5.1", + "griffe-frequenz-core == 1.0.0", + "griffe-warnings-deprecated == 1.1.1", "mike == 2.2.0", "mkdocs-gen-files == 0.6.1", "mkdocs-literate-nav == 0.6.3", diff --git a/src/frequenz/client/common/grid/_delivery_area.py b/src/frequenz/client/common/grid/_delivery_area.py index c51ba40e..820b3619 100644 --- a/src/frequenz/client/common/grid/_delivery_area.py +++ b/src/frequenz/client/common/grid/_delivery_area.py @@ -8,6 +8,7 @@ from typing import Any, Self, assert_never from frequenz.core.enum import Enum, deprecated_member, unique +from frequenz.core.warnings import ignoring_deprecations from .._exception import ( InvalidAttributeError, @@ -46,8 +47,9 @@ class EnergyMarketCodeType(Enum): UNSPECIFIED = deprecated_member( 0, - "EnergyMarketCodeType.UNSPECIFIED is deprecated; use the `int` value `0` " - "instead if you really need to check for this low-level value.", + "frequenz.client.common.grid.EnergyMarketCodeType.UNSPECIFIED is " + "deprecated since v0.4.1. Use the int value 0 instead if you really " + "need to check for this low-level value.", ) """Unspecified type. This value is a placeholder and should not be used.""" @@ -72,11 +74,11 @@ class BaseDeliveryArea: code: str | None """The code representing the unique identifier for the delivery area. - Warning: Using `None` is deprecated - This field is required for a well-formed `DeliveryArea`, so we are - making this more explicit by deprecating the use of `None` here. In the - future, `| None` will be removed so passing `None` will fail type - checking. + Deprecated: + Passing `None` is deprecated since v0.4.1. This field is required for a + well-formed `DeliveryArea`, so we are making this more explicit by + deprecating the use of `None` here. In the future, `| None` will be + removed so passing `None` will fail type checking. """ code_type: EnergyMarketCodeType | int @@ -122,11 +124,11 @@ class DeliveryArea(BaseDeliveryArea): location. Delivery areas can have different codes based on the jurisdiction in which they operate. - Warning: Construction of invalid instances is deprecated - A well-formed `DeliveryArea` carries a non-empty [`code`][.code] and a - specified [`code_type`][.code_type]. Constructing one with data that - violates this invariant is **deprecated**, and will raise a - [`ValueError`][] in a future release. + Deprecated: + Constructing a `DeliveryArea` with invalid data is deprecated since + v0.4.1, and will raise a [`ValueError`][] in a future release. The type + itself is not deprecated. A well-formed `DeliveryArea` carries a + non-empty [`code`][.code] and a specified [`code_type`][.code_type]. You can temporarily use the `_raise_on_invalid` keyword argument to get the upcoming behavior now (raising instead of deprecation warning). @@ -161,8 +163,7 @@ def __post_init__(self, _raise_on_invalid: bool) -> None: DeprecationWarning, stacklevel=3, ) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): unspecified_code_type = EnergyMarketCodeType.UNSPECIFIED if self.code_type in (0, unspecified_code_type): if _raise_on_invalid: @@ -200,8 +201,7 @@ def get_code_type(self) -> EnergyMarketCodeType: available on the exception's `value` attribute. """ # Suppressing the deprecation warning can be removed when UNSPECIFIED is removed - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): match self.code_type: case 0 | EnergyMarketCodeType.UNSPECIFIED: raise UnspecifiedEnumValueError(self, "code_type") @@ -228,8 +228,7 @@ def __str__(self) -> str: """Return a human-readable string representation of this instance.""" # Suppressing the deprecation warning can be removed when UNSPECIFIED # is removed - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): match self.code_type: case 0 | EnergyMarketCodeType.UNSPECIFIED: code_type = "type=" diff --git a/src/frequenz/client/common/grid/proto/v1alpha8/_delivery_area.py b/src/frequenz/client/common/grid/proto/v1alpha8/_delivery_area.py index 131e591b..d2d8ec94 100644 --- a/src/frequenz/client/common/grid/proto/v1alpha8/_delivery_area.py +++ b/src/frequenz/client/common/grid/proto/v1alpha8/_delivery_area.py @@ -4,9 +4,9 @@ """Conversion of DeliveryArea and EnergyMarketCodeType to/from protobuf v1alpha8.""" import logging -import warnings from frequenz.api.common.v1alpha8.grid import delivery_area_pb2 +from frequenz.core.warnings import ignoring_deprecations from typing_extensions import deprecated from ....proto import enum_from_proto @@ -45,21 +45,20 @@ def energy_market_code_type_to_proto( @deprecated( - "`delivery_area_from_proto` is deprecated; use " - "`delivery_area_from_proto2` (returns " - "`DeliveryArea | InvalidDeliveryArea`) instead." + "frequenz.client.common.grid.proto.v1alpha8.delivery_area_from_proto is " + "deprecated since v0.4.1. Use " + "[frequenz.client.common.grid.proto.v1alpha8.delivery_area_from_proto2][] " + "instead." ) def delivery_area_from_proto( # noqa: DOC502 message: delivery_area_pb2.DeliveryArea, ) -> DeliveryArea: """Convert a protobuf message to a [`DeliveryArea`][....DeliveryArea] object. - Warning: Deprecated - Use [`delivery_area_from_proto2`][..delivery_area_from_proto2] - instead. The new converter distinguishes well-formed from - malformed data at the type level - (`DeliveryArea | InvalidDeliveryArea`) rather than silently - constructing a `DeliveryArea` with invalid content. + [`delivery_area_from_proto2`][..delivery_area_from_proto2] distinguishes + well-formed from malformed data at the type level + (`DeliveryArea | InvalidDeliveryArea`) rather than silently constructing a + `DeliveryArea` with invalid content. Args: message: The protobuf message to convert. @@ -95,8 +94,7 @@ def delivery_area_from_proto( # noqa: DOC502 # invalid data. This function is `@deprecated` itself, callers will see the # outer notice pointing to `delivery_area_from_proto2`. Suppress the inner # warning here so we don't double-warn. - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): return DeliveryArea(code=code, code_type=code_type) diff --git a/src/frequenz/client/common/metrics/_metric.py b/src/frequenz/client/common/metrics/_metric.py index 587684cd..e121afa0 100644 --- a/src/frequenz/client/common/metrics/_metric.py +++ b/src/frequenz/client/common/metrics/_metric.py @@ -41,8 +41,9 @@ class Metric(Enum): UNSPECIFIED = deprecated_member( 0, - "Metric.UNSPECIFIED is deprecated; use the `int` value `0` " - "instead if you really need to check for this low-level value.", + "frequenz.client.common.metrics.Metric.UNSPECIFIED is deprecated since " + "v0.4.1. Use the int value 0 instead if you really need to check for " + "this low-level value.", ) """The metric is unspecified (this should not be used).""" diff --git a/src/frequenz/client/common/metrics/_sample.py b/src/frequenz/client/common/metrics/_sample.py index ea80c836..1ead06df 100644 --- a/src/frequenz/client/common/metrics/_sample.py +++ b/src/frequenz/client/common/metrics/_sample.py @@ -11,6 +11,7 @@ from frequenz.core.enum import Enum, deprecated_member, unique from frequenz.core.typing import FloatInt +from frequenz.core.warnings import ignoring_deprecations from typing_extensions import deprecated from .._datetime import InvalidDatetime, InvalidDatetimeError @@ -78,8 +79,9 @@ class MetricConnectionCategory(Enum): UNSPECIFIED = deprecated_member( 0, - "MetricConnectionCategory.UNSPECIFIED is deprecated; use the `int` value `0` " - "instead if you really need to check for this low-level value.", + "frequenz.client.common.metrics.MetricConnectionCategory.UNSPECIFIED " + "is deprecated since v0.4.1. Use the int value 0 instead if you really " + "need to check for this low-level value.", ) """The connection category was not specified (do not use).""" @@ -129,8 +131,7 @@ class MetricConnection: def __str__(self) -> str: """Return a string representation of this connection.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): match self.category: case 0 | MetricConnectionCategory.UNSPECIFIED: category_name = "cat=" @@ -160,8 +161,7 @@ def get_category(self) -> MetricConnectionCategory: client does not recognize. The raw value is available on the error's `value` attribute. """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): match self.category: case 0 | MetricConnectionCategory.UNSPECIFIED: raise UnspecifiedEnumValueError(self, "category") @@ -273,6 +273,10 @@ def __init__( # noqa: DOC502 ) -> None: """Initialize this metric sample. + Deprecated: + The `bounds` argument is deprecated since v0.4.1. Use `bounds_set` + instead. + Args: sample_time: The moment when the metric was sampled, as a well-formed [`datetime`][datetime.datetime]. This spelling @@ -364,8 +368,7 @@ def _resolve_bounds_set( def __str__(self) -> str: """Return a compact string representation of this sample.""" - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): match self.metric: case 0 | Metric.UNSPECIFIED: metric = "" @@ -381,16 +384,23 @@ def __str__(self) -> str: return sample @property - @deprecated("`MetricSample.sample_time` is deprecated; use `sample_time2` instead.") + @deprecated( + "frequenz.client.common.metrics.MetricSample.sample_time is deprecated " + "since v0.4.1. Use " + "[frequenz.client.common.metrics.MetricSample.get_sample_time][] instead." + ) def sample_time(self) -> datetime: # noqa: DOC502 """The moment when the metric was sampled. - Warning: Deprecated - Use [`sample_time2`][..sample_time2] instead, or - [`get_sample_time()`][..get_sample_time] when a valid - [`datetime`][datetime.datetime] is required. This property keeps - the released `datetime` type, so it cannot express a malformed wire - timestamp and raises for one instead. + Deprecated: + This property is deprecated since v0.4.1. Use + [`get_sample_time()`][..get_sample_time] instead. + + This property keeps the released `datetime` type, so it cannot express + a malformed wire timestamp and raises for one instead, exactly like + [`get_sample_time()`][..get_sample_time]. Read + [`sample_time2`][..sample_time2] to get a malformed timestamp as an + [`InvalidDatetime`][....InvalidDatetime] instead. Returns: The sample time, when it is a valid @@ -404,16 +414,22 @@ def sample_time(self) -> datetime: # noqa: DOC502 return self.get_sample_time() @property - @deprecated("`MetricSample.bounds` is deprecated; use `bounds_set` instead.") + @deprecated( + "frequenz.client.common.metrics.MetricSample.bounds is deprecated " + "since v0.4.1. Use " + "[frequenz.client.common.metrics.MetricSample.bounds_set][] instead." + ) def bounds(self) -> list[Bounds]: """The valid bounds that apply to the metric sample. - Warning: Deprecated - Use `bounds_set` instead. For backward compatibility this returns - only the valid [`Bounds`][...Bounds] from `bounds_set` (dropping any - malformed entries, as the old field did), but it returns the - normalized, merged bounds rather than the raw list received on the - wire. + Deprecated: + This property is deprecated since v0.4.1. Use + [`bounds_set`][..bounds_set] instead. + + For backward compatibility this returns only the valid + [`Bounds`][...Bounds] from `bounds_set` (dropping any malformed entries, + as the old field did), but it returns the normalized, merged bounds + rather than the raw list received on the wire. Returns: The valid bounds in `bounds_set`. @@ -499,8 +515,7 @@ def get_metric(self) -> Metric: does not recognize. The raw value is available on the error's `value` attribute. """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): match self.metric: case 0 | Metric.UNSPECIFIED: raise UnspecifiedEnumValueError(self, "metric") diff --git a/src/frequenz/client/common/metrics/proto/v1alpha8/_bounds.py b/src/frequenz/client/common/metrics/proto/v1alpha8/_bounds.py index 33368eb4..2181e782 100644 --- a/src/frequenz/client/common/metrics/proto/v1alpha8/_bounds.py +++ b/src/frequenz/client/common/metrics/proto/v1alpha8/_bounds.py @@ -13,17 +13,17 @@ @deprecated( - "`bounds_from_proto` is deprecated; use " - "`bounds_from_proto2` (returns `Bounds | InvalidBounds`) instead." + "frequenz.client.common.metrics.proto.v1alpha8.bounds_from_proto is " + "deprecated since v0.4.1. Use " + "[frequenz.client.common.metrics.proto.v1alpha8.bounds_from_proto2][] " + "instead." ) def bounds_from_proto(message: bounds_pb2.Bounds) -> Bounds: # noqa: DOC502 """Create a [`Bounds`][....Bounds] object from a protobuf message. - Warning: Deprecated - Use [`bounds_from_proto2`][..bounds_from_proto2] instead. The new - converter distinguishes well-formed from malformed data at the - type level (`Bounds | InvalidBounds`) rather than raising a - `ValueError` when the invariant fires. + [`bounds_from_proto2`][..bounds_from_proto2] distinguishes well-formed from + malformed data at the type level (`Bounds | InvalidBounds`) rather than + raising a `ValueError` when the invariant fires. Args: message: The protobuf message to convert. @@ -101,9 +101,9 @@ def bounds_set_from_proto( @deprecated( - "`bounds_from_proto_with_issues` is deprecated; use " - "`bounds_from_proto2` (returns `Bounds | InvalidBounds`) and inspect " - "the returned type instead." + "frequenz.client.common.metrics.proto.v1alpha8.bounds_from_proto_with_issues " + "is deprecated since v0.4.1. Use " + "[frequenz.client.common.metrics.proto.v1alpha8.bounds_from_proto2][] instead." ) def bounds_from_proto_with_issues( message: bounds_pb2.Bounds, @@ -113,12 +113,9 @@ def bounds_from_proto_with_issues( ) -> Bounds | None: # noqa: DOC502 """Create a [`Bounds`][....Bounds] object from a protobuf message, collecting issues. - Warning: Deprecated - Use [`bounds_from_proto2`][..bounds_from_proto2] instead and - inspect the returned type. The new converter distinguishes - well-formed from malformed data at the type level - (`Bounds | InvalidBounds`) rather than routing invalid data - through a side-channel string list. + [`bounds_from_proto2`][..bounds_from_proto2] distinguishes well-formed from + malformed data at the type level (`Bounds | InvalidBounds`) rather than + routing invalid data through a side-channel string list. Args: message: The protobuf message to convert. diff --git a/src/frequenz/client/common/metrics/proto/v1alpha8/_sample.py b/src/frequenz/client/common/metrics/proto/v1alpha8/_sample.py index c2abadad..b4b2cae4 100644 --- a/src/frequenz/client/common/metrics/proto/v1alpha8/_sample.py +++ b/src/frequenz/client/common/metrics/proto/v1alpha8/_sample.py @@ -3,9 +3,8 @@ """Loading of MetricSample and AggregatedMetricValue objects from protobuf messages.""" -import warnings - from frequenz.api.common.v1alpha8.metrics import metrics_pb2 +from frequenz.core.warnings import ignoring_deprecations from typing_extensions import deprecated from ...._datetime import InvalidDatetime @@ -122,8 +121,10 @@ def metric_sample_from_proto( @deprecated( - "`metric_connection_from_proto_with_issues` is deprecated; use " - "`metric_connection_from_proto` and inspect the returned type instead." + "frequenz.client.common.metrics.proto.v1alpha8." + "metric_connection_from_proto_with_issues is deprecated since v0.4.1. Use " + "[frequenz.client.common.metrics.proto.v1alpha8.metric_connection_from_proto][] " + "instead." ) def metric_connection_from_proto_with_issues( message: metrics_pb2.MetricConnection, @@ -133,12 +134,10 @@ def metric_connection_from_proto_with_issues( ) -> MetricConnection: """Convert a protobuf message to a [`MetricConnection`][....MetricConnection] object. - Warning: Deprecated - Use [`metric_connection_from_proto`][..metric_connection_from_proto] - instead and inspect the returned type. The new converter encodes an - unspecified or unrecognized category in the returned - `MetricConnection.category` field (`MetricConnectionCategory | int`) - rather than routing it through a side-channel string list. + [`metric_connection_from_proto`][..metric_connection_from_proto] encodes an + unspecified or unrecognized category in the returned + `MetricConnection.category` field (`MetricConnectionCategory | int`) rather + than routing it through a side-channel string list. Args: message: The protobuf message to convert. @@ -165,8 +164,10 @@ def metric_connection_from_proto_with_issues( @deprecated( - "`metric_sample_from_proto_with_issues` is deprecated; use " - "`metric_sample_from_proto` and inspect the returned type instead." + "frequenz.client.common.metrics.proto.v1alpha8." + "metric_sample_from_proto_with_issues is deprecated since v0.4.1. Use " + "[frequenz.client.common.metrics.proto.v1alpha8.metric_sample_from_proto][] " + "and inspect the returned type instead." ) def metric_sample_from_proto_with_issues( message: metrics_pb2.MetricSample, @@ -176,13 +177,11 @@ def metric_sample_from_proto_with_issues( ) -> MetricSample: """Convert a protobuf message to a [`MetricSample`][....MetricSample] object. - Warning: Deprecated - Use [`metric_sample_from_proto`][..metric_sample_from_proto] instead - and inspect the returned type. The new converter encodes an - unspecified or unrecognized `metric` (`Metric | int`), malformed - bounds (`InvalidBoundsSet`) and an unrepresentable sample time - (`InvalidDatetime`) in the returned `MetricSample` rather than - routing them through a side-channel string list. + [`metric_sample_from_proto`][..metric_sample_from_proto] encodes an + unspecified or unrecognized `metric` (`Metric | int`), malformed bounds + (`InvalidBoundsSet`) and an unrepresentable sample time (`InvalidDatetime`) + in the returned `MetricSample` rather than routing them through a + side-channel string list. Note: A malformed `sample_time` still raises `ValueError`, as it did when the @@ -228,8 +227,7 @@ def metric_sample_from_proto_with_issues( connection = None if message.HasField("connection"): - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): connection = metric_connection_from_proto_with_issues( message.connection, major_issues=major_issues, minor_issues=minor_issues ) diff --git a/src/frequenz/client/common/microgrid/components/__init__.py b/src/frequenz/client/common/microgrid/components/__init__.py index 5c1037c3..88bdd4c1 100644 --- a/src/frequenz/client/common/microgrid/components/__init__.py +++ b/src/frequenz/client/common/microgrid/components/__init__.py @@ -10,9 +10,9 @@ @deprecated( - "frequenz.client.common.microgrid.components.ComponentId is deprecated. " - "Use frequenz.client.common.microgrid.electrical_components." - "ElectricalComponentId instead." + "frequenz.client.common.microgrid.components.ComponentId is deprecated " + "since v0.4.1. Use [frequenz.client.common.microgrid." + "electrical_components.ElectricalComponentId][] instead." ) @final class ComponentId(BaseId, str_prefix="CID"): diff --git a/src/frequenz/client/common/microgrid/electrical_components/_category.py b/src/frequenz/client/common/microgrid/electrical_components/_category.py index 1843f99d..9498abd6 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/_category.py +++ b/src/frequenz/client/common/microgrid/electrical_components/_category.py @@ -6,10 +6,12 @@ import typing_extensions from frequenz.core.enum import Enum, deprecated_member, unique -_DEPRECATION_MESSAGE = ( - "ElectricalComponentCategory is deprecated; use the ElectricalComponent class " - "hierarchy (isinstance) or electrical_component_class_to_proto()/" - "electrical_component_class_from_proto()." +_QUALNAME = "frequenz.client.common.microgrid.electrical_components" + +_REPLACEMENT = ( + f"Use the [{_QUALNAME}.ElectricalComponent][] class hierarchy with isinstance(), " + f"or [{_QUALNAME}.proto.v1alpha8.electrical_component_class_to_proto][] and " + f"[{_QUALNAME}.proto.v1alpha8.electrical_component_class_from_proto][], instead." ) @@ -23,82 +25,212 @@ def _member_message(name: str) -> str: The full deprecation message for that member. """ return ( - f"ElectricalComponentCategory.{name} is deprecated; use the " - "ElectricalComponent class hierarchy (isinstance) or " - "electrical_component_class_to_proto()/electrical_component_class_from_proto()." + f"{_QUALNAME}.ElectricalComponentCategory.{name} is deprecated since " + f"v0.4.1. {_REPLACEMENT}" ) -@typing_extensions.deprecated(_DEPRECATION_MESSAGE) +# The message is spelled out as a literal rather than built from the constants +# above because griffe-warnings-deprecated only renders the admonition when it +# can read the message statically; a module-level name renders nothing. +@typing_extensions.deprecated( + "frequenz.client.common.microgrid.electrical_components." + "ElectricalComponentCategory is deprecated since v0.4.1. Use the " + "[frequenz.client.common.microgrid.electrical_components." + "ElectricalComponent][] class hierarchy with isinstance(), or " + "[frequenz.client.common.microgrid.electrical_components.proto.v1alpha8." + "electrical_component_class_to_proto][] and " + "[frequenz.client.common.microgrid.electrical_components.proto.v1alpha8." + "electrical_component_class_from_proto][], instead." +) @unique class ElectricalComponentCategory(Enum): """Possible types of microgrid electrical component.""" UNSPECIFIED = deprecated_member(0, _member_message("UNSPECIFIED")) - """The component category is unspecified. This should not be used.""" + """The component category is unspecified. This should not be used. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ GRID_CONNECTION_POINT = deprecated_member( 1, _member_message("GRID_CONNECTION_POINT") ) - """The point where the local microgrid is connected to the grid.""" + """The point where the local microgrid is connected to the grid. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ METER = deprecated_member(2, _member_message("METER")) - """A meter, for measuring electrical metrics, e.g., current, voltage, etc.""" + """A meter, for measuring electrical metrics, e.g., current, voltage, etc. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ INVERTER = deprecated_member(3, _member_message("INVERTER")) - """An inverter that converts DC to AC power and vice versa.""" + """An inverter that converts DC to AC power and vice versa. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ CONVERTER = deprecated_member(4, _member_message("CONVERTER")) - """An electricity converter, e.g., a DC-DC converter.""" + """An electricity converter, e.g., a DC-DC converter. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ BATTERY = deprecated_member(5, _member_message("BATTERY")) - """A battery energy storage system.""" + """A battery energy storage system. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ EV_CHARGER = deprecated_member(6, _member_message("EV_CHARGER")) - """A station for charging electrical vehicles.""" + """A station for charging electrical vehicles. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ BREAKER = deprecated_member(7, _member_message("BREAKER")) - """A circuit breaker, providing protection and switching by disconnecting circuits.""" + """A circuit breaker, providing protection and switching by disconnecting circuits. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ PRECHARGER = deprecated_member(8, _member_message("PRECHARGER")) - """A precharger, used for preparing electrical circuits for switching on.""" + """A precharger, used for preparing electrical circuits for switching on. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ CHP = deprecated_member(9, _member_message("CHP")) """A combined heat and power (CHP) plant. + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + It generates electricity and useful heat from a single energy source. """ ELECTROLYZER = deprecated_member(10, _member_message("ELECTROLYZER")) - """A device for splitting water into hydrogen and oxygen using electricity.""" + """A device for splitting water into hydrogen and oxygen using electricity. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ POWER_TRANSFORMER = deprecated_member(11, _member_message("POWER_TRANSFORMER")) - """A transformer, used for changing the voltage of electrical circuits.""" + """A transformer, used for changing the voltage of electrical circuits. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ HVAC = deprecated_member(12, _member_message("HVAC")) - """A heating, ventilation, and air conditioning (HVAC) system.""" + """A heating, ventilation, and air conditioning (HVAC) system. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ PLC = deprecated_member(13, _member_message("PLC")) - """A programmable logic controller (PLC).""" + """A programmable logic controller (PLC). + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ CRYPTO_MINER = deprecated_member(14, _member_message("CRYPTO_MINER")) - """A device for mining cryptocurrencies.""" + """A device for mining cryptocurrencies. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ STATIC_TRANSFER_SWITCH = deprecated_member( 15, _member_message("STATIC_TRANSFER_SWITCH") ) - """A static transfer switch, used for switching between power sources.""" + """A static transfer switch, used for switching between power sources. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ UNINTERRUPTIBLE_POWER_SUPPLY = deprecated_member( 16, _member_message("UNINTERRUPTIBLE_POWER_SUPPLY") ) - """An uninterruptible power supply (UPS), used to provide backup power.""" + """An uninterruptible power supply (UPS), used to provide backup power. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ CAPACITOR_BANK = deprecated_member(17, _member_message("CAPACITOR_BANK")) - """A capacitor bank, used for power factor correction and reactive power compensation.""" + """A capacitor bank, used for power factor correction and reactive power compensation. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ WIND_TURBINE = deprecated_member(18, _member_message("WIND_TURBINE")) - """A wind turbine, used to generate electricity from wind energy.""" + """A wind turbine, used to generate electricity from wind energy. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ STEAM_BOILER = deprecated_member(19, _member_message("STEAM_BOILER")) - """A steam boiler, used to generate steam for heating or industrial processes.""" + """A steam boiler, used to generate steam for heating or industrial processes. + + Deprecated: + This member is deprecated since v0.4.1. See + [`ElectricalComponentCategory`][...ElectricalComponentCategory] for + what to use instead. + """ diff --git a/src/frequenz/client/common/microgrid/electrical_components/_diagnostic_code.py b/src/frequenz/client/common/microgrid/electrical_components/_diagnostic_code.py index 092da57f..33d1ff33 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/_diagnostic_code.py +++ b/src/frequenz/client/common/microgrid/electrical_components/_diagnostic_code.py @@ -12,8 +12,10 @@ class ElectricalComponentDiagnosticCode(Enum): UNSPECIFIED = deprecated_member( 0, - "ElectricalComponentDiagnosticCode.UNSPECIFIED is deprecated; use the `int` value `0` " - "instead if you really need to check for this low-level value.", + "frequenz.client.common.microgrid.electrical_components." + "ElectricalComponentDiagnosticCode.UNSPECIFIED is deprecated since " + "v0.4.1. Use the int value 0 instead if you really need to check for " + "this low-level value.", ) """Default value. No specific error is specified.""" diff --git a/src/frequenz/client/common/microgrid/electrical_components/_state_code.py b/src/frequenz/client/common/microgrid/electrical_components/_state_code.py index 36bd66dd..35264b5f 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/_state_code.py +++ b/src/frequenz/client/common/microgrid/electrical_components/_state_code.py @@ -12,8 +12,10 @@ class ElectricalComponentStateCode(Enum): UNSPECIFIED = deprecated_member( 0, - "ElectricalComponentStateCode.UNSPECIFIED is deprecated; use the `int` value `0` " - "instead if you really need to check for this low-level value.", + "frequenz.client.common.microgrid.electrical_components." + "ElectricalComponentStateCode.UNSPECIFIED is deprecated since v0.4.1. " + "Use the int value 0 instead if you really need to check for this " + "low-level value.", ) """Default value when the component state is not explicitly set.""" diff --git a/src/frequenz/client/common/microgrid/electrical_components/proto/v1alpha8/_category.py b/src/frequenz/client/common/microgrid/electrical_components/proto/v1alpha8/_category.py index 1abc3305..a838a198 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/proto/v1alpha8/_category.py +++ b/src/frequenz/client/common/microgrid/electrical_components/proto/v1alpha8/_category.py @@ -3,20 +3,21 @@ """Conversion of electrical component categories to/from protobuf v1alpha8.""" -import warnings - import typing_extensions from frequenz.api.common.v1alpha8.microgrid.electrical_components import ( electrical_components_pb2, ) +from frequenz.core.warnings import ignoring_deprecations from .....proto import enum_from_proto from ... import ElectricalComponentCategory @typing_extensions.deprecated( - "electrical_component_category_from_proto() is deprecated; use " - "electrical_component_class_from_proto() instead." + "frequenz.client.common.microgrid.electrical_components.proto.v1alpha8." + "electrical_component_category_from_proto is deprecated since v0.4.1. Use " + "[frequenz.client.common.microgrid.electrical_components.proto.v1alpha8." + "electrical_component_class_from_proto][] instead." ) def electrical_component_category_from_proto( message: electrical_components_pb2.ElectricalComponentCategory.ValueType, @@ -31,14 +32,15 @@ def electrical_component_category_from_proto( [`ElectricalComponentCategory`][....ElectricalComponentCategory] enum member, or the raw [`int`][] if the protobuf value is not recognized. """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): return enum_from_proto(message, ElectricalComponentCategory) @typing_extensions.deprecated( - "electrical_component_category_to_proto() is deprecated; use " - "electrical_component_class_to_proto() instead." + "frequenz.client.common.microgrid.electrical_components.proto.v1alpha8." + "electrical_component_category_to_proto is deprecated since v0.4.1. Use " + "[frequenz.client.common.microgrid.electrical_components.proto.v1alpha8." + "electrical_component_class_to_proto][] instead." ) def electrical_component_category_to_proto( category: ElectricalComponentCategory, @@ -53,8 +55,7 @@ def electrical_component_category_to_proto( Returns: The corresponding protobuf `ElectricalComponentCategory` value. """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): return electrical_components_pb2.ElectricalComponentCategory.ValueType( category.value ) diff --git a/src/frequenz/client/common/microgrid/electrical_components/proto/v1alpha8/_electrical_component.py b/src/frequenz/client/common/microgrid/electrical_components/proto/v1alpha8/_electrical_component.py index 84f06aa1..68d1c33b 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/proto/v1alpha8/_electrical_component.py +++ b/src/frequenz/client/common/microgrid/electrical_components/proto/v1alpha8/_electrical_component.py @@ -3,7 +3,6 @@ """Conversion of electrical components to/from protobuf v1alpha8.""" -import warnings from collections.abc import Mapping, Sequence from typing import Final, NamedTuple, TypeAlias, assert_never, overload @@ -11,6 +10,7 @@ from frequenz.api.common.v1alpha8.microgrid.electrical_components import ( electrical_components_pb2, ) +from frequenz.core.warnings import ignoring_deprecations from google.protobuf.json_format import MessageToDict from .....metrics import BoundsSet, InvalidBoundsSet, Metric @@ -938,8 +938,7 @@ def _electrical_component_base_from_proto( Returns: An `_ElectricalComponentBaseData` named tuple containing the extracted data. """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): component_id = ElectricalComponentId(message.id) microgrid_id = MicrogridId(message.microgrid_id) @@ -1012,8 +1011,7 @@ def electrical_component_from_proto( Returns: The resulting electrical component instance. """ - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): base_data = _electrical_component_base_from_proto(message) if base_data.category_mismatched: @@ -1242,8 +1240,7 @@ def _metric_config_bounds_from_proto( """ grouped: dict[Metric | int, list[bounds_pb2.Bounds]] = {} for metric_bound in message: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) + with ignoring_deprecations(): metric = enum_from_proto(metric_bound.metric, Metric) if metric is Metric.UNSPECIFIED: metric = metric.value diff --git a/src/frequenz/client/common/pagination/proto/v1alpha8/_pagination_info.py b/src/frequenz/client/common/pagination/proto/v1alpha8/_pagination_info.py index f510b2ec..de511bbe 100644 --- a/src/frequenz/client/common/pagination/proto/v1alpha8/_pagination_info.py +++ b/src/frequenz/client/common/pagination/proto/v1alpha8/_pagination_info.py @@ -13,8 +13,8 @@ @deprecated( "frequenz.client.common.pagination.proto.v1alpha8.pagination_info_from_proto " - "is deprecated. Use " - "frequenz.client.common.pagination.proto.v1alpha8.pagination_info_from_proto2 " + "is deprecated since v0.4.1. Use " + "[frequenz.client.common.pagination.proto.v1alpha8.pagination_info_from_proto2][] " "instead." ) def pagination_info_from_proto( # noqa: DOC502 @@ -22,11 +22,10 @@ def pagination_info_from_proto( # noqa: DOC502 ) -> PaginationInfo: """Convert a protobuf message to a [`PaginationInfo`][....PaginationInfo] object. - Warning: Deprecated - Use [`pagination_info_from_proto2`][..pagination_info_from_proto2] - instead. The new converter distinguishes well-formed from malformed - data at the type level (`PaginationInfo | InvalidPaginationInfo`) - rather than raising a `ValueError` when the invariant fires. + [`pagination_info_from_proto2`][..pagination_info_from_proto2] distinguishes + well-formed from malformed data at the type level + (`PaginationInfo | InvalidPaginationInfo`) rather than raising a + `ValueError` when the invariant fires. Args: message: The protobuf message to convert. diff --git a/src/frequenz/client/common/proto/_datetime.py b/src/frequenz/client/common/proto/_datetime.py index 80bf1f0c..a59d557d 100644 --- a/src/frequenz/client/common/proto/_datetime.py +++ b/src/frequenz/client/common/proto/_datetime.py @@ -60,22 +60,20 @@ def datetime_to_proto(dt: datetime | None) -> timestamp_pb2.Timestamp | None: @deprecated( - "`datetime_from_proto` is deprecated; use " - "`datetime_from_proto2` (returns `datetime | InvalidDatetime`) instead." + "frequenz.client.common.proto.datetime_from_proto is deprecated since " + "v0.4.1. Use [frequenz.client.common.proto.datetime_from_proto2][] instead." ) def datetime_from_proto( # noqa: DOC502 ts: timestamp_pb2.Timestamp, tz: timezone = timezone.utc ) -> datetime: """Convert a protobuf Timestamp to a datetime. - Warning: Deprecated - Use [`datetime_from_proto2`][..datetime_from_proto2] instead. The new - conversion function keeps a malformed timestamp in its return type - (`datetime | InvalidDatetime`) rather than raising or silently - repairing it, and is exact across the whole protobuf range, where this - function loses sub-second precision far from the epoch. It always - returns UTC; call [`astimezone()`][datetime.datetime.astimezone] on the - result instead of passing `tz`. + [`datetime_from_proto2`][..datetime_from_proto2] keeps a malformed + timestamp in its return type (`datetime | InvalidDatetime`) rather than + raising or silently repairing it, and is exact across the whole protobuf + range, where this function loses sub-second precision far from the epoch. + It always returns UTC; call [`astimezone()`][datetime.datetime.astimezone] + on the result instead of passing `tz`. Args: ts: The Timestamp object to convert. diff --git a/src/frequenz/client/common/streaming/_event.py b/src/frequenz/client/common/streaming/_event.py index 6907b476..e05d19db 100644 --- a/src/frequenz/client/common/streaming/_event.py +++ b/src/frequenz/client/common/streaming/_event.py @@ -12,8 +12,9 @@ class Event(Enum): UNSPECIFIED = deprecated_member( 0, - "Event.UNSPECIFIED is deprecated; use the `int` value `0` " - "instead if you really need to check for this low-level value.", + "frequenz.client.common.streaming.Event.UNSPECIFIED is deprecated " + "since v0.4.1. Use the int value 0 instead if you really need to check " + "for this low-level value.", ) """Unspecified event type.""" diff --git a/src/frequenz/client/common/test/enum_parity.py b/src/frequenz/client/common/test/enum_parity.py index ec1fc369..a0e46c5f 100644 --- a/src/frequenz/client/common/test/enum_parity.py +++ b/src/frequenz/client/common/test/enum_parity.py @@ -22,12 +22,12 @@ from __future__ import annotations import contextlib -import warnings from collections.abc import Callable, Iterator from enum import Enum from typing import Any, ClassVar import pytest +from frequenz.core.warnings import ignoring_deprecations class EnumParityTest: @@ -162,8 +162,7 @@ def _maybe_ignore_deprecation(self, name: str) -> Iterator[None]: when ``name`` is in `deprecated_members`. """ if name in self.deprecated_members: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): yield else: yield @@ -182,8 +181,7 @@ def _maybe_silence_converter_deprecation(self) -> Iterator[None]: when `silence_deprecations` is `True`. """ if self.silence_deprecations: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): yield else: yield diff --git a/tests/grid/_delivery_area/test_delivery_area.py b/tests/grid/_delivery_area/test_delivery_area.py index eb9b5c1b..cc1619f6 100644 --- a/tests/grid/_delivery_area/test_delivery_area.py +++ b/tests/grid/_delivery_area/test_delivery_area.py @@ -3,10 +3,10 @@ """Tests for the DeliveryArea class.""" -import warnings from dataclasses import dataclass import pytest +from frequenz.core.warnings import asserting_no_deprecations from frequenz.client.common import ( UnrecognizedEnumValueError, @@ -62,8 +62,7 @@ class _TestCase: ) def test_creation_valid(case: _TestCase) -> None: """Well-formed DeliveryArea construction succeeds without warnings.""" - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): area = DeliveryArea(code=case.code, code_type=case.code_type) assert area.code == case.code assert area.code_type == case.code_type @@ -161,8 +160,7 @@ def test_creation_with_unspecified_code_type_member_emits_deprecation_warning() ) def test_creation_raises_on_invalid_code(case: _TestCase) -> None: """`_raise_on_invalid=True` raises `ValueError` on empty/`None` code.""" - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): with pytest.raises(ValueError, match="`code` cannot be None or empty"): DeliveryArea( code=case.code, @@ -173,8 +171,7 @@ def test_creation_raises_on_invalid_code(case: _TestCase) -> None: def test_creation_raises_on_unspecified_int_code_type() -> None: """`_raise_on_invalid=True` raises `ValueError` on `code_type=0`.""" - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): with pytest.raises( ValueError, match="`code_type` cannot be 0 \\(UNSPECIFIED\\)" ): @@ -185,8 +182,7 @@ def test_creation_raises_on_unspecified_member_code_type() -> None: """`_raise_on_invalid=True` raises `ValueError` on the UNSPECIFIED member.""" with pytest.deprecated_call(): unspecified = EnergyMarketCodeType.UNSPECIFIED - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): with pytest.raises( ValueError, match="`code_type` cannot be 0 \\(UNSPECIFIED\\)" ): @@ -195,8 +191,7 @@ def test_creation_raises_on_unspecified_member_code_type() -> None: def test_creation_does_not_raise_when_valid() -> None: """`_raise_on_invalid=True` does not raise on well-formed data.""" - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): area = DeliveryArea( code="DE", code_type=EnergyMarketCodeType.EUROPE_EIC, @@ -263,8 +258,7 @@ def test_get_code_type_raises_unspecified_for_value_zero_member() -> None: unspecified = EnergyMarketCodeType.UNSPECIFIED with pytest.deprecated_call(): area = DeliveryArea(code="TEST", code_type=unspecified) - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): with pytest.raises(UnspecifiedEnumValueError): area.get_code_type() diff --git a/tests/grid/_delivery_area/test_invalid_delivery_area.py b/tests/grid/_delivery_area/test_invalid_delivery_area.py index 2c5f6024..8dd1945c 100644 --- a/tests/grid/_delivery_area/test_invalid_delivery_area.py +++ b/tests/grid/_delivery_area/test_invalid_delivery_area.py @@ -3,10 +3,10 @@ """Tests for the InvalidDeliveryArea class.""" -import warnings from dataclasses import dataclass import pytest +from frequenz.core.warnings import asserting_no_deprecations from frequenz.client.common.grid import ( BaseDeliveryArea, @@ -70,8 +70,7 @@ def test_is_base_delivery_area_subclass() -> None: ) def test_creation(case: _TestCase) -> None: """`InvalidDeliveryArea` accepts any data with no invariants and no warnings.""" - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): area = InvalidDeliveryArea(code=case.code, code_type=case.code_type) assert area.code == case.code assert area.code_type == case.code_type diff --git a/tests/grid/proto/v1alpha8/test_delivery_area.py b/tests/grid/proto/v1alpha8/test_delivery_area.py index c11dab4f..f2b5771d 100644 --- a/tests/grid/proto/v1alpha8/test_delivery_area.py +++ b/tests/grid/proto/v1alpha8/test_delivery_area.py @@ -8,6 +8,7 @@ import pytest from frequenz.api.common.v1alpha8.grid import delivery_area_pb2 +from frequenz.core.warnings import asserting_no_deprecations from frequenz.client.common import UnspecifiedEnumValueError from frequenz.client.common.grid import ( @@ -154,6 +155,31 @@ def test_from_proto_emits_deprecation_warning() -> None: delivery_area_from_proto(proto) +def test_from_proto_keeps_warnings_deduplicated() -> None: + """Silencing its internal deprecation doesn't make warnings show again. + + A `warnings.catch_warnings()` block resets the deduplication history on + every call (python/cpython#73858), so both warnings would show on every + iteration instead of once. + """ + proto = delivery_area_pb2.DeliveryArea( + code="DE", + code_type=( + delivery_area_pb2.EnergyMarketCodeType.ENERGY_MARKET_CODE_TYPE_EUROPE_EIC + ), + ) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("default") + for _ in range(3): + warnings.warn("an unrelated warning", UserWarning) + delivery_area_from_proto(proto) + + assert [warning.category for warning in caught] == [ + UserWarning, + DeprecationWarning, + ] + + @dataclass(frozen=True, kw_only=True) class _FromProto2TestCase: """Test case for `delivery_area_from_proto2` conversion.""" @@ -239,8 +265,7 @@ def test_from_proto2( code=case.code, code_type=case.code_type # type: ignore[arg-type] ) with caplog.at_level("WARNING"): - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): area = delivery_area_from_proto2(proto) assert isinstance(area, case.expected_type) diff --git a/tests/metrics/proto/v1alpha8/test_sample_metric_connection.py b/tests/metrics/proto/v1alpha8/test_sample_metric_connection.py index d78cc42d..f53a8121 100644 --- a/tests/metrics/proto/v1alpha8/test_sample_metric_connection.py +++ b/tests/metrics/proto/v1alpha8/test_sample_metric_connection.py @@ -3,10 +3,9 @@ """Tests for MetricConnection protobuf conversion.""" -import warnings - import pytest from frequenz.api.common.v1alpha8.metrics import metrics_pb2 +from frequenz.core.warnings import asserting_no_deprecations from frequenz.client.common.metrics import MetricConnectionCategory from frequenz.client.common.metrics.proto.v1alpha8 import ( @@ -111,8 +110,7 @@ def test_from_proto_unspecified_category() -> None: name="some_connection", ) - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): connection = metric_connection_from_proto(proto) assert connection.category == 0 diff --git a/tests/metrics/proto/v1alpha8/test_sample_metric_sample.py b/tests/metrics/proto/v1alpha8/test_sample_metric_sample.py index 13f85d37..22d9b5af 100644 --- a/tests/metrics/proto/v1alpha8/test_sample_metric_sample.py +++ b/tests/metrics/proto/v1alpha8/test_sample_metric_sample.py @@ -4,13 +4,13 @@ """Tests for MetricSample protobuf conversion.""" import math -import warnings from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Final import pytest from frequenz.api.common.v1alpha8.metrics import bounds_pb2, metrics_pb2 +from frequenz.core.warnings import asserting_no_deprecations from google.protobuf.timestamp_pb2 import Timestamp from frequenz.client.common import InvalidDatetime, InvalidDatetimeError @@ -373,8 +373,7 @@ def test_from_proto_unspecified_metric() -> None: ), ) - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): sample = metric_sample_from_proto(proto) assert sample.metric == 0 diff --git a/tests/metrics/test_sample_metric_connection.py b/tests/metrics/test_sample_metric_connection.py index 1b2cfe1b..474828f0 100644 --- a/tests/metrics/test_sample_metric_connection.py +++ b/tests/metrics/test_sample_metric_connection.py @@ -3,6 +3,8 @@ """Tests for MetricConnection and MetricConnectionCategory classes.""" +import warnings + import pytest from frequenz.client.common import ( @@ -137,6 +139,23 @@ def test_get_category_unspecified_member_raises() -> None: connection.get_category() +def test_str_keeps_warnings_deduplicated() -> None: + """Silencing the internal deprecation doesn't make warnings show again. + + A `warnings.catch_warnings()` block resets the deduplication history on + every call (python/cpython#73858), so the unrelated warning would show on + every iteration instead of once. + """ + connection = MetricConnection(category=0) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("default") + for _ in range(3): + warnings.warn("an unrelated warning", UserWarning) + assert str(connection) == ":cat=" + + assert [warning.category for warning in caught] == [UserWarning] + + def test_get_category_unrecognized_int_raises() -> None: """get_category raises UnrecognizedEnumValueError carrying the raw int value.""" connection = MetricConnection(category=99999) diff --git a/tests/metrics/test_sample_metric_sample.py b/tests/metrics/test_sample_metric_sample.py index 1b90f58c..e40c4099 100644 --- a/tests/metrics/test_sample_metric_sample.py +++ b/tests/metrics/test_sample_metric_sample.py @@ -413,7 +413,12 @@ def test_deprecated_sample_time_property(now: datetime) -> None: ) assert sample.sample_time2 is now with pytest.deprecated_call( - match="`MetricSample.sample_time` is deprecated; use `sample_time2` instead." + match=( + r"^frequenz\.client\.common\.metrics\.MetricSample\.sample_time is " + r"deprecated since v0\.4\.1\. Use " + r"\[frequenz\.client\.common\.metrics\.MetricSample\.get_sample_time\]" + r"\[\] instead\.$" + ) ): assert sample.sample_time is now diff --git a/tests/microgrid/electrical_components/proto/v1alpha8/conftest.py b/tests/microgrid/electrical_components/proto/v1alpha8/conftest.py index 3e445151..53dff2af 100644 --- a/tests/microgrid/electrical_components/proto/v1alpha8/conftest.py +++ b/tests/microgrid/electrical_components/proto/v1alpha8/conftest.py @@ -3,7 +3,6 @@ """Fixtures and utilities for testing electrical component protobuf conversion.""" -import warnings from datetime import datetime, timezone import pytest @@ -12,6 +11,7 @@ from frequenz.api.common.v1alpha8.microgrid.electrical_components import ( electrical_components_pb2, ) +from frequenz.core.warnings import ignoring_deprecations from google.protobuf.timestamp_pb2 import Timestamp from frequenz.client.common.metrics import Bounds, BoundsSet, Metric @@ -53,8 +53,7 @@ def default_component_base_data( component_id: ElectricalComponentId, microgrid_id: MicrogridId ) -> _ElectricalComponentBaseData: """Provide a fixture for common component fields.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.UNSPECIFIED return _ElectricalComponentBaseData( component_id=component_id, diff --git a/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_base.py b/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_base.py index 67d3eed3..edaf9a15 100644 --- a/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_base.py +++ b/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_base.py @@ -4,7 +4,6 @@ """Tests for protobuf conversion of the base/common part of electrical components.""" import math -import warnings from datetime import timezone import pytest @@ -12,6 +11,7 @@ from frequenz.api.common.v1alpha8.microgrid.electrical_components import ( electrical_components_pb2, ) +from frequenz.core.warnings import ignoring_deprecations from google.protobuf.timestamp_pb2 import Timestamp from frequenz.client.common.metrics import ( @@ -85,8 +85,7 @@ def test_operational_mode_to_bools( def test_complete(default_component_base_data: _ElectricalComponentBaseData) -> None: """Test parsing of a complete base component proto.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.CHP # Just to pick a valid category base_data = default_component_base_data._replace(category=category) proto = base_data_as_proto(base_data) @@ -99,8 +98,7 @@ def test_missing_category_specific_info( default_component_base_data: _ElectricalComponentBaseData, ) -> None: """Test parsing with missing optional category specific info.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.UNSPECIFIED base_data = default_component_base_data._replace( name="", @@ -122,8 +120,7 @@ def test_empty_lifetime_is_unbounded( default_component_base_data: _ElectricalComponentBaseData, ) -> None: """A present but empty protobuf lifetime becomes an unbounded `Lifetime`.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.CHP base_data = default_component_base_data._replace( category=category, @@ -141,8 +138,7 @@ def test_category_specific_info_mismatch( default_component_base_data: _ElectricalComponentBaseData, ) -> None: """Test category and category specific info mismatch.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.GRID_CONNECTION_POINT base_data = default_component_base_data._replace( category=category, @@ -165,8 +161,7 @@ def test_invalid_lifetime( default_component_base_data: _ElectricalComponentBaseData, ) -> None: """Test invalid lifetime (start after end).""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.CHP base_data = default_component_base_data._replace( category=category, @@ -204,8 +199,7 @@ def _metric_bound( def test_metric_config_bounds_stores_unspecified_as_int() -> None: """Test UNSPECIFIED metric bounds load as plain int key 0.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): unspecified = Metric.UNSPECIFIED message = [ _metric_bound(int(unspecified.value), 0.0, 1.0), diff --git a/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_simple.py b/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_simple.py index c40974d3..56e15cb6 100644 --- a/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_simple.py +++ b/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_simple.py @@ -3,12 +3,11 @@ """Tests for protobuf conversion of simple electrical components.""" -import warnings - import pytest from frequenz.api.common.v1alpha8.microgrid.electrical_components import ( electrical_components_pb2, ) +from frequenz.core.warnings import ignoring_deprecations from frequenz.client.common.microgrid.electrical_components import ( Breaker, @@ -100,8 +99,7 @@ def test_category_mismatch( assert electrical_component_class_to_proto(component) == (1, None) -with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) +with ignoring_deprecations(): _TRIVIAL_CASES = [ pytest.param(ElectricalComponentCategory.BREAKER, Breaker, id="Breaker"), pytest.param( @@ -167,8 +165,7 @@ def test_power_transformer( secondary: float | None, ) -> None: """Test PowerTransformer component.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.POWER_TRANSFORMER base_data = default_component_base_data._replace(category=category) @@ -196,8 +193,7 @@ def test_grid( rated_fuse_current: int | None, ) -> None: """Test GridConnectionPoint component with default values.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.GRID_CONNECTION_POINT base_data = default_component_base_data._replace(category=category) diff --git a/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_with_type.py b/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_with_type.py index 6597878c..b1c0ccfa 100644 --- a/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_with_type.py +++ b/tests/microgrid/electrical_components/proto/v1alpha8/test_electrical_component_with_type.py @@ -3,12 +3,11 @@ """Tests for protobuf conversion of components with a type.""" -import warnings - import pytest from frequenz.api.common.v1alpha8.microgrid.electrical_components import ( electrical_components_pb2, ) +from frequenz.core.warnings import ignoring_deprecations from frequenz.client.common.microgrid.electrical_components import ( AcEvCharger, @@ -72,8 +71,7 @@ def test_battery( pb_battery_type: int, ) -> None: """Test battery component.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.BATTERY base_data = default_component_base_data._replace(category=category) proto = base_data_as_proto(base_data) @@ -126,8 +124,7 @@ def test_ev_charger( pb_ev_charger_type: int, ) -> None: """Test EV Charger component.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.EV_CHARGER base_data = default_component_base_data._replace(category=category) proto = base_data_as_proto(base_data) @@ -180,8 +177,7 @@ def test_inverter( pb_inverter_type: int, ) -> None: """Test inverter component.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.INVERTER base_data = default_component_base_data._replace(category=category) proto = base_data_as_proto(base_data) diff --git a/tests/microgrid/electrical_components/proto/v1alpha8/test_enum_deprecation.py b/tests/microgrid/electrical_components/proto/v1alpha8/test_enum_deprecation.py index c8b1454a..f3bff74e 100644 --- a/tests/microgrid/electrical_components/proto/v1alpha8/test_enum_deprecation.py +++ b/tests/microgrid/electrical_components/proto/v1alpha8/test_enum_deprecation.py @@ -3,12 +3,11 @@ """Tests for deprecation of the category enum and its proto converters.""" -import warnings - import pytest from frequenz.api.common.v1alpha8.microgrid.electrical_components import ( electrical_components_pb2, ) +from frequenz.core.warnings import asserting_no_deprecations, ignoring_deprecations from frequenz.client.common.microgrid.electrical_components import ( ElectricalComponentCategory, @@ -29,8 +28,7 @@ def test_electrical_component_category_member_warns() -> None: def test_electrical_component_category_to_proto_warns() -> None: """Calling `electrical_component_category_to_proto` must warn.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): member = ElectricalComponentCategory.BATTERY with pytest.deprecated_call(): _ = electrical_component_category_to_proto(member) @@ -46,8 +44,7 @@ def test_electrical_component_category_from_proto_warns() -> None: def test_class_to_proto_does_not_warn() -> None: """The non-deprecated `electrical_component_class_to_proto` must NOT warn.""" - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): result = electrical_component_class_to_proto(LiIonBattery) assert result == ( electrical_components_pb2.ELECTRICAL_COMPONENT_CATEGORY_BATTERY, diff --git a/tests/microgrid/electrical_components/proto/v1alpha8/test_raw_storage.py b/tests/microgrid/electrical_components/proto/v1alpha8/test_raw_storage.py index 68eb4960..331648ec 100644 --- a/tests/microgrid/electrical_components/proto/v1alpha8/test_raw_storage.py +++ b/tests/microgrid/electrical_components/proto/v1alpha8/test_raw_storage.py @@ -4,11 +4,11 @@ """Tests for the raw category/type values carried by problematic components.""" import dataclasses -import warnings from frequenz.api.common.v1alpha8.microgrid.electrical_components import ( electrical_components_pb2, ) +from frequenz.core.warnings import asserting_no_deprecations, ignoring_deprecations from frequenz.client.common.microgrid.electrical_components import ( CategorySpecificInfo, @@ -32,8 +32,7 @@ def _li_ion_battery( default_component_base_data: _ElectricalComponentBaseData, ) -> LiIonBattery: """Build a `LiIonBattery` through the protobuf converter.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.BATTERY base_data = default_component_base_data._replace(category=category) proto = base_data_as_proto(base_data) @@ -203,15 +202,13 @@ def test_from_proto_emits_no_deprecation_warning( default_component_base_data: _ElectricalComponentBaseData, ) -> None: """Converting a protobuf message must not emit a `DeprecationWarning`.""" - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): category = ElectricalComponentCategory.BATTERY base_data = default_component_base_data._replace(category=category) proto = base_data_as_proto(base_data) proto.category_specific_info.battery.type = ( electrical_components_pb2.BATTERY_TYPE_LI_ION ) - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) + with asserting_no_deprecations(): component = electrical_component_from_proto(proto) assert isinstance(component, LiIonBattery) diff --git a/tests/pagination/proto/v1alpha8/test_pagination_info.py b/tests/pagination/proto/v1alpha8/test_pagination_info.py index 9230b803..d2aea970 100644 --- a/tests/pagination/proto/v1alpha8/test_pagination_info.py +++ b/tests/pagination/proto/v1alpha8/test_pagination_info.py @@ -39,9 +39,9 @@ def test_from_proto_emits_deprecation_warning() -> None: proto = pagination_info_pb2.PaginationInfo(total_items=1) with pytest.deprecated_call( match=r"^frequenz\.client\.common\.pagination\.proto\.v1alpha8\." - r"pagination_info_from_proto is deprecated\. Use " - r"frequenz\.client\.common\.pagination\.proto\.v1alpha8\." - r"pagination_info_from_proto2 instead\.$" + r"pagination_info_from_proto is deprecated since v0\.4\.1\. Use " + r"\[frequenz\.client\.common\.pagination\.proto\.v1alpha8\." + r"pagination_info_from_proto2\]\[\] instead\.$" ): pagination_info_from_proto(proto) diff --git a/tests/proto/test_datetime.py b/tests/proto/test_datetime.py index eb2b82d8..0c63e51a 100644 --- a/tests/proto/test_datetime.py +++ b/tests/proto/test_datetime.py @@ -83,8 +83,9 @@ def test_no_none_datetime(dt: datetime) -> None: def test_from_proto_is_deprecated() -> None: """`datetime_from_proto` warns and still converts as it always did.""" with pytest.deprecated_call( - match=r"`datetime_from_proto` is deprecated; use `datetime_from_proto2` " - r"\(returns `datetime \| InvalidDatetime`\) instead\." + match=r"^frequenz\.client\.common\.proto\.datetime_from_proto is " + r"deprecated since v0\.4\.1\. Use " + r"\[frequenz\.client\.common\.proto\.datetime_from_proto2\]\[\] instead\.$" ): converted = datetime_from_proto(Timestamp(seconds=1, nanos=500000000)) assert converted == datetime(1970, 1, 1, 0, 0, 1, 500000, tzinfo=timezone.utc) diff --git a/tests/test/test_enum_parity.py b/tests/test/test_enum_parity.py index 9076e8b9..c237329f 100644 --- a/tests/test/test_enum_parity.py +++ b/tests/test/test_enum_parity.py @@ -27,6 +27,7 @@ import typing_extensions from frequenz.core.enum import Enum as DeprecatingEnum from frequenz.core.enum import deprecated_member, unique +from frequenz.core.warnings import ignoring_deprecations from pytest_mock import MockerFixture from frequenz.client.common.proto import enum_from_proto @@ -222,8 +223,7 @@ def _gone_from_proto(value: int) -> _GoneColor | int: Returns: The `_GoneColor` member, or the raw `int` for unknown values. """ - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): return enum_from_proto(value, _GoneColor) @@ -237,8 +237,7 @@ def _gone_to_proto(member: _GoneColor) -> int: Returns: The member's numeric value. """ - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) + with ignoring_deprecations(): return int(member.value)