Metadata-Version: 2.4
Name: mpatch
Version: 1.7.0
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing
Requires-Dist: pytest>=7.0 ; extra == 'test'
Provides-Extra: test
Summary: A smart, context-aware patch tool that applies diffs using fuzzy matching, ideal for AI-generated code.
Author-email: Romelium <author@romelium.com>
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Bug Tracker, https://github.com/romelium/mpatch/issues
Project-URL: Homepage, https://github.com/romelium/mpatch
Project-URL: Repository, https://github.com/romelium/mpatch

# Mpatch (Python)

[![PyPI version](https://img.shields.io/pypi/v/mpatch.svg?style=flat-square&logo=pypi&logoColor=white)](https://pypi.org/project/mpatch/)
[![Python versions](https://img.shields.io/pypi/pyversions/mpatch.svg?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/mpatch/)
[![Type Hints](https://img.shields.io/badge/Typing-Strict-success.svg?style=flat-square)](https://pypi.org/project/mpatch/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)

**`mpatch`** is a blazing-fast, context-aware patching library made with Rust. It applies diffs using **fuzzy matching**, making it the perfect tool for processing patches generated by LLMs (ChatGPT, Claude, Gemini, etc.) that often hallucinate line numbers or drift in context.

Standard `patch` or `git apply` fails if the target file has been modified even slightly. `mpatch` looks for the surrounding lines, dynamically adjusts indentation, and applies the change safely.

> **🦀 Looking for the CLI tool or Rust library?** This is the documentation for the Python bindings. Check out the [main `mpatch` repository and CLI documentation](https://github.com/romelium/mpatch).

---

## 💡 The Problem vs. The Solution

When an AI tries to edit a file that has been modified locally since the AI last saw it, standard tools fail.

| Original File | AI-Generated Patch | Standard `patch` | `mpatch` |
| :--- | :--- | :--- | :--- |
| <pre>def main():<br>    # Updated comment<br>    print("Hello")</pre> | <pre> def main():<br>-    print("Hello")<br>+    print("World")</pre> | <pre>❌ Failed<br>Hunk #1 FAILED at 1.<br>1 out of 1 hunk FAILED</pre> | <pre>def main():<br>    # Updated comment<br>    print("World")</pre> |

## ✨ Features

- **🧠 Fuzzy Matching:** Resilient to stale context, whitespace changes, and minor code drift.
- **🔒 Atomic (All-or-Nothing) Mode:** Pass `atomic=True` to only write changes to disk if all patches and hunks succeed cleanly.
- **🤖 Format Independent:** Automatically recognizes Unified Diffs, Markdown code blocks (` ```diff `), Aider Search/Replace blocks (`<<<<<<< ORIGINAL` `=======` `>>>>>>> UPDATED` and variants), and Conflict Markers (`<<<<` `====` `>>>>`).
- **🔍 Wildcard & Ellipsis Support:** Supports `...` and comment-wrapped ellipsis lines (e.g. `// ... existing code ...`) in search/replace blocks, preserving unchanged code gaps.
- **🔀 Three-Way Line Merge:** Built-in line-level 3-way merge via `mpatch.merge_three_way` with Diff3 conflict markers.
- **💡 Sub-Line Word Diffs:** Highlighting word-level additions/deletions via `mpatch.format_inline_diff`.
- **✨ Smart Indentation:** Automatically translates tabs/spaces and aligns injected code to match the target file perfectly.
- **🛡️ Secure:** Built-in protection against directory traversal attacks (e.g., `--- a/../../../etc/passwd`).
- **⚡ Blazing Fast & Concurrent:** Written in Rust. It heavily optimizes the diffing algorithms and releases the GIL during patching, allowing true multithreading in Python.
- **🐍 Pythonic & Typed:** Supports slice indexing, unaries (`~patch` to invert), rich error handling, and ships with comprehensive `.pyi` stubs.

## 📦 Installation

Install directly from PyPI (pre-compiled wheels are provided for macOS, Linux, and Windows):

```bash
pip install mpatch
```

---

## 🚀 Quick Start

The simplest way to use `mpatch` is the high-level `patch_content` function. It takes a diff (even one wrapped in Markdown) and applies it to a string.

````python
import mpatch

original_code = """\
def greet():
    print("Hello, old friend")
"""

# Input can be a raw diff, or a diff wrapped in Markdown (common chat output)
diff = """\
Here is the fix:
```diff
--- a/greet.py
+++ b/greet.py
@@ -1,2 +1,2 @@
 def greet():
-    print("Hello, old friend")
+    print("Hello, new world!")
```
"""

# Apply the patch in-memory!
new_code = mpatch.patch_content(diff, original=original_code)

print(new_code)
# Output:
# def greet():
#     print("Hello, new world!")
````

---

## 🛠️ Advanced API Tour

For tool builders and Agent frameworks, `mpatch` provides a rich Object-Oriented API for parsing, inspecting, and applying patches.

### 1. Parsing Diffs
You can parse a diff string into a list of `Patch` objects. `mpatch` automatically detects the format.

```python
import mpatch

patches = mpatch.parse_auto(diff_string)

for patch in patches:
    print(f"Target file: {patch.file_path}")
    print(f"Number of hunks: {len(patch)}")

    if patch.is_creation:
        print("This patch creates a new file.")
```

### 2. Inspecting Hunks
Patches are composed of `Hunk` objects. You can slice, index, and inspect the exact lines added or removed.

```python
patch = mpatch.parse_auto(diff_string)[0]

for i, hunk in enumerate(patch):
    print(f"Hunk {i + 1} changes:")
    print(f"  Removed: {hunk.removed_lines}")
    print(f"  Added:   {hunk.added_lines}")
    print(f"  Required match span: {hunk.required_match_span} lines")
```

To parse a diff that is expected to contain exactly one patch:

```python
try:
    patch = mpatch.parse_single_patch(single_diff_str)
    print(f"Parsed single patch targeting: {patch.file_path}")
except mpatch.ParseError as e:
    print(f"Expected single patch, but failed: {e}")
```

### 3. Applying to the Filesystem (Batch Processing)
Apply patches directly to a directory on disk. `mpatch` handles file reading, writing, creation, and deletion automatically.

```python
from pathlib import Path
import mpatch

diff = """... your diff here ..."""
target_directory = Path("./my_project")

# Apply all patches in a diff directly to the filesystem
success = mpatch.apply_directory(diff, target_directory)

# Or ensure all-or-nothing atomicity (no disk changes if any hunk fails):
success = mpatch.apply_directory(diff, target_directory, atomic=True)

if success:
    print("All files updated successfully!")
```

For granular control over multiple patches, use `apply_patches_to_dir`:

```python
patches = mpatch.parse_auto(diff)
batch_result = mpatch.apply_patches_to_dir(patches, target_directory)

if not batch_result.all_succeeded:
    # Inspect hard failures (e.g., IO errors, Permission denied, Path traversal)
    for path, error_msg in batch_result.hard_failures:
        print(f"Critical error on {path}: {error_msg}")

if batch_result.has_failures:
    print("One or more patches or hunks failed to apply cleanly.")
    # Dedicated atomic function also available: mpatch.apply_patches_to_dir_atomic(...)
```

### 4. Detailed Reporting & Error Handling
When applying a specific `Patch`, you receive a rich `PatchResult` or `InMemoryResult` detailing exactly what happened to every hunk.

```python
patch = mpatch.parse_auto(diff)[0]
result = patch.apply_to_file("./my_project")

if result.report.has_failures:
    print(f"Patch partially failed. {result.report.failure_count} hunks failed.")

    for failure in result.report.failures:
        print(f"Hunk {failure.hunk_index} failed due to: {failure.error_type}")

        # If it was a fuzzy match that didn't meet the threshold:
        if failure.error_type == "FuzzyMatchBelowThreshold":
            print(f"Best score was {failure.best_score}, needed {failure.threshold}")
            if (
                failure.location_start is not None
                and failure.location_length is not None
            ):
                print(
                    f"Near-miss candidate located at lines {failure.location_start + 1}..{failure.location_start + failure.location_length}"
                )
```

### 5. Dry Runs & Fuzz Factor
Want to see what *would* happen without modifying your files? Use `dry_run=True`. You can also tweak the `fuzz_factor` (0.0 to 1.0, default is 0.7).

```python
result = patch.apply_to_file(
    "./my_project",
    fuzz_factor=0.5,  # Lower is more lenient, higher is stricter (0.0 is exact match only)
    dry_run=True,  # Generate a diff without writing to disk
)

if result.diff:
    print("Proposed changes:")
    print(result.diff)
```

### 6. Reversing Patches
You can reverse a patch (swap additions and deletions) easily using Python's bitwise NOT operator (`~`) or the `invert()` method.

```python
patch = mpatch.parse_auto(diff)[0]

# Invert the patch
reversed_patch = ~patch

# Apply the reversed patch to undo changes
reversed_patch.apply_to_file("./my_project")
```

### 7. Deduplicating and Merging Patches
Consolidate multiple patches or blocks targeting the same file into unified `Patch` objects:

```python
import mpatch

# Combine all patches targeting the same file path
consolidated = mpatch.merge_patches(patches)

# Or merge an individual patch into another:
patch1.merge(patch2)
```

### 8. Creating Diffs Programmatically
Need to generate a unified diff between two strings? You can generate the raw string representation or get a `Patch` object directly.

```python
old_text = "apple\nbanana\npineapple\n"
new_text = "apple\norange\npineapple\n"

# 1. Generate a raw unified diff string
diff_str = mpatch.create_unified_diff("fruits.txt", old_text, new_text)
print(diff_str)
# --- a/fruits.txt
# +++ b/fruits.txt
# @@ -1,3 +1,3 @@
#  apple
# -banana
# +orange
#  pineapple

# 2. Generate a Patch object directly for programmatic manipulation
patch = mpatch.Patch.from_texts("fruits.txt", old_text, new_text)
print(patch[0].removed_lines)  # ['banana']
```

### 9. Aider Search/Replace Blocks with Wildcards
Apply Aider-style search/replace blocks directly. The parser automatically detects file paths and supports wildcard ellipsis lines (`...`) to preserve intermediate code gaps.

```python
import mpatch

original_code = """\
def process_data(data):
    validate(data)
    # Step 1: Normalize
    normalized = [x.strip() for x in data]
    # Step 2: Transform
    transformed = [x.upper() for x in normalized]
    # Step 3: Output
    return transformed
"""

diff = """\
process.py
<<<<<<< SEARCH
def process_data(data):
    ...
    # Step 2: Transform
    transformed = [x.upper() for x in normalized]
=======
def process_data(data):
    ...
    # Step 2: Transform
    transformed = [x.lower() for x in normalized]
>>>>>>> REPLACE
"""

result = mpatch.patch_content(diff, original=original_code)
# Normalization and output steps are preserved; Step 2 is updated!
```

### 10. Three-Way Line Merging
Perform line-level 3-way merges between an ancestor (`base`), local code (`ours`), and incoming changes (`theirs`):

```python
import mpatch

base = "Apples\nBananas\nCherries\nDates\n"
ours = "Apples\nBlueberries\nCherries\nDates\n"
theirs = "Apples\nBananas\nCherries\nDragonfruit\n"

merged, is_conflicted = mpatch.merge_three_way(base, ours, theirs)
assert not is_conflicted
print(merged)
# Apples
# Blueberries
# Cherries
# Dragonfruit

# If there is a conflict, standard Diff3 markers are inserted:
conflict_merged, is_conflicted = mpatch.merge_three_way(
    "val = 1\n",
    "val = 2\n",
    "val = 3\n",
    labels=("base", "ours", "theirs"),
)
assert is_conflicted
print(conflict_merged)
# <<<<<<< ours
# val = 2
# ||||||| base
# val = 1
# =======
# val = 3
# >>>>>>> theirs
```

### 11. Sub-Line Word Diff Visualization
Format near-miss comparisons or word-level diffs with highlighted changes:

```python
import mpatch

expected = ["def calculate(val: int, factor: float = 1.0) -> float:"]
actual = ["def calculate(val: int, factor: float = 2.5) -> float:"]

diff_view = mpatch.format_inline_diff(expected, actual)
print(diff_view)
```

### 12. Format Detection
Inspect the format of an incoming patch string before processing:

```python
import mpatch

fmt = mpatch.detect_patch(diff_string)
# Returns: 'Markdown', 'Unified', 'Aider', 'Conflict', or 'Unknown'
```

### 13. Path Validation & Missing Target Suggestions
Suggest close matching files in the target directory when a patch specifies an unknown or misspelled file path:

```python
import mpatch
from pathlib import Path

# Suggest close matches in ./src for a misspelled filename:
suggestions = mpatch.suggest_close_file_paths("calculate.rs", Path("./src"), limit=3)
for candidate in suggestions:
    print(f"Did you mean: {candidate}?")

# Validate that a target path remains safely inside the base directory:
safe_path = mpatch.ensure_path_is_safe("./src", "utils/helpers.py")
```

### 14. Wildcard Ellipsis & Path Utilities
Check for wildcard ellipsis lines or extract file paths from conversational prose:

```python
import mpatch

# Ellipsis checking:
assert mpatch.is_ellipsis_line("// ... existing code ...") is True
assert mpatch.is_ellipsis_line("const copy = [...items];") is False

# Conversational path extraction:
path = mpatch.extract_file_path_from_line("In `src/server.ts`, replace the handler:")
assert path is not None and path.as_posix() == "src/server.ts"
assert mpatch.is_plausible_file_path("src/server.ts") is True
```

---

## 🚨 Exceptions

`mpatch` provides specific Python exceptions so you can handle failures gracefully:

- `mpatch.MpatchError`: Base exception for all mpatch errors.
- `mpatch.ParseError`: Raised when the diff format is malformed or invalid.
- `mpatch.ApplyError`: Raised during strict patch application when a hunk fails.
- `mpatch.PathTraversalError`: Raised when a patch attempts to modify files outside the target directory (e.g., `../../../etc/passwd`).

```python
try:
    mpatch.apply_patch_to_file(malicious_patch, "./src")
except mpatch.PathTraversalError as e:
    print(f"Security blocked path traversal: {e}")
```

---

## 📄 License

This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
