Skip to content

Commit a3fe218

Browse files
committed
Fix the new pydoclint check DOC503
This check verifies that the `Raises` section of a method docstring contains the same exception types present in `raise` statements in the code. Some checks were correctly failing because of wrong documentation or typos, but some fail only because `pydoclint` is not smart enough to figure out the type of variables, so one must always have `raise ExceptionType` in the code to make the check pass. It also fails when the exception is raised indirectly, but we want to document it anyways, for example if raised by some utility function or private method. For cases where this is possible, the code was changed to do explicit `raise ExceptionType`, for other cases the check is simply disabled. In other cases a `raise` was replaced with an `assert_never` to make the check pass and also make sure a failure is detected by `mypy` instead of at runtime. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 5ee9a78 commit a3fe218

File tree

9 files changed

+35
-25
lines changed

9 files changed

+35
-25
lines changed

src/frequenz/sdk/_internal/_channels.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,20 +153,20 @@ def get_or_create(self, message_type: type[T], key: str) -> Broadcast[T]:
153153

154154
entry = self._channels[key]
155155
if entry.message_type is not message_type:
156-
exception = ValueError(
156+
error_message = (
157157
f"Type mismatch, a channel for key {key!r} exists and the requested "
158158
f"message type {message_type} is not the same as the existing "
159159
f"message type {entry.message_type}."
160160
)
161161
if _logger.isEnabledFor(logging.DEBUG):
162162
_logger.debug(
163163
"%s at:\n%s",
164-
str(exception),
164+
error_message,
165165
# We skip the last frame because it's this method, and limit the
166166
# stack to 9 frames to avoid adding too much noise.
167167
"".join(traceback.format_stack(limit=10)[:9]),
168168
)
169-
raise exception
169+
raise ValueError(error_message)
170170

171171
return typing.cast(Broadcast[T], entry.channel)
172172

src/frequenz/sdk/actor/_background_service.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ def cancel(self, msg: str | None = None) -> None:
129129
for task in self._tasks:
130130
task.cancel(msg)
131131

132-
async def stop(self, msg: str | None = None) -> None:
132+
# We need the noqa because pydoclint can't figure out `rest` is
133+
# a `BaseExceptionGroup` instance.
134+
async def stop(self, msg: str | None = None) -> None: # noqa: DOC503
133135
"""Stop this background service.
134136
135137
This method cancels all running tasks spawned by this service and waits for them

src/frequenz/sdk/timeseries/_moving_window.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import math
1010
from collections.abc import Sequence
1111
from datetime import datetime, timedelta
12-
from typing import SupportsIndex, overload
12+
from typing import SupportsIndex, assert_never, overload
1313

1414
import numpy as np
1515
from frequenz.channels import Broadcast, Receiver, Sender
@@ -282,7 +282,7 @@ def at(self, key: int | datetime) -> float: # pylint: disable=invalid-name
282282
assert timestamp is not None
283283
return self._buffer[self._buffer.to_internal_index(timestamp)]
284284

285-
raise TypeError("Key has to be either a timestamp or an integer.")
285+
assert_never(key)
286286

287287
def window(
288288
self,
@@ -385,7 +385,10 @@ def __getitem__(self, key: datetime) -> float:
385385
def __getitem__(self, key: slice) -> ArrayLike:
386386
"""See the main __getitem__ method."""
387387

388-
def __getitem__(self, key: SupportsIndex | datetime | slice) -> float | ArrayLike:
388+
# We need the noqa because `IndexError` is raised indirectly by `at()` and `window()`
389+
def __getitem__( # noqa: DOC503
390+
self, key: SupportsIndex | datetime | slice
391+
) -> float | ArrayLike:
389392
"""
390393
Return a sub window of the `MovingWindow`.
391394
@@ -400,12 +403,15 @@ def __getitem__(self, key: SupportsIndex | datetime | slice) -> float | ArrayLik
400403
where the bounds correspond to the slice bounds.
401404
Note that a half open interval, which is open at the end, is returned.
402405
406+
Note:
407+
Slicing with a step other than 1 is not supported.
408+
403409
Args:
404410
key: Either an integer or a timestamp or a slice of timestamps or integers.
405411
406412
Raises:
407413
IndexError: when requesting an out of range timestamp or index
408-
TypeError: when the key is not a datetime or slice object.
414+
ValueError: when requesting a slice with a step other than 1
409415
410416
Returns:
411417
A float if the key is a number or a timestamp.
@@ -422,10 +428,7 @@ def __getitem__(self, key: SupportsIndex | datetime | slice) -> float | ArrayLik
422428
if isinstance(key, SupportsIndex):
423429
return self.at(key.__index__())
424430

425-
raise TypeError(
426-
"Key has to be either a timestamp or an integer "
427-
"or a slice of timestamps or integers"
428-
)
431+
assert_never(key)
429432

430433

431434
# We need to register the class as a subclass of Sequence like this because

src/frequenz/sdk/timeseries/_resampling.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,9 @@ async def _receive_samples(self) -> None:
801801
if sample.value is not None and not sample.value.isnan():
802802
self._helper.add_sample(sample)
803803

804-
async def resample(self, timestamp: datetime) -> None:
804+
# We need the noqa because pydoclint can't figure out that `recv_exception` is an
805+
# `Exception` instance.
806+
async def resample(self, timestamp: datetime) -> None: # noqa: DOC503
805807
"""Calculate a new sample for the passed `timestamp` and send it.
806808
807809
The helper is used to calculate the new sample and the sender is used

src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_battery_power_formula.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ def generate(
4444
ComponentNotFound: if there are no batteries in the component graph, or if
4545
they don't have an inverter as a predecessor.
4646
FormulaGenerationError: If a battery has a non-inverter predecessor
47-
in the component graph.
48-
FormulaGenerationError: If not all batteries behind a set of inverters
49-
have been requested.
47+
in the component graph, or if not all batteries behind a set of
48+
inverters have been requested.
5049
"""
5150
builder = self._get_builder(
5251
"battery-power", ComponentMetricId.ACTIVE_POWER, Power.from_watts

src/frequenz/sdk/timeseries/formula_engine/_formula_generators/_consumer_power_formula.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def _are_grid_meters(self, grid_successors: set[Component]) -> bool:
4949
for successor in grid_successors
5050
)
5151

52-
def generate(self) -> FormulaEngine[Power]:
52+
# We need the noqa here because `RuntimeError` is raised indirectly
53+
def generate(self) -> FormulaEngine[Power]: # noqa: DOC503
5354
"""Generate formula for calculating consumer power from the component graph.
5455
5556
Returns:

tests/actor/test_run_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async def _run(self) -> None:
3535
"""Run the faulty actor.
3636
3737
Raises:
38-
CancelledError: the exception causes the actor to be cancelled
38+
asyncio.CancelledError: the exception causes the actor to be cancelled
3939
"""
4040
self.is_cancelled = True
4141
raise asyncio.CancelledError(f"Faulty Actor {self.name} failed")

tests/timeseries/_battery_pool/test_battery_pool.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ async def run_scenarios(
263263
264264
Raises:
265265
TimeoutError: If metric update was not received.
266-
AssertError: If received metric is not as expected.
266+
AssertionError: If received metric is not as expected.
267267
"""
268268
for idx, scenario in enumerate(scenarios):
269269
# Update data stream
@@ -278,19 +278,19 @@ async def run_scenarios(
278278
# Wait for result and check if received expected message
279279
try:
280280
msg = await asyncio.wait_for(receiver.receive(), timeout=waiting_time_sec)
281-
except TimeoutError as err:
281+
except TimeoutError:
282282
_logger.error("Test scenario %d failed with timeout error.", idx)
283-
raise err
283+
raise
284284

285285
if scenario.expected_result is None:
286286
assert msg is None
287287
continue
288288

289289
try:
290290
compare_messages(msg, scenario.expected_result)
291-
except AssertionError as err:
291+
except AssertionError:
292292
_logger.error("Test scenario: %d failed.", idx)
293-
raise err
293+
raise
294294

295295

296296
async def test_all_batteries_capacity(
@@ -404,7 +404,8 @@ def compare_messages(msg: Any, expected_msg: Any) -> None:
404404
assert msg_dict == expected_dict
405405

406406

407-
async def run_test_battery_status_channel( # pylint: disable=too-many-arguments
407+
# pylint: disable-next=too-many-arguments,too-many-positional-arguments
408+
async def run_test_battery_status_channel(
408409
battery_status_sender: Sender[ComponentPoolStatus],
409410
battery_pool_metric_receiver: Receiver[T],
410411
all_batteries: set[int],

tests/utils/mock_microgrid_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ def component_graph(self) -> ComponentGraph:
126126
"""
127127
return self._component_graph
128128

129-
async def send(self, data: ComponentData) -> None:
129+
# We need the noqa because the `SenderError` is raised indirectly by `send()`.
130+
async def send(self, data: ComponentData) -> None: # noqa: DOC503
130131
"""Send component data using channel.
131132
132133
This simulates component sending data. Right now only battery and inverter
@@ -136,6 +137,7 @@ async def send(self, data: ComponentData) -> None:
136137
data: Data to be send
137138
138139
Raises:
140+
RuntimeError: if the type of data is not supported.
139141
SenderError: if the underlying channel was closed.
140142
A [ChannelClosedError][frequenz.channels.ChannelClosedError] is
141143
set as the cause.

0 commit comments

Comments
 (0)