Skip to content

Commit 97459ce

Browse files
noxfile, README: Add per-class coverage check functionality
- Updated `noxfile.py` with a new `class-coverage` session to analyze coverage for specified client classes using `coverage.json`. - Updated README with instructions for using `class-coverage` to monitor per-function coverage and reuse existing coverage data without rerunning tests. - Added utility methods in `noxfile.py` to streamline dependency installation, pytest argument management, and coverage file handling. Assisted-by: Codex
1 parent 84482a7 commit 97459ce

2 files changed

Lines changed: 122 additions & 32 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ Run the test suite with:
3737
uv run pytest
3838
```
3939

40+
Check per-function coverage for the client classes:
41+
42+
```bash
43+
uvx nox -s class-coverage
44+
```
45+
46+
To reuse an existing `coverage/py<version>/coverage.json` without rerunning
47+
tests, add `-- --no-tests` (and optional `--coverage-json path`).
48+
4049
Check sync/async parity for changed tests (defaults to `upstream/main..HEAD`):
4150

4251
```bash

noxfile.py

Lines changed: 113 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,74 @@
1717
PROJECT_ROOT = Path(__file__).resolve().parent
1818
DEFAULT_EXAMPLE_PYTHON = "3.11"
1919
EXAMPLES_DIR = PROJECT_ROOT / "examples"
20+
DEFAULT_COVERAGE_CLASSES = ("PdfRestClient", "AsyncPdfRestClient")
21+
22+
23+
def _install_test_dependencies(session: nox.Session) -> None:
24+
_ = session.run_install(
25+
"uv",
26+
"sync",
27+
"--no-default-groups",
28+
"--group=dev",
29+
"--reinstall-package=pdfrest",
30+
f"--python={session.virtualenv.location}",
31+
env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location},
32+
)
33+
34+
35+
def _coverage_dir_for_session(session: nox.Session) -> Path:
36+
coverage_dir = PROJECT_ROOT / "coverage" / f"py{session.python}"
37+
coverage_dir.mkdir(parents=True, exist_ok=True)
38+
return coverage_dir
39+
40+
41+
def _pytest_args_from_session(session: nox.Session) -> list[str]:
42+
parser = argparse.ArgumentParser(add_help=False)
43+
_ = parser.add_argument("--no-parallel", action="store_true")
44+
_ = parser.add_argument("-n", "--workers", "--numprocesses")
45+
custom, remaining = parser.parse_known_args(session.posargs)
46+
47+
pytest_args = list(remaining)
48+
49+
if custom.no_parallel:
50+
return pytest_args
51+
if custom.workers:
52+
pytest_args[:0] = ["-n", custom.workers, "--maxschedchunk", "2"]
53+
else:
54+
pytest_args[:0] = ["-n", "8", "--maxschedchunk", "2"]
55+
56+
return pytest_args
57+
58+
59+
def _run_pytest_with_coverage(session: nox.Session, pytest_args: Iterable[str]) -> Path:
60+
coverage_dir = _coverage_dir_for_session(session)
61+
htmlcov_dir = coverage_dir / "html"
62+
xml_report = coverage_dir / "coverage.xml"
63+
md_report = coverage_dir / "coverage.md"
64+
json_report = coverage_dir / "coverage.json"
65+
_ = session.run(
66+
"pytest",
67+
"--cov=pdfrest",
68+
"--cov-report=term-missing",
69+
f"--cov-report=html:{htmlcov_dir}",
70+
f"--cov-report=xml:{xml_report}",
71+
f"--cov-report=markdown:{md_report}",
72+
f"--cov-report=json:{json_report}",
73+
*pytest_args,
74+
)
75+
return coverage_dir
76+
77+
78+
def _parse_class_values(values: Iterable[str]) -> list[str]:
79+
classes: list[str] = []
80+
for value in values:
81+
if not value:
82+
continue
83+
for item in value.split(","):
84+
item = item.strip()
85+
if item:
86+
classes.append(item)
87+
return classes
2088

2189

2290
@dataclass(frozen=True)
@@ -162,48 +230,61 @@ def _infer_python_version_from_path(script: Path) -> str | None:
162230

163231
@nox.session(name="tests", python=python_versions, reuse_venv=True)
164232
def tests(session: nox.Session) -> None:
165-
# Define only custom flags
233+
pytest_args = _pytest_args_from_session(session)
234+
235+
_install_test_dependencies(session)
236+
_ = _run_pytest_with_coverage(session, pytest_args)
237+
238+
239+
@nox.session(name="class-coverage", python=python_versions, reuse_venv=True)
240+
def class_coverage(session: nox.Session) -> None:
166241
parser = argparse.ArgumentParser(add_help=False)
167242
_ = parser.add_argument("--no-parallel", action="store_true")
168-
_ = parser.add_argument(
169-
"-n", "--workers", "--numprocesses"
170-
) # e.g., -n 4 to set workers
243+
_ = parser.add_argument("-n", "--workers", "--numprocesses")
244+
_ = parser.add_argument("--no-tests", action="store_true")
245+
_ = parser.add_argument("--coverage-json", type=Path, default=None)
246+
_ = parser.add_argument("--markdown-report", type=Path, default=None)
247+
_ = parser.add_argument("--fail-under", type=float, default=90.0)
248+
_ = parser.add_argument("--class", dest="classes", action="append", default=[])
249+
_ = parser.add_argument("--classes", dest="classes_csv", default="")
171250
custom, remaining = parser.parse_known_args(session.posargs)
172251

173252
pytest_args = list(remaining)
253+
if not custom.no_parallel:
254+
if custom.workers:
255+
pytest_args[:0] = ["-n", custom.workers, "--maxschedchunk", "2"]
256+
else:
257+
pytest_args[:0] = ["-n", "8", "--maxschedchunk", "2"]
174258

175-
# Default to parallel unless disabled or overridden
176-
if custom.no_parallel:
177-
pass
178-
elif custom.workers:
179-
pytest_args[:0] = ["-n", custom.workers, "--maxschedchunk", "2"]
259+
if custom.no_tests:
260+
coverage_dir = _coverage_dir_for_session(session)
180261
else:
181-
pytest_args[:0] = ["-n", "8", "--maxschedchunk", "2"]
262+
_install_test_dependencies(session)
263+
coverage_dir = _run_pytest_with_coverage(session, pytest_args)
182264

183-
_ = session.run_install(
184-
"uv",
185-
"sync",
186-
"--no-default-groups",
187-
"--group=dev",
188-
"--reinstall-package=pdfrest",
189-
f"--python={session.virtualenv.location}",
190-
env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location},
191-
)
192-
coverage_dir = PROJECT_ROOT / "coverage" / f"py{session.python}"
193-
coverage_dir.mkdir(parents=True, exist_ok=True)
194-
htmlcov_dir = coverage_dir / "html"
195-
xml_report = coverage_dir / "coverage.xml"
196-
md_report = coverage_dir / "coverage.md"
197-
_ = session.run(
198-
"pytest",
199-
"--cov=pdfrest",
200-
"--cov-report=term-missing",
201-
f"--cov-report=html:{htmlcov_dir}",
202-
f"--cov-report=xml:{xml_report}",
203-
f"--cov-report=markdown:{md_report}",
204-
*pytest_args,
265+
coverage_json = custom.coverage_json or (coverage_dir / "coverage.json")
266+
markdown_report = custom.markdown_report or (
267+
coverage_dir / "class-function-coverage.md"
205268
)
206269

270+
classes = _parse_class_values([*custom.classes, custom.classes_csv])
271+
if not classes:
272+
classes = list(DEFAULT_COVERAGE_CLASSES)
273+
274+
script_args = [
275+
"python",
276+
str(PROJECT_ROOT / "scripts" / "check_class_function_coverage.py"),
277+
str(coverage_json),
278+
"--fail-under",
279+
f"{custom.fail_under}",
280+
"--markdown-report",
281+
str(markdown_report),
282+
]
283+
for class_name in classes:
284+
script_args.extend(["--class", class_name])
285+
286+
_ = session.run(*script_args)
287+
207288

208289
@nox.session(name="examples", python=python_versions, reuse_venv=True)
209290
def run_examples(session: nox.Session) -> None:

0 commit comments

Comments
 (0)