mseep-patche 1.0.1__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.
Files changed (34) hide show
  1. mseep_patche-1.0.1/LICENSE +21 -0
  2. mseep_patche-1.0.1/PKG-INFO +14 -0
  3. mseep_patche-1.0.1/pyproject.toml +45 -0
  4. mseep_patche-1.0.1/src/Patche/__init__.py +1 -0
  5. mseep_patche-1.0.1/src/Patche/__main__.py +3 -0
  6. mseep_patche-1.0.1/src/Patche/__version__.py +9 -0
  7. mseep_patche-1.0.1/src/Patche/app.py +32 -0
  8. mseep_patche-1.0.1/src/Patche/commands/apply.py +107 -0
  9. mseep_patche-1.0.1/src/Patche/commands/help.py +12 -0
  10. mseep_patche-1.0.1/src/Patche/commands/show.py +41 -0
  11. mseep_patche-1.0.1/src/Patche/config.py +24 -0
  12. mseep_patche-1.0.1/src/Patche/mcp/__init__.py +42 -0
  13. mseep_patche-1.0.1/src/Patche/mcp/__main__.py +3 -0
  14. mseep_patche-1.0.1/src/Patche/mcp/model.py +29 -0
  15. mseep_patche-1.0.1/src/Patche/mcp/prompts.py +313 -0
  16. mseep_patche-1.0.1/src/Patche/mcp/server.py +101 -0
  17. mseep_patche-1.0.1/src/Patche/mcp/tools.py +160 -0
  18. mseep_patche-1.0.1/src/Patche/model.py +75 -0
  19. mseep_patche-1.0.1/src/Patche/utils/common.py +73 -0
  20. mseep_patche-1.0.1/src/Patche/utils/header.py +34 -0
  21. mseep_patche-1.0.1/src/Patche/utils/parse.py +269 -0
  22. mseep_patche-1.0.1/src/Patche/utils/resolve.py +168 -0
  23. mseep_patche-1.0.1/tests/bench_parse.py +129 -0
  24. mseep_patche-1.0.1/tests/cases/904d88-email.patch +52 -0
  25. mseep_patche-1.0.1/tests/cases/904d88-git.patch +45 -0
  26. mseep_patche-1.0.1/tests/cases/923936.patch +33 -0
  27. mseep_patche-1.0.1/tests/cases/abc +1 -0
  28. mseep_patche-1.0.1/tests/cases/diff-unified.diff +19 -0
  29. mseep_patche-1.0.1/tests/cases/diff-unified2.diff +5 -0
  30. mseep_patche-1.0.1/tests/cases/efg +2 -0
  31. mseep_patche-1.0.1/tests/cases/lao +11 -0
  32. mseep_patche-1.0.1/tests/cases/tzu +13 -0
  33. mseep_patche-1.0.1/tests/test_apply.py +67 -0
  34. mseep_patche-1.0.1/tests/test_parse.py +1006 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 jingfelix
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.1
2
+ Name: mseep-patche
3
+ Version: 1.0.1
4
+ Summary: Modern Patch in Python
5
+ Author-Email: mseep <support@skydeck.ai>
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: typer[all]>=0.9.0
9
+ Requires-Dist: pydantic>=2.5.3
10
+ Requires-Dist: pydantic-settings>=2.2.1
11
+ Requires-Dist: whatthepatch-pydantic==1.0.6a3
12
+ Description-Content-Type: text/plain
13
+
14
+ Package managed by MseeP.ai
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "mseep-patche"
3
+ version = "1.0.1"
4
+ description = "Modern Patch in Python"
5
+ authors = [
6
+ { name = "mseep", email = "support@skydeck.ai" },
7
+ ]
8
+ dependencies = [
9
+ "typer[all]>=0.9.0",
10
+ "pydantic>=2.5.3",
11
+ "pydantic-settings>=2.2.1",
12
+ "whatthepatch-pydantic==1.0.6a3",
13
+ ]
14
+ requires-python = ">=3.10"
15
+
16
+ [project.readme]
17
+ content-type = "text/plain"
18
+ text = "Package managed by MseeP.ai"
19
+
20
+ [project.license]
21
+ text = "MIT"
22
+
23
+ [project.scripts]
24
+ patche = "Patche.__init__:app"
25
+ patche-mcp = "Patche.mcp.__init__:app"
26
+
27
+ [build-system]
28
+ requires = [
29
+ "pdm-backend",
30
+ ]
31
+ build-backend = "pdm.backend"
32
+
33
+ [tool.pdm]
34
+ distribution = true
35
+
36
+ [tool.pdm.dev-dependencies]
37
+ dev = [
38
+ "viztracer>=0.16.3",
39
+ "whatthepatch>=1.0.7",
40
+ ]
41
+
42
+ [dependency-groups]
43
+ mcp = [
44
+ "mcp[cli]>=1.6.0",
45
+ ]
@@ -0,0 +1 @@
1
+ from .app import __version__, app
@@ -0,0 +1,3 @@
1
+ from .app import app
2
+
3
+ app()
@@ -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")
@@ -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)
@@ -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()
@@ -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()
@@ -0,0 +1,3 @@
1
+ from Patche.mcp import main
2
+
3
+ main()
@@ -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"