makepatch
A Git-based patch tool for Python packages that integrates with Hatchling.
Inspired by paperweight and pnpm patch.
| Mode | Approach | Output |
|---|---|---|
Source patch (makepatch src) |
Fetch an upstream Git repository at a pinned ref and patch it | sdist/wheel of a new package (fork) |
Package patch (makepatch pkg) |
Patch installed packages in site-packages | patched virtual environment |
There are two kinds of patches:
- Source patch: created with
git diffand applied withgit apply. One patch per file. - Feature patch: created with
git format-patch -p --minimal --zero-commit --no-numberedand applied withgit am --3way. One patch per commit; available in source patch mode only.
All patches are applied statically; there is no runtime monkey-patching.
Requirements
- Python ≥ 3.13
- Git ≥ 2.32
Source patch mode
A fork repository needs only pyproject.toml and patches/.
[build-system]
requires = ["hatchling", "makepatch"]
build-backend = "hatchling.build"
[project] # Define the fork's metadata yourself.
name = "requests-fork"
version = "2.32.3.post1"
dependencies = ["urllib3>=1.21.1,<3", "idna>=2.5,<4", "charset_normalizer>=2,<4", "certifi>=2017.4.17"]
[tool.makepatch.source]
upstream = "https://github.com/psf/requests.git"
ref = "v2.32.3" # a 40-digit commit SHA is recommended; tags and branches work too
include = { "src/requests" = "requests" } # upstream path → wheel path
exclude = ["**/*.pyi"] # excluded from sdist/wheel (relative to include paths)
work-exclude = ["docs/", "tests/"] # excluded from work/ and the build tree (relative to the upstream root)
# work-dir = "work" # default
# patches-dir = "patches" # default
[tool.hatch.build.hooks.makepatch] # enables the build hook (settings live in the table above)
[tool.hatch.build.targets.wheel]
bypass-selection = true
Add work/ and .makepatch/ to .gitignore so that they are excluded from the sdist.
Excluding files
Both options use gitignore syntax (including ! negation), but differ in base path and effect.
| Option | Base path | Effect |
|---|---|---|
exclude |
each include path (e.g. src/requests) |
Excluded from the sdist and wheel. Still present in work/. |
work-exclude |
upstream root | Not checked out into work/ or the build tree, via git sparse-checkout. Meant to save time and disk space. |
- If a patch touches a file excluded by
work-exclude,makepatch src applyand the build stop with an error. - After changing
work-exclude, runmakepatch src applyagain.
Workflow
makepatch src apply # fetch upstream → create work/ → apply source patches → git am feature patches
# edit files in work/
makepatch src fixup # fold working tree changes into the source patch commit
# or make regular commits in work/ → feature patches
makepatch src rebuild # regenerate patches/sources/** and patches/features/*.patch
makepatch src status
The work/ repository is laid out as follows:
<upstream ref> tag makepatch/base
makepatch: source patches tag makepatch/sources ← patches/sources/<path>.patch
<feature commits> ... ← patches/features/NNNN-*.patch
If a feature patch conflicts, the git am session is left in place. Resolve the conflict in work/, run git am --continue, then run makepatch src rebuild.
Building
uv build
- The build hook does not use
work/; it applies the patches afresh in.makepatch/build/tree. Uncommitted work therefore never leaks into the output. - The sdist contains the patched sources (
_makepatch/tree/). Building a wheel from the sdist needs neither git nor network access. - Set
MAKEPATCH_OFFLINE=1to use only the.makepatch/upstream.gitcache. - Upstream's own build steps (C extensions, etc.) are not run. Pure Python sources are the target.
Package patch mode
Install makepatch as a development dependency of the project.
uv add --dev makepatch
uv run makepatch pkg edit requests # create an editable copy in .makepatch/edit/requests@2.32.3/
# edit files in the copy
uv run makepatch pkg commit requests # save patches/packages/requests@2.32.3.patch and apply it
uv run makepatch pkg apply # apply every patch (--check: only check that they apply)
uv run makepatch pkg status
uv run makepatch pkg revert requests # restore the original files
- Patch files are named
<normalized name>@<version>.patch, with paths relative to site-packages. An error is raised if the installed version differs. - Deleting a patch file and running
pkg applyrestores that package to its original state. - The patch directory can be changed with
[tool.makepatch.packages] patches-dir. --python <interpreter>targets a different environment.- Editable installs are not supported; edit their sources directly.
Compatibility with uv
- Cache protection: on Linux, uv installs files from its cache as hardlinks by default (
--link-modeselects clone, copy or symlink instead). makepatch never modifies files in place: it runsgit applyon the affected files in a temporary directory and swaps them in withos.replace. Only the replaced paths get new inodes, so the uv cache stays untouched. The same holds in symlink mode. - Records: the hashes in
RECORDare updated, and the applied state (makepatch.json,makepatch.patch) is written into the dist-info and registered inRECORD, so it is removed together with the package. - Recovery after reinstall:
uv syncdoes not inspect the contents of installed files, so patches persist. If a reinstall or version change drops a patch, themakepatch-startup.pthinstalled by makepatch detects this at interpreter start-up and re-applies it.- Normally it only compares the hashes of the patch files and markers.
- Only on a mismatch does it take the environment lock and do the same work as
pkg apply. - Disable it with
MAKEPATCH_DISABLE_STARTUP=1. - It is skipped when the interpreter runs with
-I(isolated), as when uv queries an interpreter.
Development
uv sync
uv run pytest
Release files for makepatch 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| makepatch-0.3.0.tar.gz | 52.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| makepatch-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.9 kB
Release files / makepatch-0.3.0.tar.gz
| Download URL | makepatch-0.3.0.tar.gz |
|---|---|
| Size | 52.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0b33ae7a27872eb92a89c9118c97fca9cfc204f55c7d9194b2d3886398364f62
|
|
BLAKE2b-256 checksum How to use checksums |
fc931f80e8c8bab048632274b44d77fed5940d764d36d1d5dec63694736a1bec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"45","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / makepatch-0.3.0-py3-none-any.whl
| Download URL | makepatch-0.3.0-py3-none-any.whl |
|---|---|
| Size | 27.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c68b6799a24331be8573991cc6be36c8856fa6ae8e7088ad8ba50206d34a82dc
|
|
BLAKE2b-256 checksum How to use checksums |
f87062003d79eaa2529d6e6768f9a6064f05aa63a0dcc81bca40d562c0e44e29
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"45","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|