Skip to main content

apply-edit-block

Apply search/replace edit blocks from coding agents to source text, with a fallback ladder for near-exact matches. This is the Python port of the JavaScript apply-edit-block package.

The problem

Coding agents (and the humans steering them) emit edits as a block of "find this text, replace it with this text." The search text is almost never byte-exact: the model drops trailing whitespace, reindents a block, paraphrases a comment, or gets one word wrong in an otherwise correct match. Aider, Cline, Roo, Continue, and a pile of homegrown tools each reimplement their own ladder of fallback matching strategies to cope with this, usually as a tangle of regexes buried inside a larger apply-patch function. There are a handful of small competing packages for this and no clear winner, and none of them report which strategy actually matched so callers can log and tune it.

This package is that ladder, pulled out on its own. It matches search text against source text using five strategies of decreasing strictness, applies the replacement, and reports which strategy it used and how confident the match was. It does the matching only: no filesystem access, no git, no diff generation.

Install

pip install apply-edit-block

Usage

from apply_edit_block import apply_edit, apply_edits, parse_blocks, similarity

source = '''function greet(name):
    print("hi " + name)
'''

# The model dropped a trailing space, but the exact strategy still
# finds it via the fallback ladder.
edit = {
    "search": '    print("hi " + name) ',
    "replace": '    print(f"hi {name}")',
}

result = apply_edit(source, edit)
print(result.strategy)  # 'trailing-ws'
print(result.text)
# function greet(name):
#     print(f"hi {name}")

# Parse the conventional fenced format agents emit and apply every block.
patch = '''
<<<<<<< SEARCH
    print("hi " + name)
=======
    print(f"hi {name}")
>>>>>>> REPLACE
'''
edits = parse_blocks(patch)
multi = apply_edits(source, edits)
print(multi.ok, multi.applied)  # True 1

# similarity() is the same scoring function 'fuzzy' uses internally.
print(similarity("a\nb\nc", "a\nb\nz"))  # 0.6666666666666666

API

apply_edit(source, edit, *, anchor_slack=2, threshold=0.85) -> EditResult

  • source: str - the full file contents.
  • edit - an Edit dataclass, or a dict with search: str and replace: str keys.
  • anchor_slack: int (default 2) - for the 'anchor' strategy, how far the source's line gap between the first/last non-empty search lines may differ from the search's.
  • threshold: float (default 0.85) - for the 'fuzzy' strategy, the minimum similarity score required to accept a window.
  • Returns an EditResult, a frozen dataclass:
    @dataclass(frozen=True)
    class EditResult:
        ok: bool
        text: str                    # edited source on success, ORIGINAL source on failure
        strategy: Optional[str]      # 'exact' | 'trailing-ws' | 'indent' | 'anchor' | 'fuzzy' | 'empty-search' | None
        similarity: float            # 1 on exact match; best similarity found otherwise (0..1)
        start: int                   # char offset of match start in the original source, -1 on failure
        end: int                     # char offset of match end (exclusive) in the original source, -1 on failure
    
  • Raises TypeError only if source or the edit's search is not a string. A failed match never raises; it returns ok=False.
  • An empty search string means "prepend replace to the file": ok=True, strategy='empty-search', start=0, end=0.

The strategies are tried in this order, stopping at the first success:

  1. exact - plain substring search.
  2. trailing-ws - line-by-line comparison with trailing whitespace stripped from every line on both sides.
  3. indent - line-by-line comparison with each line's leading whitespace stripped. On success, the indent delta (the matched source line's indentation minus the search's first line's indentation) is applied to every line of the replacement: add spaces for a positive delta, strip up to that many leading spaces for a negative one. Blank replacement lines are left blank.
  4. anchor - matches only on the first and last non-empty lines of search, and requires the line gap between them in the source to be within anchor_slack of the search's gap. Useful when an interior line was paraphrased.
  5. fuzzy - slides a window the size of search's line count over the source and scores each window with similarity(); the best-scoring window is accepted if its score is at least threshold.

On failure, similarity reports the best score fuzzy saw while sliding, so callers can tune threshold.

apply_edits(source, edits, *, anchor_slack=2, threshold=0.85) -> MultiResult

Applies a list of edits in order, each to the output of the previous one.

@dataclass(frozen=True)
class MultiResult:
    ok: bool
    text: str
    results: List[EditResult]
    applied: int

ok is true only if every edit applied. On the first failure it stops immediately: text is the source as of the last successful edit, results holds one EditResult per edit attempted (including the failing one), and applied is the count that succeeded.

parse_blocks(text) -> List[Edit]

Parses the conventional fenced format:

<<<<<<< SEARCH
old code
=======
new code
>>>>>>> REPLACE

Marker lines are matched by prefix (<{3,}, ={3,}, >{3,}), so 3 or more marker characters and trailing text on the marker line (<<<<<<< SEARCH, ======= divider) are both tolerated. Text outside a block is ignored. Returns [] when there are no blocks. A block that opens but never closes (no matching >>>>>>> line before the text ends, or before a new <<<<<<< line starts another block) is skipped, not treated as an error.

Returns a list of Edit dataclass instances (not dicts):

@dataclass(frozen=True)
class Edit:
    search: str
    replace: str

Edit instances and plain {"search": ..., "replace": ...} dicts are interchangeable everywhere an edit argument is accepted, so the output of parse_blocks can be passed straight into apply_edit / apply_edits, and so can your own dicts.

similarity(a, b) -> float

Normalized line-level similarity between two strings, 0..1. This is the exact function the 'fuzzy' strategy uses internally, exported so callers can score candidate matches themselves or tune threshold against real data.

How it works

similarity() splits both strings on \n and computes the longest common subsequence (LCS) of the two line arrays, using exact string equality per line, then divides by the length of the longer array. This is a classic O(n*m) dynamic-programming LCS, not a character-level edit distance. That tradeoff is deliberate: it is cheap to reason about and it is what makes the 'fuzzy' strategy tolerate one bad line out of ten (LCS of 9, divided by 10, is 0.9) without needing a fuzzy string-distance library.

The tradeoff has a real limit: a line that differs by even one character (extra indentation, a changed variable name, a dropped semicolon) counts as a total non-match for that line in the LCS, since comparison is exact-string, not per-character. That is why 'indent' and 'trailing-ws' exist as their own strategies rather than being folded into 'fuzzy': they normalize a specific, common kind of per-line noise before comparing, so a whole block that only differs in leading or trailing whitespace still counts as fully matched rather than scoring low on similarity.

The 'anchor' strategy is the loosest exact-match strategy: it trusts only the first and last non-empty lines of search and a line-count budget for what's in between, so it can survive a paraphrased comment or a rewritten line in the middle of an otherwise-recognizable block. It does not use similarity at all.

All offsets (start, end) are character indices into the original source string that was passed in, and end is exclusive, so source[start:end] is always the exact text that was replaced.

This module has zero runtime dependencies and is a single file (apply_edit_block.py).

The original JavaScript version, with the same behavior, lives at the repository root: ../index.js.

License

MIT

Metadata

Release files for apply-edit-block 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for apply-edit-block 0.1.0
File Size Uploaded Provenance
apply_edit_block-0.1.0.tar.gz 9.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for apply-edit-block 0.1.0
File Interpreter ABI Platform
apply_edit_block-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 18.6 kB

Release files / apply_edit_block-0.1.0.tar.gz

Download URL apply_edit_block-0.1.0.tar.gz
Size 9.9 kB
Tags Source
SHA-256 checksum
How to use checksums
dffc0129de5077937d82008780aeed7d22a13709efe8901dbe0e82ed5299b861
BLAKE2b-256 checksum
How to use checksums
4cb771f5eaeceb536cf62e4e6b981c5a7957f116e178dea83032d39912265966
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / apply_edit_block-0.1.0-py3-none-any.whl

Download URL apply_edit_block-0.1.0-py3-none-any.whl
Size 8.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3361d9453fc0d0f7d90b9bb05de365c60ac724721114f2a72cdbca4216081ddf
BLAKE2b-256 checksum
How to use checksums
6de58853e12320d8ff81486627c99688ba7e078eb20f6dce4575bffc31ba7b45
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for apply-edit-block 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page