rgapi 0.2.3__tar.gz → 0.2.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.
- {rgapi-0.2.3 → rgapi-0.2.4}/Cargo.lock +12 -12
- {rgapi-0.2.3 → rgapi-0.2.4}/Cargo.toml +1 -1
- {rgapi-0.2.3 → rgapi-0.2.4}/DEV.md +2 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/PKG-INFO +4 -2
- {rgapi-0.2.3 → rgapi-0.2.4}/README.md +3 -1
- {rgapi-0.2.3 → rgapi-0.2.4}/py/src/lib.rs +29 -14
- {rgapi-0.2.3 → rgapi-0.2.4}/python/rgapi/skill.py +1 -1
- {rgapi-0.2.3 → rgapi-0.2.4}/src/block.rs +7 -6
- rgapi-0.2.4/src/lib.rs +32 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/src/matcher.rs +6 -4
- {rgapi-0.2.3 → rgapi-0.2.4}/src/nb.rs +19 -18
- {rgapi-0.2.3 → rgapi-0.2.4}/src/search.rs +16 -14
- {rgapi-0.2.3 → rgapi-0.2.4}/src/walk.rs +29 -16
- {rgapi-0.2.3 → rgapi-0.2.4}/tests/test_rgapi.py +39 -10
- rgapi-0.2.3/src/lib.rs +0 -24
- {rgapi-0.2.3 → rgapi-0.2.4}/.github/workflows/ci.yml +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/.gitignore +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/LICENSE +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/_config.yml +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/_layouts/default.html +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/py/Cargo.toml +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/py/build.rs +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/pyproject.toml +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/python/rgapi/__init__.py +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/python/rgapi/_cli.py +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/python/rgapi/block.py +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/python/rgapi/nb.py +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/rustfmt.toml +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/tests/test_async.py +0 -0
- {rgapi-0.2.3 → rgapi-0.2.4}/tools/bench.py +0 -0
|
@@ -50,9 +50,9 @@ checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
|
|
|
50
50
|
|
|
51
51
|
[[package]]
|
|
52
52
|
name = "crc32fast"
|
|
53
|
-
version = "1.5.
|
|
53
|
+
version = "1.5.4"
|
|
54
54
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
55
|
-
checksum = "
|
|
55
|
+
checksum = "5f97d3416d5337d15c4b0d78c8c91df922ca143f57ec20e2d1c188333047ef04"
|
|
56
56
|
dependencies = [
|
|
57
57
|
"cfg-if",
|
|
58
58
|
]
|
|
@@ -84,9 +84,9 @@ checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
|
|
84
84
|
|
|
85
85
|
[[package]]
|
|
86
86
|
name = "encoding_rs"
|
|
87
|
-
version = "0.8.
|
|
87
|
+
version = "0.8.44"
|
|
88
88
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
89
|
-
checksum = "
|
|
89
|
+
checksum = "89253175e704ab869b04f9db224d38630e0211a53841a79181deae0889e169f5"
|
|
90
90
|
dependencies = [
|
|
91
91
|
"cfg-if",
|
|
92
92
|
"core_detect",
|
|
@@ -118,9 +118,9 @@ dependencies = [
|
|
|
118
118
|
|
|
119
119
|
[[package]]
|
|
120
120
|
name = "globset"
|
|
121
|
-
version = "0.4.
|
|
121
|
+
version = "0.4.21"
|
|
122
122
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
123
|
-
checksum = "
|
|
123
|
+
checksum = "f98b0d85522dc0a824cfebbc9594ff4a9b289c568bea0370319dfbdf20ccdd2d"
|
|
124
124
|
dependencies = [
|
|
125
125
|
"aho-corasick",
|
|
126
126
|
"bstr",
|
|
@@ -161,9 +161,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
|
161
161
|
|
|
162
162
|
[[package]]
|
|
163
163
|
name = "ignore"
|
|
164
|
-
version = "0.4.
|
|
164
|
+
version = "0.4.34"
|
|
165
165
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
166
|
-
checksum = "
|
|
166
|
+
checksum = "484be5657551d7868d67587df967fb4fcbe9d266a761cc855aaa148fda0b3cc7"
|
|
167
167
|
dependencies = [
|
|
168
168
|
"crossbeam-deque",
|
|
169
169
|
"globset",
|
|
@@ -189,9 +189,9 @@ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
|
|
189
189
|
|
|
190
190
|
[[package]]
|
|
191
191
|
name = "log"
|
|
192
|
-
version = "0.4.
|
|
192
|
+
version = "0.4.35"
|
|
193
193
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
194
|
-
checksum = "
|
|
194
|
+
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
|
|
195
195
|
|
|
196
196
|
[[package]]
|
|
197
197
|
name = "memchr"
|
|
@@ -320,7 +320,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|
|
320
320
|
|
|
321
321
|
[[package]]
|
|
322
322
|
name = "rgapi"
|
|
323
|
-
version = "0.2.
|
|
323
|
+
version = "0.2.4"
|
|
324
324
|
dependencies = [
|
|
325
325
|
"crc32fast",
|
|
326
326
|
"fancy-regex",
|
|
@@ -335,7 +335,7 @@ dependencies = [
|
|
|
335
335
|
|
|
336
336
|
[[package]]
|
|
337
337
|
name = "rgapi-py"
|
|
338
|
-
version = "0.2.
|
|
338
|
+
version = "0.2.4"
|
|
339
339
|
dependencies = [
|
|
340
340
|
"grep-matcher",
|
|
341
341
|
"pyo3",
|
|
@@ -43,6 +43,8 @@ All user-supplied regexes use the shared `RegexMatcher` in `src/matcher.rs`, bac
|
|
|
43
43
|
|
|
44
44
|
The matcher centralizes smart-case analysis and line-ending policy. Line-safe patterns can search a whole buffer; patterns that can consume line endings, inspect neighbouring lines or need the backtracking engine are evaluated on individual lines, so that engine's step budget covers one line, not a whole buffer. Notebook multiline matching instead uses the whole cell. Filename/path regexes match their whole input. Backtracking-limit errors propagate through the existing result channels, including directory-pruning errors; cancellation must not hide a queued error from async consumers.
|
|
45
45
|
|
|
46
|
+
A walk error, a file the walk can't read, or a notebook `nbrg` can't parse is a skipped error (`RgApiError::is_skipped`). `send_error` sends a skipped error without ending the walk. Any other error ends it. The Python bindings issue a `RuntimeWarning` for each skipped error and raise `ValueError` for any other (`skip_or_raise`, and `worker_error` on async worker threads). Rust's collecting functions, such as `find`, `rg`, `nb_search` and `StreamIter::collect_timeout`, return the first error of either kind. `entry_err` drops a permission error silently at the `max_depth` cap, where the walker opens a directory it never descends into.
|
|
47
|
+
|
|
46
48
|
Python discovery and `paths=True` results contain absolute `pathlib.Path` objects. Structured search rows hold base-relative string labels with `/` separators. Traversal uses `ignore::WalkParallel`, so result order is not part of the API contract. Search results are structured rows; collected result lists use rg-style `str()` and notebook display. `SearchLine.lnhash` is computed with the same CRC-32-based line-content hash format as exhash (`lineno|hash|`, low 12 bits of CRC-32 over the line's UTF-8 bytes, encoded as two Base64url characters); `lnhashs=True` only changes row display, not `line_number` or matching behavior. Path regexes filter returned/searched paths; `skip_dir` and `skip_dir_re` prune traversal through `ignore::WalkBuilder::filter_entry`. Depth, size, filesystem, hidden, and ignore options use `ignore::WalkBuilder` settings. The `ignore` walker follows every root it is given. Discovery checks each root with `symlink_metadata`. The worker sends each unfollowed link root as a result before walking the other roots. This also supports dangling roots, which the underlying walker would reject. `ls` sets `walk_root_links`, which gives a root link to a directory to the walker. The walker then reports paths under the link. Other discovery roots use absolute paths without canonicalizing. Content searches retain canonical root resolution. `rg_iter` exposes the same parallel search stream that `rg` collects by default; `paths=True` and `count=True` consume that stream with different reducers. Text search skips binary files and invalid UTF-8 content.
|
|
47
49
|
|
|
48
50
|
Streaming engine: `walk.rs` owns the generic machinery. `StreamIter<T>` is the worker-thread-plus-bounded-channel iterator (`sync_channel(8192)`, so producers block rather than buffer without limit when a consumer lags), and `spawn_walk` owns the shared scaffold: walker config, panic catching, cancel flag, and worker thread. `rg_iter` (`T = SearchLine`), `block_iter` (`T = SearchBlock`), `nb_iter` (`T = NbCell`), and `find_iter` (`T = PathBuf`, the path walk) plug entry closures into that engine. Block search reads each file once, searches it once, groups nonblank lines into blocks, maps matching lines to their blocks, and expands context by block index. Each `SearchBlock` carries numeric boundaries plus hashes for its first and last source lines. Python keeps both and chooses the displayed address without another file read.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rgapi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Requires-Dist: fastcore>=2.2.29
|
|
@@ -85,6 +85,8 @@ Discovery uses the `ignore` crate with ripgrep's default filters. It reads `.git
|
|
|
85
85
|
|
|
86
86
|
Hidden files are skipped unless `hidden=True`. Discovery returns symlinks themselves, including explicitly named roots and dangling links. Set `follow_links=True` to traverse their targets. Paths retain the symlink spelling. Use `same_file_system=True` to avoid crossing filesystem boundaries.
|
|
87
87
|
|
|
88
|
+
A walk skips each directory or file that it can't read, such as one without read permission, and issues a `RuntimeWarning` that names it. The rest of the walk continues, as ripgrep's does.
|
|
89
|
+
|
|
88
90
|
Traversal runs in parallel without guaranteed result order. Use `sorted(...)` when order matters.
|
|
89
91
|
|
|
90
92
|
`fd` takes `pattern` first and `root` second, as `rg` does. With no pattern, it returns every file under `root`. `pattern` is a smart-case regex matched against each basename. Lowercase patterns match case-insensitively. A pattern containing uppercase letters is case-sensitive. Use `path_re` to match the slash-separated relative path instead.
|
|
@@ -235,7 +237,7 @@ A matching cell's display starts at its first matched line. Earlier lines are re
|
|
|
235
237
|
- `count=True` returns the number of matching cells.
|
|
236
238
|
- `timeout_ms` applies a deadline with the same `stop_reason` values as `rg`.
|
|
237
239
|
|
|
238
|
-
The parser reads only each cell's `id`, `cell_type`, and `source`. It skips outputs and metadata without loading embedded images or plots. `search_nb(pattern, path, ...)` searches a single notebook file in the same way.
|
|
240
|
+
The parser reads only each cell's `id`, `cell_type`, and `source`. It skips outputs and metadata without loading embedded images or plots. `search_nb(pattern, path, ...)` searches a single notebook file in the same way. `nbrg` skips a `.ipynb` file that isn't valid JSON and issues a `RuntimeWarning` naming the file and the parse error. `search_nb` raises `ValueError` for such a file.
|
|
239
241
|
|
|
240
242
|
`rgapi-nbrg` exposes notebook search without requiring a Python kernel:
|
|
241
243
|
|
|
@@ -64,6 +64,8 @@ Discovery uses the `ignore` crate with ripgrep's default filters. It reads `.git
|
|
|
64
64
|
|
|
65
65
|
Hidden files are skipped unless `hidden=True`. Discovery returns symlinks themselves, including explicitly named roots and dangling links. Set `follow_links=True` to traverse their targets. Paths retain the symlink spelling. Use `same_file_system=True` to avoid crossing filesystem boundaries.
|
|
66
66
|
|
|
67
|
+
A walk skips each directory or file that it can't read, such as one without read permission, and issues a `RuntimeWarning` that names it. The rest of the walk continues, as ripgrep's does.
|
|
68
|
+
|
|
67
69
|
Traversal runs in parallel without guaranteed result order. Use `sorted(...)` when order matters.
|
|
68
70
|
|
|
69
71
|
`fd` takes `pattern` first and `root` second, as `rg` does. With no pattern, it returns every file under `root`. `pattern` is a smart-case regex matched against each basename. Lowercase patterns match case-insensitively. A pattern containing uppercase letters is case-sensitive. Use `path_re` to match the slash-separated relative path instead.
|
|
@@ -214,7 +216,7 @@ A matching cell's display starts at its first matched line. Earlier lines are re
|
|
|
214
216
|
- `count=True` returns the number of matching cells.
|
|
215
217
|
- `timeout_ms` applies a deadline with the same `stop_reason` values as `rg`.
|
|
216
218
|
|
|
217
|
-
The parser reads only each cell's `id`, `cell_type`, and `source`. It skips outputs and metadata without loading embedded images or plots. `search_nb(pattern, path, ...)` searches a single notebook file in the same way.
|
|
219
|
+
The parser reads only each cell's `id`, `cell_type`, and `source`. It skips outputs and metadata without loading embedded images or plots. `search_nb(pattern, path, ...)` searches a single notebook file in the same way. `nbrg` skips a `.ipynb` file that isn't valid JSON and issues a `RuntimeWarning` naming the file and the parse error. `search_nb` raises `ValueError` for such a file.
|
|
218
220
|
|
|
219
221
|
`rgapi-nbrg` exposes notebook search without requiring a Python kernel:
|
|
220
222
|
|
|
@@ -3,14 +3,15 @@ use std::path::PathBuf;
|
|
|
3
3
|
use std::sync::Arc;
|
|
4
4
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
5
5
|
use std::sync::mpsc::RecvTimeoutError;
|
|
6
|
+
use std::thread;
|
|
6
7
|
use std::time::{Duration, Instant};
|
|
7
8
|
|
|
8
|
-
use pyo3::exceptions::PyValueError;
|
|
9
|
+
use pyo3::exceptions::{PyRuntimeWarning, PyValueError};
|
|
9
10
|
use pyo3::prelude::*;
|
|
10
11
|
use pyo3::types::{PyAny, PyDict};
|
|
11
12
|
|
|
12
13
|
use rgapi::{
|
|
13
|
-
FindIter, FindOptions, NbCell, NbIter, NbOptions, RgIter, RgOptions, SearchBlock, SearchLine, StreamIter, WalkOptions as WalkOptionsCore,
|
|
14
|
+
FindIter, FindOptions, NbCell, NbIter, NbOptions, RgApiError, RgIter, RgOptions, SearchBlock, SearchLine, StreamIter, WalkOptions as WalkOptionsCore,
|
|
14
15
|
block_iter as block_iter_core, compile_regex, find, find_iter as find_iter_core, find_iter_with, nb_iter as nb_iter_core, nb_search_file, resolve_roots,
|
|
15
16
|
rg_iter as rg_iter_core, search_path as search_path_core, search_text as search_text_core, spans_for,
|
|
16
17
|
};
|
|
@@ -105,14 +106,26 @@ fn check_signals_or_cancel<T>(py: Python<'_>, iter: &StreamIter<T>) -> PyResult<
|
|
|
105
106
|
Ok(())
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/// Warn about a path that a walk skipped because it couldn't read it, as ripgrep reports such a path. Any other error raises `ValueError`.
|
|
110
|
+
fn skip_or_raise(py: Python<'_>, err: RgApiError) -> PyResult<()> {
|
|
111
|
+
if !err.is_skipped() { return Err(PyValueError::new_err(err.to_string())); }
|
|
112
|
+
PyErr::warn(py, py.get_type::<PyRuntimeWarning>().as_any(), &std::ffi::CString::new(err.to_string())?, 1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// `skip_or_raise` on a worker thread. Gives the message of an error that ends the stream, or `None` when the walk only skipped a path.
|
|
116
|
+
fn worker_error(err: RgApiError) -> Option<String> {
|
|
117
|
+
if !err.is_skipped() { return Some(err.to_string()); }
|
|
118
|
+
Python::attach(|py| skip_or_raise(py, err).err().map(|e| e.to_string()))
|
|
119
|
+
}
|
|
120
|
+
|
|
108
121
|
fn next_stream_py<T: Send>(py: Python<'_>, iter: &mut StreamIter<T>) -> PyResult<Option<T>> {
|
|
109
122
|
loop {
|
|
110
123
|
check_signals_or_cancel(py, iter)?;
|
|
111
124
|
let res = py.detach(|| iter.next_timeout(Duration::from_millis(50)));
|
|
112
125
|
check_signals_or_cancel(py, iter)?;
|
|
113
126
|
match res {
|
|
114
|
-
Ok(Ok(line)) => return Ok(Some(line)), Ok(Err(err)) =>
|
|
115
|
-
Err(RecvTimeoutError::
|
|
127
|
+
Ok(Ok(line)) => return Ok(Some(line)), Ok(Err(err)) => skip_or_raise(py, err)?, Err(RecvTimeoutError::Disconnected) => return Ok(None),
|
|
128
|
+
Err(RecvTimeoutError::Timeout) => continue
|
|
116
129
|
}
|
|
117
130
|
}
|
|
118
131
|
}
|
|
@@ -131,8 +144,8 @@ fn collect_stream_py<T: Send, P>(py: Python<'_>, mut iter: StreamIter<T>, conv:
|
|
|
131
144
|
let next = py.detach(|| iter.next_timeout(wait));
|
|
132
145
|
check_signals_or_cancel(py, &iter)?;
|
|
133
146
|
match next {
|
|
134
|
-
Ok(Ok(line)) => res.push(conv(line)), Ok(Err(err)) =>
|
|
135
|
-
Err(RecvTimeoutError::
|
|
147
|
+
Ok(Ok(line)) => res.push(conv(line)), Ok(Err(err)) => skip_or_raise(py, err)?, Err(RecvTimeoutError::Disconnected) => return Ok((res, false)),
|
|
148
|
+
Err(RecvTimeoutError::Timeout) => {}
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
}
|
|
@@ -278,9 +291,11 @@ fn drain_stream<T>(iter: &mut StreamIter<T>, flag: &Arc<AtomicBool>, deadline: O
|
|
|
278
291
|
match iter.next_timeout(wait) {
|
|
279
292
|
Ok(Ok(line)) => rows.push(line),
|
|
280
293
|
Ok(Err(e)) => {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
if let Some(e) = worker_error(e) {
|
|
295
|
+
err = Some(e);
|
|
296
|
+
iter.cancel();
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
284
299
|
}
|
|
285
300
|
Err(RecvTimeoutError::Disconnected) => break,
|
|
286
301
|
Err(RecvTimeoutError::Timeout) => {}
|
|
@@ -293,7 +308,7 @@ fn stream_async<T, C>(cb: Py<PyAny>, mut iter: StreamIter<T>, deadline: Option<I
|
|
|
293
308
|
C: Fn(Python<'_>, Vec<T>, bool) -> PyResult<Py<PyAny>> + Send + 'static {
|
|
294
309
|
let cancel = iter.cancel_flag();
|
|
295
310
|
let flag = cancel.clone();
|
|
296
|
-
|
|
311
|
+
thread::spawn(move || {
|
|
297
312
|
let (rows, timed_out, err) = drain_stream(&mut iter, &flag, deadline);
|
|
298
313
|
drop(iter);
|
|
299
314
|
Python::attach(|py| {
|
|
@@ -312,18 +327,18 @@ fn stream_iter_async<T, C>(cb: Py<PyAny>, mut iter: StreamIter<T>, batch_max: us
|
|
|
312
327
|
C: Fn(Python<'_>, Vec<T>) -> PyResult<Py<PyAny>> + Send + 'static {
|
|
313
328
|
let cancel = iter.cancel_flag();
|
|
314
329
|
let flag = cancel.clone();
|
|
315
|
-
|
|
330
|
+
thread::spawn(move || {
|
|
316
331
|
let mut err: Option<String> = None;
|
|
317
332
|
loop {
|
|
318
333
|
if flag.load(Ordering::Relaxed) { break; }
|
|
319
334
|
let first = match iter.next_timeout(Duration::from_millis(50)) {
|
|
320
|
-
Ok(Ok(line)) => line, Ok(Err(e)) => { err = Some(e
|
|
321
|
-
Err(RecvTimeoutError::Timeout) => continue
|
|
335
|
+
Ok(Ok(line)) => line, Ok(Err(e)) => match worker_error(e) { Some(e) => { err = Some(e); break; } None => continue },
|
|
336
|
+
Err(RecvTimeoutError::Disconnected) => break, Err(RecvTimeoutError::Timeout) => continue
|
|
322
337
|
};
|
|
323
338
|
let mut batch = vec![first];
|
|
324
339
|
while batch.len() < batch_max {
|
|
325
340
|
match iter.next_timeout(Duration::ZERO) {
|
|
326
|
-
Ok(Ok(line)) => batch.push(line), Ok(Err(e)) => { err = Some(e
|
|
341
|
+
Ok(Ok(line)) => batch.push(line), Ok(Err(e)) => { if let Some(e) = worker_error(e) { err = Some(e); break; } } Err(_) => break
|
|
327
342
|
}
|
|
328
343
|
}
|
|
329
344
|
let failed = Python::attach(|py| match conv(py, batch) { Ok(res) => cb.call1(py, (res, None::<String>)).is_err(), Err(_) => true });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Find files, search text, and search notebook cell sources from Python with ripgrep semantics and structured results. Use for fd-style discovery, regex searches, and notebook searches returning stable cell IDs rather than escaped JSON.
|
|
2
2
|
|
|
3
|
-
Built on ripgrep's `ignore` and `grep-searcher` crates with pure-Rust `fancy-regex`: ignore files, hidden files, globs and extensions follow ripgrep conventions. All regex parameters support backreferences and lookaround without an engine flag. Content matching is line-local unless `nbrg(multiline=True)` is used; filename/path regexes match their full input. Regex compilation or backtracking-limit failures raise `ValueError`; cancellation cannot interrupt an in-progress regex evaluation. Prefer these to shell parsing or manual file scans: `fd`/`ls` for listings, `rg` for files, `nbrg` for notebook cells, `rgstr` for text already in hand.
|
|
3
|
+
Built on ripgrep's `ignore` and `grep-searcher` crates with pure-Rust `fancy-regex`: ignore files, hidden files, globs and extensions follow ripgrep conventions. All regex parameters support backreferences and lookaround without an engine flag. Content matching is line-local unless `nbrg(multiline=True)` is used; filename/path regexes match their full input. Regex compilation or backtracking-limit failures raise `ValueError`; cancellation cannot interrupt an in-progress regex evaluation. A walk skips each directory or file that it can't read, and each notebook that `nbrg` can't parse. Each skip issues a `RuntimeWarning` that names the path. Prefer these to shell parsing or manual file scans: `fd`/`ls` for listings, `rg` for files, `nbrg` for notebook cells, `rgstr` for text already in hand.
|
|
4
4
|
|
|
5
5
|
Orient with `rg(summary=True)`: one row per block (a paragraph of prose/config) with boundary addresses, where line results show fragments that may need another view. Switch to line results where needed; add `lnhashs=True` when edits may follow. Display results bare; narrow oversized results with parameters, not by joining, slicing, or reformatting.
|
|
6
6
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
use std::collections::{BTreeMap, HashMap};
|
|
2
|
+
use std::fs;
|
|
2
3
|
use std::path::Path;
|
|
4
|
+
use std::str;
|
|
3
5
|
use std::sync::Arc;
|
|
4
6
|
use std::sync::atomic::Ordering;
|
|
5
7
|
|
|
6
8
|
use crate::RegexMatcher;
|
|
7
9
|
use ignore::{DirEntry, WalkState};
|
|
8
10
|
|
|
9
|
-
use crate::RgApiError;
|
|
10
|
-
use crate::compile_regex;
|
|
11
11
|
use crate::search::{RgOptions, SearchLine, format_lnhash, search_text};
|
|
12
|
-
use crate::walk::{PathFilters, StreamIter, entry_err, rel_path, resolve_roots, spawn_walk};
|
|
12
|
+
use crate::walk::{PathFilters, StreamIter, entry_err, rel_path, resolve_roots, send_error, spawn_walk};
|
|
13
|
+
use crate::{RgApiError, compile_regex};
|
|
13
14
|
|
|
14
15
|
/// One blank-line-delimited block containing a match, or context for one.
|
|
15
16
|
pub struct SearchBlock {
|
|
@@ -48,7 +49,7 @@ fn split_blocks(text: &str) -> Vec<BlockInfo> {
|
|
|
48
49
|
|
|
49
50
|
fn process_file(disp: String, bytes: &[u8], matcher: &RegexMatcher, before_context: usize, after_context: usize) -> Result<Vec<SearchBlock>, RgApiError> {
|
|
50
51
|
if bytes.contains(&0) { return Ok(Vec::new()); }
|
|
51
|
-
let text = match
|
|
52
|
+
let text = match str::from_utf8(bytes) { Ok(text) => text, Err(_) => return Ok(Vec::new()) };
|
|
52
53
|
let blocks = split_blocks(text);
|
|
53
54
|
let hits = search_text(disp.clone(), text, matcher.clone(), 0, 0, false)?;
|
|
54
55
|
if hits.is_empty() { return Ok(Vec::new()); }
|
|
@@ -83,7 +84,7 @@ fn block_entry(
|
|
|
83
84
|
if !ft.is_file() { return Ok(Vec::new()); }
|
|
84
85
|
let rel = rel_path(base, path);
|
|
85
86
|
if !filters.path_allowed(Path::new(&rel))? { return Ok(Vec::new()); }
|
|
86
|
-
let bytes =
|
|
87
|
+
let bytes = fs::read(path).map_err(|err| RgApiError::unreadable(path, err))?;
|
|
87
88
|
process_file(rel, &bytes, matcher, before_context, after_context)
|
|
88
89
|
}
|
|
89
90
|
|
|
@@ -100,7 +101,7 @@ pub fn block_iter(opts: &RgOptions) -> Result<BlockIter, RgApiError> {
|
|
|
100
101
|
for block in blocks { if cancel.load(Ordering::Relaxed) || tx.send(Ok(block)).is_err() { return WalkState::Quit; } }
|
|
101
102
|
WalkState::Continue
|
|
102
103
|
}
|
|
103
|
-
Err(err) =>
|
|
104
|
+
Err(err) => send_error(tx, err),
|
|
104
105
|
}
|
|
105
106
|
}))
|
|
106
107
|
}
|
rgapi-0.2.4/src/lib.rs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//! rgapi - Python-friendly wrappers around ripgrep's walking and searching crates.
|
|
2
|
+
|
|
3
|
+
use std::fmt::Display;
|
|
4
|
+
mod block;
|
|
5
|
+
mod matcher;
|
|
6
|
+
mod nb;
|
|
7
|
+
mod search;
|
|
8
|
+
mod walk;
|
|
9
|
+
|
|
10
|
+
pub use block::{BlockIter, SearchBlock, block_iter};
|
|
11
|
+
pub use matcher::{RegexMatcher, compile_regex};
|
|
12
|
+
pub use nb::{CellRefs, NbCell, NbIter, NbOptions, ancestor_indices, cell_refs, heading_level, nb_iter, nb_search, nb_search_file, section_range};
|
|
13
|
+
pub use search::{MatchSpan, RgIter, RgOptions, SearchError, SearchKind, SearchLine, rg, rg_iter, search_path, search_text, spans_for};
|
|
14
|
+
pub use walk::{FindIter, FindOptions, StreamIter, WalkOptions, find, find_iter, find_iter_with, resolve_roots};
|
|
15
|
+
|
|
16
|
+
/// An rgapi failure. `is_skipped` marks an error naming one path that a walk couldn't read, such as a directory without read permission.
|
|
17
|
+
/// The walk sends that error and continues, as ripgrep does. Rust's collecting functions, such as `find`, return the first error of either kind.
|
|
18
|
+
#[derive(Debug, Clone)]
|
|
19
|
+
pub struct RgApiError { msg: String, skipped: bool }
|
|
20
|
+
|
|
21
|
+
impl RgApiError {
|
|
22
|
+
pub(crate) fn new(msg: impl Into<String>) -> Self { Self { msg: msg.into(), skipped: false } }
|
|
23
|
+
pub(crate) fn skipped(msg: impl Display) -> Self { Self { msg: msg.to_string(), skipped: true } }
|
|
24
|
+
pub(crate) fn unreadable(path: &std::path::Path, err: impl Display) -> Self { Self::skipped(format!("{}: {err}", path.display())) }
|
|
25
|
+
pub fn is_skipped(&self) -> bool { self.skipped }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
impl Display for RgApiError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.msg) } }
|
|
29
|
+
|
|
30
|
+
impl std::error::Error for RgApiError {}
|
|
31
|
+
|
|
32
|
+
impl From<std::io::Error> for RgApiError { fn from(err: std::io::Error) -> Self { Self::new(err.to_string()) } }
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
use ast::ClassSetItem;
|
|
1
2
|
use fancy_regex::{Assertion, Expr, Regex, RegexBuilder};
|
|
2
3
|
use grep_matcher::{LineTerminator, Match, Matcher, NoCaptures};
|
|
3
4
|
use regex_syntax::{ast::{self, Ast, Visitor}, hir::{Class, Hir, HirKind}};
|
|
5
|
+
use std::fmt::Display;
|
|
4
6
|
|
|
5
7
|
use crate::RgApiError;
|
|
6
8
|
|
|
@@ -68,7 +70,7 @@ pub fn compile_regex(pattern: &str, case_sensitive: Option<bool>, smart_case: bo
|
|
|
68
70
|
RegexMatcher::build(pattern, case_sensitive, smart_case, !multiline, true)
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
fn regex_error(error: impl
|
|
73
|
+
fn regex_error(error: impl Display) -> RgApiError { RgApiError::new(error.to_string()) }
|
|
72
74
|
|
|
73
75
|
impl Matcher for RegexMatcher {
|
|
74
76
|
type Captures = NoCaptures;
|
|
@@ -160,10 +162,10 @@ impl Visitor for ClassLiterals {
|
|
|
160
162
|
type Err = std::convert::Infallible;
|
|
161
163
|
fn finish(self) -> Result<Self, Self::Err> { Ok(self) }
|
|
162
164
|
fn visit_pre(&mut self, ast: &Ast) -> Result<(), Self::Err> { if let Ast::Literal(lit) = ast { self.add(lit.c); } Ok(()) }
|
|
163
|
-
fn visit_class_set_item_pre(&mut self, item: &
|
|
165
|
+
fn visit_class_set_item_pre(&mut self, item: &ClassSetItem) -> Result<(), Self::Err> {
|
|
164
166
|
match item {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
ClassSetItem::Literal(lit) => self.add(lit.c),
|
|
168
|
+
ClassSetItem::Range(r) => {
|
|
167
169
|
self.add(r.start.c);
|
|
168
170
|
self.add(r.end.c);
|
|
169
171
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
use serde_json::Value;
|
|
1
2
|
use std::collections::{BTreeMap, HashMap};
|
|
3
|
+
use std::fs;
|
|
2
4
|
use std::path::Path;
|
|
3
5
|
use std::sync::atomic::Ordering;
|
|
4
6
|
use std::sync::{Arc, LazyLock};
|
|
@@ -8,10 +10,9 @@ use fancy_regex::Regex;
|
|
|
8
10
|
use ignore::{DirEntry, WalkState};
|
|
9
11
|
use serde::Deserialize;
|
|
10
12
|
|
|
11
|
-
use crate::RgApiError;
|
|
12
|
-
use crate::compile_regex;
|
|
13
13
|
use crate::search::{SearchLine, search_text};
|
|
14
|
-
use crate::walk::{PathFilters, StreamIter, WalkOptions, entry_err, rel_path, resolve_roots, spawn_walk};
|
|
14
|
+
use crate::walk::{PathFilters, StreamIter, WalkOptions, entry_err, rel_path, resolve_roots, send_error, spawn_walk};
|
|
15
|
+
use crate::{RgApiError, compile_regex};
|
|
15
16
|
|
|
16
17
|
static HEADING_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^#{1,6} \w").unwrap());
|
|
17
18
|
|
|
@@ -56,8 +57,8 @@ fn tool_names(text: &str) -> Vec<String> {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/// nbformat multiline text, a string or a list of strings, as one string.
|
|
59
|
-
fn nb_text(v: &
|
|
60
|
-
match v {
|
|
60
|
+
fn nb_text(v: &Value) -> String {
|
|
61
|
+
match v { Value::String(s) => s.clone(), Value::Array(a) => a.iter().filter_map(|o| o.as_str()).collect(), _ => String::new() }
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/// The sigil references in one notebook cell, in order of appearance.
|
|
@@ -75,7 +76,7 @@ pub struct CellRefs {
|
|
|
75
76
|
/// `vars` and `cmds` come from a prompt cell's source. `tools` comes from the source of a prompt or Markdown cell.
|
|
76
77
|
/// For every other cell, `tools` comes from the `text/markdown` data of its `display_data` and `execute_result` outputs.
|
|
77
78
|
/// A prompt cell's outputs are never read.
|
|
78
|
-
pub fn cell_refs(cell: &
|
|
79
|
+
pub fn cell_refs(cell: &Value) -> CellRefs {
|
|
79
80
|
let src = nb_text(&cell["source"]);
|
|
80
81
|
let prompt = cell["metadata"]["solveit_ai"] == true;
|
|
81
82
|
let mut res = CellRefs::default();
|
|
@@ -109,12 +110,10 @@ pub struct NbCell {
|
|
|
109
110
|
struct RawNb { #[serde(default)] cells: Vec<RawCell> }
|
|
110
111
|
|
|
111
112
|
#[derive(Deserialize)]
|
|
112
|
-
struct RawCell { #[serde(default)] id: Option<
|
|
113
|
+
struct RawCell { #[serde(default)] id: Option<Value>, #[serde(default)] cell_type: Option<String>, #[serde(default)] source: Source }
|
|
113
114
|
|
|
114
115
|
impl RawCell {
|
|
115
|
-
fn id_string(&self, index: usize) -> String {
|
|
116
|
-
match &self.id { Some(serde_json::Value::String(s)) => s.clone(), Some(v) => v.to_string(), None => index.to_string() }
|
|
117
|
-
}
|
|
116
|
+
fn id_string(&self, index: usize) -> String { match &self.id { Some(Value::String(s)) => s.clone(), Some(v) => v.to_string(), None => index.to_string() } }
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
// nbformat `source` is a list of lines or a single string (or absent/null).
|
|
@@ -126,9 +125,13 @@ impl Source {
|
|
|
126
125
|
fn text(&self) -> String { match self { Source::Lines(v) => v.concat(), Source::Text(s) => s.clone(), Source::Empty => String::new() } }
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let
|
|
128
|
+
/// The notebook at `path`. A file that can't be read, or parsed as a notebook, gives a skipped error naming `path`.
|
|
129
|
+
fn notebook(path: &Path) -> Result<RawNb, RgApiError> {
|
|
130
|
+
let bytes = fs::read(path).map_err(|err| RgApiError::unreadable(path, err))?;
|
|
131
|
+
serde_json::from_slice(&bytes).map_err(|err| RgApiError::unreadable(path, err))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn process_file(disp: String, nb: RawNb, matcher: &RegexMatcher, cell_context: usize, multiline: bool) -> Result<Vec<NbCell>, RgApiError> {
|
|
132
135
|
let n = nb.cells.len();
|
|
133
136
|
let mut info = Vec::with_capacity(n);
|
|
134
137
|
let mut matched: Vec<(usize, Vec<SearchLine>)> = Vec::new();
|
|
@@ -158,8 +161,7 @@ pub fn nb_search_file(
|
|
|
158
161
|
path: &Path, display_path: String, pattern: &str, case_sensitive: Option<bool>, smart_case: bool, cell_context: usize, multiline: bool
|
|
159
162
|
) -> Result<Vec<NbCell>, RgApiError> {
|
|
160
163
|
let matcher = compile_regex(pattern, case_sensitive, smart_case, multiline)?;
|
|
161
|
-
|
|
162
|
-
process_file(display_path, &bytes, &matcher, cell_context, multiline)
|
|
164
|
+
process_file(display_path, notebook(path)?, &matcher, cell_context, multiline)
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
fn nb_entry(
|
|
@@ -171,8 +173,7 @@ fn nb_entry(
|
|
|
171
173
|
if !ft.is_file() { return Ok(Vec::new()); }
|
|
172
174
|
let rel = rel_path(base, path);
|
|
173
175
|
if !filters.path_allowed(Path::new(&rel))? { return Ok(Vec::new()); }
|
|
174
|
-
|
|
175
|
-
process_file(rel, &bytes, matcher, cell_context, multiline)
|
|
176
|
+
process_file(rel, notebook(path)?, matcher, cell_context, multiline)
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
pub type NbIter = StreamIter<NbCell>;
|
|
@@ -188,7 +189,7 @@ pub fn nb_iter(opts: &NbOptions) -> Result<NbIter, RgApiError> {
|
|
|
188
189
|
for cell in cells { if cancel.load(Ordering::Relaxed) || tx.send(Ok(cell)).is_err() { return WalkState::Quit; } }
|
|
189
190
|
WalkState::Continue
|
|
190
191
|
}
|
|
191
|
-
Err(err) =>
|
|
192
|
+
Err(err) => send_error(tx, err),
|
|
192
193
|
}
|
|
193
194
|
}))
|
|
194
195
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
use grep_searcher::Searcher;
|
|
2
|
+
use std::fmt::Display;
|
|
1
3
|
use std::path::Path;
|
|
4
|
+
use std::str;
|
|
2
5
|
use std::sync::{Arc, atomic::{AtomicBool, Ordering}, mpsc::SyncSender};
|
|
3
6
|
|
|
4
7
|
use crate::matcher::RegexMatcher;
|
|
@@ -6,7 +9,7 @@ use grep_matcher::LineTerminator;
|
|
|
6
9
|
use grep_searcher::{BinaryDetection, SearcherBuilder, Sink, SinkContext, SinkContextKind, SinkError, SinkMatch};
|
|
7
10
|
use ignore::{DirEntry, WalkState};
|
|
8
11
|
|
|
9
|
-
use crate::walk::{PathFilters, StreamIter, WalkOptions, entry_err, rel_path, resolve_roots, spawn_walk};
|
|
12
|
+
use crate::walk::{PathFilters, StreamIter, WalkOptions, entry_err, rel_path, resolve_roots, send_error, spawn_walk};
|
|
10
13
|
use crate::{RgApiError, compile_regex};
|
|
11
14
|
|
|
12
15
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -46,27 +49,25 @@ fn search_entry(
|
|
|
46
49
|
max_depth: Option<usize>, tx: &SyncSender<Result<SearchLine, RgApiError>>, cancel: &Arc<AtomicBool>) -> WalkState {
|
|
47
50
|
if is_cancelled(cancel) { return WalkState::Quit; }
|
|
48
51
|
let dent = match entry {
|
|
49
|
-
Ok(dent) => dent, Err(err) => { return match entry_err(err, max_depth) { Some(e) =>
|
|
52
|
+
Ok(dent) => dent, Err(err) => { return match entry_err(err, max_depth) { Some(e) => send_error(tx, e), None => WalkState::Continue }; }
|
|
50
53
|
};
|
|
51
54
|
let path = dent.path();
|
|
52
55
|
let Some(ft) = dent.file_type() else { return WalkState::Continue; };
|
|
53
56
|
if !ft.is_file() { return WalkState::Continue; }
|
|
54
57
|
let rel = rel_path(base, path);
|
|
55
|
-
match filters.path_allowed(Path::new(&rel)) { Ok(true) => {} Ok(false) => return WalkState::Continue, Err(err) => return
|
|
58
|
+
match filters.path_allowed(Path::new(&rel)) { Ok(true) => {} Ok(false) => return WalkState::Continue, Err(err) => return send_error(tx, err) }
|
|
56
59
|
match search_path_cancelable(path, rel, matcher.clone(), before_context, after_context, Some(cancel.clone())) {
|
|
57
60
|
Ok(lines) => {
|
|
58
61
|
if is_cancelled(cancel) { return WalkState::Quit; }
|
|
59
62
|
for line in lines { if is_cancelled(cancel) || tx.send(Ok(line)).is_err() { return WalkState::Quit; } }
|
|
60
63
|
WalkState::Continue
|
|
61
64
|
}
|
|
62
|
-
Err(err) =>
|
|
65
|
+
Err(err) => send_error(tx, err),
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
fn is_cancelled(cancel: &Arc<AtomicBool>) -> bool { cancel.load(Ordering::Relaxed) }
|
|
67
70
|
|
|
68
|
-
fn send_search_error(tx: &SyncSender<Result<SearchLine, RgApiError>>, err: RgApiError) -> WalkState { let _ = tx.send(Err(err)); WalkState::Quit }
|
|
69
|
-
|
|
70
71
|
pub(crate) fn format_lnhash(lineno: u64, line: &str) -> String {
|
|
71
72
|
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
72
73
|
let hash = crc32fast::hash(line.as_bytes()) as usize;
|
|
@@ -88,7 +89,8 @@ fn search_path_cancelable(
|
|
|
88
89
|
let mut out = Vec::new();
|
|
89
90
|
let search_matcher = matcher.clone();
|
|
90
91
|
let sink = CollectSink { path: display_path, matcher, lines: &mut out, cancel };
|
|
91
|
-
|
|
92
|
+
let file = std::fs::File::open(path).map_err(|err| RgApiError::unreadable(path, err))?;
|
|
93
|
+
match searcher.search_file(search_matcher, &file, sink) {
|
|
92
94
|
Ok(()) => Ok(out), Err(SearchError::InvalidUtf8) => Ok(Vec::new()), Err(err) => Err(RgApiError::new(err.to_string()))
|
|
93
95
|
}
|
|
94
96
|
}
|
|
@@ -114,7 +116,7 @@ impl CollectSink<'_> { fn cancelled(&self) -> bool { match &self.cancel { Some(c
|
|
|
114
116
|
impl Sink for CollectSink<'_> {
|
|
115
117
|
type Error = SearchError;
|
|
116
118
|
|
|
117
|
-
fn matched(&mut self, _searcher: &
|
|
119
|
+
fn matched(&mut self, _searcher: &Searcher, mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {
|
|
118
120
|
if self.cancelled() { return Ok(false); }
|
|
119
121
|
let line = bytes_to_line(mat.bytes())?;
|
|
120
122
|
let line_number = mat.line_number().unwrap_or(0);
|
|
@@ -124,7 +126,7 @@ impl Sink for CollectSink<'_> {
|
|
|
124
126
|
Ok(!self.cancelled())
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
fn context(&mut self, _searcher: &
|
|
129
|
+
fn context(&mut self, _searcher: &Searcher, ctx: &SinkContext<'_>) -> Result<bool, Self::Error> {
|
|
128
130
|
if self.cancelled() { return Ok(false); }
|
|
129
131
|
let kind = match ctx.kind() {
|
|
130
132
|
SinkContextKind::Before => SearchKind::Before, SinkContextKind::After => SearchKind::After, SinkContextKind::Other => SearchKind::Context
|
|
@@ -134,27 +136,27 @@ impl Sink for CollectSink<'_> {
|
|
|
134
136
|
self.lines.push(SearchLine { kind, path: self.path.clone(), line_number, lnhash: format_lnhash(line_number, &line), line, matches: Vec::new() });
|
|
135
137
|
Ok(!self.cancelled())
|
|
136
138
|
}
|
|
137
|
-
fn binary_data(&mut self, _searcher: &
|
|
139
|
+
fn binary_data(&mut self, _searcher: &Searcher, _binary_byte_offset: u64) -> Result<bool, Self::Error> { self.lines.clear(); Ok(false) }
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
#[derive(Debug)]
|
|
141
143
|
/// A search failure, including invalid UTF-8 in matched text.
|
|
142
144
|
pub enum SearchError { Message(String), InvalidUtf8 }
|
|
143
|
-
impl
|
|
145
|
+
impl Display for SearchError {
|
|
144
146
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
145
147
|
match self { Self::Message(msg) => write!(f, "{msg}"), Self::InvalidUtf8 => write!(f, "invalid utf-8") }
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
impl std::error::Error for SearchError {}
|
|
149
|
-
impl SinkError for SearchError { fn error_message<T:
|
|
151
|
+
impl SinkError for SearchError { fn error_message<T: Display>(message: T) -> Self { Self::Message(message.to_string()) } }
|
|
150
152
|
fn bytes_to_line(bytes: &[u8]) -> Result<String, SearchError> {
|
|
151
|
-
let s =
|
|
153
|
+
let s = str::from_utf8(bytes).map_err(|_| SearchError::InvalidUtf8)?;
|
|
152
154
|
Ok(s.trim_end_matches(['\r', '\n']).to_string())
|
|
153
155
|
}
|
|
154
156
|
/// Return byte offsets for every match in a line.
|
|
155
157
|
pub fn spans_for(matcher: &RegexMatcher, bytes: &[u8]) -> Result<Vec<MatchSpan>, SearchError> {
|
|
156
158
|
let mut spans = Vec::new();
|
|
157
|
-
let text =
|
|
159
|
+
let text = str::from_utf8(bytes).map_err(|_| SearchError::InvalidUtf8)?;
|
|
158
160
|
matcher.for_each_match(text, |start, end| spans.push(MatchSpan { start, end })).map_err(|e| SearchError::Message(e.to_string()))?;
|
|
159
161
|
Ok(spans)
|
|
160
162
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
use mpsc::{RecvTimeoutError, SyncSender};
|
|
1
2
|
use std::collections::HashSet;
|
|
2
3
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
3
4
|
use std::path::{Path, PathBuf};
|
|
4
5
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
5
6
|
use std::sync::{Arc, Mutex, mpsc};
|
|
7
|
+
use std::thread;
|
|
8
|
+
use std::thread::JoinHandle;
|
|
9
|
+
use std::time::{Duration, Instant};
|
|
6
10
|
|
|
7
11
|
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
|
|
8
12
|
use grep_matcher::Matcher;
|
|
@@ -85,12 +89,12 @@ pub fn find_iter_with(opts: &FindOptions, walk_root_links: bool) -> Result<FindI
|
|
|
85
89
|
if panic_probe { panic!("rgapi: deliberate panic for tests (panic_probe)"); }
|
|
86
90
|
match find_entry(dent, base, filters, pattern.as_ref(), files, dirs, special_files, max_depth) {
|
|
87
91
|
Ok(Some(path)) => { if cancel.load(Ordering::Relaxed) || tx.send(Ok(path)).is_err() { return WalkState::Quit; } WalkState::Continue }
|
|
88
|
-
Ok(None) => WalkState::Continue, Err(err) =>
|
|
92
|
+
Ok(None) => WalkState::Continue, Err(err) => send_error(tx, err)
|
|
89
93
|
}
|
|
90
94
|
}))
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
pub struct StreamIter<T> { rx: mpsc::Receiver<Result<T, RgApiError>>, cancel: Arc<AtomicBool>, worker: Option<
|
|
97
|
+
pub struct StreamIter<T> { rx: mpsc::Receiver<Result<T, RgApiError>>, cancel: Arc<AtomicBool>, worker: Option<JoinHandle<()>> }
|
|
94
98
|
|
|
95
99
|
impl<T> StreamIter<T> {
|
|
96
100
|
pub fn cancel(&self) { self.cancel.store(true, Ordering::Relaxed); }
|
|
@@ -108,19 +112,19 @@ impl<T> StreamIter<T> {
|
|
|
108
112
|
Ok(())
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
pub fn next_timeout(&mut self, timeout:
|
|
115
|
+
pub fn next_timeout(&mut self, timeout: Duration) -> Result<Result<T, RgApiError>, RecvTimeoutError> { self.rx.recv_timeout(timeout) }
|
|
112
116
|
|
|
113
117
|
/// Collect all items, stopping at `timeout_ms`; the bool is true when the deadline stopped it.
|
|
114
118
|
pub fn collect_timeout(mut self, timeout_ms: Option<u64>) -> Result<(Vec<T>, bool), RgApiError> {
|
|
115
119
|
let Some(ms) = timeout_ms else { return Ok((self.collect::<Result<Vec<_>, _>>()?, false)); };
|
|
116
|
-
let deadline =
|
|
120
|
+
let deadline = Instant::now() + Duration::from_millis(ms);
|
|
117
121
|
let mut res = Vec::new();
|
|
118
122
|
loop {
|
|
119
|
-
let left = deadline.saturating_duration_since(
|
|
123
|
+
let left = deadline.saturating_duration_since(Instant::now());
|
|
120
124
|
if left.is_zero() { return Ok((res, true)); }
|
|
121
125
|
match self.next_timeout(left) {
|
|
122
|
-
Ok(Ok(item)) => res.push(item), Ok(Err(err)) => return Err(err), Err(
|
|
123
|
-
Err(
|
|
126
|
+
Ok(Ok(item)) => res.push(item), Ok(Err(err)) => return Err(err), Err(RecvTimeoutError::Disconnected) => return Ok((res, false)),
|
|
127
|
+
Err(RecvTimeoutError::Timeout) => return Ok((res, true))
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
}
|
|
@@ -136,6 +140,7 @@ impl<T> Drop for StreamIter<T> { fn drop(&mut self) { self.cancel(); } }
|
|
|
136
140
|
#[cfg(test)]
|
|
137
141
|
mod close_tests {
|
|
138
142
|
use super::*;
|
|
143
|
+
use std::thread;
|
|
139
144
|
|
|
140
145
|
#[test]
|
|
141
146
|
fn cancel_and_join_drains_full_channel_and_waits_for_worker() {
|
|
@@ -145,7 +150,7 @@ mod close_tests {
|
|
|
145
150
|
let worker_cancel = cancel.clone();
|
|
146
151
|
let done = Arc::new(AtomicBool::new(false));
|
|
147
152
|
let worker_done = done.clone();
|
|
148
|
-
let worker =
|
|
153
|
+
let worker = thread::spawn(move || {
|
|
149
154
|
tx.send(Ok(1)).unwrap();
|
|
150
155
|
started.send(()).unwrap();
|
|
151
156
|
// This blocks while the channel is full; close must drain, not just join.
|
|
@@ -161,14 +166,14 @@ mod close_tests {
|
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
pub(crate) fn spawn_walk<T, F>(roots: Vec<PathBuf>, base: PathBuf, walk: &WalkOptions, filters: Arc<PathFilters>, ready: Vec<T>, entry: F) -> StreamIter<T>
|
|
164
|
-
where T: Send + 'static, F: Fn(Result<DirEntry, ignore::Error>, &Path, &PathFilters, &
|
|
165
|
-
+
|
|
169
|
+
where T: Send + 'static, F: Fn(Result<DirEntry, ignore::Error>, &Path, &PathFilters, &SyncSender<Result<T, RgApiError>>, &Arc<AtomicBool>) -> WalkState + Send
|
|
170
|
+
+ Sync + Clone + 'static {
|
|
166
171
|
let cancel = Arc::new(AtomicBool::new(false));
|
|
167
172
|
let worker_cancel = cancel.clone();
|
|
168
173
|
let (tx, rx) = mpsc::sync_channel(8192);
|
|
169
174
|
let walk = walk.clone();
|
|
170
175
|
let seen = roots.iter().any(|a| roots.iter().any(|b| a != b && a.starts_with(b))).then(|| Arc::new(Mutex::new(HashSet::new())));
|
|
171
|
-
let worker =
|
|
176
|
+
let worker = thread::spawn(move || {
|
|
172
177
|
for item in ready { if tx.send(Ok(item)).is_err() { return; } }
|
|
173
178
|
for root in &roots {
|
|
174
179
|
if worker_cancel.load(Ordering::Relaxed) { return; }
|
|
@@ -234,11 +239,17 @@ fn file_root_flags(root: &Path, ignore: bool, hidden: bool) -> (bool, bool) { if
|
|
|
234
239
|
|
|
235
240
|
// At the max_depth cap the walker opens directories it will never descend into
|
|
236
241
|
// (readdir precedes the depth check in `ignore`), so permission failures there are
|
|
237
|
-
// harmless: skip them (None).
|
|
242
|
+
// harmless: skip them silently (None). Report every other walk error as a skipped path (Some).
|
|
238
243
|
pub(crate) fn entry_err(err: ignore::Error, max_depth: Option<usize>) -> Option<RgApiError> {
|
|
239
244
|
let at_cap = max_depth.is_some_and(|m| err.depth().is_some_and(|d| d >= m));
|
|
240
245
|
let denied = err.io_error().is_some_and(|e| e.kind() == std::io::ErrorKind::PermissionDenied);
|
|
241
|
-
if at_cap && denied { None } else { Some(RgApiError::
|
|
246
|
+
if at_cap && denied { None } else { Some(RgApiError::skipped(err)) }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Send `err`, then end the walk unless `err` only skipped a path.
|
|
250
|
+
pub(crate) fn send_error<T>(tx: &SyncSender<Result<T, RgApiError>>, err: RgApiError) -> WalkState {
|
|
251
|
+
let skipped = err.is_skipped();
|
|
252
|
+
if tx.send(Err(err)).is_ok() && skipped { WalkState::Continue } else { WalkState::Quit }
|
|
242
253
|
}
|
|
243
254
|
|
|
244
255
|
fn normalize_root(path: &Path) -> Result<PathBuf, RgApiError> {
|
|
@@ -287,7 +298,7 @@ fn configure_walker(walker: &mut WalkBuilder, root: &Path, walk: &WalkOptions) {
|
|
|
287
298
|
}
|
|
288
299
|
|
|
289
300
|
fn filter_dirs<T: Send + 'static>(
|
|
290
|
-
walker: &mut WalkBuilder, base: &Path, filters: Arc<PathFilters>, tx:
|
|
301
|
+
walker: &mut WalkBuilder, base: &Path, filters: Arc<PathFilters>, tx: SyncSender<Result<T, RgApiError>>, cancel: Arc<AtomicBool>) {
|
|
291
302
|
let base = base.to_path_buf();
|
|
292
303
|
walker.filter_entry(move |entry| {
|
|
293
304
|
if cancel.load(Ordering::Relaxed) { false } else {
|
|
@@ -350,6 +361,8 @@ fn re_match(matcher: &RegexMatcher, rel: &str) -> Result<bool, RgApiError> { mat
|
|
|
350
361
|
#[cfg(test)]
|
|
351
362
|
mod tests {
|
|
352
363
|
use super::*;
|
|
364
|
+
use std::thread;
|
|
365
|
+
use std::time::Duration;
|
|
353
366
|
|
|
354
367
|
#[test]
|
|
355
368
|
fn globs_match_names_or_root_relative_components() {
|
|
@@ -366,8 +379,8 @@ mod tests {
|
|
|
366
379
|
fn iter_of(items: Vec<u32>, delay_ms: u64) -> StreamIter<u32> {
|
|
367
380
|
let (tx, rx) = mpsc::channel();
|
|
368
381
|
let cancel = Arc::new(AtomicBool::new(false));
|
|
369
|
-
let worker =
|
|
370
|
-
for i in items {
|
|
382
|
+
let worker = thread::spawn(move || {
|
|
383
|
+
for i in items { thread::sleep(Duration::from_millis(delay_ms)); if tx.send(Ok(i)).is_err() { return; } }
|
|
371
384
|
});
|
|
372
385
|
StreamIter { rx, cancel, worker: Some(worker) }
|
|
373
386
|
}
|
|
@@ -371,8 +371,7 @@ def test_backreferences_and_lookaround(tmp_path):
|
|
|
371
371
|
|
|
372
372
|
|
|
373
373
|
def test_line_local_regexes_and_smart_case():
|
|
374
|
-
for pattern in [r"foo\s+foo", r"foo[^x]+foo", r"(?s:foo.+foo)", r"foo(?=\s+foo)"]:
|
|
375
|
-
assert rgstr(pattern, "foo\nfoo\n") == []
|
|
374
|
+
for pattern in [r"foo\s+foo", r"foo[^x]+foo", r"(?s:foo.+foo)", r"foo(?=\s+foo)"]: assert rgstr(pattern, "foo\nfoo\n") == []
|
|
376
375
|
assert [r.matches for r in rgstr(r"(?s:foo.*|bar)", "foo\nbar\n")] == [[(0, 3)], [(0, 3)]]
|
|
377
376
|
assert compile(r"\Afoo\z").finditer("foo\nfoo\n") == [(0, 3), (4, 7)]
|
|
378
377
|
empty = compile(r"\A\z")
|
|
@@ -395,7 +394,7 @@ def test_regex_runtime_errors(tmp_path):
|
|
|
395
394
|
p.mkdir()
|
|
396
395
|
(tmp_path/(text+".txt")).write_text(text)
|
|
397
396
|
for search in [lambda: compile(pattern).is_match(text), lambda: rgstr(pattern, text),
|
|
398
|
-
|
|
397
|
+
lambda: fd(pattern, tmp_path), lambda: fd(root=tmp_path, path_re=pattern), lambda: fd(root=tmp_path, skip_dir_re=pattern)]:
|
|
399
398
|
with pytest.raises(ValueError, match="backtrack"): search()
|
|
400
399
|
|
|
401
400
|
|
|
@@ -498,12 +497,13 @@ def test_file_as_root_searches_just_that_file(tmp_path):
|
|
|
498
497
|
with pytest.raises(Exception): rg("TODO", str(tmp_path / "nope.txt"))
|
|
499
498
|
|
|
500
499
|
|
|
501
|
-
def
|
|
502
|
-
from rgapi import nbrg
|
|
500
|
+
def test_nbrg_warns_on_bad_json(tmp_path):
|
|
501
|
+
from rgapi import nbrg, search_nb
|
|
503
502
|
write_nb(tmp_path / "good.ipynb", [_cell("code", ["read_csv(x)\n"], cid="g1")])
|
|
504
503
|
(tmp_path / "bad.ipynb").write_text("{not valid json")
|
|
505
|
-
a = nbrg("read_csv",
|
|
504
|
+
with pytest.warns(RuntimeWarning, match=r"bad\.ipynb: .* at line 1 column"): a = nbrg("read_csv", tmp_path)
|
|
506
505
|
assert {c.cell_id for c in a} == {"g1"}
|
|
506
|
+
with pytest.raises(ValueError, match=r"bad\.ipynb: "): search_nb("read_csv", tmp_path / "bad.ipynb")
|
|
507
507
|
|
|
508
508
|
|
|
509
509
|
def test_search_nb_single_and_asdict_str(tmp_path):
|
|
@@ -717,12 +717,41 @@ def test_file_root_ignores_siblings_and_depth_cap_permissions(tmp_path):
|
|
|
717
717
|
got = sorted({r.path for r in rg("x", tmp_path, max_depth=1, ignore=False, hidden=True)})
|
|
718
718
|
assert got == [".gitignore", ".hid.txt", "f.txt"] # depth-cap dir skipped silently
|
|
719
719
|
assert list(fd(root=tmp_path, max_depth=1, ignore=False)) == [f]
|
|
720
|
-
with pytest.
|
|
721
|
-
rg("x", tmp_path)
|
|
722
|
-
with pytest.raises(ValueError, match="ermission"):
|
|
723
|
-
rg("x", tmp_path, max_depth=2) # cap above the dir: descent needed, fatal
|
|
720
|
+
with pytest.warns(RuntimeWarning, match="locked: Permission denied"):
|
|
721
|
+
assert not rg("x", tmp_path, max_depth=2) # cap above the dir: descent needed, so it's reported
|
|
724
722
|
finally: locked.chmod(0o755)
|
|
725
723
|
|
|
724
|
+
def test_unreadable_paths_warn_and_walks_continue(tmp_path):
|
|
725
|
+
from fastcore.aio import run_sync
|
|
726
|
+
from rgapi import nbrg, rga, rga_iter
|
|
727
|
+
(tmp_path / "a.txt").write_text("x\n")
|
|
728
|
+
write_nb(tmp_path / "a.ipynb", [_cell("code", ["x\n"], cid="c1")])
|
|
729
|
+
secret = tmp_path / "secret.ipynb"
|
|
730
|
+
write_nb(secret, [_cell("code", ["x\n"], cid="c2")])
|
|
731
|
+
locked = tmp_path / "locked"
|
|
732
|
+
locked.mkdir()
|
|
733
|
+
(locked / "b.txt").write_text("x\n")
|
|
734
|
+
def walk(f):
|
|
735
|
+
"`f()`'s results, and the names of the paths it warned it skipped"
|
|
736
|
+
with pytest.warns(RuntimeWarning) as caught: res = f()
|
|
737
|
+
return res, sorted(Path(str(w.message).partition(": ")[0]).name for w in caught)
|
|
738
|
+
async def listed(it): return [r async for r in it]
|
|
739
|
+
searches = [lambda: rg("x", tmp_path), lambda: list(rg_iter("x", tmp_path)), lambda: run_sync(rga("x", tmp_path)),
|
|
740
|
+
lambda: run_sync(listed(rga_iter("x", tmp_path))), lambda: rg("x", tmp_path, summary=True)]
|
|
741
|
+
secret.chmod(0o000)
|
|
742
|
+
locked.chmod(0o000)
|
|
743
|
+
try:
|
|
744
|
+
for search in searches:
|
|
745
|
+
res, skipped = walk(search)
|
|
746
|
+
assert sorted({r.path for r in res}) == ["a.ipynb", "a.txt"] and skipped == ["locked", "secret.ipynb"]
|
|
747
|
+
res, skipped = walk(lambda: nbrg("x", tmp_path))
|
|
748
|
+
assert [c.cell_id for c in res] == ["c1"] and skipped == ["locked", "secret.ipynb"]
|
|
749
|
+
res, skipped = walk(lambda: fd(root=tmp_path)) # discovery reads no files, so only the dir is skipped
|
|
750
|
+
assert sorted(p.name for p in res) == ["a.ipynb", "a.txt", "secret.ipynb"] and skipped == ["locked"]
|
|
751
|
+
finally:
|
|
752
|
+
secret.chmod(0o644)
|
|
753
|
+
locked.chmod(0o755)
|
|
754
|
+
|
|
726
755
|
def test_symlink_link_target_and_show_target(tmp_path):
|
|
727
756
|
(tmp_path / "real").mkdir()
|
|
728
757
|
(tmp_path / "f.txt").write_text("x\n")
|
rgapi-0.2.3/src/lib.rs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
//! rgapi - Python-friendly wrappers around ripgrep's walking and searching crates.
|
|
2
|
-
|
|
3
|
-
mod block;
|
|
4
|
-
mod matcher;
|
|
5
|
-
mod nb;
|
|
6
|
-
mod search;
|
|
7
|
-
mod walk;
|
|
8
|
-
|
|
9
|
-
pub use block::{BlockIter, SearchBlock, block_iter};
|
|
10
|
-
pub use matcher::{RegexMatcher, compile_regex};
|
|
11
|
-
pub use nb::{CellRefs, NbCell, NbIter, NbOptions, ancestor_indices, cell_refs, heading_level, nb_iter, nb_search, nb_search_file, section_range};
|
|
12
|
-
pub use search::{MatchSpan, RgIter, RgOptions, SearchError, SearchKind, SearchLine, rg, rg_iter, search_path, search_text, spans_for};
|
|
13
|
-
pub use walk::{FindIter, FindOptions, StreamIter, WalkOptions, find, find_iter, find_iter_with, resolve_roots};
|
|
14
|
-
|
|
15
|
-
#[derive(Debug, Clone)]
|
|
16
|
-
pub struct RgApiError { msg: String }
|
|
17
|
-
|
|
18
|
-
impl RgApiError { pub(crate) fn new(msg: impl Into<String>) -> Self { Self { msg: msg.into() } } }
|
|
19
|
-
|
|
20
|
-
impl std::fmt::Display for RgApiError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.msg) } }
|
|
21
|
-
|
|
22
|
-
impl std::error::Error for RgApiError {}
|
|
23
|
-
|
|
24
|
-
impl From<std::io::Error> for RgApiError { fn from(err: std::io::Error) -> Self { Self::new(err.to_string()) } }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|