diff --git a/.gitignore b/.gitignore index e6921a9..fcf1350 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ .ruff_cache docker-compose.yml .env -letsencrypt \ No newline at end of file +letsencrypt +.coverage +htmlcov +coverage.xml \ No newline at end of file diff --git a/Makefile b/Makefile index b244a94..f122616 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,8 @@ endif UV ?= uv RUFF ?= $(UV) run --active ruff TY ?= $(UV) run --active ty +UNITTEST ?= $(UV) run --active -m unittest +COVERAGE ?= $(UV) run --active coverage DOCKER ?= docker DOCKER_TAG ?= localhost/stapler:latest PORT ?= 8080 @@ -35,6 +37,36 @@ print-%: .venv: uv.lock @$(MAKE) -s uv-sync +# ACTIONS + +.PHONY: install +install: uv-sync ## install project + +.PHONY: update +update: uv-upgrade ## update project dependencies + +.PHONY: format +format: ruff-fix ruff-format ## format project + +.PHONY: lint +lint: ruff ruff-format-check ty ## lint project + +.PHONY: build +build: docker-build ## build project + +.PHONY: start +start: build docker-run ## start server in localhost + +.PHONY: test +test: unittest ## test project + +.PHONY: test-% +test-%: ## test project with specific test + @$(MAKE) -s unittest-$* + +.PHONY: coverage +coverage: coverage-unittest coverage-xml coverage-report ## test project with coverage + # TOOLS .PHONY: uv-sync @@ -65,6 +97,30 @@ ruff-format-check: .venv ## ruff format (check only) ty: .venv ## ty check @$(TY) check +.PHONY: unittest +unittest: .venv ## unittest + @$(UNITTEST) -v + +.PHONY: unittest-% +unittest-%: .venv ## unittest -k [filter] + @$(UNITTEST) -v -k $* + +.PHONY: coverage-unittest +coverage-unittest: .venv ## coverage run -m unittest + @$(COVERAGE) run -m unittest -v + +.PHONY: coverage-report +coverage-report: .venv ## coverage report + @$(COVERAGE) report + +.PHONY: coverage-html +coverage-html: .venv ## coverage html + @$(COVERAGE) html + +.PHONY: coverage-xml +coverage-xml: .venv ## coverage xml + @$(COVERAGE) xml + .PHONY: docker-build docker-build: ## docker build @$(DOCKER) build . -t $(DOCKER_TAG) @@ -72,23 +128,3 @@ docker-build: ## docker build .PHONY: docker-run docker-run: docker-build ## docker run @$(DOCKER) run -it -p $(PORT):80 -v ./data:/data $(DOCKER_TAG) --debug --no-certbot --no-https --host localhost:$(PORT) run - -# ACTIONS - -.PHONY: install -install: uv-sync ## install project - -.PHONY: update -update: uv-upgrade ## update project dependencies - -.PHONY: format -format: ruff-fix ruff-format ## format project - -.PHONY: lint -lint: ruff ruff-format-check ty ## lint project - -.PHONY: build -build: docker-build ## build project - -.PHONY: start -start: build docker-run ## start server in localhost diff --git a/README.md b/README.md index 1e533d5..124ada3 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,15 @@ Usage: make [target1] [target2] ... Commands/Targets: help show this message +install install project +update update project dependencies +format format project +lint lint project +build build project +start start server in localhost +test test project +test-% test project with specific test +coverage test project with coverage uv-sync uv sync uv-upgrade uv sync upgrade ruff ruff check @@ -132,21 +141,12 @@ ruff-fix ruff check (and fix) ruff-format ruff format ruff-format-check ruff format (check only) ty ty check +unittest unittest +unittest-% unittest -k [filter] +coverage-unittest coverage run -m unittest +coverage-report coverage report +coverage-html coverage html +coverage-xml coverage xml docker-build docker build docker-run docker run -install install project -update update project dependencies -format format project -lint lint project -build build project -start start server in localhost - -Environment: -UV = uv -RUFF = uv run --active ruff -TY = uv run --active ty -DOCKER = docker -DOCKER_TAG = localhost/stapler:latest -TOKEN = secret -PORT = 8080 ``` diff --git a/pyproject.toml b/pyproject.toml index 926b2fd..ca07438 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,10 +10,15 @@ dependencies = [ [dependency-groups] dev = [ + "coverage>=7.13.5", "ruff>=0.15.10", "ty>=0.0.29", ] [tool.ruff.lint] select = ["ALL"] -ignore = ["D", "E501", "S104", "PLR2004", "ANN401", "BLE001", "COM812", "S603", "PLR0911"] \ No newline at end of file +ignore = ["D", "E501", "S104", "PLR2004", "ANN401", "BLE001", "COM812", "S603", "PLR0911", "S101", "PT"] + +[tool.coverage.run] +source = ["src"] +omit = ["src/logs.py"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..30cd4e9 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,156 @@ +import contextlib +import pathlib +import tempfile +import typing +import unittest +import unittest.mock + + +class BaseTestCase(unittest.TestCase): + @typing.override + def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: + self.mocks: list[unittest.mock.Mock] = [] + self.tmp_dir: tempfile.TemporaryDirectory | None = None + self.tmp_path = pathlib.Path() + super().__init__(*args, **kwargs) + + @typing.override + def tearDown(self) -> None: + if self.tmp_dir is not None: + self.tmp_dir.cleanup() + self.tmp_dir = None + super().tearDown() + + def get_tmp_dir(self) -> str: + self.tmp_dir = tempfile.TemporaryDirectory(delete=False) + self.tmp_path = pathlib.Path(self.tmp_dir.name) + return self.tmp_dir.name + + def mock(self, spec: type | None = None) -> unittest.mock.Mock: + mock = unittest.mock.Mock(spec) + self.mocks += [mock] + return mock + + @contextlib.contextmanager + def patch( + self, target: str, return_value: typing.Any = None, count: int = 1 + ) -> typing.Iterator[unittest.mock.Mock]: + with unittest.mock.patch( + target, return_value=return_value, create=True + ) as mock: + yield mock + self.assertEqual(mock.call_count, count, mock) + + @contextlib.contextmanager + def patch_calls( + self, + target: str, + args: list[typing.Iterable[typing.Any]] | None = None, + return_values: list[typing.Any] | None = None, + ) -> typing.Iterator[unittest.mock.Mock]: + if args is None: + args = [[]] + if return_values is None: + return_values = [None] * len(args) + with unittest.mock.patch( + target, side_effect=return_values, create=True + ) as mock: + yield mock + self.__check_calls(mock, args) + + @contextlib.contextmanager + def patch_call( + self, + target: str, + args: typing.Iterable[typing.Any] | None = None, + return_value: typing.Any = None, + ) -> typing.Iterator[unittest.mock.Mock]: + if args is None: + args = [] + with self.patch_calls(target, [args], [return_value]) as mock: + yield mock + + @contextlib.contextmanager + def seal_mocks(self, *extra_mocks: unittest.mock.Mock) -> typing.Iterator[None]: + for mock in self.mocks: + unittest.mock.seal(mock) + for mock in extra_mocks: + unittest.mock.seal(mock) + yield + + @contextlib.contextmanager + def mock_calls( + self, + mock: unittest.mock.Mock, + args: list[typing.Iterable[typing.Any]] | None = None, + return_values: list[typing.Any] | None = None, + ) -> typing.Iterator[None]: + if args is None: + args = [[]] + if return_values is None: + return_values = [None] * len(args) + mock.side_effect = return_values + mock.reset_mock() + yield + self.__check_calls(mock, args) + + @contextlib.contextmanager + def mock_call( + self, + mock: unittest.mock.Mock, + args: typing.Iterable[typing.Any] | None = None, + return_value: typing.Any = None, + ) -> typing.Iterator[None]: + if args is None: + args = [] + with self.mock_calls(mock, [args], [return_value]): + yield + + @contextlib.contextmanager + def mock_calls_unchecked( + self, + mock: unittest.mock.Mock, + count: int = 1, + return_values: list[typing.Any] | None = None, + ) -> typing.Iterator[None]: + if return_values is None: + return_values = [None] * count + mock.side_effect = return_values + mock.reset_mock() + yield + self.assertEqual(mock.call_count, count, mock) + + @contextlib.contextmanager + def mock_call_unchecked( + self, + mock: unittest.mock.Mock, + return_value: typing.Any = None, + ) -> typing.Iterator[None]: + with self.mock_calls_unchecked(mock, 1, [return_value]): + yield + + def assert_file_content(self, file: pathlib.Path, *expected_content: str) -> None: + assert file.parent.is_dir(), file + assert file.exists(), file + assert file.is_file(), file + with file.open() as file_content: + self.assertEqual(file_content.read(), "\n".join(expected_content)) + + def __check_calls( + self, + mock: unittest.mock.Mock, + args: list[typing.Iterable[typing.Any]], + ) -> None: + for i, values in enumerate( + zip( + mock.mock_calls + + [None] + * (max(len(args), len(mock.method_calls)) - len(mock.mock_calls)), + args + [[]] * (max(len(args), len(mock.method_calls)) - len(args)), + strict=False, + ) + ): + real_call, expected_args = values + self.assertEqual( + real_call, unittest.mock.call(*expected_args), f"{i + 1}: {mock}" + ) diff --git a/uv.lock b/uv.lock index 32694bf..3efbfb2 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,45 @@ version = 1 revision = 3 requires-python = ">=3.14" +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + [[package]] name = "ruff" version = "0.15.10" @@ -37,6 +76,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "coverage" }, { name = "ruff" }, { name = "ty" }, ] @@ -46,6 +86,7 @@ requires-dist = [{ name = "toml", specifier = ">=0.10.2" }] [package.metadata.requires-dev] dev = [ + { name = "coverage", specifier = ">=7.13.5" }, { name = "ruff", specifier = ">=0.15.10" }, { name = "ty", specifier = ">=0.0.29" }, ]