rgapi 0.2.0__tar.gz → 0.2.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.
- {rgapi-0.2.0 → rgapi-0.2.1}/.github/workflows/ci.yml +3 -2
- {rgapi-0.2.0 → rgapi-0.2.1}/Cargo.lock +24 -14
- {rgapi-0.2.0 → rgapi-0.2.1}/Cargo.toml +24 -11
- {rgapi-0.2.0 → rgapi-0.2.1}/DEV.md +5 -5
- {rgapi-0.2.0 → rgapi-0.2.1}/PKG-INFO +2 -2
- rgapi-0.2.1/py/Cargo.toml +18 -0
- rgapi-0.2.1/py/build.rs +1 -0
- rgapi-0.2.0/src/python.rs → rgapi-0.2.1/py/src/lib.rs +54 -23
- {rgapi-0.2.0 → rgapi-0.2.1}/pyproject.toml +3 -3
- {rgapi-0.2.0 → rgapi-0.2.1}/src/block.rs +4 -9
- {rgapi-0.2.0 → rgapi-0.2.1}/src/lib.rs +2 -5
- {rgapi-0.2.0 → rgapi-0.2.1}/src/nb.rs +11 -15
- {rgapi-0.2.0 → rgapi-0.2.1}/src/search.rs +13 -15
- {rgapi-0.2.0 → rgapi-0.2.1}/src/walk.rs +34 -38
- {rgapi-0.2.0 → rgapi-0.2.1}/.gitignore +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/LICENSE +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/README.md +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/_config.yml +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/_layouts/default.html +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/python/rgapi/__init__.py +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/python/rgapi/_cli.py +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/python/rgapi/block.py +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/python/rgapi/nb.py +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/python/rgapi/skill.py +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/rustfmt.toml +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/tests/test_async.py +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/tests/test_rgapi.py +0 -0
- {rgapi-0.2.0 → rgapi-0.2.1}/tools/bench.py +0 -0
|
@@ -12,11 +12,12 @@ jobs:
|
|
|
12
12
|
steps:
|
|
13
13
|
- uses: actions/checkout@v7
|
|
14
14
|
- uses: dtolnay/rust-toolchain@stable
|
|
15
|
-
- run: cargo test
|
|
16
15
|
- uses: actions/setup-python@v7
|
|
17
16
|
with:
|
|
18
17
|
python-version: '3.12'
|
|
18
|
+
- run: cargo test
|
|
19
19
|
- run: pip install -e '.[dev]'
|
|
20
|
+
- run: cargo develop
|
|
20
21
|
- run: pytest -q
|
|
21
22
|
|
|
22
23
|
build:
|
|
@@ -61,7 +62,7 @@ jobs:
|
|
|
61
62
|
- uses: dtolnay/rust-toolchain@stable
|
|
62
63
|
- id: crates-auth
|
|
63
64
|
uses: rust-lang/crates-io-auth-action@v1
|
|
64
|
-
- run: cargo publish
|
|
65
|
+
- run: cargo publish -p rgapi
|
|
65
66
|
env:
|
|
66
67
|
CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}
|
|
67
68
|
- uses: actions/download-artifact@v8
|
|
@@ -171,9 +171,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
|
171
171
|
|
|
172
172
|
[[package]]
|
|
173
173
|
name = "libc"
|
|
174
|
-
version = "0.2.
|
|
174
|
+
version = "0.2.190"
|
|
175
175
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
176
|
-
checksum = "
|
|
176
|
+
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
|
177
177
|
|
|
178
178
|
[[package]]
|
|
179
179
|
name = "log"
|
|
@@ -225,9 +225,9 @@ dependencies = [
|
|
|
225
225
|
|
|
226
226
|
[[package]]
|
|
227
227
|
name = "pyo3"
|
|
228
|
-
version = "0.29.
|
|
228
|
+
version = "0.29.3"
|
|
229
229
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
230
|
-
checksum = "
|
|
230
|
+
checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
|
|
231
231
|
dependencies = [
|
|
232
232
|
"libc",
|
|
233
233
|
"once_cell",
|
|
@@ -239,18 +239,18 @@ dependencies = [
|
|
|
239
239
|
|
|
240
240
|
[[package]]
|
|
241
241
|
name = "pyo3-build-config"
|
|
242
|
-
version = "0.29.
|
|
242
|
+
version = "0.29.3"
|
|
243
243
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
244
|
-
checksum = "
|
|
244
|
+
checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
|
|
245
245
|
dependencies = [
|
|
246
246
|
"target-lexicon",
|
|
247
247
|
]
|
|
248
248
|
|
|
249
249
|
[[package]]
|
|
250
250
|
name = "pyo3-ffi"
|
|
251
|
-
version = "0.29.
|
|
251
|
+
version = "0.29.3"
|
|
252
252
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
253
|
-
checksum = "
|
|
253
|
+
checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
|
|
254
254
|
dependencies = [
|
|
255
255
|
"libc",
|
|
256
256
|
"pyo3-build-config",
|
|
@@ -258,9 +258,9 @@ dependencies = [
|
|
|
258
258
|
|
|
259
259
|
[[package]]
|
|
260
260
|
name = "pyo3-macros"
|
|
261
|
-
version = "0.29.
|
|
261
|
+
version = "0.29.3"
|
|
262
262
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
263
|
-
checksum = "
|
|
263
|
+
checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
|
|
264
264
|
dependencies = [
|
|
265
265
|
"proc-macro2",
|
|
266
266
|
"pyo3-macros-backend",
|
|
@@ -270,9 +270,9 @@ dependencies = [
|
|
|
270
270
|
|
|
271
271
|
[[package]]
|
|
272
272
|
name = "pyo3-macros-backend"
|
|
273
|
-
version = "0.29.
|
|
273
|
+
version = "0.29.3"
|
|
274
274
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
275
|
-
checksum = "
|
|
275
|
+
checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
|
|
276
276
|
dependencies = [
|
|
277
277
|
"heck",
|
|
278
278
|
"proc-macro2",
|
|
@@ -308,7 +308,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|
|
308
308
|
|
|
309
309
|
[[package]]
|
|
310
310
|
name = "rgapi"
|
|
311
|
-
version = "0.2.
|
|
311
|
+
version = "0.2.1"
|
|
312
312
|
dependencies = [
|
|
313
313
|
"crc32fast",
|
|
314
314
|
"globset",
|
|
@@ -316,11 +316,21 @@ dependencies = [
|
|
|
316
316
|
"grep-regex",
|
|
317
317
|
"grep-searcher",
|
|
318
318
|
"ignore",
|
|
319
|
-
"pyo3",
|
|
320
319
|
"serde",
|
|
321
320
|
"serde_json",
|
|
322
321
|
]
|
|
323
322
|
|
|
323
|
+
[[package]]
|
|
324
|
+
name = "rgapi-py"
|
|
325
|
+
version = "0.2.1"
|
|
326
|
+
dependencies = [
|
|
327
|
+
"grep-matcher",
|
|
328
|
+
"grep-regex",
|
|
329
|
+
"pyo3",
|
|
330
|
+
"pyo3-build-config",
|
|
331
|
+
"rgapi",
|
|
332
|
+
]
|
|
333
|
+
|
|
324
334
|
[[package]]
|
|
325
335
|
name = "rustversion"
|
|
326
336
|
version = "1.0.23"
|
|
@@ -1,37 +1,44 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
members = ["py"]
|
|
3
|
+
|
|
4
|
+
[workspace.package]
|
|
5
|
+
version = "0.2.1"
|
|
6
|
+
edition = "2024"
|
|
7
|
+
|
|
8
|
+
[workspace.dependencies]
|
|
9
|
+
grep-matcher = ">=0.1.8"
|
|
10
|
+
grep-regex = ">=0.1.14"
|
|
11
|
+
|
|
1
12
|
[package]
|
|
2
13
|
name = "rgapi"
|
|
3
|
-
version =
|
|
4
|
-
edition =
|
|
14
|
+
version.workspace = true
|
|
15
|
+
edition.workspace = true
|
|
5
16
|
rust-version = "1.91"
|
|
6
17
|
license = "Apache-2.0"
|
|
7
18
|
description = "Python API for ripgrep-style file walking and searching"
|
|
8
19
|
repository = "https://github.com/AnswerDotAI/rgapi"
|
|
9
20
|
homepage = "https://github.com/AnswerDotAI/rgapi"
|
|
10
21
|
documentation = "https://github.com/AnswerDotAI/rgapi"
|
|
11
|
-
readme = "README.md"
|
|
12
22
|
|
|
13
23
|
[lib]
|
|
14
24
|
name = "rgapi"
|
|
15
|
-
crate-type = ["cdylib", "rlib"]
|
|
16
25
|
|
|
17
26
|
[dependencies]
|
|
18
27
|
crc32fast = "1"
|
|
19
28
|
globset = ">=0.4.18"
|
|
20
|
-
grep-matcher =
|
|
21
|
-
grep-regex =
|
|
29
|
+
grep-matcher.workspace = true
|
|
30
|
+
grep-regex.workspace = true
|
|
22
31
|
grep-searcher = ">=0.1.16"
|
|
23
32
|
ignore = ">=0.4.26"
|
|
24
|
-
pyo3 = { version = ">=0.28", optional = true }
|
|
25
33
|
serde = { version = "=1", features = ["derive"] }
|
|
26
34
|
serde_json = "=1"
|
|
27
35
|
|
|
28
|
-
[features]
|
|
29
|
-
python = ["dep:pyo3"]
|
|
30
|
-
extension-module = ["python", "pyo3/extension-module"]
|
|
31
|
-
|
|
32
36
|
[lints.clippy]
|
|
33
37
|
too_many_arguments = "allow"
|
|
34
38
|
|
|
39
|
+
[profile.test]
|
|
40
|
+
inherits = "release"
|
|
41
|
+
|
|
35
42
|
[profile.release]
|
|
36
43
|
lto = false
|
|
37
44
|
codegen-units = 16
|
|
@@ -39,6 +46,9 @@ codegen-units = 16
|
|
|
39
46
|
[profile.release.package.rgapi]
|
|
40
47
|
incremental = true
|
|
41
48
|
|
|
49
|
+
[profile.release.package.rgapi-py]
|
|
50
|
+
incremental = true
|
|
51
|
+
|
|
42
52
|
[profile.dist]
|
|
43
53
|
inherits = "release"
|
|
44
54
|
lto = true
|
|
@@ -48,3 +58,6 @@ strip = true
|
|
|
48
58
|
|
|
49
59
|
[profile.dist.package.rgapi]
|
|
50
60
|
incremental = false
|
|
61
|
+
|
|
62
|
+
[profile.dist.package.rgapi-py]
|
|
63
|
+
incremental = false
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
src/walk.rs ignore/globset/grep-regex-backed path walking and filtering
|
|
9
9
|
src/search.rs grep-regex/grep-searcher-backed searching
|
|
10
10
|
src/block.rs blank-line-delimited block grouping, matching, and block context
|
|
11
|
-
src/
|
|
11
|
+
py/src/lib.rs PyO3 classes and private core functions
|
|
12
12
|
python/rgapi/ public Python wrappers over `rgapi._core`, plus the `rgapi-nbrg` CLI
|
|
13
13
|
tests/ pytest coverage for the Python API
|
|
14
14
|
```
|
|
@@ -18,11 +18,11 @@ The public Python API lives in `python/rgapi/__init__.py`. The extension module
|
|
|
18
18
|
## Commands
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
|
|
21
|
+
cargo develop
|
|
22
22
|
pytest -q
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
`cargo develop` and bare `cargo test` use the same Cargo profile and share the core and binding libraries. Unit tests also build a separate `cfg(test)` executable. The published `rgapi` crate has no Python dependency; the unpublished `rgapi-py` crate in `py/` builds the extension. Run `cargo fmt` after Rust edits and `chkstyle` before committing Python edits.
|
|
26
26
|
|
|
27
27
|
## Release
|
|
28
28
|
|
|
@@ -30,8 +30,8 @@ The canonical version lives in `Cargo.toml`. `pyproject.toml` gets the Python pa
|
|
|
30
30
|
|
|
31
31
|
Release flow is: release first, then bump - `ship-release` does both.
|
|
32
32
|
|
|
33
|
-
1. Run `
|
|
34
|
-
2. Confirm the release version in `Cargo.toml` (`[package].version`).
|
|
33
|
+
1. Run `cargo develop && pytest -q`.
|
|
34
|
+
2. Confirm the release version in `Cargo.toml` (`[workspace.package].version`).
|
|
35
35
|
3. Run `ship-release`. It tags `v<version>`, pushes branch and tag (CI builds and publishes), then bumps `Cargo.toml`, refreshes the editable install, and pushes the bump without a tag.
|
|
36
36
|
|
|
37
37
|
The GitHub workflow builds wheels for Python 3.10-3.13 on Linux and macOS and publishes the Rust crate, GitHub release artifacts, and PyPI package when a `v*` tag is pushed.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rgapi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Requires-Dist: fastcore>=2.2.29
|
|
7
|
+
Requires-Dist: fastws-cli>=0.0.20 ; extra == 'dev'
|
|
7
8
|
Requires-Dist: fastship>=0.0.12 ; extra == 'dev'
|
|
8
9
|
Requires-Dist: maturin>=1.0,<2.0 ; extra == 'dev'
|
|
9
10
|
Requires-Dist: pytest ; extra == 'dev'
|
|
10
11
|
Provides-Extra: dev
|
|
11
12
|
License-File: LICENSE
|
|
12
13
|
Summary: Python API for ripgrep-style file walking and searching
|
|
13
|
-
Home-Page: https://github.com/AnswerDotAI/rgapi
|
|
14
14
|
Author-email: Jeremy Howard <j@fast.ai>
|
|
15
15
|
License-Expression: Apache-2.0
|
|
16
16
|
Requires-Python: >=3.10
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rgapi-py"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
crate-type = ["cdylib", "rlib"]
|
|
9
|
+
test = false
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
rgapi = { path = ".." }
|
|
13
|
+
grep-matcher.workspace = true
|
|
14
|
+
grep-regex.workspace = true
|
|
15
|
+
pyo3 = { version = ">=0.29", features = ["extension-module"] }
|
|
16
|
+
|
|
17
|
+
[build-dependencies]
|
|
18
|
+
pyo3-build-config = "0.29"
|
rgapi-0.2.1/py/build.rs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fn main() { pyo3_build_config::add_extension_module_link_args(); }
|
|
@@ -9,15 +9,38 @@ use pyo3::exceptions::PyValueError;
|
|
|
9
9
|
use pyo3::prelude::*;
|
|
10
10
|
use pyo3::types::{PyAny, PyDict};
|
|
11
11
|
|
|
12
|
-
use
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
compile_regex, find, find_iter as find_iter_core, nb_iter as nb_iter_core, nb_search_file, rg_iter as rg_iter_core, search_path as search_path_core,
|
|
17
|
-
search_text as search_text_core,
|
|
12
|
+
use rgapi::{
|
|
13
|
+
FindIter, FindOptions, NbCell, NbIter, NbOptions, RgIter, RgOptions, SearchBlock, SearchLine, StreamIter, WalkOptions as WalkOptionsCore,
|
|
14
|
+
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
|
+
rg_iter as rg_iter_core, search_path as search_path_core, search_text as search_text_core, spans_for,
|
|
18
16
|
};
|
|
19
17
|
use std::path::Path;
|
|
20
18
|
|
|
19
|
+
struct WalkOptions(WalkOptionsCore);
|
|
20
|
+
|
|
21
|
+
impl<'a, 'py> FromPyObject<'a, 'py> for WalkOptions {
|
|
22
|
+
type Error = PyErr;
|
|
23
|
+
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
|
|
24
|
+
Ok(Self(WalkOptionsCore {
|
|
25
|
+
roots: obj.get_item("roots")?.extract()?,
|
|
26
|
+
includes: obj.get_item("includes")?.extract()?,
|
|
27
|
+
excludes: obj.get_item("excludes")?.extract()?,
|
|
28
|
+
exts: obj.get_item("exts")?.extract()?,
|
|
29
|
+
path_re: obj.get_item("path_re")?.extract()?,
|
|
30
|
+
skip_path_re: obj.get_item("skip_path_re")?.extract()?,
|
|
31
|
+
skip_dirs: obj.get_item("skip_dirs")?.extract()?,
|
|
32
|
+
skip_dir_re: obj.get_item("skip_dir_re")?.extract()?,
|
|
33
|
+
hidden: obj.get_item("hidden")?.extract()?,
|
|
34
|
+
ignore: obj.get_item("ignore")?.extract()?,
|
|
35
|
+
max_depth: obj.get_item("max_depth")?.extract()?,
|
|
36
|
+
min_depth: obj.get_item("min_depth")?.extract()?,
|
|
37
|
+
max_filesize: obj.get_item("max_filesize")?.extract()?,
|
|
38
|
+
follow_links: obj.get_item("follow_links")?.extract()?,
|
|
39
|
+
same_file_system: obj.get_item("same_file_system")?.extract()?,
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
#[pyclass(name = "SearchLine", eq, skip_from_py_object)]
|
|
22
45
|
#[derive(Clone)]
|
|
23
46
|
struct SearchLinePy {
|
|
@@ -193,12 +216,20 @@ fn compile_regex_py(pattern: String, case_sensitive: Option<bool>, smart_case: b
|
|
|
193
216
|
fn compile_py(pattern: String, case_sensitive: Option<bool>, smart_case: bool) -> PyResult<RegexPy> { compile_regex_py(pattern, case_sensitive, smart_case) }
|
|
194
217
|
#[pyfunction(name = "walk_base")]
|
|
195
218
|
fn walk_base_py(walk: WalkOptions, canonical: bool, walk_root_links: bool) -> PyResult<PathBuf> {
|
|
196
|
-
resolve_roots(&walk, canonical, walk_root_links).map(|(_, base)| base).map_err(|e| PyValueError::new_err(e.to_string()))
|
|
219
|
+
resolve_roots(&walk.0, canonical, walk_root_links).map(|(_, base)| base).map_err(|e| PyValueError::new_err(e.to_string()))
|
|
197
220
|
}
|
|
198
221
|
|
|
199
222
|
#[pyfunction(name = "find")]
|
|
200
|
-
fn find_py(
|
|
201
|
-
|
|
223
|
+
fn find_py(
|
|
224
|
+
py: Python<'_>,
|
|
225
|
+
walk: WalkOptions,
|
|
226
|
+
pattern: Option<String>,
|
|
227
|
+
files: bool,
|
|
228
|
+
dirs: bool,
|
|
229
|
+
timeout_ms: Option<u64>,
|
|
230
|
+
walk_root_links: bool,
|
|
231
|
+
) -> PyResult<(Vec<PathBuf>, bool)> {
|
|
232
|
+
let opts = FindOptions { walk: walk.0, pattern, files, dirs, ..FindOptions::default() };
|
|
202
233
|
let iter = find_iter_with(&opts, walk_root_links).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
203
234
|
collect_stream_py(py, iter, |p| p, timeout_ms)
|
|
204
235
|
}
|
|
@@ -235,7 +266,7 @@ fn rg_py(
|
|
|
235
266
|
lnhash: bool,
|
|
236
267
|
timeout_ms: Option<u64>,
|
|
237
268
|
) -> PyResult<(Vec<SearchLinePy>, bool)> {
|
|
238
|
-
let opts = RgOptions { walk, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
269
|
+
let opts = RgOptions { walk: walk.0, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
239
270
|
let iter = rg_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
240
271
|
collect_stream_py(py, iter, move |l| search_line_py(l, lnhash), timeout_ms)
|
|
241
272
|
}
|
|
@@ -251,7 +282,7 @@ fn block_search_py(
|
|
|
251
282
|
after_context: usize,
|
|
252
283
|
timeout_ms: Option<u64>,
|
|
253
284
|
) -> PyResult<(Vec<BlockRow>, bool)> {
|
|
254
|
-
let opts = RgOptions { walk, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
285
|
+
let opts = RgOptions { walk: walk.0, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
255
286
|
let iter = block_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
256
287
|
collect_stream_py(py, iter, block_row, timeout_ms)
|
|
257
288
|
}
|
|
@@ -265,7 +296,7 @@ fn rg_iter_py(
|
|
|
265
296
|
after_context: usize,
|
|
266
297
|
lnhash: bool,
|
|
267
298
|
) -> PyResult<RgIterPy> {
|
|
268
|
-
let opts = RgOptions { walk, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
299
|
+
let opts = RgOptions { walk: walk.0, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
269
300
|
rg_iter_core(&opts).map(|inner| RgIterPy { inner, display_lnhash: lnhash }).map_err(|e| PyValueError::new_err(e.to_string()))
|
|
270
301
|
}
|
|
271
302
|
|
|
@@ -281,7 +312,7 @@ impl FindIterPy {
|
|
|
281
312
|
|
|
282
313
|
#[pyfunction(name = "find_iter")]
|
|
283
314
|
fn find_iter_py(walk: WalkOptions, pattern: Option<String>, files: bool, dirs: bool) -> PyResult<FindIterPy> {
|
|
284
|
-
let opts = FindOptions { walk, pattern, files, dirs, ..FindOptions::default() };
|
|
315
|
+
let opts = FindOptions { walk: walk.0, pattern, files, dirs, ..FindOptions::default() };
|
|
285
316
|
find_iter_core(&opts).map(|inner| FindIterPy { inner }).map_err(|e| PyValueError::new_err(e.to_string()))
|
|
286
317
|
}
|
|
287
318
|
|
|
@@ -293,7 +324,7 @@ impl AsyncHandlePy { fn cancel(&self) { self.cancel.store(true, Ordering::Relaxe
|
|
|
293
324
|
|
|
294
325
|
#[pyfunction(name = "find_async")]
|
|
295
326
|
fn find_async_py(cb: Py<PyAny>, walk: WalkOptions, pattern: Option<String>, files: bool, dirs: bool, timeout_ms: Option<u64>) -> PyResult<AsyncHandlePy> {
|
|
296
|
-
let opts = FindOptions { walk, pattern, files, dirs, ..FindOptions::default() };
|
|
327
|
+
let opts = FindOptions { walk: walk.0, pattern, files, dirs, ..FindOptions::default() };
|
|
297
328
|
let iter = find_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
298
329
|
let deadline = timeout_ms.map(|ms| Instant::now() + Duration::from_millis(ms));
|
|
299
330
|
Ok(stream_async(cb, iter, deadline, |py, paths, timed_out| Ok((paths, timed_out).into_pyobject(py)?.into_any().unbind())))
|
|
@@ -301,7 +332,7 @@ fn find_async_py(cb: Py<PyAny>, walk: WalkOptions, pattern: Option<String>, file
|
|
|
301
332
|
|
|
302
333
|
#[pyfunction(name = "find_iter_async")]
|
|
303
334
|
fn find_iter_async_py(cb: Py<PyAny>, batch_max: usize, walk: WalkOptions, pattern: Option<String>, files: bool, dirs: bool) -> PyResult<AsyncHandlePy> {
|
|
304
|
-
let opts = FindOptions { walk, pattern, files, dirs, ..FindOptions::default() };
|
|
335
|
+
let opts = FindOptions { walk: walk.0, pattern, files, dirs, ..FindOptions::default() };
|
|
305
336
|
let iter = find_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
306
337
|
Ok(stream_iter_async(cb, iter, batch_max, |py, paths| Ok(paths.into_pyobject(py)?.into_any().unbind())))
|
|
307
338
|
}
|
|
@@ -411,7 +442,7 @@ fn rg_async_py(
|
|
|
411
442
|
lnhash: bool,
|
|
412
443
|
timeout_ms: Option<u64>,
|
|
413
444
|
) -> PyResult<AsyncHandlePy> {
|
|
414
|
-
let opts = RgOptions { walk, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
445
|
+
let opts = RgOptions { walk: walk.0, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
415
446
|
let iter = rg_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
416
447
|
let deadline = timeout_ms.map(|ms| Instant::now() + Duration::from_millis(ms));
|
|
417
448
|
Ok(stream_async(cb, iter, deadline, move |py, rows, timed_out| {
|
|
@@ -431,7 +462,7 @@ fn block_search_async_py(
|
|
|
431
462
|
after_context: usize,
|
|
432
463
|
timeout_ms: Option<u64>,
|
|
433
464
|
) -> PyResult<AsyncHandlePy> {
|
|
434
|
-
let opts = RgOptions { walk, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
465
|
+
let opts = RgOptions { walk: walk.0, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
435
466
|
let iter = block_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
436
467
|
let deadline = timeout_ms.map(|ms| Instant::now() + Duration::from_millis(ms));
|
|
437
468
|
Ok(stream_async(cb, iter, deadline, |py, rows, timed_out| {
|
|
@@ -452,7 +483,7 @@ fn rg_iter_async_py(
|
|
|
452
483
|
after_context: usize,
|
|
453
484
|
lnhash: bool,
|
|
454
485
|
) -> PyResult<AsyncHandlePy> {
|
|
455
|
-
let opts = RgOptions { walk, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
486
|
+
let opts = RgOptions { walk: walk.0, pattern, case_sensitive, smart_case, before_context, after_context, ..RgOptions::default() };
|
|
456
487
|
let iter = rg_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
457
488
|
Ok(stream_iter_async(cb, iter, batch_max, move |py, rows| {
|
|
458
489
|
let rows: Vec<SearchLinePy> = rows.into_iter().map(|l| search_line_py(l, lnhash)).collect();
|
|
@@ -462,7 +493,7 @@ fn rg_iter_async_py(
|
|
|
462
493
|
|
|
463
494
|
#[pyfunction(name = "panic_probe")]
|
|
464
495
|
fn panic_probe_py(py: Python<'_>, root: PathBuf, walk: bool) -> PyResult<()> {
|
|
465
|
-
let walk_opts =
|
|
496
|
+
let walk_opts = WalkOptionsCore { roots: vec![root], ..WalkOptionsCore::default() };
|
|
466
497
|
if walk {
|
|
467
498
|
let opts = FindOptions { walk: walk_opts, panic_probe: true, ..FindOptions::default() };
|
|
468
499
|
find(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
@@ -518,14 +549,14 @@ fn nb_search_py(
|
|
|
518
549
|
multiline: bool,
|
|
519
550
|
timeout_ms: Option<u64>,
|
|
520
551
|
) -> PyResult<(Vec<NbRow>, bool)> {
|
|
521
|
-
let opts = NbOptions { walk, pattern, case_sensitive, smart_case, cell_context, multiline };
|
|
552
|
+
let opts = NbOptions { walk: walk.0, pattern, case_sensitive, smart_case, cell_context, multiline };
|
|
522
553
|
let iter = nb_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
523
554
|
collect_stream_py(py, iter, nb_row, timeout_ms)
|
|
524
555
|
}
|
|
525
556
|
|
|
526
557
|
#[pyfunction(name = "nb_iter")]
|
|
527
558
|
fn nb_iter_py(walk: WalkOptions, pattern: String, case_sensitive: Option<bool>, smart_case: bool, cell_context: usize, multiline: bool) -> PyResult<NbIterPy> {
|
|
528
|
-
let opts = NbOptions { walk, pattern, case_sensitive, smart_case, cell_context, multiline };
|
|
559
|
+
let opts = NbOptions { walk: walk.0, pattern, case_sensitive, smart_case, cell_context, multiline };
|
|
529
560
|
nb_iter_core(&opts).map(|inner| NbIterPy { inner }).map_err(|e| PyValueError::new_err(e.to_string()))
|
|
530
561
|
}
|
|
531
562
|
|
|
@@ -540,7 +571,7 @@ fn nb_search_async_py(
|
|
|
540
571
|
multiline: bool,
|
|
541
572
|
timeout_ms: Option<u64>,
|
|
542
573
|
) -> PyResult<AsyncHandlePy> {
|
|
543
|
-
let opts = NbOptions { walk, pattern, case_sensitive, smart_case, cell_context, multiline };
|
|
574
|
+
let opts = NbOptions { walk: walk.0, pattern, case_sensitive, smart_case, cell_context, multiline };
|
|
544
575
|
let iter = nb_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
545
576
|
let deadline = timeout_ms.map(|ms| Instant::now() + Duration::from_millis(ms));
|
|
546
577
|
Ok(stream_async(cb, iter, deadline, |py, rows, timed_out| {
|
|
@@ -560,7 +591,7 @@ fn nb_iter_async_py(
|
|
|
560
591
|
cell_context: usize,
|
|
561
592
|
multiline: bool,
|
|
562
593
|
) -> PyResult<AsyncHandlePy> {
|
|
563
|
-
let opts = NbOptions { walk, pattern, case_sensitive, smart_case, cell_context, multiline };
|
|
594
|
+
let opts = NbOptions { walk: walk.0, pattern, case_sensitive, smart_case, cell_context, multiline };
|
|
564
595
|
let iter = nb_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
565
596
|
Ok(stream_iter_async(cb, iter, batch_max, |py, rows| {
|
|
566
597
|
let rows: Vec<NbRow> = rows.into_iter().map(nb_row).collect();
|
|
@@ -17,7 +17,7 @@ classifiers = [
|
|
|
17
17
|
dependencies = ["fastcore>=2.2.29"]
|
|
18
18
|
|
|
19
19
|
[project.optional-dependencies]
|
|
20
|
-
dev = ["fastship>=0.0.12", "maturin>=1.0,<2.0", "pytest"]
|
|
20
|
+
dev = ["fastws-cli>=0.0.20", "fastship>=0.0.12", "maturin>=1.0,<2.0", "pytest"]
|
|
21
21
|
|
|
22
22
|
[project.urls]
|
|
23
23
|
Homepage = "https://github.com/AnswerDotAI/rgapi"
|
|
@@ -34,12 +34,12 @@ rgapi = "rgapi"
|
|
|
34
34
|
rgapi-nbrg = "rgapi._cli:nbrg_cli"
|
|
35
35
|
|
|
36
36
|
[tool.maturin]
|
|
37
|
-
|
|
37
|
+
manifest-path = "py/Cargo.toml"
|
|
38
38
|
python-source = "python"
|
|
39
39
|
module-name = "rgapi._core"
|
|
40
40
|
|
|
41
41
|
[tool.uv]
|
|
42
|
-
cache-keys = [{ file = "pyproject.toml" }, { file = "
|
|
42
|
+
cache-keys = [{ file = "pyproject.toml" }, { file = "Cargo.toml" }, { file = "py/Cargo.toml" }]
|
|
43
43
|
|
|
44
44
|
[tool.fastship]
|
|
45
45
|
branch = "main"
|
|
@@ -109,13 +109,8 @@ pub fn block_iter(opts: &RgOptions) -> Result<BlockIter, RgApiError> {
|
|
|
109
109
|
let filters = Arc::new(PathFilters::new(&opts.walk)?);
|
|
110
110
|
let matcher = compile_regex(&opts.pattern, opts.case_sensitive, opts.smart_case, false)?;
|
|
111
111
|
let (before_context, after_context, max_depth) = (opts.before_context, opts.after_context, opts.walk.max_depth);
|
|
112
|
-
Ok(spawn_walk(
|
|
113
|
-
|
|
114
|
-
base,
|
|
115
|
-
&opts.walk,
|
|
116
|
-
filters,
|
|
117
|
-
Vec::new(),
|
|
118
|
-
move |dent, base, filters, tx, cancel| match block_entry(dent, base, filters, &matcher, before_context, after_context, max_depth) {
|
|
112
|
+
Ok(spawn_walk(roots, base, &opts.walk, filters, Vec::new(), move |dent, base, filters, tx, cancel| {
|
|
113
|
+
match block_entry(dent, base, filters, &matcher, before_context, after_context, max_depth) {
|
|
119
114
|
Ok(blocks) => {
|
|
120
115
|
for block in blocks { if cancel.load(Ordering::Relaxed) || tx.send(Ok(block)).is_err() { return WalkState::Quit; } }
|
|
121
116
|
WalkState::Continue
|
|
@@ -124,6 +119,6 @@ pub fn block_iter(opts: &RgOptions) -> Result<BlockIter, RgApiError> {
|
|
|
124
119
|
let _ = tx.send(Err(err));
|
|
125
120
|
WalkState::Quit
|
|
126
121
|
}
|
|
127
|
-
}
|
|
128
|
-
))
|
|
122
|
+
}
|
|
123
|
+
}))
|
|
129
124
|
}
|
|
@@ -5,13 +5,10 @@ mod nb;
|
|
|
5
5
|
mod search;
|
|
6
6
|
mod walk;
|
|
7
7
|
|
|
8
|
-
#[cfg(feature = "python")]
|
|
9
|
-
mod python;
|
|
10
|
-
|
|
11
8
|
pub use block::{BlockIter, SearchBlock, block_iter};
|
|
12
9
|
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, SearchKind, SearchLine, compile_regex, rg, rg_iter, search_path, search_text};
|
|
14
|
-
pub use walk::{FindIter, FindOptions, StreamIter, WalkOptions, find, find_iter};
|
|
10
|
+
pub use search::{MatchSpan, RgIter, RgOptions, SearchError, SearchKind, SearchLine, compile_regex, rg, rg_iter, search_path, search_text, spans_for};
|
|
11
|
+
pub use walk::{FindIter, FindOptions, StreamIter, WalkOptions, find, find_iter, find_iter_with, resolve_roots};
|
|
15
12
|
|
|
16
13
|
#[derive(Debug, Clone)]
|
|
17
14
|
pub struct RgApiError { msg: String }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use std::collections::{BTreeMap, HashMap};
|
|
2
2
|
use std::path::Path;
|
|
3
|
-
use std::sync::{Arc, LazyLock};
|
|
4
3
|
use std::sync::atomic::Ordering;
|
|
4
|
+
use std::sync::{Arc, LazyLock};
|
|
5
5
|
|
|
6
6
|
use grep_matcher::{Captures, Matcher};
|
|
7
7
|
use grep_regex::RegexMatcher;
|
|
@@ -47,10 +47,8 @@ fn sigil_caps(text: &str, sigil: &str, body: &str) -> Vec<String> {
|
|
|
47
47
|
let re = RegexMatcher::new(&format!("{sigil}`({body})`")).expect("sigil pattern compiles");
|
|
48
48
|
let mut caps = re.new_captures().expect("captures allocate");
|
|
49
49
|
let mut res = Vec::new();
|
|
50
|
-
re.captures_iter(text.as_bytes(), &mut caps, |c| {
|
|
51
|
-
|
|
52
|
-
true
|
|
53
|
-
}).expect("regex search is infallible");
|
|
50
|
+
re.captures_iter(text.as_bytes(), &mut caps, |c| { if let Some(m) = c.get(1) { res.push(text[m.start()..m.end()].to_string()); } true })
|
|
51
|
+
.expect("regex search is infallible");
|
|
54
52
|
res
|
|
55
53
|
}
|
|
56
54
|
|
|
@@ -85,7 +83,10 @@ pub fn cell_refs(cell: &serde_json::Value) -> CellRefs {
|
|
|
85
83
|
let prompt = cell["metadata"]["solveit_ai"] == true;
|
|
86
84
|
let mut res = CellRefs::default();
|
|
87
85
|
if prompt { (res.vars, res.cmds) = (sigil_caps(&src, r"\$", "[^`]+"), sigil_caps(&src, "!", "[^`]+")); }
|
|
88
|
-
if prompt || cell["cell_type"] == "markdown" {
|
|
86
|
+
if prompt || cell["cell_type"] == "markdown" {
|
|
87
|
+
res.tools = tool_names(&src);
|
|
88
|
+
return res;
|
|
89
|
+
}
|
|
89
90
|
for o in cell["outputs"].as_array().into_iter().flatten() {
|
|
90
91
|
if matches!(o["output_type"].as_str(), Some("display_data" | "execute_result")) { res.tools.extend(tool_names(&nb_text(&o["data"]["text/markdown"]))); }
|
|
91
92
|
}
|
|
@@ -222,13 +223,8 @@ pub fn nb_iter(opts: &NbOptions) -> Result<NbIter, RgApiError> {
|
|
|
222
223
|
let filters = Arc::new(PathFilters::new(&opts.walk)?);
|
|
223
224
|
let matcher = compile_nb_regex(&opts.pattern, opts.case_sensitive, opts.smart_case, opts.multiline)?;
|
|
224
225
|
let (cell_context, multiline, max_depth) = (opts.cell_context, opts.multiline, opts.walk.max_depth);
|
|
225
|
-
Ok(spawn_walk(
|
|
226
|
-
|
|
227
|
-
base,
|
|
228
|
-
&opts.walk,
|
|
229
|
-
filters,
|
|
230
|
-
Vec::new(),
|
|
231
|
-
move |dent, base, filters, tx, cancel| match nb_entry(dent, base, filters, &matcher, cell_context, multiline, max_depth) {
|
|
226
|
+
Ok(spawn_walk(roots, base, &opts.walk, filters, Vec::new(), move |dent, base, filters, tx, cancel| {
|
|
227
|
+
match nb_entry(dent, base, filters, &matcher, cell_context, multiline, max_depth) {
|
|
232
228
|
Ok(cells) => {
|
|
233
229
|
for cell in cells { if cancel.load(Ordering::Relaxed) || tx.send(Ok(cell)).is_err() { return WalkState::Quit; } }
|
|
234
230
|
WalkState::Continue
|
|
@@ -237,8 +233,8 @@ pub fn nb_iter(opts: &NbOptions) -> Result<NbIter, RgApiError> {
|
|
|
237
233
|
let _ = tx.send(Err(err));
|
|
238
234
|
WalkState::Quit
|
|
239
235
|
}
|
|
240
|
-
}
|
|
241
|
-
))
|
|
236
|
+
}
|
|
237
|
+
}))
|
|
242
238
|
}
|
|
243
239
|
|
|
244
240
|
pub fn nb_search(opts: &NbOptions) -> Result<Vec<NbCell>, RgApiError> { nb_iter(opts)?.collect() }
|
|
@@ -97,10 +97,7 @@ fn search_entry(
|
|
|
97
97
|
|
|
98
98
|
fn is_cancelled(cancel: &Arc<AtomicBool>) -> bool { cancel.load(Ordering::Relaxed) }
|
|
99
99
|
|
|
100
|
-
fn send_search_error(tx: &SyncSender<Result<SearchLine, RgApiError>>, err: RgApiError) -> WalkState {
|
|
101
|
-
let _ = tx.send(Err(err));
|
|
102
|
-
WalkState::Quit
|
|
103
|
-
}
|
|
100
|
+
fn send_search_error(tx: &SyncSender<Result<SearchLine, RgApiError>>, err: RgApiError) -> WalkState { let _ = tx.send(Err(err)); WalkState::Quit }
|
|
104
101
|
|
|
105
102
|
pub fn compile_regex(pattern: &str, case_sensitive: Option<bool>, smart_case: bool, multiline: bool) -> Result<RegexMatcher, RgApiError> {
|
|
106
103
|
if pattern.is_empty() { return Err(RgApiError::new("pattern may not be empty")); }
|
|
@@ -146,7 +143,12 @@ fn search_path_cancelable(
|
|
|
146
143
|
cancel: Option<Arc<AtomicBool>>,
|
|
147
144
|
) -> Result<Vec<SearchLine>, RgApiError> {
|
|
148
145
|
let mut builder = SearcherBuilder::new();
|
|
149
|
-
builder
|
|
146
|
+
builder
|
|
147
|
+
.line_number(true)
|
|
148
|
+
.line_terminator(LineTerminator::crlf())
|
|
149
|
+
.before_context(before_context)
|
|
150
|
+
.after_context(after_context)
|
|
151
|
+
.binary_detection(BinaryDetection::quit(0));
|
|
150
152
|
let mut searcher = builder.build();
|
|
151
153
|
let mut out = Vec::new();
|
|
152
154
|
let search_matcher = matcher.clone();
|
|
@@ -222,14 +224,12 @@ impl Sink for CollectSink<'_> {
|
|
|
222
224
|
self.lines.push(SearchLine { kind, path: self.path.clone(), line_number, lnhash: format_lnhash(line_number, &line), line, matches: Vec::new() });
|
|
223
225
|
Ok(!self.cancelled())
|
|
224
226
|
}
|
|
225
|
-
fn binary_data(&mut self, _searcher: &grep_searcher::Searcher, _binary_byte_offset: u64) -> Result<bool, Self::Error> {
|
|
226
|
-
self.lines.clear();
|
|
227
|
-
Ok(false)
|
|
228
|
-
}
|
|
227
|
+
fn binary_data(&mut self, _searcher: &grep_searcher::Searcher, _binary_byte_offset: u64) -> Result<bool, Self::Error> { self.lines.clear(); Ok(false) }
|
|
229
228
|
}
|
|
230
229
|
|
|
231
230
|
#[derive(Debug)]
|
|
232
|
-
|
|
231
|
+
/// A search failure, including invalid UTF-8 in matched text.
|
|
232
|
+
pub enum SearchError { Message(String), InvalidUtf8 }
|
|
233
233
|
impl std::fmt::Display for SearchError {
|
|
234
234
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
235
235
|
match self { Self::Message(msg) => write!(f, "{msg}"), Self::InvalidUtf8 => write!(f, "invalid utf-8") }
|
|
@@ -241,13 +241,11 @@ fn bytes_to_line(bytes: &[u8]) -> Result<String, SearchError> {
|
|
|
241
241
|
let s = std::str::from_utf8(bytes).map_err(|_| SearchError::InvalidUtf8)?;
|
|
242
242
|
Ok(s.trim_end_matches(['\r', '\n']).to_string())
|
|
243
243
|
}
|
|
244
|
-
|
|
244
|
+
/// Return byte offsets for every match in a line.
|
|
245
|
+
pub fn spans_for(matcher: &RegexMatcher, bytes: &[u8]) -> Result<Vec<MatchSpan>, SearchError> {
|
|
245
246
|
let mut spans = Vec::new();
|
|
246
247
|
matcher
|
|
247
|
-
.find_iter(bytes, |m| {
|
|
248
|
-
spans.push(MatchSpan { start: m.start(), end: m.end() });
|
|
249
|
-
true
|
|
250
|
-
})
|
|
248
|
+
.find_iter(bytes, |m| { spans.push(MatchSpan { start: m.start(), end: m.end() }); true })
|
|
251
249
|
.map_err(|e| SearchError::Message(e.to_string()))?;
|
|
252
250
|
Ok(spans)
|
|
253
251
|
}
|
|
@@ -13,7 +13,6 @@ use crate::RgApiError;
|
|
|
13
13
|
|
|
14
14
|
/// Where to walk and which paths to keep. `FindOptions`, `RgOptions` and `NbOptions` each hold one.
|
|
15
15
|
#[derive(Debug, Clone)]
|
|
16
|
-
#[cfg_attr(feature = "python", derive(pyo3::FromPyObject), pyo3(from_item_all))]
|
|
17
16
|
pub struct WalkOptions {
|
|
18
17
|
/// Directories or files to walk. Result paths are relative to the base of the roots. Filters match the same relative paths.
|
|
19
18
|
/// A directory root is its own base. A file root, or a link root that is not followed, has its parent as its base.
|
|
@@ -80,38 +79,29 @@ pub fn find_iter(opts: &FindOptions) -> Result<FindIter, RgApiError> { find_iter
|
|
|
80
79
|
|
|
81
80
|
/// Like `find_iter`, except that `walk_root_links` walks a root link to a directory instead of returning the link.
|
|
82
81
|
/// Paths under that root are reported under the link.
|
|
83
|
-
pub
|
|
82
|
+
pub fn find_iter_with(opts: &FindOptions, walk_root_links: bool) -> Result<FindIter, RgApiError> {
|
|
84
83
|
let walk = &opts.walk;
|
|
85
84
|
let (roots, base) = resolve_roots(walk, false, walk_root_links)?;
|
|
86
85
|
let filters = Arc::new(PathFilters::new(walk)?);
|
|
87
86
|
let pattern = opts.pattern.as_deref().map(build_fd_re).transpose()?;
|
|
88
87
|
// The walker follows every root it is given. A link root returned as itself must not reach it.
|
|
89
88
|
let (links, roots): (Vec<_>, Vec<_>) = roots.into_iter().partition(|r| r.is_symlink() && !walk.follow_links && !(walk_root_links && r.is_dir()));
|
|
90
|
-
let ready = if walk.min_depth.unwrap_or(0) > 0 { Vec::new() } else {
|
|
91
|
-
links.iter().map(|r| relative_path(&base, r)).filter(|rel| find_matches(rel, &filters, pattern.as_ref())).map(Path::to_path_buf).collect()
|
|
92
|
-
};
|
|
89
|
+
let ready = if walk.min_depth.unwrap_or(0) > 0 { Vec::new() } else { links.iter().map(|r| relative_path(&base, r)).filter(|rel| find_matches(rel, &filters, pattern.as_ref())).map(Path::to_path_buf).collect() };
|
|
93
90
|
let (files, dirs, special_files, panic_probe, max_depth) = (opts.files, opts.dirs, opts.special_files, opts.panic_probe, walk.max_depth);
|
|
94
|
-
Ok(spawn_walk(
|
|
95
|
-
|
|
96
|
-
base,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
WalkState::Continue
|
|
106
|
-
}
|
|
107
|
-
Ok(None) => WalkState::Continue,
|
|
108
|
-
Err(err) => {
|
|
109
|
-
let _ = tx.send(Err(err));
|
|
110
|
-
WalkState::Quit
|
|
111
|
-
}
|
|
91
|
+
Ok(spawn_walk(roots, base, walk, filters, ready, move |dent, base, filters, tx, cancel| {
|
|
92
|
+
if panic_probe { panic!("rgapi: deliberate panic for tests (panic_probe)"); }
|
|
93
|
+
match find_entry(dent, base, filters, pattern.as_ref(), files, dirs, special_files, max_depth) {
|
|
94
|
+
Ok(Some(path)) => {
|
|
95
|
+
if cancel.load(Ordering::Relaxed) || tx.send(Ok(path)).is_err() { return WalkState::Quit; }
|
|
96
|
+
WalkState::Continue
|
|
97
|
+
}
|
|
98
|
+
Ok(None) => WalkState::Continue,
|
|
99
|
+
Err(err) => {
|
|
100
|
+
let _ = tx.send(Err(err));
|
|
101
|
+
WalkState::Quit
|
|
112
102
|
}
|
|
113
|
-
}
|
|
114
|
-
))
|
|
103
|
+
}
|
|
104
|
+
}))
|
|
115
105
|
}
|
|
116
106
|
|
|
117
107
|
pub struct StreamIter<T> { rx: mpsc::Receiver<Result<T, RgApiError>>, cancel: Arc<AtomicBool>, worker: Option<std::thread::JoinHandle<()>> }
|
|
@@ -211,7 +201,9 @@ where
|
|
|
211
201
|
let (tx, base, filters, cancel, entry, seen) = (tx.clone(), base.clone(), filters.clone(), worker_cancel.clone(), entry.clone(), seen.clone());
|
|
212
202
|
Box::new(move |dent| {
|
|
213
203
|
if cancel.load(Ordering::Relaxed) { return WalkState::Quit; }
|
|
214
|
-
if let (Some(seen), Ok(d)) = (&seen, &dent)
|
|
204
|
+
if let (Some(seen), Ok(d)) = (&seen, &dent)
|
|
205
|
+
&& !seen.lock().unwrap().insert(d.path().to_path_buf())
|
|
206
|
+
{ return WalkState::Continue; }
|
|
215
207
|
catch_unwind(AssertUnwindSafe(|| entry(dent, &base, &filters, &tx, &cancel))).unwrap_or_else(|_| {
|
|
216
208
|
let _ = tx.send(Err(RgApiError::new("internal error during search (this is a bug, please report it)")));
|
|
217
209
|
WalkState::Quit
|
|
@@ -263,9 +255,7 @@ fn dangling_link(err: &ignore::Error) -> Option<&Path> {
|
|
|
263
255
|
}
|
|
264
256
|
|
|
265
257
|
fn find_matches(path: &Path, filters: &PathFilters, pattern: Option<&RegexMatcher>) -> bool {
|
|
266
|
-
if let Some(pattern) = pattern {
|
|
267
|
-
if !re_match(pattern, &path.file_name().unwrap_or_default().to_string_lossy()) { return false; }
|
|
268
|
-
}
|
|
258
|
+
if let Some(pattern) = pattern { if !re_match(pattern, &path.file_name().unwrap_or_default().to_string_lossy()) { return false; } }
|
|
269
259
|
filters.path_allowed(path)
|
|
270
260
|
}
|
|
271
261
|
|
|
@@ -288,14 +278,19 @@ fn normalize_root(path: &Path) -> Result<PathBuf, RgApiError> {
|
|
|
288
278
|
}
|
|
289
279
|
|
|
290
280
|
/// Return the distinct roots and their base. Each root is made absolute. With `canonical`, each root is also canonicalized.
|
|
291
|
-
pub
|
|
281
|
+
pub fn resolve_roots(walk: &WalkOptions, canonical: bool, walk_root_links: bool) -> Result<(Vec<PathBuf>, PathBuf), RgApiError> {
|
|
292
282
|
let mut roots = Vec::new();
|
|
293
283
|
for root in &walk.roots {
|
|
294
|
-
let root = if canonical { normalize_root(root)? } else {
|
|
284
|
+
let root = if canonical { normalize_root(root)? } else {
|
|
285
|
+
let r = std::path::absolute(root)?;
|
|
286
|
+
r.symlink_metadata()?;
|
|
287
|
+
r
|
|
288
|
+
};
|
|
295
289
|
if !roots.contains(&root) { roots.push(root); }
|
|
296
290
|
}
|
|
297
291
|
let follow = walk.follow_links || walk_root_links;
|
|
298
|
-
let mut bases =
|
|
292
|
+
let mut bases =
|
|
293
|
+
roots.iter().map(|r| if (follow || !r.is_symlink()) && r.is_dir() { r.clone() } else { r.parent().map_or_else(|| r.clone(), Path::to_path_buf) });
|
|
299
294
|
let mut base = bases.next().unwrap_or_default();
|
|
300
295
|
for b in bases { while !b.starts_with(&base) && base.pop() {} }
|
|
301
296
|
Ok((roots, base))
|
|
@@ -416,8 +411,12 @@ mod tests {
|
|
|
416
411
|
|
|
417
412
|
#[test]
|
|
418
413
|
fn globs_match_names_or_root_relative_components() {
|
|
419
|
-
for (glob, yes, no) in [
|
|
420
|
-
("
|
|
414
|
+
for (glob, yes, no) in [
|
|
415
|
+
("*.py", "src/deep/app.py", "src/app.rs"),
|
|
416
|
+
("src/*", "src/app.py", "src/deep/app.py"),
|
|
417
|
+
("src/**", "src/deep/app.py", "other/src/app.py"),
|
|
418
|
+
("tests", "src/tests", "src/tests/app.py"),
|
|
419
|
+
] {
|
|
421
420
|
let globs = build_globs(&[glob.into()]).unwrap().unwrap();
|
|
422
421
|
assert!(globs.is_match(yes), "{glob}: {yes}");
|
|
423
422
|
assert!(!globs.is_match(no), "{glob}: {no}");
|
|
@@ -428,10 +427,7 @@ mod tests {
|
|
|
428
427
|
let (tx, rx) = mpsc::channel();
|
|
429
428
|
let cancel = Arc::new(AtomicBool::new(false));
|
|
430
429
|
let worker = std::thread::spawn(move || {
|
|
431
|
-
for i in items {
|
|
432
|
-
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
|
|
433
|
-
if tx.send(Ok(i)).is_err() { return; }
|
|
434
|
-
}
|
|
430
|
+
for i in items { std::thread::sleep(std::time::Duration::from_millis(delay_ms)); if tx.send(Ok(i)).is_err() { return; } }
|
|
435
431
|
});
|
|
436
432
|
StreamIter { rx, cancel, worker: Some(worker) }
|
|
437
433
|
}
|
|
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
|