makepatch 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
makepatch/__hook__.py ADDED
@@ -0,0 +1,7 @@
1
+ from hatchling.plugin import hookimpl as _hook
2
+ from src.clippy.hooks import ClippyPatchHook
3
+
4
+
5
+ @_hook
6
+ def __register_hatch_build_hooks():
7
+ return ClippyPatchHook
makepatch/__init__.py ADDED
File without changes
makepatch/_config.py ADDED
@@ -0,0 +1,18 @@
1
+ from collections.abc import Iterable
2
+ from typing import final, Annotated
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ __config = None
8
+
9
+ @final
10
+ class ClippyConfig(BaseModel):
11
+ module_source: Annotated[str, Field(alias="module-source")]
12
+ excludes: Annotated[Iterable[str], Field(default_factory=frozenset)]
13
+ dev_excludes: Annotated[Iterable[str], Field(alias="module-source", default_factory=frozenset)]
14
+
15
+ def __init__(self, /):
16
+ global __config
17
+ if __config:
18
+ raise RuntimeError("Configuration already initialized")
@@ -0,0 +1 @@
1
+ from ._patch import ClippyPatchHook
@@ -0,0 +1,88 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import override
4
+
5
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
6
+
7
+ import shutil, subprocess, tomllib
8
+
9
+ if __debug__ and __import__("typing").TYPE_CHECKING:
10
+ from typing import Any
11
+
12
+
13
+ class ClippyPatchHook(BuildHookInterface):
14
+ @property
15
+ def __root(self):
16
+ return Path(self.root)
17
+
18
+ @property
19
+ def __config(self):
20
+ with open(self.__root / "pyproject.toml", "rb") as f:
21
+ pyproject = tomllib.load(f)
22
+
23
+ try:
24
+ config = pyproject["tool"]["patcher"]
25
+ except KeyError as e:
26
+ raise RuntimeError("[tool.patcher] not found in pyproject.toml") from e
27
+ return config["module-source"], frozenset(config.get("excludes", set()))
28
+
29
+ @override
30
+ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
31
+ module_name, exclude_globs = self.__config
32
+
33
+ src = self.__root / "work" / "sources" / module_name
34
+ if not src.exists(): raise RuntimeError(f"{src} not exists")
35
+ elif not src.is_dir(): raise RuntimeError(f"{src} is not a directory")
36
+
37
+ excludes = set()
38
+ for glob in exclude_globs:
39
+ excludes |= set(str(excluded.relative_to(src)) for excluded in src.rglob(glob) if str(excluded.relative_to(src)).startswith(glob.split(os.sep)[0]))
40
+
41
+ patches = self.__root / "patches"
42
+ if not patches.exists(): raise RuntimeError(f"{patches} not exists")
43
+ elif not patches.is_dir(): raise RuntimeError(f"{patches} is not a directory")
44
+
45
+ if str(module_name).startswith("src"):
46
+ module_name = module_name[4:]
47
+ dst = self.__root / "src" / module_name
48
+ if dst.exists():
49
+ if not dst.is_dir(): raise RuntimeError(f"{dst} is not a directory")
50
+ shutil.rmtree(dst)
51
+ dst.mkdir(parents=True)
52
+
53
+ # Copy sources, skipping excludes
54
+ for src_file in src.rglob("*"):
55
+ if not src_file.is_file(): continue
56
+
57
+ rel = src_file.relative_to(src)
58
+ if str(rel) in excludes: continue
59
+
60
+ dst_file = dst / rel
61
+ dst_file.parent.mkdir(parents=True, exist_ok=True)
62
+ shutil.copy2(src_file, dst_file)
63
+
64
+ # Apply patches
65
+ for patch_file in sorted(patches.rglob("*.patch")):
66
+ rel_patch = patch_file.relative_to(patches)
67
+ # e.g. foo.py.patch → foo.py
68
+ target_rel = rel_patch.with_suffix("")
69
+ target = dst / target_rel
70
+
71
+ if not target.parent.exists():
72
+ target.parent.mkdir(parents=True, exist_ok=False)
73
+
74
+ result = subprocess.run(
75
+ ["patch", "--unified", str(target), str(patch_file)],
76
+ capture_output=True,
77
+ text=True,
78
+ )
79
+ if result.returncode != 0:
80
+ raise RuntimeError(f"Failed to apply {rel_patch}:\n{result.stderr}")
81
+
82
+ def finalize(self, version: str, build_data: dict[str, Any], artifact_path: str) -> None:
83
+ module_source, _ = self.__config
84
+ if str(module_source).startswith("src"):
85
+ module_source = module_source[4:]
86
+ dst = self.__root / "src" / module_source
87
+ if dst.exists():
88
+ shutil.rmtree(dst)
File without changes
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.3
2
+ Name: makepatch
3
+ Version: 0.0.1
4
+ Summary: Hatch build hook to easily hard-fork packages with patches!
5
+ Classifier: Framework :: Hatch
6
+ Requires-Dist: hatchling>=1.29.0
7
+ Requires-Dist: pydantic>=2.13.4
8
+ Requires-Python: >=3.12
@@ -0,0 +1,10 @@
1
+ makepatch/__hook__.py,sha256=ov-ZExdRe2dhvnocD01_58Z37eBryz8sTaTAbFEHM-4,171
2
+ makepatch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ makepatch/_config.py,sha256=GlyVVm9ZsolzDKSiupoTMTmOHVSNCMdyo-_AzArw3Wo,565
4
+ makepatch/hooks/__init__.py,sha256=8JvonoGcrgihzhk7eUIwQPwr2oTL58IfF44tAk2vOYc,37
5
+ makepatch/hooks/_patch.py,sha256=xjaKYDJkhekGfBomXOKH5pOiP209hNtt6G4kHv-kfQs,3335
6
+ makepatch/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ makepatch-0.0.1.dist-info/WHEEL,sha256=fWriCkzqm-pffF5af4gJC9iI5FMFaJTuN9UxxxzOmdY,81
8
+ makepatch-0.0.1.dist-info/entry_points.txt,sha256=8xpxWiGS9XHoBCcyoVcfkRfjW2WnvamePMfI3_WJsOs,141
9
+ makepatch-0.0.1.dist-info/METADATA,sha256=2kQkR1g2KEOP011WEC73brgoEzEW_wM-HQsAhJbm6Q8,242
10
+ makepatch-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.14
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,7 @@
1
+ [console_scripts]
2
+ init = makepatch.scripts:init
3
+ rebuild-patches = makepatch.scripts:rebuild_patches
4
+
5
+ [hatch]
6
+ makepatch = makepatch.__hook__
7
+