description: Set up the project with uv, run the checks, and the two testing traps: sync and async modules, and greenlet-aware coverage.¶
Contributing¶
Thanks for taking the time. Issues, questions and pull requests are all welcome.
Getting set up¶
The project uses uv. It manages the interpreter as well as the dependencies, so there is nothing to install first beyond uv itself.
git clone https://github.com/uNickz/WebSite2PDF
cd WebSite2PDF
uv sync --all-extras --all-groups
uv run playwright install chromium
Install the git hooks so the same checks CI runs also run before each commit:
The checks¶
uv run ruff check . # lint
uv run ruff format . # format
uv run mypy # types, strict
uv run pytest # tests
uv run pytest --cov # tests with the coverage gate
uv run pre-commit run --all-files runs the lot in one go.
CI requires all of them, plus a coverage floor of 85%.
Tests¶
Tests split in two:
tests/unit/never launches a browser and runs in well under a second.tests/integration/drives real Chromium against fixtures served over loopback fromtests/fixtures/. No test may reach the public internet, the 0.x suite convertedpypi.orgon every run, which made a red build ambiguous and a green one close to meaningless.
Skip the browser-backed tests when you are iterating on pure logic:
One rule that is easy to trip over¶
Do not mix synchronous Client tests and async tests in the same module.
Playwright's synchronous driver runs an event loop on the calling thread using
greenlets. While a sync Client is open, asyncio.run() on that thread fails
with a message about asyncio that has nothing to do with your test. The shared
client fixture is therefore module-scoped rather than session-scoped, so the
driver is released at each module boundary.
If you add async tests to a module that already exercises the sync client, split
them into their own file, as test_render_options.py and
test_render_options_async.py do.
Coverage and greenlets¶
pyproject.toml sets concurrency = ["thread", "greenlet"] for coverage. Do not
remove it. Without it coverage.py loses its tracer at Playwright's first greenlet
switch, and the synchronous client reports around 65% while its tests are in fact
exercising every line.
Style¶
Ruff and mypy decide almost everything, so there is little left to argue about. Beyond them:
- Docstrings follow the Google convention; mkdocstrings builds the API reference from them.
- Explain why in comments, not what. The code already says what.
- Public API lives in
website2pdf/__init__.pyand__all__. Modules starting with_are private and may change without notice.
Commits and pull requests¶
Commit messages follow Conventional
Commits: feat:, fix:, docs:,
test:, ci:, chore:, with ! for a breaking change.
Say what changed and why in the body. If you fixed a bug, describe the failure someone would have seen, that is what a future reader needs.
Add an entry to CHANGELOG.md under Unreleased for anything user-visible.
Releasing¶
Maintainers only:
- Move the
Unreleasedentries into a new version section inCHANGELOG.md. - Bump
versioninpyproject.toml. - Merge to
main, then push a matching tag:git tag v1.2.3 && git push origin v1.2.3.
release.yml refuses to publish if the tag and the version in pyproject.toml
disagree, then builds and publishes to PyPI through Trusted Publishing. There is
no API token to manage.