Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 diff and applied with git apply. One patch per file.
  • Feature patch: created with git format-patch -p --minimal --zero-commit --no-numbered and applied with git 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 apply and the build stop with an error.
  • After changing work-exclude, run makepatch src apply again.

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=1 to use only the .makepatch/upstream.git cache.
  • 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 apply restores 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-mode selects clone, copy or symlink instead). makepatch never modifies files in place: it runs git apply on the affected files in a temporary directory and swaps them in with os.replace. Only the replaced paths get new inodes, so the uv cache stays untouched. The same holds in symlink mode.
  • Records: the hashes in RECORD are updated, and the applied state (makepatch.json, makepatch.patch) is written into the dist-info and registered in RECORD, so it is removed together with the package.
  • Recovery after reinstall: uv sync does not inspect the contents of installed files, so patches persist. If a reinstall or version change drops a patch, the makepatch-startup.pth installed 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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages