Development Environment¶
Installation¶
The development environment is managed with uv, not with pip.
Create it, and bring it back in step after a change to the dependencies, with:
This writes a .venv directory holding Snipwise in editable mode,
together with pytest, reuse and zensical.
Activate it with source .venv/bin/activate.
Whoever uses direnv can have that line run
on entering the directory, by writing it to an .envrc file:
That file is not tracked by git, because activating an environment is a choice each contributor makes for themselves.
The snipwise command of that environment runs the working tree,
and so does python -m snipwise from the root of the repository,
which needs no installation at all
because the package sits in the directory the interpreter starts in.
Tests¶
Every module of the package that holds logic has a test module named after it.
errors and names are the exception,
holding one exception class and two constants between them.
The command line has tests/test_cli.py rather than a test module for __main__,
which builds a small project in a temporary directory
and checks what snipwise writes to it, prints and exits with.
A change in behaviour is a change in that file.
Warnings are errors, through the addopts of [tool.pytest.ini_options] in
pyproject.toml,
so a deprecation from a dependency fails the suite instead of scrolling past.
Every test is bounded by pytest-timeout,
configured with the timeout setting in the same table.
Setup, call and teardown of one test share a budget of 30 seconds,
which is generous for a suite that runs in a few seconds,
and still leaves room for the slowest continuous integration runner.
Without it, a test that waits for something that never arrives,
such as a git subprocess asking for input,
hangs until the whole workflow is cancelled,
which says nothing about the test that got stuck.
Pre-commit¶
The hooks in .pre-commit-config.yaml run on every commit
once pre-commit install has been run.
Run them over the whole repository with:
A new file has to be added to the index with git add -N before pre-commit can see it.
The hooks do all the linting and all the formatting:
ruff-formatandruff-checkfor Python, with the rule set and the line length in[tool.ruff]in pyproject.toml.mypyfor the annotations, configured under[tool.mypy]in the same file. It is given no file names, because a check of the files of one commit would report a call against a signature that the same run does not read.pymarkdownfor the Markdown files, configured under[tool.pymarkdown]in the same file. Every deviation from the default configuration carries a comment saying why it is there.reusefor the SPDX header that every file in the repository carries.- The hygiene hooks of
pre-commit-hooks, andcheck-github-workflowsfor the workflow files. - Snipwise itself, as a local hook that runs
python -m snipwise fix.
The same hooks run in continuous integration on pre-commit.ci,
which is why no GitHub Actions workflow lints anything.
Linting does not belong in .github/workflows/.
Type Checking¶
The package is checked with mypy under its strict rule set,
because Snipwise ships a py.typed marker,
which promises its annotations to whoever imports it.
Every function of snipwise therefore carries a full signature,
and a value whose type says it may be None is narrowed before it is used.
The test suite is held to a weaker rule set, spelled out in an override in pyproject.toml. A test does not have to annotate what it takes or returns, because a test that spells out the type of every fixture says the same thing as the fixture and drifts away from it. The bodies of the tests are checked all the same, which is what catches a call that no longer matches the function it calls.
Run it over the repository with:
There is no mypy in the dev extra, as there is no ruff either.
pre-commit builds the environment that runs it,
which is the environment continuous integration uses as well,
and additional_dependencies of the hook holds what Snipwise and its tests import,
because mypy reads the annotations of those packages.
The run-time dependencies in that list are the ones of pyproject.toml,
copied there by Snipwise itself.
Snipwise Applied to Itself¶
The local hook runs the version being developed rather than a released one,
so every change to Snipwise is tried out on a real project before it is released.
Which files it keeps in step, and why, is written in the
snipwise.md of this repository, next to the rules that do it.
Two consequences are worth knowing before editing anything:
a run-time dependency is added or bumped in pyproject.toml and nowhere else,
and a commit that changes one of the files Snipwise owns is rewritten by the hook,
so it has to be staged again.
Documentation¶
The documentation is a Zensical site
built from the Markdown files in the docs directory,
configured by zensical.toml in the root of the repository.
Preview it, with a browser pointed at the address it prints, using:
zensical build writes the same site to site, which is not tracked by git,
and reports a broken link between pages as a warning.
zensical build --strict turns such a warning into a failure,
which is what continuous integration runs.
Two things are worth knowing before adding a page.
The navigation is the nav list of zensical.toml and is not inferred from the directory,
so a new file is invisible until it is listed there.
A link that leaves the docs directory does not survive the build,
because the site has no copy of the rest of the repository,
so a reference to a file such as pyproject.toml is written as a link to GitHub
or as plain text.
The changelog page includes the CHANGELOG.md of the repository
rather than holding a second copy of it,
because the release workflow reads that file to build its release notes.
Every page carries a description in its front matter,
which becomes its <meta name="description">,
because a site whose pages share one description has them dropped from search results.
The front matter has to be the first thing in a file, so the SPDX header sits below it.
The overrides directory is the theme.custom_dir of zensical.toml.
It holds a single template that adds the Open Graph and Twitter card tags
that the Zensical templates leave out,
using the image built from docs/assets/images/social-card.svg.
Continuous Integration¶
The pytest workflow runs the suite six times,
on Linux, on macOS and on Windows, with Python 3.11 and with Python 3.14 on each.
Every 3.11 job installs with --resolution lowest-direct,
which pins each dependency of Snipwise to its lower bound in pyproject.toml,
so those bounds are a claim that is tested rather than a guess.
Every 3.14 job resolves with --resolution highest, the default,
and so takes whatever the resolver picks today.
The macOS and the Windows jobs are what back the Operating System :: OS Independent classifier.
Those jobs install the tests extra rather than dev,
which is why that extra exists: reuse has no wheel for Windows,
and neither reuse nor zensical is needed to run the suite.
The zensical workflow builds the site on every push to main
and on every pull request that touches it.
It builds with --strict, so a broken link between pages fails the workflow.
The site is deployed to GitHub Pages only for a tag of the form vX.Y.Z,
so the published documentation matches the latest stable release
and a pre-release tag leaves it untouched.
There is no version selector, because Zensical has no native versioning yet.
The release workflow builds the distribution on every push to main and on every v* tag.
A push to main publishes it to TestPyPI and stops there.
A tag also takes the release notes out of the section of the changelog
that matches it, publishes to PyPI,
and signs the distribution with Sigstore for a GitHub release.
A version therefore needs its changelog section before it is tagged,
because the workflow fails when it finds no section for the version it is building.
The version number itself comes from the tag, through setuptools_scm,
and is written nowhere in the repository.
Every job carries a timeout-minutes,
and every step of the pytest workflow carries one of its own,
because a step that hangs would otherwise hold a runner for six hours,
which is the default GitHub Actions allows.
The limits are well above what a healthy run needs,
so one that is hit means something is wrong rather than slow.