chore: template
This commit is contained in:
@@ -1,32 +0,0 @@
|
|||||||
name: Python Test CI
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: test-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- '.github/workflows/test.yml'
|
|
||||||
- 'pyproject.toml'
|
|
||||||
- 'uv.lock'
|
|
||||||
- '.python-version'
|
|
||||||
- '**/*.py'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
coverage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
- name: Set up UV
|
|
||||||
uses: actions/setup-uv@v1
|
|
||||||
- name: Run tests with coverage
|
|
||||||
run: uv run coverage run -m unittest -v
|
|
||||||
- name: Generate coverage XML report
|
|
||||||
run: uv run coverage xml
|
|
||||||
- name: Create coverage summary
|
|
||||||
uses: actions/code-coverage-summary@v1.3.0
|
|
||||||
with:
|
|
||||||
filename: coverage.xml
|
|
||||||
@@ -1,11 +1 @@
|
|||||||
[](https://git.klemek.fr/klemek/python-template/actions?workflow=lint.yml) [](https://git.klemek.fr/klemek/python-template/actions?workflow=test.yml)
|
[](https://git.klemek.fr/klemek/tout-doux/actions?workflow=lint.yml)
|
||||||
|
|
||||||
|
|
||||||
# Python Template
|
|
||||||
|
|
||||||
## Use this template
|
|
||||||
|
|
||||||
- [ ] 1. rename foler `sample` to match project
|
|
||||||
- [ ] 2. rename project in `pyproject.toml`
|
|
||||||
- [ ] 3. update `README.md`
|
|
||||||
- [ ] 4. enjoy
|
|
||||||
|
|||||||
+5
-9
@@ -1,13 +1,13 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "sample" # TODO: 2. rename project
|
name = "tout-doux"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "Add your description here"
|
description = ""
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
sample = "sample.__main__:main" # TODO: 2. rename project
|
tout-doux = "app.__main__:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["uv_build>=0.11.7,<0.12"]
|
requires = ["uv_build>=0.11.7,<0.12"]
|
||||||
@@ -26,18 +26,14 @@ add-bounds = "major"
|
|||||||
|
|
||||||
[tool.uv.build-backend]
|
[tool.uv.build-backend]
|
||||||
module-root = ""
|
module-root = ""
|
||||||
module-name = "sample" # TODO: 2. rename project
|
module-name = "app"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["ALL"]
|
select = ["ALL"]
|
||||||
ignore = ["D", "E501", "ANN401", "BLE001", "COM812", "S101", "PT"]
|
ignore = ["D", "E501", "ANN401", "BLE001", "COM812", "S101", "PT"]
|
||||||
|
|
||||||
[tool.ty.src]
|
[tool.ty.src]
|
||||||
include = ["sample", "tests"] # TODO: 2. rename project
|
include = ["app", "tests"]
|
||||||
|
|
||||||
[tool.ty.environment]
|
[tool.ty.environment]
|
||||||
python = "./.venv/bin/python3"
|
python = "./.venv/bin/python3"
|
||||||
|
|
||||||
[tool.coverage.run]
|
|
||||||
source = ["sample"] # TODO: 2. rename project
|
|
||||||
omit = ["sample/logs.py", "sample/__main__.py"]
|
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
import contextlib
|
|
||||||
import pathlib
|
|
||||||
import tempfile
|
|
||||||
import typing
|
|
||||||
import unittest
|
|
||||||
import unittest.mock
|
|
||||||
|
|
||||||
__import__("sys").modules["unittest.util"]._MAX_LENGTH = 999999999 # ty:ignore[unresolved-attribute] # noqa: SLF001
|
|
||||||
|
|
||||||
|
|
||||||
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 = 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 new_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,
|
|
||||||
kwargs: list[dict[str, typing.Any]] | None = None,
|
|
||||||
) -> typing.Iterator[unittest.mock.Mock]:
|
|
||||||
if args is None:
|
|
||||||
args = [[]]
|
|
||||||
if return_values is None:
|
|
||||||
return_values = [None] * len(args)
|
|
||||||
if kwargs is None:
|
|
||||||
kwargs = [{}] * len(args)
|
|
||||||
with unittest.mock.patch(
|
|
||||||
target, side_effect=return_values, create=True
|
|
||||||
) as mock:
|
|
||||||
yield mock
|
|
||||||
self.__check_calls(mock, args, kwargs)
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def patch_call(
|
|
||||||
self,
|
|
||||||
target: str,
|
|
||||||
args: typing.Iterable[typing.Any] | None = None,
|
|
||||||
return_value: typing.Any = None,
|
|
||||||
kwargs: dict[str, typing.Any] | None = None,
|
|
||||||
) -> typing.Iterator[unittest.mock.Mock]:
|
|
||||||
if args is None:
|
|
||||||
args = []
|
|
||||||
if kwargs is None:
|
|
||||||
kwargs = {}
|
|
||||||
with self.patch_calls(target, [args], [return_value], [kwargs]) 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,
|
|
||||||
kwargs: list[dict[str, typing.Any]] | None = None,
|
|
||||||
) -> typing.Iterator[None]:
|
|
||||||
if args is None:
|
|
||||||
args = [[]]
|
|
||||||
if return_values is None:
|
|
||||||
return_values = [None] * len(args)
|
|
||||||
if kwargs is None:
|
|
||||||
kwargs = [{}] * len(args)
|
|
||||||
mock.side_effect = return_values
|
|
||||||
mock.reset_mock()
|
|
||||||
yield
|
|
||||||
self.__check_calls(mock, args, kwargs)
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def mock_call(
|
|
||||||
self,
|
|
||||||
mock: unittest.mock.Mock,
|
|
||||||
args: typing.Iterable[typing.Any] | None = None,
|
|
||||||
return_value: typing.Any = None,
|
|
||||||
kwargs: dict[str, typing.Any] | None = None,
|
|
||||||
) -> typing.Iterator[None]:
|
|
||||||
if args is None:
|
|
||||||
args = []
|
|
||||||
if kwargs is None:
|
|
||||||
kwargs = {}
|
|
||||||
with self.mock_calls(mock, [args], [return_value], [kwargs]):
|
|
||||||
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]],
|
|
||||||
kwargs: list[dict[str, typing.Any]],
|
|
||||||
) -> None:
|
|
||||||
total_rows = max(len(args), len(mock.method_calls), len(kwargs))
|
|
||||||
missing_calls = max(0, total_rows - len(mock.mock_calls))
|
|
||||||
missing_args = max(0, total_rows - len(args))
|
|
||||||
missing_kwargs = max(0, total_rows - len(kwargs))
|
|
||||||
for i, values in enumerate(
|
|
||||||
zip(
|
|
||||||
mock.mock_calls + [None] * missing_calls,
|
|
||||||
args + [[]] * missing_args,
|
|
||||||
kwargs + [{}] * missing_kwargs,
|
|
||||||
strict=False,
|
|
||||||
)
|
|
||||||
):
|
|
||||||
real_call, expected_args, expected_kwargs = values
|
|
||||||
self.assertEqual(
|
|
||||||
real_call,
|
|
||||||
unittest.mock.call(*expected_args, **expected_kwargs),
|
|
||||||
f"{i + 1}: {mock}",
|
|
||||||
)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from . import BaseTestCase
|
|
||||||
|
|
||||||
|
|
||||||
class TestSample(BaseTestCase):
|
|
||||||
def test_sample(self) -> None:
|
|
||||||
self.skipTest("TODO")
|
|
||||||
@@ -128,7 +128,7 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sample"
|
name = "tout-doux"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
|
|
||||||
@@ -144,10 +144,10 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "coverage", specifier = ">=7.13.5" },
|
{ name = "coverage", specifier = ">=7.13.5,<8.0.0" },
|
||||||
{ name = "pytest", specifier = ">=9.0.3" },
|
{ name = "pytest", specifier = ">=9.0.3,<10.0.0" },
|
||||||
{ name = "ruff", specifier = ">=0.15.10" },
|
{ name = "ruff", specifier = ">=0.15.12,<0.16.0" },
|
||||||
{ name = "ty", specifier = ">=0.0.29" },
|
{ name = "ty", specifier = ">=0.0.34,<0.0.35" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user