Skip to content

Commit a7aedc8

Browse files
authored
Fix mypy invocation in noxfile (#219)
When invoking `mypy`, the `src` directory (`frequenz.sdk` package) isn't being checked because 2 bugs: * The `src` path is excluded * A transformation of `src` into a package name was a ill done copy&paste from the channels repository, and said `frequenz.channels` instead of `frequenz.sdk` This commit fixes this issue by reworking how the conversion between paths and packages needed by `mypy` is done. Now the conversion is more generic and explicit, providing a list of paths and a mapping between paths and packages. This is a bit more verbose to declare but it can be reused more safely and gives some extra flexibility, which can be handy when we move this to a general tooling repo. We also use package names for plain python files (like `noxfile.py`), so we don't need to invoke `mypy` twice. Also fixes most of the typing errors that sneaked in the SDK while the type checking wasn't running, except for the `formula_enging` package which has too many errors and will be addressed in separate PR.
2 parents ab0f903 + 8f3b555 commit a7aedc8

File tree

18 files changed

+208
-112
lines changed

18 files changed

+208
-112
lines changed

noxfile.py

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,29 @@
5050
nox -R -e pytest_min -- -s -x tests/timeseries/test_logical_meter.py
5151
"""
5252

53-
from typing import List
53+
from __future__ import annotations
54+
55+
from typing import Any, Iterable
5456

5557
import nox
5658
import toml
5759

60+
DEFAULT_PATH_PACKAGES = {
61+
"benchmarks": "benchmarks",
62+
"docs": "docs",
63+
"examples": "examples",
64+
"src": "frequenz.sdk",
65+
"tests": "tests",
66+
"noxfile.py": "noxfile",
67+
}
68+
"""A list of path to be used by default and its corresponding package name.
69+
70+
The package name is needed for mypy, as it takes packages when full import
71+
checking needs to be done.
72+
"""
73+
5874

59-
def min_dependencies() -> List[str]:
75+
def min_dependencies() -> list[str]:
6076
"""Extract the minimum dependencies from pyproject.toml.
6177
6278
Raises:
@@ -74,7 +90,7 @@ def min_dependencies() -> List[str]:
7490
if not dependencies:
7591
raise RuntimeError(f"No dependencies found in file: {toml_file.name}")
7692

77-
min_deps: List[str] = []
93+
min_deps: list[str] = []
7894
for dep in dependencies:
7995
min_dep = dep.split(",")[0]
8096
if any(op in min_dep for op in (">=", "==")):
@@ -84,7 +100,7 @@ def min_dependencies() -> List[str]:
84100
return min_deps
85101

86102

87-
def _source_file_paths(session: nox.Session) -> List[str]:
103+
def _source_file_paths(session: nox.Session) -> list[str]:
88104
"""Return the file paths to run the checks on.
89105
90106
If positional arguments are present in the nox session, we use those as the
@@ -98,14 +114,7 @@ def _source_file_paths(session: nox.Session) -> List[str]:
98114
"""
99115
if session.posargs:
100116
return session.posargs
101-
return [
102-
"benchmarks",
103-
"docs",
104-
"examples",
105-
"src",
106-
"tests",
107-
"noxfile.py",
108-
]
117+
return list(DEFAULT_PATH_PACKAGES.keys())
109118

110119

111120
# Run all checks except `ci_checks` by default. When running locally with just
@@ -131,7 +140,7 @@ def ci_checks_max(session: nox.Session) -> None:
131140
Args:
132141
session: the nox session.
133142
"""
134-
session.install(".[dev]")
143+
session.install("-e", ".[dev]")
135144

136145
formatting(session, False)
137146
mypy(session, False)
@@ -149,7 +158,7 @@ def formatting(session: nox.Session, install_deps: bool = True) -> None:
149158
install_deps: True if dependencies should be installed.
150159
"""
151160
if install_deps:
152-
session.install(".[format]")
161+
session.install("-e", ".[format]")
153162

154163
paths = _source_file_paths(session)
155164
session.run("black", "--check", *paths)
@@ -169,36 +178,24 @@ def mypy(session: nox.Session, install_deps: bool = True) -> None:
169178
# fast local tests with `nox -R -e mypy`.
170179
session.install("-e", ".[mypy]")
171180

172-
common_args = [
181+
def _flatten(iterable: Iterable[Iterable[Any]]) -> Iterable[Any]:
182+
return [item for sublist in iterable for item in sublist]
183+
184+
args = (
185+
session.posargs
186+
if session.posargs
187+
else _flatten(("-p", p) for p in DEFAULT_PATH_PACKAGES.values())
188+
)
189+
190+
session.run(
191+
"mypy",
173192
"--install-types",
174193
"--namespace-packages",
175194
"--non-interactive",
176195
"--explicit-package-bases",
177196
"--strict",
178-
]
179-
180-
# If we have session arguments, we just use those...
181-
if session.posargs:
182-
session.run("mypy", *common_args, *session.posargs)
183-
return
184-
185-
check_paths = _source_file_paths(session)
186-
pkg_paths = [
187-
path
188-
for path in check_paths
189-
if not path.startswith("src") and not path.endswith(".py")
190-
]
191-
file_paths = [path for path in check_paths if path.endswith(".py")]
192-
193-
pkg_args = []
194-
for pkg in pkg_paths:
195-
if pkg == "src":
196-
pkg = "frequenz.channels"
197-
pkg_args.append("-p")
198-
pkg_args.append(pkg)
199-
200-
session.run("mypy", *common_args, *pkg_args)
201-
session.run("mypy", *common_args, *file_paths)
197+
*args,
198+
)
202199

203200

204201
@nox.session
@@ -231,7 +228,7 @@ def docstrings(session: nox.Session, install_deps: bool = True) -> None:
231228
install_deps: True if dependencies should be installed.
232229
"""
233230
if install_deps:
234-
session.install(".[docs-lint]")
231+
session.install("-e", ".[docs-lint]")
235232

236233
paths = _source_file_paths(session)
237234
session.run("pydocstyle", *paths)

pyproject.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ dependencies = [
3333
"networkx >= 2.8, < 3",
3434
"pandas >= 1.5.2, < 2",
3535
"protobuf >= 4.21.6, < 5",
36-
"pyarrow >= 10.0.1, < 11",
3736
"pydantic >= 1.9",
3837
"sympy >= 1.10.1, < 2",
3938
"toml >= 0.10",
@@ -59,6 +58,7 @@ docs-gen = [
5958
docs-lint = [
6059
"pydocstyle",
6160
"darglint",
61+
"tomli", # Needed by pydocstyle to read pyproject.toml
6262
]
6363
format = [
6464
"black",
@@ -135,10 +135,19 @@ src_paths = ["src", "examples", "tests"]
135135
asyncio_mode = "auto"
136136
required_plugins = [ "pytest-asyncio", "pytest-mock" ]
137137

138+
[tool.mypy]
139+
# This is a temporary hack, for details see:
140+
# * https://github.com/frequenz-floss/frequenz-sdk-python/pull/219
141+
# * https://github.com/frequenz-floss/frequenz-sdk-python/issues/226
142+
exclude = ["src/frequenz/sdk/timeseries/_formula_engine/"]
143+
138144
[[tool.mypy.overrides]]
139145
module = [
140146
"grpc.aio",
141-
"grpc.aio.*"
147+
"grpc.aio.*",
148+
# There is a stubs package available, but it's not working:
149+
# https://github.com/eggplants/networkx-stubs/issues/1
150+
"networkx",
142151
]
143152
ignore_missing_imports = true
144153

src/frequenz/sdk/_internal/asyncio.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33

44
"""General purpose async tools."""
55

6+
from __future__ import annotations
7+
68
import asyncio
79
from abc import ABC
10+
from typing import Any
811

912

10-
async def cancel_and_await(task: asyncio.Task) -> None:
13+
async def cancel_and_await(task: asyncio.Task[Any]) -> None:
1114
"""Cancel a task and wait for it to finish.
1215
1316
The `CancelledError` is suppresed, but any other exception will be propagated.

src/frequenz/sdk/actor/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33

44
"""A base class for creating simple composable actors."""
55

6+
from ..timeseries._resampling import ResamplerConfig
67
from ._channel_registry import ChannelRegistry
78
from ._config_managing import ConfigManagingActor
89
from ._data_sourcing import ComponentMetricRequest, DataSourcingActor
910
from ._decorator import actor
10-
from ._resampling import ComponentMetricsResamplingActor, ResamplerConfig
11+
from ._resampling import ComponentMetricsResamplingActor
1112

1213
__all__ = [
1314
"ChannelRegistry",

src/frequenz/sdk/actor/_resampling.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ async def run(self) -> None:
105105
106106
# noqa: DAR401 error
107107
"""
108-
tasks_to_cancel: set[asyncio.Task] = set()
108+
tasks_to_cancel: set[asyncio.Task[None]] = set()
109109
try:
110110
subscriptions_task = asyncio.create_task(
111111
self._process_resampling_requests()

src/frequenz/sdk/actor/power_distributing/_battery_pool_status.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class _BatteryStatusChannelHelper:
5454
battery_id: int
5555
"""Id of the battery for which we should create channel."""
5656

57-
def __post_init__(self):
57+
def __post_init__(self) -> None:
5858
self.name: str = f"battery-{self.battery_id}-status"
5959
channel = Broadcast[Status](self.name)
6060

src/frequenz/sdk/actor/power_distributing/_battery_status.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class _BlockingStatus:
7979
min_duration_sec: float
8080
max_duration_sec: float
8181

82-
def __post_init__(self):
82+
def __post_init__(self) -> None:
8383
self.last_blocking_duration_sec: float = self.min_duration_sec
8484
self.blocked_until: Optional[datetime] = None
8585

@@ -114,7 +114,7 @@ def block(self) -> float:
114114

115115
return self.last_blocking_duration_sec
116116

117-
def unblock(self):
117+
def unblock(self) -> None:
118118
"""Unblock battery.
119119
120120
This will reset duration of the next blocking timeout.
@@ -186,24 +186,26 @@ def __init__( # pylint: disable=too-many-arguments
186186
self._max_data_age = max_data_age_sec
187187
# First battery is considered as not working.
188188
# Change status after first messages are received.
189-
self._last_status = Status.NOT_WORKING
190-
self._blocking_status = _BlockingStatus(1.0, max_blocking_duration_sec)
189+
self._last_status: Status = Status.NOT_WORKING
190+
self._blocking_status: _BlockingStatus = _BlockingStatus(
191+
1.0, max_blocking_duration_sec
192+
)
191193

192194
inverter_id = self._find_adjacent_inverter_id(battery_id)
193195
if inverter_id is None:
194196
raise RuntimeError(f"Can't find inverter adjacent to battery: {battery_id}")
195197

196-
self._battery = _ComponentStreamStatus(
198+
self._battery: _ComponentStreamStatus = _ComponentStreamStatus(
197199
battery_id, data_recv_timer=Timer(max_data_age_sec)
198200
)
199-
self._inverter = _ComponentStreamStatus(
201+
self._inverter: _ComponentStreamStatus = _ComponentStreamStatus(
200202
inverter_id, data_recv_timer=Timer(max_data_age_sec)
201203
)
202204

203205
# Select needs receivers that can be get in async way only.
204-
self._select = None
206+
self._select: Select | None = None
205207

206-
self._task = asyncio.create_task(
208+
self._task: asyncio.Task[None] = asyncio.create_task(
207209
self._run(status_sender, set_power_result_receiver)
208210
)
209211

src/frequenz/sdk/actor/power_distributing/power_distributing.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def __init__(
189189
max_data_age_sec=10.0,
190190
)
191191

192-
def _create_users_tasks(self) -> List[asyncio.Task[Empty]]:
192+
def _create_users_tasks(self) -> List[asyncio.Task[None]]:
193193
"""For each user create a task to wait for request.
194194
195195
Returns:
@@ -298,6 +298,7 @@ async def run(self) -> None:
298298
api, distribution, request.request_timeout_sec
299299
)
300300

301+
response: Success | PartialFailure
301302
if len(failed_batteries) > 0:
302303
succeed_batteries = set(battery_distribution.keys()) - failed_batteries
303304
response = PartialFailure(

src/frequenz/sdk/microgrid/_graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def __init__(
139139
InvalidGraphError: if `components` and `connections` are not both `None`
140140
and either of them is either `None` or empty
141141
"""
142-
self._graph = nx.DiGraph()
142+
self._graph: nx.DiGraph = nx.DiGraph()
143143

144144
if components is None and connections is None:
145145
return

0 commit comments

Comments
 (0)