patch-package-py 0.1.4__tar.gz → 0.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patch-package-py
3
- Version: 0.1.4
3
+ Version: 0.2.0
4
4
  Summary: patch 3rd party Python packages
5
5
  Project-URL: Homepage, https://github.com/nomyfan/patch-package-py
6
6
  Project-URL: Repository, https://github.com/nomyfan/patch-package-py
@@ -30,6 +30,12 @@ A Python package patching tool that allows you to make and apply patches to thir
30
30
  uv add patch-package-py
31
31
  ```
32
32
 
33
+ ## Agent skill
34
+
35
+ This repository includes an
36
+ [agent skill](skills/patch-package-py/) for AI agents that support agent
37
+ skills.
38
+
33
39
  ## Usage
34
40
 
35
41
  The tool provides three main commands via the `p12y` CLI:
@@ -58,14 +64,15 @@ p12y patch requests [-e <environment-path>]
58
64
  ### 2. Commit changes and create patch file
59
65
 
60
66
  ```bash
61
- p12y commit <edit_path>
67
+ p12y commit <edit_path> [--skip-restore]
62
68
  ```
63
69
 
64
70
  After editing the package files, use this command to:
65
71
 
66
72
  - Generate a git diff of your changes
67
73
  - Create a `.patch` file in the `patches/` directory
68
- - Test that the patch can be applied successfully
74
+ - Reinstall the original package in the target environment
75
+ - Apply the new patch to the target environment
69
76
 
70
77
  Example:
71
78
 
@@ -73,6 +80,9 @@ Example:
73
80
  p12y commit /tmp/patch-requests-2.28.1-abc123/venv/lib/python3.11/site-packages/requests
74
81
  ```
75
82
 
83
+ Use `--skip-restore` to write the patch file and apply it to the current target
84
+ environment directly.
85
+
76
86
  ### 3. Apply patches
77
87
 
78
88
  ```bash
@@ -97,13 +107,21 @@ This command:
97
107
 
98
108
  - Uses `uv` for fast virtual environment creation and package installation
99
109
  - Leverages git for tracking changes and generating diffs
110
+ - Reinstalls the target package during `commit` before applying the newly generated patch
100
111
  - Stores patch files in a `patches/` directory in your project root
101
112
  - Patch files are named using the format: `<package-name>+<version>.patch`
102
113
 
103
- ## Using with poetry
114
+ ## Using with Poetry
104
115
 
105
- - detect the environment path using `poetry show -v`
106
- - use the -e / --env-path option for patch and/or apply.
116
+ - Detect the environment path using `poetry env info --path`.
117
+ - Use the `-e` / `--env-path` option for `patch` and `apply`.
118
+ - `commit` reuses the environment path recorded by `patch -e`.
119
+
120
+ ```bash
121
+ p12y patch requests -e "$(poetry env info --path)"
122
+ p12y commit <edit_path>
123
+ p12y apply -e "$(poetry env info --path)"
124
+ ```
107
125
 
108
126
  ## Requirements
109
127
 
@@ -8,6 +8,12 @@ A Python package patching tool that allows you to make and apply patches to thir
8
8
  uv add patch-package-py
9
9
  ```
10
10
 
11
+ ## Agent skill
12
+
13
+ This repository includes an
14
+ [agent skill](skills/patch-package-py/) for AI agents that support agent
15
+ skills.
16
+
11
17
  ## Usage
12
18
 
13
19
  The tool provides three main commands via the `p12y` CLI:
@@ -36,14 +42,15 @@ p12y patch requests [-e <environment-path>]
36
42
  ### 2. Commit changes and create patch file
37
43
 
38
44
  ```bash
39
- p12y commit <edit_path>
45
+ p12y commit <edit_path> [--skip-restore]
40
46
  ```
41
47
 
42
48
  After editing the package files, use this command to:
43
49
 
44
50
  - Generate a git diff of your changes
45
51
  - Create a `.patch` file in the `patches/` directory
46
- - Test that the patch can be applied successfully
52
+ - Reinstall the original package in the target environment
53
+ - Apply the new patch to the target environment
47
54
 
48
55
  Example:
49
56
 
@@ -51,6 +58,9 @@ Example:
51
58
  p12y commit /tmp/patch-requests-2.28.1-abc123/venv/lib/python3.11/site-packages/requests
52
59
  ```
53
60
 
61
+ Use `--skip-restore` to write the patch file and apply it to the current target
62
+ environment directly.
63
+
54
64
  ### 3. Apply patches
55
65
 
56
66
  ```bash
@@ -75,13 +85,21 @@ This command:
75
85
 
76
86
  - Uses `uv` for fast virtual environment creation and package installation
77
87
  - Leverages git for tracking changes and generating diffs
88
+ - Reinstalls the target package during `commit` before applying the newly generated patch
78
89
  - Stores patch files in a `patches/` directory in your project root
79
90
  - Patch files are named using the format: `<package-name>+<version>.patch`
80
91
 
81
- ## Using with poetry
92
+ ## Using with Poetry
82
93
 
83
- - detect the environment path using `poetry show -v`
84
- - use the -e / --env-path option for patch and/or apply.
94
+ - Detect the environment path using `poetry env info --path`.
95
+ - Use the `-e` / `--env-path` option for `patch` and `apply`.
96
+ - `commit` reuses the environment path recorded by `patch -e`.
97
+
98
+ ```bash
99
+ p12y patch requests -e "$(poetry env info --path)"
100
+ p12y commit <edit_path>
101
+ p12y apply -e "$(poetry env info --path)"
102
+ ```
85
103
 
86
104
  ## Requirements
87
105
 
@@ -33,7 +33,9 @@ def cmd_patch(args):
33
33
  )
34
34
  sys.exit(1)
35
35
  module_path, version = package
36
- prepare_patch_workspace(module_path, package_name, version)
36
+ prepare_patch_workspace(
37
+ module_path, package_name, version, env_path, amend=args.amend
38
+ )
37
39
 
38
40
 
39
41
  def cmd_commit(args):
@@ -57,7 +59,14 @@ def cmd_commit(args):
57
59
 
58
60
  info = json.load(f)
59
61
  site_packages_dir = Path(info["site_packages_path"])
60
- commit_changes(info["package_name"], info["version"], site_packages_dir)
62
+ target_env_path = Path(info["target_env_path"])
63
+ commit_changes(
64
+ info["package_name"],
65
+ info["version"],
66
+ site_packages_dir,
67
+ target_env_path,
68
+ restore_target_package=not args.skip_restore,
69
+ )
61
70
  import shutil
62
71
 
63
72
  shutil.rmtree(info["temp_dir"])
@@ -91,9 +100,17 @@ def cmd_apply(args):
91
100
 
92
101
 
93
102
  def cli():
103
+ from importlib.metadata import version
104
+
94
105
  parser = argparse.ArgumentParser(
95
106
  prog=CLI_NAME, description="A Python package patching tool"
96
107
  )
108
+ parser.add_argument(
109
+ "-V",
110
+ "--version",
111
+ action="version",
112
+ version=f"patch-package-py {version('patch-package-py')}",
113
+ )
97
114
 
98
115
  subparsers = parser.add_subparsers(dest="command", help="Available commands")
99
116
 
@@ -103,6 +120,11 @@ def cli():
103
120
  )
104
121
  workspace_parser.add_argument("package", help="Package name")
105
122
  workspace_parser.add_argument("-e", "--env-path", help="Environment Path")
123
+ workspace_parser.add_argument(
124
+ "--amend",
125
+ action="store_true",
126
+ help="Apply existing patch file to the workspace so you can continue editing",
127
+ )
106
128
  workspace_parser.set_defaults(func=cmd_patch)
107
129
 
108
130
  # commit command
@@ -110,6 +132,11 @@ def cli():
110
132
  "commit", help="Commit changes and create a patch file"
111
133
  )
112
134
  commit_parser.add_argument("path", help="Edit patch given by `patch` command")
135
+ commit_parser.add_argument(
136
+ "--skip-restore",
137
+ action="store_true",
138
+ help="Skip reinstalling the target package before applying the new patch",
139
+ )
113
140
  commit_parser.set_defaults(func=cmd_commit)
114
141
 
115
142
  # apply command
@@ -89,8 +89,20 @@ class Resolver:
89
89
  return PurePosixPath(common_path_str)
90
90
 
91
91
 
92
+ def find_existing_patch(package_name: str, version: str) -> Union[Path, None]:
93
+ patch_file = Path.cwd() / "patches" / f"{package_name}+{version}.patch"
94
+ if patch_file.exists():
95
+ return patch_file
96
+ return None
97
+
98
+
92
99
  def prepare_patch_workspace(
93
- module_path: PurePosixPath, package_name: str, version: str
100
+ module_path: PurePosixPath,
101
+ package_name: str,
102
+ version: str,
103
+ target_env_path: Path,
104
+ *,
105
+ amend: bool = False,
94
106
  ):
95
107
  temp_dir = Path(tempfile.mkdtemp(prefix=f"patch-{package_name}-{version}-"))
96
108
  venv_path = temp_dir / "venv"
@@ -105,6 +117,7 @@ def prepare_patch_workspace(
105
117
  "pip",
106
118
  "install",
107
119
  "--no-deps",
120
+ "--link-mode=copy",
108
121
  f"{package_name}=={version}",
109
122
  "--python",
110
123
  str(
@@ -144,6 +157,7 @@ def prepare_patch_workspace(
144
157
  "site_packages_path": str(site_packages_path.absolute()),
145
158
  "package_name": package_name,
146
159
  "version": version,
160
+ "target_env_path": str(target_env_path.absolute()),
147
161
  },
148
162
  f,
149
163
  indent=2,
@@ -154,6 +168,13 @@ def prepare_patch_workspace(
154
168
  stderr=subprocess.DEVNULL,
155
169
  stdout=subprocess.DEVNULL,
156
170
  )
171
+ subprocess.check_call(
172
+ ["git", "config", "user.name", "patch-package-py"], cwd=git_path
173
+ )
174
+ subprocess.check_call(
175
+ ["git", "config", "user.email", "noreply@patch-package-py.local"],
176
+ cwd=git_path,
177
+ )
157
178
  subprocess.check_call(
158
179
  ["git", "add", "."],
159
180
  cwd=git_path,
@@ -173,12 +194,85 @@ def prepare_patch_workspace(
173
194
  stdout=subprocess.DEVNULL,
174
195
  )
175
196
 
197
+ if amend:
198
+ existing_patch = find_existing_patch(package_name, version)
199
+ if existing_patch:
200
+ logger.info(f"Applying existing patch: {existing_patch.name}")
201
+ patch_args = [
202
+ "patch",
203
+ "-p1",
204
+ "-N",
205
+ "--forward",
206
+ "-i",
207
+ str(existing_patch.absolute()),
208
+ ]
209
+ try:
210
+ # Validate before modifying the workspace; recovery below
211
+ # handles residue if the real apply still fails unexpectedly.
212
+ subprocess.check_call(
213
+ [*patch_args, "--dry-run"],
214
+ cwd=site_packages_path,
215
+ stderr=subprocess.DEVNULL,
216
+ stdout=subprocess.DEVNULL,
217
+ )
218
+ subprocess.check_call(
219
+ patch_args,
220
+ cwd=site_packages_path,
221
+ )
222
+ except subprocess.CalledProcessError:
223
+ logger.warning(
224
+ "Failed to apply existing patch. Starting from clean state."
225
+ )
226
+ subprocess.check_call(
227
+ ["git", "add", "."],
228
+ cwd=git_path,
229
+ stderr=subprocess.DEVNULL,
230
+ stdout=subprocess.DEVNULL,
231
+ )
232
+ subprocess.check_call(
233
+ ["git", "reset", "--hard", "HEAD"],
234
+ cwd=git_path,
235
+ stderr=subprocess.DEVNULL,
236
+ stdout=subprocess.DEVNULL,
237
+ )
238
+ subprocess.check_call(
239
+ ["git", "clean", "-fdX"],
240
+ cwd=git_path,
241
+ stderr=subprocess.DEVNULL,
242
+ stdout=subprocess.DEVNULL,
243
+ )
244
+
176
245
  logger.info(
177
246
  f"You can now edit the package in: {edit_path}. When done, run `{CLI_NAME} commit {edit_path}` in this directory to create the patch file."
178
247
  )
179
248
 
180
249
 
181
- def commit_changes(package_name: str, version: str, site_packages_path: Path) -> None:
250
+ def venv_python(venv_path: Path) -> Path:
251
+ return venv_path / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
252
+
253
+
254
+ def restore_clean_package(package_name: str, version: str, env_path: Path) -> None:
255
+ subprocess.check_call(
256
+ [
257
+ "uv",
258
+ "pip",
259
+ "install",
260
+ "--force-reinstall",
261
+ "--no-deps",
262
+ f"{package_name}=={version}",
263
+ "--python",
264
+ str(venv_python(env_path)),
265
+ ]
266
+ )
267
+
268
+
269
+ def commit_changes(
270
+ package_name: str,
271
+ version: str,
272
+ site_packages_path: Path,
273
+ target_env_path: Path,
274
+ restore_target_package: bool = True,
275
+ ) -> None:
182
276
  diff_content = subprocess.check_output(
183
277
  ["git", "diff", "--relative"],
184
278
  cwd=site_packages_path,
@@ -195,13 +289,26 @@ def commit_changes(package_name: str, version: str, site_packages_path: Path) ->
195
289
  with open(patch_file_path, "w") as f:
196
290
  f.write(diff_content)
197
291
 
198
- current_site_packages = find_site_packages(Path.cwd() / ".venv")
292
+ if restore_target_package:
293
+ restore_clean_package(package_name, version, target_env_path)
294
+
295
+ current_site_packages = find_site_packages(target_env_path)
199
296
  try:
200
297
  apply_patch(patch_file_path, current_site_packages)
201
298
  except subprocess.CalledProcessError:
202
- logger.error(
203
- f"Error: failed to apply the patch after creation. There's maybe a conflict, you can try to reinstall the package and apply the patch manually via `{CLI_NAME} apply {patch_file_name}`"
204
- )
299
+ msg = "Error: failed to apply the patch after creation."
300
+ if restore_target_package:
301
+ msg += (
302
+ " This may be caused by leftover files from a previous patch."
303
+ " Try manually removing them from the target environment"
304
+ f" and then run `{CLI_NAME} apply`."
305
+ )
306
+ else:
307
+ msg += (
308
+ " There's maybe a conflict, you can try to reinstall the package"
309
+ f" and apply the patch manually via `{CLI_NAME} apply`."
310
+ )
311
+ logger.error(msg)
205
312
  return
206
313
  logger.info(f"Patch created and applied for {package_name}=={version}")
207
314
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "patch-package-py"
3
- version = "0.1.4"
3
+ version = "0.2.0"
4
4
  description = "patch 3rd party Python packages"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"