mseep-patche 1.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.
Patche/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .app import __version__, app
Patche/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .app import app
2
+
3
+ app()
Patche/__version__.py ADDED
@@ -0,0 +1,9 @@
1
+ import sys
2
+
3
+ if sys.version_info >= (3, 10):
4
+ import importlib.metadata as importlib_metadata
5
+ else:
6
+ import importlib_metadata
7
+
8
+
9
+ __version__ = importlib_metadata.version("Patche")
Patche/app.py ADDED
@@ -0,0 +1,32 @@
1
+ import logging
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.logging import RichHandler
6
+
7
+ logging.basicConfig(level=logging.INFO, handlers=[RichHandler()], format="%(message)s")
8
+ logger = logging.getLogger()
9
+
10
+ from Patche.__version__ import __version__
11
+ from Patche.utils.common import post_executed
12
+
13
+ app = typer.Typer(result_callback=post_executed, no_args_is_help=True)
14
+
15
+
16
+ @app.callback(invoke_without_command=True)
17
+ def callback(verbose: bool = False, version: bool = False):
18
+ """
19
+ Entry for public options
20
+ """
21
+ if verbose:
22
+ logger.setLevel(logging.DEBUG)
23
+
24
+ if version:
25
+ console = Console()
26
+ console.print(f"patche version {__version__}")
27
+ raise typer.Exit()
28
+
29
+
30
+ from Patche.commands.apply import apply
31
+ from Patche.commands.help import show_settings
32
+ from Patche.commands.show import show
@@ -0,0 +1,107 @@
1
+ import os
2
+ from typing import Annotated
3
+
4
+ import typer
5
+
6
+ from Patche.app import app, logger
7
+ from Patche.model import File
8
+ from Patche.utils.parse import parse_patch
9
+ from Patche.utils.resolve import apply_change
10
+
11
+
12
+ @app.command()
13
+ def apply(
14
+ patch_path: Annotated[str, typer.Argument(help="Path to the patch file")],
15
+ reverse: Annotated[
16
+ bool,
17
+ typer.Option(
18
+ "-R",
19
+ "--reverse",
20
+ help="Assume patches were created with old and new files swapped.",
21
+ ),
22
+ ] = False,
23
+ fuzz: Annotated[
24
+ int,
25
+ typer.Option(
26
+ "-F", "--fuzz", help="Set the fuzz factor to LINES for inexact matching."
27
+ ),
28
+ ] = 0,
29
+ ):
30
+ """
31
+ Apply a patch to a file.
32
+ """
33
+
34
+ if not os.path.exists(patch_path):
35
+ logger.error(f"Warning: {patch_path} not found!")
36
+ raise typer.Exit(code=1)
37
+
38
+ if reverse:
39
+ logger.info("Reversing patch...")
40
+
41
+ has_failed = False
42
+
43
+ with open(patch_path, mode="r", encoding="utf-8") as (f):
44
+ diffes = parse_patch(f.read()).diff
45
+
46
+ for diff in diffes:
47
+
48
+ old_filename = diff.header.old_path
49
+ new_filename = diff.header.new_path
50
+ if reverse:
51
+ old_filename, new_filename = new_filename, old_filename
52
+
53
+ logger.debug(f"old_filename: {old_filename} new_filename: {new_filename}")
54
+
55
+ if old_filename == "/dev/null":
56
+ # 根据 diffes 创建新文件
57
+ try:
58
+ assert len(diff.hunks) == 1
59
+
60
+ new_line_list = []
61
+ for line in diff.changes:
62
+
63
+ assert line.old is None
64
+ new_line_list.append(line)
65
+
66
+ with open(new_filename, mode="w+", encoding="utf-8") as f:
67
+ for line in new_line_list:
68
+ f.write(line.content + "\n")
69
+
70
+ except AssertionError:
71
+ logger.error("Failed to create new file: invalid diff!")
72
+ raise typer.Exit(code=1)
73
+
74
+ elif new_filename == "/dev/null":
75
+ # 移除 old_filename
76
+ if os.path.exists(old_filename):
77
+ os.remove(old_filename)
78
+ else:
79
+ logger.error(f"{old_filename} not found!")
80
+ raise typer.Exit(code=1)
81
+ else:
82
+ if os.path.exists(old_filename):
83
+
84
+ logger.info(f"Applying patch to {old_filename}...")
85
+
86
+ new_line_list = File(file_path=old_filename).line_list
87
+ apply_result = apply_change(
88
+ diff.hunks, new_line_list, reverse=reverse, fuzz=fuzz
89
+ )
90
+ new_line_list = apply_result.new_line_list
91
+
92
+ # 检查失败数
93
+ for failed_hunk in apply_result.failed_hunk_list:
94
+ has_failed = True
95
+ logger.error(f"Failed hunk: {failed_hunk.index}")
96
+ else:
97
+ logger.error(f"{old_filename} not found!")
98
+ raise typer.Exit(code=1)
99
+
100
+ # 写入文件
101
+ if not has_failed:
102
+ with open(new_filename, mode="w+", encoding="utf-8") as f:
103
+ for line in new_line_list:
104
+ if line.status:
105
+ f.write(line.content + "\n")
106
+
107
+ raise typer.Exit(code=1 if has_failed else 0)
@@ -0,0 +1,12 @@
1
+ from rich import print
2
+
3
+ from Patche.app import app
4
+ from Patche.config import settings
5
+
6
+
7
+ @app.command("settings")
8
+ def show_settings():
9
+ """
10
+ Show current settings
11
+ """
12
+ print(settings.model_dump_json())
@@ -0,0 +1,41 @@
1
+ import os
2
+ from typing import Annotated
3
+
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+ from Patche.app import app, logger
9
+ from Patche.utils.parse import parse_patch
10
+
11
+
12
+ @app.command()
13
+ def show(filename: Annotated[str, typer.Argument(help="Path to the patch file")]):
14
+ """
15
+ Show details of a patch file.
16
+ """
17
+ if not os.path.exists(filename):
18
+ logger.error(f"Warning: {filename} not found!")
19
+ return
20
+
21
+ content = ""
22
+ with open(filename, mode="r", encoding="utf-8") as (f):
23
+ content = f.read()
24
+
25
+ patch = parse_patch(content)
26
+
27
+ table = Table(box=None)
28
+ table.add_column("Field", justify="left", style="cyan")
29
+ table.add_column("Value", justify="left", style="magenta")
30
+
31
+ table.add_row("Patch", filename)
32
+ table.add_row("Sha", patch.sha)
33
+ table.add_row("Author", patch.author)
34
+ table.add_row("Date", (patch.date))
35
+ table.add_row("Subject", patch.subject)
36
+
37
+ for diff in patch.diff:
38
+ table.add_row("Diff", f"{diff.header.old_path} -> {diff.header.new_path}")
39
+
40
+ console = Console()
41
+ console.print(table)
Patche/config.py ADDED
@@ -0,0 +1,24 @@
1
+ import importlib.resources as pkg_resources
2
+ import os
3
+ from functools import lru_cache
4
+
5
+ from pydantic_settings import BaseSettings
6
+
7
+
8
+ @lru_cache()
9
+ def get_settings():
10
+ _settings = Settings()
11
+ if not os.path.exists(_settings.Config.env_file):
12
+ open(_settings.Config.env_file, "w").close()
13
+
14
+ return _settings
15
+
16
+
17
+ class Settings(BaseSettings):
18
+ max_diff_lines: int = 3
19
+
20
+ class Config:
21
+ env_file = os.path.join(os.environ.get("HOME"), ".Patche.env")
22
+
23
+
24
+ settings = get_settings()
Patche/mcp/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ import typer
5
+ from typer import Typer
6
+
7
+ from Patche.app import logger
8
+ from Patche.mcp.server import serve
9
+
10
+ app = Typer()
11
+
12
+
13
+ @app.command()
14
+ # @app.option(
15
+ # "--repository",
16
+ # help="Repository to apply the patch",
17
+ # default=None,
18
+ # )
19
+ # @app.option(
20
+ # "--debug",
21
+ # help="Enable debug mode",
22
+ # default=False,
23
+ # )
24
+ def main(
25
+ repository: str = typer.Option(
26
+ None, "--repository", "-r", help="Repository to apply the patch"
27
+ ),
28
+ debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug mode"),
29
+ ):
30
+ """
31
+ Main entry point for the Patche MCP server.
32
+ """
33
+
34
+ logger.info("Starting Patche MCP server...")
35
+ if debug:
36
+ logger.setLevel(logging.DEBUG)
37
+
38
+ asyncio.run(serve(repository))
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
Patche/mcp/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from Patche.mcp import main
2
+
3
+ main()
Patche/mcp/model.py ADDED
@@ -0,0 +1,29 @@
1
+ from enum import Enum
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ # 定义 Patche 相关的数据模型
7
+ class PatcheConfig(BaseModel):
8
+ pass
9
+
10
+
11
+ class PatcheList(BaseModel):
12
+ patche_dir: str
13
+
14
+
15
+ class PatcheShow(BaseModel):
16
+ patch_path: str
17
+
18
+
19
+ class PatcheApply(BaseModel):
20
+ patch_path: str
21
+ target_dir: str
22
+ reverse: bool = False
23
+
24
+
25
+ class PatcheTools(str, Enum):
26
+ CONFIG = "patche_config"
27
+ LIST = "patche_list"
28
+ SHOW = "patche_show"
29
+ APPLY = "patche_apply"
Patche/mcp/prompts.py ADDED
@@ -0,0 +1,313 @@
1
+ from mcp.types import Prompt
2
+
3
+ from Patche.mcp.model import (
4
+ PatcheApply,
5
+ PatcheConfig,
6
+ PatcheList,
7
+ PatcheShow,
8
+ PatcheTools,
9
+ )
10
+
11
+ base_info_prompt = """
12
+ <system>
13
+
14
+ You are a tool that helps the user to manage patches in a directory.
15
+
16
+ There are several tools available to you, you can use them to perform different actions.
17
+
18
+ When using a tool, you should call it with given arguments, and do not add any other information.
19
+
20
+ </system>
21
+ """
22
+
23
+ show_config_prompt = f"""
24
+ <user>
25
+
26
+ Please show the configuration of the tool.
27
+
28
+ Parameters:
29
+ - config_path: string, path to the configuration file (optional)
30
+ - patche_dir: string, directory containing patches (required)
31
+
32
+ </user>
33
+
34
+ Please use the following format to show the configuration:
35
+
36
+ <input>
37
+ {PatcheConfig.model_json_schema()}
38
+ </input>
39
+ """
40
+
41
+ list_prompt = f"""
42
+ <user>
43
+
44
+ Please list all the patches in the directory.
45
+
46
+ Parameters:
47
+ - patche_dir: string, directory path containing patches (required)
48
+
49
+ </user>
50
+
51
+ Please use the following format to show the patches:
52
+
53
+ <input>
54
+ {PatcheList.model_json_schema()}
55
+ </input>
56
+ """
57
+
58
+ show_prompt = f"""
59
+ <user>
60
+
61
+ Please show infomation of the patch.
62
+
63
+ Parameters:
64
+ - patch_path: string, path to the patch file to display (required)
65
+
66
+ </user>
67
+
68
+ Please use the following format to show the patch:
69
+
70
+ <input>
71
+ {PatcheShow.model_json_schema()}
72
+ </input>
73
+ """
74
+
75
+ apply_prompt = f"""
76
+ <user>
77
+
78
+ Please apply a patch to a target directory.
79
+
80
+ Parameters:
81
+ - patch_path: string, path to the patch file (required)
82
+ - target_dir: string, directory to apply the patch to (required)
83
+ - reverse: boolean, whether to reverse apply the patch (optional)
84
+
85
+ </user>
86
+
87
+ Please use the following format to apply the patch:
88
+
89
+ <input>
90
+ {PatcheApply.model_json_schema()}
91
+ </input>
92
+
93
+ The patch will be applied to the target directory. You can also set `reverse` to `true` to reverse the patch application.
94
+ """
95
+
96
+ reverse_apply_prompt = f"""
97
+ <user>
98
+
99
+ Please reverse apply a patch to a target directory.
100
+
101
+ Parameters:
102
+ - patch_path: string, path to the patch file (required)
103
+ - target_dir: string, directory to reverse apply the patch to (required)
104
+ - reverse: boolean, default true for reverse application
105
+
106
+ </user>
107
+
108
+ Please use the following format to reverse apply the patch:
109
+
110
+ <input>
111
+ {{
112
+ "patch_path": "Path to the patch file",
113
+ "target_dir": "Path to the target directory",
114
+ "reverse": true
115
+ }}
116
+ </input>
117
+
118
+ This will undo the changes made by the patch.
119
+ """
120
+
121
+ help_prompt = """
122
+ <user>
123
+
124
+ Please show me how to use patche and what commands are available.
125
+
126
+ </user>
127
+
128
+ I can help you manage patches with Patche. Here are the available commands:
129
+
130
+ 1. **Show Configuration**: View the current Patche configuration
131
+ - Use `patche_config` tool
132
+
133
+ 2. **List Patches**: See all available patches in a directory
134
+ - Use `patche_list` tool with the directory path
135
+
136
+ 3. **Show Patch Details**: View metadata and content of a specific patch
137
+ - Use `patche_show` tool with the patch file path
138
+
139
+ 4. **Apply Patch**: Apply a patch to a target directory
140
+ - Use `patche_apply` tool with patch path and target directory
141
+ - Add `"reverse": true` to undo a patch
142
+
143
+ Would you like me to help you with any of these operations?
144
+ """
145
+
146
+ patch_creation_guidance_prompt = """
147
+ <user>
148
+
149
+ How can I create a new patch?
150
+
151
+ </user>
152
+
153
+ To create a new patch with Patche, you need to:
154
+
155
+ 1. Make changes to your files that you want to include in the patch
156
+ 2. Create a backup of the original files (e.g., by copying them with a `.orig` extension)
157
+ 3. Use a command like `diff -u original_file modified_file > my_patch.patch` to generate the patch
158
+
159
+ If you're using the Patche CLI directly, you can use:
160
+ ```
161
+ patche create --name "my-patch-name" --description "What this patch does" --files file1.py file2.py
162
+ ```
163
+
164
+ The patch will be saved in your configured patches directory and can then be applied to other projects.
165
+
166
+ Would you like me to help you apply an existing patch instead?
167
+ """
168
+
169
+ patche_workflow_prompt = """
170
+ <user>
171
+
172
+ Please explain the typical workflow for using Patche.
173
+
174
+ </user>
175
+
176
+ # Typical Patche Workflow
177
+
178
+ The standard workflow for using Patche involves:
179
+
180
+ 1. **Create or obtain patches**
181
+ - Create patches from modified files
182
+ - Download or receive patches from others
183
+
184
+ 2. **Manage your patch collection**
185
+ - List available patches with `patche_list`
186
+ - Examine patch details with `patche_show`
187
+
188
+ 3. **Apply patches to projects**
189
+ - Apply a patch with `patche_apply`
190
+ - Specify target directory and patch path
191
+
192
+ 4. **Revert changes if needed**
193
+ - Use `patche_apply` with `reverse: true` to undo a patch
194
+
195
+ This workflow allows you to maintain a collection of modifications that can be applied to multiple projects or versions of code.
196
+
197
+ Would you like specific help with any of these steps?
198
+ """
199
+
200
+ patche_examples_prompt = """
201
+ <user>
202
+
203
+ Can you show me some examples of using Patche commands?
204
+
205
+ </user>
206
+
207
+ # Patche Command Examples
208
+
209
+ Here are some practical examples of using Patche commands:
210
+
211
+ **Listing patches in a directory:**
212
+ ```json
213
+ {
214
+ "patche_dir": "/path/to/patches"
215
+ }
216
+ ```
217
+
218
+ **Viewing a specific patch's details:**
219
+ ```json
220
+ {
221
+ "patch_path": "/path/to/patches/feature-fix.patch"
222
+ }
223
+ ```
224
+
225
+ **Applying a patch:**
226
+ ```json
227
+ {
228
+ "patch_path": "/path/to/patches/feature-fix.patch",
229
+ "target_dir": "/path/to/project"
230
+ }
231
+ ```
232
+
233
+ **Reversing a previously applied patch:**
234
+ ```json
235
+ {
236
+ "patch_path": "/path/to/patches/feature-fix.patch",
237
+ "target_dir": "/path/to/project",
238
+ "reverse": true
239
+ }
240
+ ```
241
+
242
+ Would you like me to help you execute any of these commands?
243
+ """
244
+
245
+ # List of all prompts
246
+ prompts: list[Prompt] = [
247
+ Prompt(
248
+ name="base_info",
249
+ description="Basic information about the Patche tool",
250
+ description_zh="Basic info of the Patche toolchain",
251
+ content=base_info_prompt,
252
+ ),
253
+ Prompt(
254
+ name="show_config",
255
+ description="Show the configuration of Patche",
256
+ content=show_config_prompt,
257
+ arguments=[
258
+ {"name": "config_path", "type": "string", "required": False},
259
+ {"name": "patche_dir", "type": "string", "required": True},
260
+ ],
261
+ ),
262
+ Prompt(
263
+ name="list_patches",
264
+ description="List all patches in a directory",
265
+ content=list_prompt,
266
+ arguments=[{"name": "patche_dir", "type": "string", "required": True}],
267
+ ),
268
+ Prompt(
269
+ name="show_patch",
270
+ description="Show detailed information about a patch",
271
+ content=show_prompt,
272
+ arguments=[{"name": "patch_path", "type": "string", "required": True}],
273
+ ),
274
+ Prompt(
275
+ name="apply_patch",
276
+ description="Apply a patch to a target directory",
277
+ content=apply_prompt,
278
+ arguments=[
279
+ {"name": "patch_path", "type": "string", "required": True},
280
+ {"name": "target_dir", "type": "string", "required": True},
281
+ {"name": "reverse", "type": "boolean", "required": False},
282
+ ],
283
+ ),
284
+ Prompt(
285
+ name="reverse_apply",
286
+ description="Reverse apply a patch to a target directory",
287
+ content=reverse_apply_prompt,
288
+ arguments=[
289
+ {"name": "patch_path", "type": "string", "required": True},
290
+ {"name": "target_dir", "type": "string", "required": True},
291
+ ],
292
+ ),
293
+ Prompt(
294
+ name="help",
295
+ description="Show help information about Patche commands",
296
+ content=help_prompt,
297
+ ),
298
+ Prompt(
299
+ name="patch_creation",
300
+ description="Guide for creating new patches",
301
+ content=patch_creation_guidance_prompt,
302
+ ),
303
+ Prompt(
304
+ name="workflow",
305
+ description="Explain the typical Patche workflow",
306
+ content=patche_workflow_prompt,
307
+ ),
308
+ Prompt(
309
+ name="examples",
310
+ description="Show examples of using Patche commands",
311
+ content=patche_examples_prompt,
312
+ ),
313
+ ]