Merge ~adam-collard/maas-ci/+git/system-tests:strict-mypy into ~maas-committers/maas-ci/+git/system-tests:master

Proposed by Adam Collard
Status: Merged
Approved by: Adam Collard
Approved revision: a91b80b938d188ca9c0eab2bd2b6d22b5b728540
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~adam-collard/maas-ci/+git/system-tests:strict-mypy
Merge into: ~maas-committers/maas-ci/+git/system-tests:master
Diff against target: 558 lines (+132/-78)
10 files modified
pyproject.toml (+1/-3)
systemtests/api.py (+94/-49)
systemtests/fixtures.py (+5/-5)
systemtests/lxd.py (+6/-5)
systemtests/region.py (+8/-4)
systemtests/subprocess.py (+4/-2)
systemtests/tests/conftest.py (+4/-4)
systemtests/tests/test_basic.py (+3/-1)
systemtests/tests/test_crud.py (+3/-3)
systemtests/utils.py (+4/-2)
Reviewer Review Type Date Requested Status
Alberto Donato (community) Approve
MAAS Lander Approve
Review via email: mp+408010@code.launchpad.net

Commit message

Add strict mypy validation.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b strict-mypy lp:~adam-collard/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: a91b80b938d188ca9c0eab2bd2b6d22b5b728540

review: Approve
Revision history for this message
Alberto Donato (ack) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/pyproject.toml b/pyproject.toml
2index 0c84d1d..cf33498 100644
3--- a/pyproject.toml
4+++ b/pyproject.toml
5@@ -13,8 +13,6 @@ log_file_date_format = "%Y-%m-%d %H:%M:%S"
6
7
8 [tool.mypy]
9-disallow_untyped_calls = true
10-disallow_untyped_defs = true
11-warn_unused_configs = true
12 install_types = true
13 non_interactive = true
14+strict = true
15diff --git a/systemtests/api.py b/systemtests/api.py
16index 4aac548..a82fde4 100644
17--- a/systemtests/api.py
18+++ b/systemtests/api.py
19@@ -29,10 +29,10 @@ class MAASAPIClient:
20 self.pull_file = partial(lxd.pull_file, maas_container)
21 self.push_file = partial(lxd.push_file, maas_container)
22
23- def execute(self, cmd: list[str]) -> CompletedProcess:
24+ def execute(self, cmd: list[str]) -> CompletedProcess[str]:
25 return self.lxd.execute(self.maas_container, ["maas"] + cmd)
26
27- def log_in(self, session: str, token: str) -> CompletedProcess:
28+ def log_in(self, session: str, token: str) -> CompletedProcess[str]:
29 return self.execute(["login", session, self.url, token])
30
31
32@@ -44,7 +44,7 @@ class AuthenticatedAPIClient:
33 def execute(
34 self,
35 cmd: list[str],
36- extra_params: dict[str, str] = None,
37+ extra_params: Optional[dict[str, str]] = None,
38 json_output: bool = True,
39 ) -> Any:
40 if extra_params:
41@@ -52,31 +52,47 @@ class AuthenticatedAPIClient:
42 result = self.api_client.execute([self.session] + cmd)
43 return json.loads(result.stdout) if json_output else result.stdout
44
45- def list_subnets(self) -> list[dict]:
46- return self.execute(["subnets", "read"])
47+ def list_subnets(self) -> list[dict[str, Any]]:
48+ subnets: list[dict[str, Any]] = self.execute(["subnets", "read"])
49+ return subnets
50
51- def create_subnet(self, name: str, cidr: str) -> dict:
52- return self.execute(["subnets", "create", "name=" + name, "cidr=" + cidr])
53+ def create_subnet(self, name: str, cidr: str) -> dict[str, Any]:
54+ subnet: dict[str, Any] = self.execute(
55+ ["subnets", "create", "name=" + name, "cidr=" + cidr]
56+ )
57+ return subnet
58
59 def delete_subnet(self, name: str) -> str:
60- return self.execute(["subnet", "delete", name], json_output=False)
61+ result: str = self.execute(["subnet", "delete", name], json_output=False)
62+ return result
63
64- def list_rack_controllers(self) -> list[dict]:
65- return self.execute(["rack-controllers", "read"])
66+ def list_rack_controllers(self) -> list[dict[str, Any]]:
67+ rack_controllers: list[dict[str, Any]] = self.execute(
68+ ["rack-controllers", "read"]
69+ )
70+ return rack_controllers
71
72- def list_ip_ranges(self) -> list[dict]:
73- return self.execute(["ipranges", "read"])
74+ def list_ip_ranges(self) -> list[dict[str, Any]]:
75+ ip_ranges: list[dict[str, Any]] = self.execute(["ipranges", "read"])
76+ return ip_ranges
77
78 def delete_ip_range(self, range_id: Union[str, int]) -> str:
79- return self.execute(["iprange", "delete", str(range_id)], json_output=False)
80+ result: str = self.execute(
81+ ["iprange", "delete", str(range_id)], json_output=False
82+ )
83+ return result
84
85- def create_ip_range(self, start: str, end: str, range_type: str) -> dict:
86- return self.execute(
87+ def create_ip_range(self, start: str, end: str, range_type: str) -> dict[str, Any]:
88+ ip_range: dict[str, Any] = self.execute(
89 ["ipranges", "create", "type=dynamic", f"start_ip={start}", f"end_ip={end}"]
90 )
91+ return ip_range
92
93- def enable_dhcp(self, fabric: str, vlan: str, primary_rack: dict[str, str]) -> dict:
94- return self.execute(
95+ def enable_dhcp(
96+ self, fabric: str, vlan: str, primary_rack: dict[str, str]
97+ ) -> dict[str, Any]:
98+
99+ vlan_obj: dict[str, Any] = self.execute(
100 [
101 "vlan",
102 "update",
103@@ -86,9 +102,13 @@ class AuthenticatedAPIClient:
104 f"primary_rack={primary_rack['system_id']}",
105 ]
106 )
107+ return vlan_obj
108
109- def disable_dhcp(self, fabric: str, vlan: str) -> Any:
110- return self.execute(["vlan", "update", fabric, vlan, "dhcp_on=False"])
111+ def disable_dhcp(self, fabric: str, vlan: str) -> dict[str, Any]:
112+ vlan_obj: dict[str, Any] = self.execute(
113+ ["vlan", "update", fabric, vlan, "dhcp_on=False"]
114+ )
115+ return vlan_obj
116
117 def import_boot_resources(self) -> str:
118 LOG.debug("Getting latest debug event for watermark")
119@@ -124,46 +144,55 @@ class AuthenticatedAPIClient:
120 return result
121
122 def is_importing_boot_resources(self) -> str:
123- return self.execute(["boot-resources", "is-importing"])
124+ result: str = self.execute(["boot-resources", "is-importing"])
125+ return result
126
127 def list_machines(self, **kwargs: str) -> list[dict[str, Any]]:
128 """
129 machines read -h to know parameters available for kwargs
130 """
131- cmd = ["machines", "read"]
132- return self.execute(cmd, extra_params=kwargs)
133+ machines: list[dict[str, Any]] = self.execute(
134+ ["machines", "read"], extra_params=kwargs
135+ )
136+ return machines
137
138 def list_boot_images(self, rack_controller: dict[str, str]) -> dict[str, str]:
139- return self.execute(
140+ boot_images: dict[str, str] = self.execute(
141 ["rack-controller", "list-boot-images", rack_controller["system_id"]]
142 )
143+ return boot_images
144
145 def import_boot_resources_in_rack(self, rack_controller: dict[str, str]) -> str:
146- return self.execute(
147+ result: str = self.execute(
148 ["rack-controller", "import-boot-images", rack_controller["system_id"]],
149 json_output=False,
150 )
151+ return result
152
153 def commission_machine(self, machine: dict[str, str]) -> dict[str, str]:
154- result = self.execute(["machine", "commission", machine["system_id"]])
155+ result: dict[str, str] = self.execute(
156+ ["machine", "commission", machine["system_id"]]
157+ )
158 assert result["status_name"] == "Commissioning"
159 return result
160
161 def deploy_machine(self, machine: dict[str, str], **kwargs: str) -> dict[str, str]:
162- result = self.execute(
163+ result: dict[str, str] = self.execute(
164 ["machine", "deploy", machine["system_id"]], extra_params=kwargs
165 )
166 assert result["status_name"] == "Deploying"
167 return result
168
169 def create_ssh_key(self, public_key: str) -> None:
170- result = self.execute(["sshkeys", "create", f"key={public_key}"])
171+ result: dict[str, str] = self.execute(
172+ ["sshkeys", "create", f"key={public_key}"]
173+ )
174 assert result["key"] == public_key
175 return
176
177 def release_machine(self, machine: dict[str, str]) -> None:
178 system_id: str = machine["system_id"]
179- result: dict = self.execute(["machine", "release", system_id])
180+ result: dict[str, Any] = self.execute(["machine", "release", system_id])
181 assert result["status_name"] == "Releasing"
182 wait_for_machines(
183 self,
184@@ -238,11 +267,13 @@ class AuthenticatedAPIClient:
185 ["zones", "create", f"name={name}", f"description={description}"]
186 )
187
188- def list_zones(self) -> list[dict]:
189- return self.execute(["zones", "read"])
190+ def list_zones(self) -> list[dict[str, Any]]:
191+ zones: list[dict[str, Any]] = self.execute(["zones", "read"])
192+ return zones
193
194 def read_zone(self, zone_name: str) -> Any:
195- return self.execute(["zone", "read", zone_name])
196+ zone: dict[str, Any] = self.execute(["zone", "read", zone_name])
197+ return zone
198
199 def update_zone(
200 self,
201@@ -256,41 +287,49 @@ class AuthenticatedAPIClient:
202 if new_description is not None:
203 cmd.append(f"description={new_description}")
204
205- return self.execute(cmd)
206+ zone: dict[str, Any] = self.execute(cmd)
207+ return zone
208
209 def delete_zone(self, zone_name: str) -> str:
210 try:
211- return self.execute(["zone", "delete", zone_name], json_output=False)
212+ result: str = self.execute(["zone", "delete", zone_name], json_output=False)
213 except CalledProcessError as err:
214 if "cannot be deleted" in err.stdout:
215 raise CannotDeleteError(err.stdout)
216 else:
217 raise
218+ else:
219+ return result
220
221 def create_pool(self, name: str, description: str) -> Any:
222 return self.execute(
223 ["resource-pools", "create", f"name={name}", f"description={description}"]
224 )
225
226- def list_pools(self) -> list[dict]:
227- return self.execute(["resource-pools", "read"])
228+ def list_pools(self) -> list[dict[str, Any]]:
229+ resource_pools: list[dict[str, Any]] = self.execute(["resource-pools", "read"])
230+ return resource_pools
231
232- def read_pool(self, pool: dict[str, Any]) -> dict:
233- return self.execute(["resource-pool", "read", str(pool["id"])])
234+ def read_pool(self, pool: dict[str, Any]) -> dict[str, Any]:
235+ resource_pool: dict[str, Any] = self.execute(
236+ ["resource-pool", "read", str(pool["id"])]
237+ )
238+ return resource_pool
239
240 def update_pool(
241 self,
242 pool: dict[str, Any],
243 new_name: Optional[str] = None,
244 new_description: Optional[str] = None,
245- ) -> Any:
246+ ) -> dict[str, Any]:
247 cmd = ["resource-pool", "update", str(pool["id"])]
248 if new_name is not None:
249 cmd.append(f"name={new_name}")
250 if new_description is not None:
251 cmd.append(f"description={new_description}")
252
253- return self.execute(cmd)
254+ pool_obj: dict[str, Any] = self.execute(cmd)
255+ return pool_obj
256
257 def delete_pool(self, pool: dict[str, Any]) -> Any:
258 try:
259@@ -303,27 +342,31 @@ class AuthenticatedAPIClient:
260 else:
261 raise
262
263- def create_space(self, name: str) -> Any:
264- return self.execute(["spaces", "create", f"name={name}"])
265+ def create_space(self, name: str) -> dict[str, Any]:
266+ space: dict[str, Any] = self.execute(["spaces", "create", f"name={name}"])
267+ return space
268
269- def list_spaces(self) -> list[dict]:
270- return self.execute(["spaces", "read"])
271+ def list_spaces(self) -> list[dict[str, Any]]:
272+ spaces: list[dict[str, Any]] = self.execute(["spaces", "read"])
273+ return spaces
274
275- def read_space(self, space: dict[str, Any]) -> Any:
276- return self.execute(["space", "read", str(space["id"])])
277+ def read_space(self, space: dict[str, Any]) -> dict[str, Any]:
278+ space_obj: dict[str, Any] = self.execute(["space", "read", str(space["id"])])
279+ return space_obj
280
281 def update_space(
282 self, space: dict[str, Any], new_name: Optional[str] = None
283- ) -> Any:
284+ ) -> dict[str, Any]:
285 cmd = ["space", "update", str(space["id"])]
286 if new_name is not None:
287 cmd.append(f"name={new_name}")
288
289- return self.execute(cmd)
290+ space_obj: dict[str, Any] = self.execute(cmd)
291+ return space_obj
292
293- def delete_space(self, space: dict[str, Any]) -> Any:
294+ def delete_space(self, space: dict[str, Any]) -> str:
295 try:
296- return self.execute(
297+ result: str = self.execute(
298 ["space", "delete", str(space["id"])], json_output=False
299 )
300 except CalledProcessError as err:
301@@ -331,3 +374,5 @@ class AuthenticatedAPIClient:
302 raise CannotDeleteError(err.stdout)
303 else:
304 raise
305+ else:
306+ return result
307diff --git a/systemtests/fixtures.py b/systemtests/fixtures.py
308index fa50527..70278a7 100644
309--- a/systemtests/fixtures.py
310+++ b/systemtests/fixtures.py
311@@ -8,7 +8,7 @@ import tempfile
312 from logging import StreamHandler, getLogger
313 from pathlib import Path
314 from textwrap import dedent
315-from typing import IO, TYPE_CHECKING, Any, Iterator, Optional
316+from typing import TYPE_CHECKING, Any, Iterator, Optional, TextIO
317
318 import paramiko
319 import pytest
320@@ -137,7 +137,7 @@ def maas_deb_repo(
321
322
323 def get_user_data(
324- devices: dict[str, dict[str, Any]], cloud_config: Optional[dict] = None
325+ devices: dict[str, dict[str, Any]], cloud_config: Optional[dict[str, Any]] = None
326 ) -> str:
327 ethernets = {}
328 for name, device in sorted(devices.items()):
329@@ -160,7 +160,7 @@ def get_user_data(
330 }
331 )
332
333- user_data = "#cloud-config\n" + yaml.dump(cloud_config, default_style="|")
334+ user_data: str = "#cloud-config\n" + yaml.dump(cloud_config, default_style="|")
335 return user_data
336
337
338@@ -325,12 +325,12 @@ def maas_api_client(maas_region: MAASRegion) -> Iterator[MAASAPIClient]:
339
340
341 @pytest.fixture
342-def logstream() -> Iterator[IO]:
343+def logstream() -> Iterator[TextIO]:
344 yield io.StringIO()
345
346
347 @pytest.fixture(autouse=True)
348-def testlog(logstream: IO) -> Iterator[Logger]:
349+def testlog(logstream: TextIO) -> Iterator[Logger]:
350 current_test = os.environ.get("PYTEST_CURRENT_TEST")
351 logger = getLogger(f"systemtests.{current_test}")
352 handler = StreamHandler(logstream)
353diff --git a/systemtests/lxd.py b/systemtests/lxd.py
354index a783f95..2c49809 100644
355--- a/systemtests/lxd.py
356+++ b/systemtests/lxd.py
357@@ -35,7 +35,7 @@ class CLILXD:
358 except subprocess.CalledProcessError:
359 return False
360
361- def _run(self, cmd: Union[str, list[str]]) -> subprocess.CompletedProcess:
362+ def _run(self, cmd: Union[str, list[str]]) -> subprocess.CompletedProcess[str]:
363 return run_with_logging(cmd, self.logger)
364
365 def create_container(
366@@ -89,7 +89,7 @@ class CLILXD:
367
368 def pull_file(
369 self, container: str, file_path: str, local_path: str
370- ) -> subprocess.CompletedProcess:
371+ ) -> subprocess.CompletedProcess[str]:
372 return self._run(
373 [
374 "lxc",
375@@ -122,9 +122,9 @@ class CLILXD:
376 def execute(
377 self,
378 container: str,
379- command: list,
380+ command: list[str],
381 environment: Optional[dict[str, str]] = None,
382- ) -> subprocess.CompletedProcess:
383+ ) -> subprocess.CompletedProcess[str]:
384 lxc_command = ["lxc", "exec", "--force-noninteractive", container]
385 if environment is not None:
386 for key, value in environment.items():
387@@ -146,7 +146,8 @@ class CLILXD:
388 for address in entry["state"]["network"]["eth0"]["addresses"]:
389 self.logger.info(f"Considering address: {address}")
390 if address["family"] == "inet":
391- return address["address"]
392+ ip: str = address["address"]
393+ return ip
394 else:
395 raise RuntimeError("Couldn't find an IP address")
396
397diff --git a/systemtests/region.py b/systemtests/region.py
398index 4e96f81..5ca3901 100644
399--- a/systemtests/region.py
400+++ b/systemtests/region.py
401@@ -21,7 +21,7 @@ class MAASRegion:
402 self.maas_container = maas_container
403 self.installed_from_snap = installed_from_snap
404
405- def execute(self, command: list[str]) -> subprocess.CompletedProcess:
406+ def execute(self, command: list[str]) -> subprocess.CompletedProcess[str]:
407 lxd = get_lxd(LOG)
408 return lxd.execute(self.maas_container, command)
409
410@@ -61,7 +61,9 @@ class MAASRegion:
411 "enable_http_proxy": enable_http_proxy,
412 }
413
414- def enable_dhcp(self, config: dict, client: api.AuthenticatedAPIClient) -> None:
415+ def enable_dhcp(
416+ self, config: dict[str, Any], client: api.AuthenticatedAPIClient
417+ ) -> None:
418 rack_controllers = get_rack_controllers(client)
419 for network in config["networks"].values():
420 primary_controller, link = get_dhcp_controller(
421@@ -134,7 +136,9 @@ class MAASRegion:
422 return self.set_config("debug", "True")
423
424
425-def get_dhcp_controller(rack_controllers: list[dict], cidr: str) -> tuple[dict, dict]:
426+def get_dhcp_controller(
427+ rack_controllers: list[dict[str, Any]], cidr: str
428+) -> tuple[dict[str, Any], dict[str, Any]]:
429 for rack_controller in rack_controllers:
430 for interface in rack_controller["interface_set"]:
431 for link in interface["links"]:
432@@ -143,7 +147,7 @@ def get_dhcp_controller(rack_controllers: list[dict], cidr: str) -> tuple[dict,
433 raise AssertionError(f"Couldn't find rack controller managing DHCP for {cidr}")
434
435
436-def get_rack_controllers(client: api.AuthenticatedAPIClient) -> list[dict]:
437+def get_rack_controllers(client: api.AuthenticatedAPIClient) -> list[dict[str, Any]]:
438 """Repeatedly attempt to get rack controllers"""
439 attempts = count(1)
440 for elapsed, _ in retries(timeout=300, delay=10):
441diff --git a/systemtests/subprocess.py b/systemtests/subprocess.py
442index 2c34389..8e4219f 100644
443--- a/systemtests/subprocess.py
444+++ b/systemtests/subprocess.py
445@@ -8,8 +8,10 @@ if TYPE_CHECKING:
446
447
448 def run_with_logging(
449- cmd: Union[str, list[str]], logger: logging.Logger, env: Optional[dict] = None
450-) -> subprocess.CompletedProcess:
451+ cmd: Union[str, list[str]],
452+ logger: logging.Logger,
453+ env: Optional[dict[str, str]] = None,
454+) -> subprocess.CompletedProcess[str]:
455 logger.info("Running command: " + " ".join(cmd))
456 process = subprocess.Popen(
457 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env
458diff --git a/systemtests/tests/conftest.py b/systemtests/tests/conftest.py
459index 7fa063d..7b8163a 100644
460--- a/systemtests/tests/conftest.py
461+++ b/systemtests/tests/conftest.py
462@@ -1,7 +1,7 @@
463 from __future__ import annotations
464
465 import argparse
466-from typing import TYPE_CHECKING, Any, Iterator
467+from typing import TYPE_CHECKING, Any, Iterator, cast
468
469 import pytest
470 import yaml
471@@ -52,14 +52,14 @@ def pytest_addoption(parser: Parser) -> None:
472
473
474 @pytest.fixture(scope="session")
475-def config(request: pytest.FixtureRequest) -> dict:
476+def config(request: pytest.FixtureRequest) -> dict[str, Any]:
477 config_file = request.config.getoption("--ss-config")
478 if not config_file:
479 config_file = open("config.yaml", "r")
480- return yaml.safe_load(config_file) if config_file else {}
481+ return cast(dict[str, Any], yaml.safe_load(config_file)) if config_file else {}
482
483
484-@pytest.hookimpl(tryfirst=True, hookwrapper=True)
485+@pytest.hookimpl(tryfirst=True, hookwrapper=True) # type: ignore
486 def pytest_runtest_makereport(item: Any, call: Any) -> Iterator[Any]:
487 # execute all other hooks to obtain the report object
488 outcome: _Result
489diff --git a/systemtests/tests/test_basic.py b/systemtests/tests/test_basic.py
490index a94c69a..a653921 100644
491--- a/systemtests/tests/test_basic.py
492+++ b/systemtests/tests/test_basic.py
493@@ -19,7 +19,9 @@ if TYPE_CHECKING:
494
495 class TestSetup:
496 @pytest.mark.skip_if_installed_from_snap("Prometheus is installed in the snap")
497- def test_setup_prometheus(self, maas_region: MAASRegion, config: dict) -> None:
498+ def test_setup_prometheus(
499+ self, maas_region: MAASRegion, config: dict[str, str]
500+ ) -> None:
501 result = maas_region.execute(
502 ["apt", "install", "python3-prometheus-client", "-y"]
503 )
504diff --git a/systemtests/tests/test_crud.py b/systemtests/tests/test_crud.py
505index 366f815..0cb76f8 100644
506--- a/systemtests/tests/test_crud.py
507+++ b/systemtests/tests/test_crud.py
508@@ -11,7 +11,7 @@ if TYPE_CHECKING:
509 from ..api import AuthenticatedAPIClient
510
511
512-@test_steps("create", "update", "delete")
513+@test_steps("create", "update", "delete") # type: ignore
514 def test_zone(authenticated_admin: AuthenticatedAPIClient) -> Iterator[None]:
515 authenticated_admin.create_zone(
516 name="test-zone", description="A zone created by system-tests"
517@@ -48,7 +48,7 @@ def test_zone(authenticated_admin: AuthenticatedAPIClient) -> Iterator[None]:
518 yield
519
520
521-@test_steps("create", "update", "delete")
522+@test_steps("create", "update", "delete") # type: ignore
523 def test_resource_pool(authenticated_admin: AuthenticatedAPIClient) -> Iterator[None]:
524 authenticated_admin.create_pool(
525 name="test-pool", description="A resource pool created by system-tests"
526@@ -87,7 +87,7 @@ def test_resource_pool(authenticated_admin: AuthenticatedAPIClient) -> Iterator[
527 yield
528
529
530-@test_steps("create", "update", "delete")
531+@test_steps("create", "update", "delete") # type: ignore
532 def test_spaces(authenticated_admin: AuthenticatedAPIClient) -> Iterator[None]:
533 authenticated_admin.create_space(name="test-space")
534 spaces = authenticated_admin.list_spaces()
535diff --git a/systemtests/utils.py b/systemtests/utils.py
536index 68e17e9..7959492 100644
537--- a/systemtests/utils.py
538+++ b/systemtests/utils.py
539@@ -3,7 +3,7 @@ from __future__ import annotations
540 import random
541 import string
542 import time
543-from typing import TYPE_CHECKING, Any, Callable, Generator, Union
544+from typing import TYPE_CHECKING, Any, Callable, Iterator, Union
545
546 import paramiko
547 from retry.api import retry_call
548@@ -42,7 +42,9 @@ def randomstring(length: int = 10) -> str:
549 return "".join(random.choice(string.ascii_lowercase) for _ in range(length))
550
551
552-def retries(timeout: Union[int, float] = 30, delay: Union[int, float] = 1) -> Generator:
553+def retries(
554+ timeout: Union[int, float] = 30, delay: Union[int, float] = 1
555+) -> Iterator[tuple[float, float]]:
556 """Helper for retrying something, sleeping between attempts.
557
558 Yields ``(elapsed, remaining)`` tuples, giving times in seconds.

Subscribers

People subscribed via source and target branches

to all changes: