exhash 0.3.2__tar.gz → 0.3.4__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.
- {exhash-0.3.2 → exhash-0.3.4}/Cargo.lock +11 -11
- {exhash-0.3.2 → exhash-0.3.4}/Cargo.toml +1 -1
- {exhash-0.3.2 → exhash-0.3.4}/DEV.md +8 -2
- {exhash-0.3.2 → exhash-0.3.4}/PKG-INFO +51 -8
- {exhash-0.3.2 → exhash-0.3.4}/README.md +50 -7
- {exhash-0.3.2 → exhash-0.3.4}/pyproject.toml +3 -1
- exhash-0.3.4/python/exhash/__init__.py +337 -0
- exhash-0.3.4/python/exhash/skill.py +65 -0
- {exhash-0.3.2 → exhash-0.3.4}/src/bin/exhash.rs +50 -30
- {exhash-0.3.2 → exhash-0.3.4}/src/engine.rs +67 -44
- {exhash-0.3.2 → exhash-0.3.4}/src/lib.rs +6 -2
- {exhash-0.3.2 → exhash-0.3.4}/src/lnhash.rs +42 -10
- {exhash-0.3.2 → exhash-0.3.4}/src/parse.rs +77 -39
- {exhash-0.3.2 → exhash-0.3.4}/src/python.rs +31 -12
- {exhash-0.3.2 → exhash-0.3.4}/tests/cli.rs +67 -9
- {exhash-0.3.2 → exhash-0.3.4}/tests/test_exhash.py +111 -0
- exhash-0.3.2/python/exhash/__init__.py +0 -99
- {exhash-0.3.2 → exhash-0.3.4}/.github/workflows/ci.yml +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/.gitignore +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/_config.yml +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/_layouts/default.html +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/python/exhash.data/scripts/.gitkeep +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/src/bin/lnhashview.rs +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/tools/build.sh +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/tools/bump.sh +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/tools/bump2.sh +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/tools/release.sh +0 -0
- {exhash-0.3.2 → exhash-0.3.4}/tools/test.sh +0 -0
|
@@ -13,9 +13,9 @@ dependencies = [
|
|
|
13
13
|
|
|
14
14
|
[[package]]
|
|
15
15
|
name = "autocfg"
|
|
16
|
-
version = "1.5.
|
|
16
|
+
version = "1.5.1"
|
|
17
17
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
|
-
checksum = "
|
|
18
|
+
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|
19
19
|
|
|
20
20
|
[[package]]
|
|
21
21
|
name = "cfg-if"
|
|
@@ -25,7 +25,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
|
25
25
|
|
|
26
26
|
[[package]]
|
|
27
27
|
name = "exhash"
|
|
28
|
-
version = "0.3.
|
|
28
|
+
version = "0.3.4"
|
|
29
29
|
dependencies = [
|
|
30
30
|
"pyo3",
|
|
31
31
|
"regex",
|
|
@@ -48,15 +48,15 @@ dependencies = [
|
|
|
48
48
|
|
|
49
49
|
[[package]]
|
|
50
50
|
name = "libc"
|
|
51
|
-
version = "0.2.
|
|
51
|
+
version = "0.2.186"
|
|
52
52
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
53
|
-
checksum = "
|
|
53
|
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
54
54
|
|
|
55
55
|
[[package]]
|
|
56
56
|
name = "memchr"
|
|
57
|
-
version = "2.8.
|
|
57
|
+
version = "2.8.1"
|
|
58
58
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
59
|
-
checksum = "
|
|
59
|
+
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
|
60
60
|
|
|
61
61
|
[[package]]
|
|
62
62
|
name = "memoffset"
|
|
@@ -162,9 +162,9 @@ dependencies = [
|
|
|
162
162
|
|
|
163
163
|
[[package]]
|
|
164
164
|
name = "regex"
|
|
165
|
-
version = "1.12.
|
|
165
|
+
version = "1.12.4"
|
|
166
166
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
167
|
-
checksum = "
|
|
167
|
+
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
|
168
168
|
dependencies = [
|
|
169
169
|
"aho-corasick",
|
|
170
170
|
"memchr",
|
|
@@ -185,9 +185,9 @@ dependencies = [
|
|
|
185
185
|
|
|
186
186
|
[[package]]
|
|
187
187
|
name = "regex-syntax"
|
|
188
|
-
version = "0.8.
|
|
188
|
+
version = "0.8.11"
|
|
189
189
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
190
|
-
checksum = "
|
|
190
|
+
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|
191
191
|
|
|
192
192
|
[[package]]
|
|
193
193
|
name = "rustversion"
|
|
@@ -18,7 +18,8 @@ src/
|
|
|
18
18
|
bin/exhash.rs CLI editor (atomic in-place edit, dry-run, stdin mode)
|
|
19
19
|
bin/lnhashview.rs CLI viewer
|
|
20
20
|
python/exhash/
|
|
21
|
-
__init__.py Python wrapper functions
|
|
21
|
+
__init__.py Python wrapper functions plus file-aware exhash_file orchestration
|
|
22
|
+
skill.py pyskills entry point exposing exhash APIs for LLM tools
|
|
22
23
|
python/exhash.data/scripts/
|
|
23
24
|
exhash native binary (built, not checked in)
|
|
24
25
|
lnhashview native binary (built, not checked in)
|
|
@@ -50,6 +51,9 @@ cargo test && pytest -q
|
|
|
50
51
|
`edit_text` verifies lnhashes command-by-command against the current in-memory buffer, immediately before each command executes (not all upfront). If an earlier command shifts or rewrites a later target line, that later command will fail with a stale-hash error unless you recompute addresses.
|
|
51
52
|
The `$` (last line) and `%` (whole file) address forms are resolved against the current buffer and do not require hashes.
|
|
52
53
|
`edit_text_with_sw` exposes configurable shift width for `<` and `>`; `edit_text` defaults to `sw=4`.
|
|
54
|
+
In CLI and Python file-helper flows, a missing file is treated as empty input only when the parsed command set is valid against an empty buffer (for example `0|0000|a`); otherwise the original file-not-found error is preserved.
|
|
55
|
+
Python `exhash_file` adds the file-qualified orchestration layer. It parses optional `path:` prefixes, applies each command to the current in-memory buffer for that file, rejects cross-file source ranges, and writes changed files only after every command succeeds.
|
|
56
|
+
`lnhashview` range requests clamp `end` past EOF to the last available line, while invalid `start` values still error.
|
|
53
57
|
|
|
54
58
|
## Release
|
|
55
59
|
|
|
@@ -86,9 +90,11 @@ Maturin's `data` option in `pyproject.toml` points to `python/exhash.data/`. Fil
|
|
|
86
90
|
|
|
87
91
|
The Rust core has three parsing functions:
|
|
88
92
|
|
|
89
|
-
- `parse_commands_from_strs(&[&str])` — for the Python API; each string is one command, text blocks
|
|
93
|
+
- `parse_commands_from_strs(&[&str])` — for the Python API; each string is one command, and multiline `a/i/c` text blocks must be in that same string using newlines, e.g. `["12|abcd|c\nnew line 1\nnew line 2"]`. Do not use `.` terminators or split the inserted text into separate command entries; a trailing `.` line is literal text and the Python binding warns about this common mistake.
|
|
90
94
|
- `parse_commands_from_script(&str)` — for script strings; commands separated by newlines, text blocks terminated by `.`
|
|
91
95
|
- `parse_commands_from_args(&[String], &mut BufRead)` — for the CLI; each arg is a command, text blocks read from stdin terminated by `.`
|
|
92
96
|
|
|
97
|
+
File-qualified addresses are parsed by the Python `exhash_file` wrapper; the Rust parser and CLI remain single-buffer.
|
|
98
|
+
|
|
93
99
|
Substitute parsing keeps Rust regex escapes intact (`\d`, `\w`, etc.) while still allowing escaped command delimiters (`\/`) in pattern and replacement.
|
|
94
100
|
Transliteration uses `y/src/dst/` and validates equal character counts at parse time.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: exhash
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Summary: Verified line-addressed file editor using lnhash addresses
|
|
@@ -52,6 +52,8 @@ lnhashview path/to/file.txt
|
|
|
52
52
|
lnhashview path/to/file.txt 10 20
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
If `end` is past EOF, `lnhashview` returns through the last available line instead of failing.
|
|
56
|
+
|
|
55
57
|
### Edit
|
|
56
58
|
|
|
57
59
|
```bash
|
|
@@ -80,6 +82,12 @@ exhash file.txt '%j'
|
|
|
80
82
|
|
|
81
83
|
# Move a line to EOF using $ as the destination
|
|
82
84
|
exhash file.txt '12|abcd|m$'
|
|
85
|
+
|
|
86
|
+
# Create a missing file by treating it as empty input
|
|
87
|
+
exhash new.txt '0|0000|a' <<'EOF'
|
|
88
|
+
first line
|
|
89
|
+
.
|
|
90
|
+
EOF
|
|
83
91
|
```
|
|
84
92
|
|
|
85
93
|
Substitute uses Rust regex syntax:
|
|
@@ -99,6 +107,8 @@ For `a/i/c` commands, provide the text block on stdin:
|
|
|
99
107
|
printf "new line 1\nnew line 2\n.\n" | exhash file.txt "2|beef|a"
|
|
100
108
|
```
|
|
101
109
|
|
|
110
|
+
If the file does not exist and the command set is valid on empty input, exhash treats it as an empty file and writes the result. For example, `0|0000|a` can create a new file.
|
|
111
|
+
|
|
102
112
|
### Stdin filter mode
|
|
103
113
|
|
|
104
114
|
```bash
|
|
@@ -117,13 +127,13 @@ from exhash import exhash, exhash_file, lnhash, lnhashview, lnhashview_file, lin
|
|
|
117
127
|
|
|
118
128
|
```py
|
|
119
129
|
text = "foo\nbar\n"
|
|
120
|
-
view = lnhashview(text)
|
|
121
|
-
view = lnhashview_file("f.py") #
|
|
130
|
+
view = lnhashview(text) # ["1|a1b2| foo", "2|c3d4| bar"]
|
|
131
|
+
view = lnhashview_file("f.py", start=1, end=260) # end past EOF is clamped
|
|
122
132
|
```
|
|
123
133
|
|
|
124
134
|
### Editing
|
|
125
135
|
|
|
126
|
-
`exhash(text, cmds, sw=4)` takes the text and a required iterable of command strings (use `[]` for no-op). `sw` controls how far `<` and `>` shift. For `a`/`i`/`c` commands,
|
|
136
|
+
`exhash(text, cmds, sw=4)` takes the text and a required iterable of command strings (use `[]` for no-op). `sw` controls how far `<` and `>` shift. For multiline `a`/`i`/`c` commands, include the inserted text in the same command string using newline characters, e.g. `["12|abcd|c\nnew line 1\nnew line 2"]`. Do not use `.` terminators, and do not split the text block into separate `cmds` entries. If you include a final `.` line, it is inserted literally and exhash emits a warning.
|
|
127
137
|
|
|
128
138
|
```py
|
|
129
139
|
addr = lnhash(1, "foo") # "1|a1b2|"
|
|
@@ -138,12 +148,15 @@ res = exhash(text, [f"{a1}s/foo/FOO/", f"{a2}s/bar/BAR/"])
|
|
|
138
148
|
# Hashes are checked just-in-time per command.
|
|
139
149
|
# If earlier commands change/shift a later target line, recompute lnhash first.
|
|
140
150
|
|
|
141
|
-
# Append multiline text (no dot terminator)
|
|
151
|
+
# Append multiline text in the same command string (no dot terminator)
|
|
142
152
|
res = exhash(text, [f"{addr}a\nnew line 1\nnew line 2"])
|
|
143
153
|
|
|
144
154
|
# Wrong for the Python API: the trailing "." would be inserted literally
|
|
145
155
|
# res = exhash(text, [f"{addr}a\nnew line 1\nnew line 2\n."])
|
|
146
156
|
|
|
157
|
+
# Also wrong: do not split the inserted text into separate cmds entries
|
|
158
|
+
# res = exhash(text, [f"{addr}a", "new line 1", "new line 2"])
|
|
159
|
+
|
|
147
160
|
# Change shift width for < and >
|
|
148
161
|
res = exhash(text, [f"{addr}>1"], sw=2)
|
|
149
162
|
|
|
@@ -157,18 +170,46 @@ res = exhash("foo\nbar\n", [f"{a1},{a2}s/foo\nbar/replaced/"])
|
|
|
157
170
|
|
|
158
171
|
### File helpers
|
|
159
172
|
|
|
160
|
-
`exhash_file`
|
|
173
|
+
`lnhashview_file` reads directly from one file path. `exhash_file(path, cmds, sw=4, inplace=False)` uses `path` as the default file context for unqualified addresses, and also accepts file-qualified source and `m`/`t` destination addresses:
|
|
161
174
|
|
|
162
175
|
```py
|
|
163
176
|
view = lnhashview_file("file.py")
|
|
164
177
|
|
|
165
|
-
# Returns
|
|
178
|
+
# Returns FileSetEditResult, files unchanged
|
|
166
179
|
res = exhash_file("file.py", [f"{addr}s/foo/bar/"])
|
|
180
|
+
print(res.changed) # ["file.py"]
|
|
181
|
+
print(res["file.py"].lines)
|
|
182
|
+
print(res.format_diff()) # includes --- file.py / +++ file.py headers
|
|
167
183
|
|
|
168
|
-
# With inplace=True, writes
|
|
184
|
+
# With inplace=True, writes changed files after every command succeeds
|
|
185
|
+
# and returns the combined diff string.
|
|
169
186
|
diff = exhash_file("file.py", [f"{addr}s/foo/bar/"], inplace=True)
|
|
187
|
+
|
|
188
|
+
# Missing files are treated as empty only when the command is valid on empty input.
|
|
189
|
+
diff = exhash_file("new.py", ["0|0000|a\nprint('hi')"], inplace=True)
|
|
190
|
+
|
|
191
|
+
# File-qualified addresses can edit or transfer lines across files.
|
|
192
|
+
cmds = [
|
|
193
|
+
"src/a.py:24|8f12|,38|c0de|m src/b.py:$",
|
|
194
|
+
r"src/a.py:5|91aa|s/from \.b import old/from \.b import helper/",
|
|
195
|
+
]
|
|
196
|
+
diff = exhash_file("src/a.py", cmds, inplace=True)
|
|
170
197
|
```
|
|
171
198
|
|
|
199
|
+
A file prefix is separated from the address with `:`. Escape literal colons in filenames as `\:` and literal backslashes as `\\`.
|
|
200
|
+
|
|
201
|
+
`exhash_file(..., inplace=False)` returns a `FileSetEditResult`:
|
|
202
|
+
|
|
203
|
+
- `res.files` — dict of path to `FileEditResult`
|
|
204
|
+
- `res.changed` — changed paths, in first-touch order
|
|
205
|
+
- `res.default_path` — the default path passed to `exhash_file`
|
|
206
|
+
- `res[path]` — shorthand for `res.files[path]`
|
|
207
|
+
- `res.format_diff(context=1)` — combined diff with `--- path` / `+++ path` headers
|
|
208
|
+
|
|
209
|
+
### Pyskill
|
|
210
|
+
|
|
211
|
+
The package registers `exhash.skill` as a pyskill exposing the primary Python APIs with LLM-oriented workflow docs. Use `doc(exhash.skill)` after importing it through a pyskills host.
|
|
212
|
+
|
|
172
213
|
### EditResult
|
|
173
214
|
|
|
174
215
|
`exhash()` returns an `EditResult` with attributes (also accessible via `res["key"]`):
|
|
@@ -184,6 +225,8 @@ diff = exhash_file("file.py", [f"{addr}s/foo/bar/"], inplace=True)
|
|
|
184
225
|
```py
|
|
185
226
|
res = exhash(text, [f"{addr}s/foo/baz/"])
|
|
186
227
|
print(res.format_diff())
|
|
228
|
+
# --- original
|
|
229
|
+
# +++ modified
|
|
187
230
|
# -1|a1b2| foo
|
|
188
231
|
# +1|c3d4| baz
|
|
189
232
|
# 2|e5f6| bar
|
|
@@ -37,6 +37,8 @@ lnhashview path/to/file.txt
|
|
|
37
37
|
lnhashview path/to/file.txt 10 20
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
If `end` is past EOF, `lnhashview` returns through the last available line instead of failing.
|
|
41
|
+
|
|
40
42
|
### Edit
|
|
41
43
|
|
|
42
44
|
```bash
|
|
@@ -65,6 +67,12 @@ exhash file.txt '%j'
|
|
|
65
67
|
|
|
66
68
|
# Move a line to EOF using $ as the destination
|
|
67
69
|
exhash file.txt '12|abcd|m$'
|
|
70
|
+
|
|
71
|
+
# Create a missing file by treating it as empty input
|
|
72
|
+
exhash new.txt '0|0000|a' <<'EOF'
|
|
73
|
+
first line
|
|
74
|
+
.
|
|
75
|
+
EOF
|
|
68
76
|
```
|
|
69
77
|
|
|
70
78
|
Substitute uses Rust regex syntax:
|
|
@@ -84,6 +92,8 @@ For `a/i/c` commands, provide the text block on stdin:
|
|
|
84
92
|
printf "new line 1\nnew line 2\n.\n" | exhash file.txt "2|beef|a"
|
|
85
93
|
```
|
|
86
94
|
|
|
95
|
+
If the file does not exist and the command set is valid on empty input, exhash treats it as an empty file and writes the result. For example, `0|0000|a` can create a new file.
|
|
96
|
+
|
|
87
97
|
### Stdin filter mode
|
|
88
98
|
|
|
89
99
|
```bash
|
|
@@ -102,13 +112,13 @@ from exhash import exhash, exhash_file, lnhash, lnhashview, lnhashview_file, lin
|
|
|
102
112
|
|
|
103
113
|
```py
|
|
104
114
|
text = "foo\nbar\n"
|
|
105
|
-
view = lnhashview(text)
|
|
106
|
-
view = lnhashview_file("f.py") #
|
|
115
|
+
view = lnhashview(text) # ["1|a1b2| foo", "2|c3d4| bar"]
|
|
116
|
+
view = lnhashview_file("f.py", start=1, end=260) # end past EOF is clamped
|
|
107
117
|
```
|
|
108
118
|
|
|
109
119
|
### Editing
|
|
110
120
|
|
|
111
|
-
`exhash(text, cmds, sw=4)` takes the text and a required iterable of command strings (use `[]` for no-op). `sw` controls how far `<` and `>` shift. For `a`/`i`/`c` commands,
|
|
121
|
+
`exhash(text, cmds, sw=4)` takes the text and a required iterable of command strings (use `[]` for no-op). `sw` controls how far `<` and `>` shift. For multiline `a`/`i`/`c` commands, include the inserted text in the same command string using newline characters, e.g. `["12|abcd|c\nnew line 1\nnew line 2"]`. Do not use `.` terminators, and do not split the text block into separate `cmds` entries. If you include a final `.` line, it is inserted literally and exhash emits a warning.
|
|
112
122
|
|
|
113
123
|
```py
|
|
114
124
|
addr = lnhash(1, "foo") # "1|a1b2|"
|
|
@@ -123,12 +133,15 @@ res = exhash(text, [f"{a1}s/foo/FOO/", f"{a2}s/bar/BAR/"])
|
|
|
123
133
|
# Hashes are checked just-in-time per command.
|
|
124
134
|
# If earlier commands change/shift a later target line, recompute lnhash first.
|
|
125
135
|
|
|
126
|
-
# Append multiline text (no dot terminator)
|
|
136
|
+
# Append multiline text in the same command string (no dot terminator)
|
|
127
137
|
res = exhash(text, [f"{addr}a\nnew line 1\nnew line 2"])
|
|
128
138
|
|
|
129
139
|
# Wrong for the Python API: the trailing "." would be inserted literally
|
|
130
140
|
# res = exhash(text, [f"{addr}a\nnew line 1\nnew line 2\n."])
|
|
131
141
|
|
|
142
|
+
# Also wrong: do not split the inserted text into separate cmds entries
|
|
143
|
+
# res = exhash(text, [f"{addr}a", "new line 1", "new line 2"])
|
|
144
|
+
|
|
132
145
|
# Change shift width for < and >
|
|
133
146
|
res = exhash(text, [f"{addr}>1"], sw=2)
|
|
134
147
|
|
|
@@ -142,18 +155,46 @@ res = exhash("foo\nbar\n", [f"{a1},{a2}s/foo\nbar/replaced/"])
|
|
|
142
155
|
|
|
143
156
|
### File helpers
|
|
144
157
|
|
|
145
|
-
`exhash_file`
|
|
158
|
+
`lnhashview_file` reads directly from one file path. `exhash_file(path, cmds, sw=4, inplace=False)` uses `path` as the default file context for unqualified addresses, and also accepts file-qualified source and `m`/`t` destination addresses:
|
|
146
159
|
|
|
147
160
|
```py
|
|
148
161
|
view = lnhashview_file("file.py")
|
|
149
162
|
|
|
150
|
-
# Returns
|
|
163
|
+
# Returns FileSetEditResult, files unchanged
|
|
151
164
|
res = exhash_file("file.py", [f"{addr}s/foo/bar/"])
|
|
165
|
+
print(res.changed) # ["file.py"]
|
|
166
|
+
print(res["file.py"].lines)
|
|
167
|
+
print(res.format_diff()) # includes --- file.py / +++ file.py headers
|
|
152
168
|
|
|
153
|
-
# With inplace=True, writes
|
|
169
|
+
# With inplace=True, writes changed files after every command succeeds
|
|
170
|
+
# and returns the combined diff string.
|
|
154
171
|
diff = exhash_file("file.py", [f"{addr}s/foo/bar/"], inplace=True)
|
|
172
|
+
|
|
173
|
+
# Missing files are treated as empty only when the command is valid on empty input.
|
|
174
|
+
diff = exhash_file("new.py", ["0|0000|a\nprint('hi')"], inplace=True)
|
|
175
|
+
|
|
176
|
+
# File-qualified addresses can edit or transfer lines across files.
|
|
177
|
+
cmds = [
|
|
178
|
+
"src/a.py:24|8f12|,38|c0de|m src/b.py:$",
|
|
179
|
+
r"src/a.py:5|91aa|s/from \.b import old/from \.b import helper/",
|
|
180
|
+
]
|
|
181
|
+
diff = exhash_file("src/a.py", cmds, inplace=True)
|
|
155
182
|
```
|
|
156
183
|
|
|
184
|
+
A file prefix is separated from the address with `:`. Escape literal colons in filenames as `\:` and literal backslashes as `\\`.
|
|
185
|
+
|
|
186
|
+
`exhash_file(..., inplace=False)` returns a `FileSetEditResult`:
|
|
187
|
+
|
|
188
|
+
- `res.files` — dict of path to `FileEditResult`
|
|
189
|
+
- `res.changed` — changed paths, in first-touch order
|
|
190
|
+
- `res.default_path` — the default path passed to `exhash_file`
|
|
191
|
+
- `res[path]` — shorthand for `res.files[path]`
|
|
192
|
+
- `res.format_diff(context=1)` — combined diff with `--- path` / `+++ path` headers
|
|
193
|
+
|
|
194
|
+
### Pyskill
|
|
195
|
+
|
|
196
|
+
The package registers `exhash.skill` as a pyskill exposing the primary Python APIs with LLM-oriented workflow docs. Use `doc(exhash.skill)` after importing it through a pyskills host.
|
|
197
|
+
|
|
157
198
|
### EditResult
|
|
158
199
|
|
|
159
200
|
`exhash()` returns an `EditResult` with attributes (also accessible via `res["key"]`):
|
|
@@ -169,6 +210,8 @@ diff = exhash_file("file.py", [f"{addr}s/foo/bar/"], inplace=True)
|
|
|
169
210
|
```py
|
|
170
211
|
res = exhash(text, [f"{addr}s/foo/baz/"])
|
|
171
212
|
print(res.format_diff())
|
|
213
|
+
# --- original
|
|
214
|
+
# +++ modified
|
|
172
215
|
# -1|a1b2| foo
|
|
173
216
|
# +1|c3d4| baz
|
|
174
217
|
# 2|e5f6| bar
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "exhash"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.4"
|
|
8
8
|
description = "Verified line-addressed file editor using lnhash addresses"
|
|
9
9
|
license = {text = "MIT OR Apache-2.0"}
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -19,6 +19,8 @@ classifiers = [
|
|
|
19
19
|
Homepage = "https://github.com/AnswerDotAI/exhash"
|
|
20
20
|
Repository = "https://github.com/AnswerDotAI/exhash"
|
|
21
21
|
Issues = "https://github.com/AnswerDotAI/exhash/issues"
|
|
22
|
+
[project.entry-points.pyskills]
|
|
23
|
+
exhash = "exhash.skill"
|
|
22
24
|
|
|
23
25
|
[tool.maturin]
|
|
24
26
|
features = ["extension-module"]
|