search-replace-py 0.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.
- search_replace_py-0.0.1/.github/workflows/lint.yml +29 -0
- search_replace_py-0.0.1/.github/workflows/test.yml +29 -0
- search_replace_py-0.0.1/.gitignore +11 -0
- search_replace_py-0.0.1/.python-version +1 -0
- search_replace_py-0.0.1/LICENSE +21 -0
- search_replace_py-0.0.1/Makefile +37 -0
- search_replace_py-0.0.1/PKG-INFO +234 -0
- search_replace_py-0.0.1/README.md +190 -0
- search_replace_py-0.0.1/main.py +6 -0
- search_replace_py-0.0.1/pyproject.toml +51 -0
- search_replace_py-0.0.1/search_replace/__init__.py +38 -0
- search_replace_py-0.0.1/search_replace/apply.py +436 -0
- search_replace_py-0.0.1/search_replace/errors.py +30 -0
- search_replace_py-0.0.1/search_replace/fuzzy.py +80 -0
- search_replace_py-0.0.1/search_replace/parser.py +220 -0
- search_replace_py-0.0.1/search_replace/prompts.py +242 -0
- search_replace_py-0.0.1/search_replace/types.py +22 -0
- search_replace_py-0.0.1/tests/fixtures/chat-history-search-replace-gold.txt +27714 -0
- search_replace_py-0.0.1/tests/fixtures/chat-history.md +99961 -0
- search_replace_py-0.0.1/tests/test_apply.py +236 -0
- search_replace_py-0.0.1/tests/test_parity_harness.py +76 -0
- search_replace_py-0.0.1/tests/test_parser.py +278 -0
- search_replace_py-0.0.1/tests/test_prompts.py +67 -0
- search_replace_py-0.0.1/uv.lock +650 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Linting
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["master"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["master"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v7
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
run: uv python install 3.14
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: uv sync
|
|
26
|
+
|
|
27
|
+
- name: Run linter
|
|
28
|
+
shell: bash
|
|
29
|
+
run: uv run -- make lint
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Unit Testing
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["master"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["master"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v7
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
run: uv python install 3.14
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: uv sync
|
|
26
|
+
|
|
27
|
+
- name: Run tests
|
|
28
|
+
shell: bash
|
|
29
|
+
run: uv run -- make test
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 marcin
|
|
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,37 @@
|
|
|
1
|
+
.PHONY: init install format lint test build publish clean
|
|
2
|
+
|
|
3
|
+
PYTHON ?= uv run
|
|
4
|
+
|
|
5
|
+
DIST_DIR := dist
|
|
6
|
+
|
|
7
|
+
init:
|
|
8
|
+
@command -v uv >/dev/null 2>&1 || { echo >&2 "Error: uv is not installed."; exit 1; }
|
|
9
|
+
|
|
10
|
+
install: init
|
|
11
|
+
@uv sync
|
|
12
|
+
|
|
13
|
+
format: init
|
|
14
|
+
@$(PYTHON) ruff check search_replace tests --fix
|
|
15
|
+
@$(PYTHON) black search_replace tests
|
|
16
|
+
|
|
17
|
+
lint: init
|
|
18
|
+
@$(PYTHON) ruff check search_replace tests
|
|
19
|
+
@$(PYTHON) black --check search_replace tests
|
|
20
|
+
@$(PYTHON) mypy search_replace
|
|
21
|
+
|
|
22
|
+
test: init
|
|
23
|
+
@$(PYTHON) pytest
|
|
24
|
+
|
|
25
|
+
# Preferred build (uv)
|
|
26
|
+
build: init
|
|
27
|
+
@rm -rf $(DIST_DIR)
|
|
28
|
+
@uv build
|
|
29
|
+
@ls -la $(DIST_DIR)
|
|
30
|
+
|
|
31
|
+
# Publish to PyPI (requires UV_PUBLISH_TOKEN)
|
|
32
|
+
publish: build
|
|
33
|
+
@test -n "$$UV_PUBLISH_TOKEN" || { echo >&2 "Error: UV_PUBLISH_TOKEN is not set"; exit 1; }
|
|
34
|
+
@uv publish
|
|
35
|
+
|
|
36
|
+
clean:
|
|
37
|
+
@rm -rf $(DIST_DIR) .pytest_cache .ruff_cache .mypy_cache
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: search-replace-py
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Parse and apply Aider-style SEARCH/REPLACE patch blocks to files
|
|
5
|
+
Project-URL: Homepage, https://github.com/marcius-llmus/search-replace-py
|
|
6
|
+
Project-URL: Repository, https://github.com/marcius-llmus/search-replace-py
|
|
7
|
+
Project-URL: Issues, https://github.com/marcius-llmus/search-replace-py/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/marcius-llmus/search-replace-py/releases
|
|
9
|
+
Author: marcin
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 marcin
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: aider,diff,editblock,llm,patch,replace,search,tooling
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
39
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
40
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
41
|
+
Classifier: Typing :: Typed
|
|
42
|
+
Requires-Python: >=3.14
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# search-replace-py
|
|
46
|
+
|
|
47
|
+
A standalone Python library for parsing and applying SEARCH/REPLACE patch blocks, extracted from [Aider's](https://github.com/Aider-AI/aider) editblock engine.
|
|
48
|
+
|
|
49
|
+
Use it to give any LLM the ability to propose and apply precise code changes using the battle-tested editblock format.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## How it works
|
|
54
|
+
|
|
55
|
+
The editblock format is Aider's primary mechanism for LLM-driven code editing. The LLM is prompted to output changes as structured `SEARCH/REPLACE` blocks:
|
|
56
|
+
|
|
57
|
+
````
|
|
58
|
+
```python
|
|
59
|
+
mathweb/flask/app.py
|
|
60
|
+
<<<<<<< SEARCH
|
|
61
|
+
from flask import Flask
|
|
62
|
+
=======
|
|
63
|
+
import math
|
|
64
|
+
from flask import Flask
|
|
65
|
+
>>>>>>> REPLACE
|
|
66
|
+
```
|
|
67
|
+
````
|
|
68
|
+
|
|
69
|
+
This library provides:
|
|
70
|
+
|
|
71
|
+
1. **`render_system_prompt()`** — returns the rendered system prompt string to instruct the LLM.
|
|
72
|
+
2. **`get_example_messages()`** — returns a `FewShotExampleMessages` named tuple with four plain strings (two user + two assistant turns) to prepend to the conversation history.
|
|
73
|
+
3. **`apply_diff(llm_response, root)`** — parses the LLM's response and applies all blocks to disk in one call.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## What is included
|
|
78
|
+
|
|
79
|
+
- Block parsing (`<<<<<<< SEARCH`, `=======`, `>>>>>>> REPLACE`) with filename discovery and fuzzy filename resolution.
|
|
80
|
+
- Three replacement strategies:
|
|
81
|
+
- exact match
|
|
82
|
+
- leading-whitespace-tolerant match
|
|
83
|
+
- dotdotdot (`...`) segmented replacement
|
|
84
|
+
- Typed errors (`ParseError`, `ApplyError`) for clean error handling in retry loops.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Installation
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install search-replace-py
|
|
92
|
+
# or with uv
|
|
93
|
+
uv add search-replace-py
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Quick start
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from pathlib import Path
|
|
102
|
+
from search_replace import render_system_prompt, get_example_messages, apply_diff
|
|
103
|
+
|
|
104
|
+
# 1. Build the system prompt — plain string, append your own context if needed
|
|
105
|
+
system_prompt = render_system_prompt()
|
|
106
|
+
|
|
107
|
+
# 2. Build the messages list; prepend few-shot examples before the real request
|
|
108
|
+
ex = get_example_messages()
|
|
109
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
110
|
+
messages += [
|
|
111
|
+
{"role": "user", "content": ex.first_user_message},
|
|
112
|
+
{"role": "assistant", "content": ex.first_assistant_message},
|
|
113
|
+
{"role": "user", "content": ex.second_user_message},
|
|
114
|
+
{"role": "assistant", "content": ex.second_assistant_message},
|
|
115
|
+
]
|
|
116
|
+
messages.append({"role": "user", "content": "Add a docstring to the greet() function in hello.py"})
|
|
117
|
+
|
|
118
|
+
# 3. Send to your LLM and get a response string
|
|
119
|
+
llm_response = "..."
|
|
120
|
+
|
|
121
|
+
# 4. Parse and apply in one call
|
|
122
|
+
apply_diff(llm_response, root=Path("."))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Integration with Pydantic AI
|
|
128
|
+
|
|
129
|
+
[Pydantic AI](https://ai.pydantic.dev) accepts a string for `instructions` and a list of `ModelMessage` objects for `message_history`.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from pathlib import Path
|
|
133
|
+
|
|
134
|
+
from pydantic_ai import Agent, ModelRequest, ModelResponse, TextPart, UserPromptPart
|
|
135
|
+
from search_replace import render_system_prompt, get_example_messages, apply_diff
|
|
136
|
+
|
|
137
|
+
ex = get_example_messages()
|
|
138
|
+
|
|
139
|
+
few_shot = [
|
|
140
|
+
ModelRequest(parts=[UserPromptPart(content=ex.first_user_message)]),
|
|
141
|
+
ModelResponse(parts=[TextPart(content=ex.first_assistant_message)]),
|
|
142
|
+
ModelRequest(parts=[UserPromptPart(content=ex.second_user_message)]),
|
|
143
|
+
ModelResponse(parts=[TextPart(content=ex.second_assistant_message)]),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
agent = Agent("openai:gpt-5.2", instructions=render_system_prompt())
|
|
147
|
+
|
|
148
|
+
auth_py = Path("auth.py").read_text()
|
|
149
|
+
result = agent.run_sync(
|
|
150
|
+
f"Refactor the login function in auth.py to use bcrypt.\n\nauth.py\n```python\n{auth_py}\n```",
|
|
151
|
+
message_history=few_shot,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
apply_diff(result.output, root=Path("."))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### With dry-run validation before writing
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from search_replace import parse_edit_blocks, apply_edits
|
|
161
|
+
from search_replace.errors import ApplyError
|
|
162
|
+
|
|
163
|
+
blocks = parse_edit_blocks(result.output)
|
|
164
|
+
|
|
165
|
+
# Validate all blocks match before touching disk.
|
|
166
|
+
# Without dry_run, blocks that match are written immediately — a later failure
|
|
167
|
+
# would leave files partially patched with no rollback.
|
|
168
|
+
try:
|
|
169
|
+
apply_edits(blocks.edits, root=Path("."), dry_run=True)
|
|
170
|
+
except ApplyError as e:
|
|
171
|
+
# feed the error back to the LLM for a retry
|
|
172
|
+
print(f"Patch would not apply: {e}")
|
|
173
|
+
else:
|
|
174
|
+
apply_edits(blocks.edits, root=Path("."))
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Public API
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from search_replace import (
|
|
183
|
+
# Prompt
|
|
184
|
+
render_system_prompt,
|
|
185
|
+
get_example_messages, # returns FewShotExampleMessages
|
|
186
|
+
FewShotExampleMessages, # NamedTuple: first/second_user/assistant_message
|
|
187
|
+
render_prompt, # render a single template string
|
|
188
|
+
EditBlockFencedPrompts, # raw class with main_system, system_reminder, example_messages
|
|
189
|
+
|
|
190
|
+
# Parse + apply (convenience)
|
|
191
|
+
apply_diff,
|
|
192
|
+
|
|
193
|
+
# Parsing
|
|
194
|
+
parse_edit_blocks,
|
|
195
|
+
find_original_update_blocks,
|
|
196
|
+
EditBlock,
|
|
197
|
+
|
|
198
|
+
# Applying
|
|
199
|
+
apply_edits, # pass dry_run=True to validate without writing
|
|
200
|
+
|
|
201
|
+
# Errors
|
|
202
|
+
ParseError,
|
|
203
|
+
ApplyError,
|
|
204
|
+
MissingFilenameError,
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Tests and validation
|
|
211
|
+
|
|
212
|
+
- `tests/test_parser.py` — block parsing, filename resolution, edge cases
|
|
213
|
+
- `tests/test_apply.py` — replacement strategies, whitespace tolerance, new-file creation
|
|
214
|
+
- `tests/test_prompts.py` — `render_system_prompt` and `get_example_messages` output
|
|
215
|
+
- `tests/test_parity_harness.py` — byte-for-byte comparison against Aider's reference output on the real 100K-line `chat-history.md` fixture
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
uv run python -m pytest tests/
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Credits
|
|
224
|
+
|
|
225
|
+
The parsing engine, replacement strategies, and prompt templates in this library are derived from [Aider](https://github.com/Aider-AI/aider), created by [Paul Gauthier](https://github.com/paul-gauthier). Aider is an outstanding AI pair-programming tool — this library simply extracts and packages its editblock mechanism so it can be reused in other applications. All credit for the original design and implementation goes to him.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Extraction notes
|
|
230
|
+
|
|
231
|
+
- Extracted from `aider/coders/editblock_coder.py` and the fenced editblock prompt module.
|
|
232
|
+
- Runtime coupling to Aider's coder/model lifecycle is fully removed.
|
|
233
|
+
- Error message contracts for malformed blocks and failed apply paths are preserved to maintain LLM retry-loop compatibility.
|
|
234
|
+
- `replace_closest_edit_distance()` remains defined but inactive, preserving the original behaviour of the early return in `replace_most_similar_chunk()`.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# search-replace-py
|
|
2
|
+
|
|
3
|
+
A standalone Python library for parsing and applying SEARCH/REPLACE patch blocks, extracted from [Aider's](https://github.com/Aider-AI/aider) editblock engine.
|
|
4
|
+
|
|
5
|
+
Use it to give any LLM the ability to propose and apply precise code changes using the battle-tested editblock format.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
The editblock format is Aider's primary mechanism for LLM-driven code editing. The LLM is prompted to output changes as structured `SEARCH/REPLACE` blocks:
|
|
12
|
+
|
|
13
|
+
````
|
|
14
|
+
```python
|
|
15
|
+
mathweb/flask/app.py
|
|
16
|
+
<<<<<<< SEARCH
|
|
17
|
+
from flask import Flask
|
|
18
|
+
=======
|
|
19
|
+
import math
|
|
20
|
+
from flask import Flask
|
|
21
|
+
>>>>>>> REPLACE
|
|
22
|
+
```
|
|
23
|
+
````
|
|
24
|
+
|
|
25
|
+
This library provides:
|
|
26
|
+
|
|
27
|
+
1. **`render_system_prompt()`** — returns the rendered system prompt string to instruct the LLM.
|
|
28
|
+
2. **`get_example_messages()`** — returns a `FewShotExampleMessages` named tuple with four plain strings (two user + two assistant turns) to prepend to the conversation history.
|
|
29
|
+
3. **`apply_diff(llm_response, root)`** — parses the LLM's response and applies all blocks to disk in one call.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## What is included
|
|
34
|
+
|
|
35
|
+
- Block parsing (`<<<<<<< SEARCH`, `=======`, `>>>>>>> REPLACE`) with filename discovery and fuzzy filename resolution.
|
|
36
|
+
- Three replacement strategies:
|
|
37
|
+
- exact match
|
|
38
|
+
- leading-whitespace-tolerant match
|
|
39
|
+
- dotdotdot (`...`) segmented replacement
|
|
40
|
+
- Typed errors (`ParseError`, `ApplyError`) for clean error handling in retry loops.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install search-replace-py
|
|
48
|
+
# or with uv
|
|
49
|
+
uv add search-replace-py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from pathlib import Path
|
|
58
|
+
from search_replace import render_system_prompt, get_example_messages, apply_diff
|
|
59
|
+
|
|
60
|
+
# 1. Build the system prompt — plain string, append your own context if needed
|
|
61
|
+
system_prompt = render_system_prompt()
|
|
62
|
+
|
|
63
|
+
# 2. Build the messages list; prepend few-shot examples before the real request
|
|
64
|
+
ex = get_example_messages()
|
|
65
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
66
|
+
messages += [
|
|
67
|
+
{"role": "user", "content": ex.first_user_message},
|
|
68
|
+
{"role": "assistant", "content": ex.first_assistant_message},
|
|
69
|
+
{"role": "user", "content": ex.second_user_message},
|
|
70
|
+
{"role": "assistant", "content": ex.second_assistant_message},
|
|
71
|
+
]
|
|
72
|
+
messages.append({"role": "user", "content": "Add a docstring to the greet() function in hello.py"})
|
|
73
|
+
|
|
74
|
+
# 3. Send to your LLM and get a response string
|
|
75
|
+
llm_response = "..."
|
|
76
|
+
|
|
77
|
+
# 4. Parse and apply in one call
|
|
78
|
+
apply_diff(llm_response, root=Path("."))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Integration with Pydantic AI
|
|
84
|
+
|
|
85
|
+
[Pydantic AI](https://ai.pydantic.dev) accepts a string for `instructions` and a list of `ModelMessage` objects for `message_history`.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from pathlib import Path
|
|
89
|
+
|
|
90
|
+
from pydantic_ai import Agent, ModelRequest, ModelResponse, TextPart, UserPromptPart
|
|
91
|
+
from search_replace import render_system_prompt, get_example_messages, apply_diff
|
|
92
|
+
|
|
93
|
+
ex = get_example_messages()
|
|
94
|
+
|
|
95
|
+
few_shot = [
|
|
96
|
+
ModelRequest(parts=[UserPromptPart(content=ex.first_user_message)]),
|
|
97
|
+
ModelResponse(parts=[TextPart(content=ex.first_assistant_message)]),
|
|
98
|
+
ModelRequest(parts=[UserPromptPart(content=ex.second_user_message)]),
|
|
99
|
+
ModelResponse(parts=[TextPart(content=ex.second_assistant_message)]),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
agent = Agent("openai:gpt-5.2", instructions=render_system_prompt())
|
|
103
|
+
|
|
104
|
+
auth_py = Path("auth.py").read_text()
|
|
105
|
+
result = agent.run_sync(
|
|
106
|
+
f"Refactor the login function in auth.py to use bcrypt.\n\nauth.py\n```python\n{auth_py}\n```",
|
|
107
|
+
message_history=few_shot,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
apply_diff(result.output, root=Path("."))
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### With dry-run validation before writing
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from search_replace import parse_edit_blocks, apply_edits
|
|
117
|
+
from search_replace.errors import ApplyError
|
|
118
|
+
|
|
119
|
+
blocks = parse_edit_blocks(result.output)
|
|
120
|
+
|
|
121
|
+
# Validate all blocks match before touching disk.
|
|
122
|
+
# Without dry_run, blocks that match are written immediately — a later failure
|
|
123
|
+
# would leave files partially patched with no rollback.
|
|
124
|
+
try:
|
|
125
|
+
apply_edits(blocks.edits, root=Path("."), dry_run=True)
|
|
126
|
+
except ApplyError as e:
|
|
127
|
+
# feed the error back to the LLM for a retry
|
|
128
|
+
print(f"Patch would not apply: {e}")
|
|
129
|
+
else:
|
|
130
|
+
apply_edits(blocks.edits, root=Path("."))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Public API
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from search_replace import (
|
|
139
|
+
# Prompt
|
|
140
|
+
render_system_prompt,
|
|
141
|
+
get_example_messages, # returns FewShotExampleMessages
|
|
142
|
+
FewShotExampleMessages, # NamedTuple: first/second_user/assistant_message
|
|
143
|
+
render_prompt, # render a single template string
|
|
144
|
+
EditBlockFencedPrompts, # raw class with main_system, system_reminder, example_messages
|
|
145
|
+
|
|
146
|
+
# Parse + apply (convenience)
|
|
147
|
+
apply_diff,
|
|
148
|
+
|
|
149
|
+
# Parsing
|
|
150
|
+
parse_edit_blocks,
|
|
151
|
+
find_original_update_blocks,
|
|
152
|
+
EditBlock,
|
|
153
|
+
|
|
154
|
+
# Applying
|
|
155
|
+
apply_edits, # pass dry_run=True to validate without writing
|
|
156
|
+
|
|
157
|
+
# Errors
|
|
158
|
+
ParseError,
|
|
159
|
+
ApplyError,
|
|
160
|
+
MissingFilenameError,
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Tests and validation
|
|
167
|
+
|
|
168
|
+
- `tests/test_parser.py` — block parsing, filename resolution, edge cases
|
|
169
|
+
- `tests/test_apply.py` — replacement strategies, whitespace tolerance, new-file creation
|
|
170
|
+
- `tests/test_prompts.py` — `render_system_prompt` and `get_example_messages` output
|
|
171
|
+
- `tests/test_parity_harness.py` — byte-for-byte comparison against Aider's reference output on the real 100K-line `chat-history.md` fixture
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
uv run python -m pytest tests/
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Credits
|
|
180
|
+
|
|
181
|
+
The parsing engine, replacement strategies, and prompt templates in this library are derived from [Aider](https://github.com/Aider-AI/aider), created by [Paul Gauthier](https://github.com/paul-gauthier). Aider is an outstanding AI pair-programming tool — this library simply extracts and packages its editblock mechanism so it can be reused in other applications. All credit for the original design and implementation goes to him.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Extraction notes
|
|
186
|
+
|
|
187
|
+
- Extracted from `aider/coders/editblock_coder.py` and the fenced editblock prompt module.
|
|
188
|
+
- Runtime coupling to Aider's coder/model lifecycle is fully removed.
|
|
189
|
+
- Error message contracts for malformed blocks and failed apply paths are preserved to maintain LLM retry-loop compatibility.
|
|
190
|
+
- `replace_closest_edit_distance()` remains defined but inactive, preserving the original behaviour of the early return in `replace_most_similar_chunk()`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "search-replace-py"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Parse and apply Aider-style SEARCH/REPLACE patch blocks to files"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
license = { file = "LICENSE" }
|
|
10
|
+
authors = [{ name = "marcin" }]
|
|
11
|
+
keywords = ["search", "replace", "patch", "diff", "editblock", "aider", "llm", "tooling"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
"Topic :: Software Development :: Build Tools",
|
|
20
|
+
"Topic :: Software Development :: Version Control",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/marcius-llmus/search-replace-py"
|
|
26
|
+
Repository = "https://github.com/marcius-llmus/search-replace-py"
|
|
27
|
+
Issues = "https://github.com/marcius-llmus/search-replace-py/issues"
|
|
28
|
+
Changelog = "https://github.com/marcius-llmus/search-replace-py/releases"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=9.0.2",
|
|
33
|
+
"ruff>=0.2.2,<1.0.0",
|
|
34
|
+
"black>=25.12.0",
|
|
35
|
+
"mypy>=1.8.0,<2.0.0",
|
|
36
|
+
"build>=1.0.0",
|
|
37
|
+
"twine>=5.0.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.uv]
|
|
41
|
+
default-groups = ["dev"]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["search_replace"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from .apply import apply_diff, apply_edits
|
|
2
|
+
from .errors import (
|
|
3
|
+
ApplyError,
|
|
4
|
+
MissingFilenameError,
|
|
5
|
+
ParseError,
|
|
6
|
+
PathEscapeError,
|
|
7
|
+
SearchReplaceError,
|
|
8
|
+
)
|
|
9
|
+
from .parser import all_fences, find_original_update_blocks, parse_edit_blocks
|
|
10
|
+
from .prompts import (
|
|
11
|
+
EditBlockFencedPrompts,
|
|
12
|
+
FewShotExampleMessages,
|
|
13
|
+
get_example_messages,
|
|
14
|
+
render_system_prompt,
|
|
15
|
+
)
|
|
16
|
+
from .types import ApplyResult, DEFAULT_FENCE, EditBlock, Fence, ParseResult
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ApplyError",
|
|
20
|
+
"ApplyResult",
|
|
21
|
+
"apply_diff",
|
|
22
|
+
"apply_edits",
|
|
23
|
+
"all_fences",
|
|
24
|
+
"DEFAULT_FENCE",
|
|
25
|
+
"EditBlock",
|
|
26
|
+
"EditBlockFencedPrompts",
|
|
27
|
+
"Fence",
|
|
28
|
+
"FewShotExampleMessages",
|
|
29
|
+
"find_original_update_blocks",
|
|
30
|
+
"get_example_messages",
|
|
31
|
+
"MissingFilenameError",
|
|
32
|
+
"parse_edit_blocks",
|
|
33
|
+
"ParseError",
|
|
34
|
+
"PathEscapeError",
|
|
35
|
+
"ParseResult",
|
|
36
|
+
"render_system_prompt",
|
|
37
|
+
"SearchReplaceError",
|
|
38
|
+
]
|