Feel free to create a new Pull request if you want to propose a new feature or fix a bug.
This project uses uv. There is no requirements.txt or setup.py -- pyproject.toml is the only packaging file, and uv run creates and syncs the environment for you, so there is no virtualenv to activate:
uv run python test_runner.py # the whole suite
uv run ruff check . # lint
uv run ruff check . --fix # and fix what's auto-fixable
To run a single test, pass a label -- test_runner.py falls back to the whole suite only when given none:
uv run python test_runner.py django_mfa.tests.test_conf
To check one Python/Django combination locally (CI runs the grid of Python 3.10-3.13 against Django 4.2, 5.2 and 6.1, minus the two combinations Django 6.1 doesn't support — it needs Python 3.12+):
uv run --python 3.12 --with "django~=4.2.0" python test_runner.py
The docs are Markdown, read by Sphinx through MyST, and live in docs/:
cd docs && make html # build into docs/_build/html
cd docs && make linkcheck # verify external links still resolve
-W is on by default, so a warning fails the build here exactly as it does in CI and on Read the Docs.
Two things that build output alone will not catch, both pinned by django_mfa/tests/test_docs.py instead:
- With MyST's
colon_fenceextension off,:::{warning}is not an error -- it renders the literal text:::{warning}into the published page and the build still reports success. - A page missing from
index.md's toctree builds fine and simply never gets linked.
When you add a setting to django_mfa/conf.py, document it in docs/settings.md in the same change; a test asserts every key in DEFAULTS appears there.
Releases publish themselves. There is no API token to hold, and nobody runs
twine upload by hand: PyPI is configured to trust .github/workflows/publish.yml
in this repository via OpenID Connect, and mints a short-lived, project-scoped
upload token for that workflow alone.
To cut a release, in full:
- Bump
versioninpyproject.tomland add that version's section toCHANGELOG.md, in the same PR. Merge it tomaster.
There is no step 2. You never create a tag and you never create a GitHub
Release -- .github/workflows/tag-release.yml sees the version change, creates
tag v<version> on the commit that carried it, publishes a GitHub Release
(marked pre-release if PEP 440 says the version is one), and starts
publish.yml against that tag.
pyproject.toml is the only place a version string is written anywhere in this
project: django_mfa.__version__ and docs/conf.py both read it back from the
installed package metadata. Deriving the tag from it too means there is nothing
left that can disagree with anything else.
publish.yml then tests the oldest and newest supported Python/Django
combinations, builds the sdist and wheel, checks the README renders on PyPI,
installs the wheel into a clean environment and starts Django against it, and
only then uploads.
Two guards survive from when tags were typed by hand, because the cost of getting a version wrong is unbounded -- PyPI does not allow re-uploading one:
- The tag must match
pyproject.toml'sversion. Nothing in the build connects them: hatchling never looks at the tag, so a release taggedv4.0.0cut from a tree still saying4.0.0a1would publish4.0.0a1and report success. That is not hypothetical -- it happened on 2026-08-13, and this guard is what stopped it. - The publish job must run on a tag ref. The
pypideployment environment only accepts tags matchingv*, and the guard fails loudly rather than skipping when handed a branch.
To release a version already sitting on master -- or the first time, when the
version predates all of this -- run Tag release from the Actions tab
manually. Re-running it is harmless: if v<version> already exists it does
nothing.
:::{warning}
Renaming publish.yml, or the pypi environment, breaks publishing --
both names are part of what PyPI trusts, and a mismatch is rejected at upload
time with an authentication error that does not mention the rename. Change it on
PyPI first.
tag-release.yml reaches publish.yml through workflow_dispatch, not through
the Release it just created, and that is not incidental. GitHub suppresses
workflow runs caused by its own GITHUB_TOKEN so workflows cannot loop, with
workflow_dispatch and repository_dispatch as the only exceptions. Chaining
on release: published instead fails silently: the Release appears, nothing
publishes, and no workflow run exists anywhere to explain why.
:::
-
Fork the repo:
https://github.com/MicroPyramid/django-mfa -
Create a branch for your specific changes:
$ git checkout master $ git pull $ git checkout -b featureTo simplify things, please, make one branch per issue (pull request). It's also important to make sure your branch is up-to-date with upstream master, so that maintainers can merge changes easily.
-
Commit changes. Please update docs, if relevant.
-
Don't forget to run the tests (and
ruff) to check that nothing breaks. -
Ideally, write your own tests for new feature/bug fix.
-
Submit a pull request.