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.
@@ -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.189"
174
+ version = "0.2.190"
175
175
  source = "registry+https://github.com/rust-lang/crates.io-index"
176
- checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
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.2"
228
+ version = "0.29.3"
229
229
  source = "registry+https://github.com/rust-lang/crates.io-index"
230
- checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
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.2"
242
+ version = "0.29.3"
243
243
  source = "registry+https://github.com/rust-lang/crates.io-index"
244
- checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
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.2"
251
+ version = "0.29.3"
252
252
  source = "registry+https://github.com/rust-lang/crates.io-index"
253
- checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
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.2"
261
+ version = "0.29.3"
262
262
  source = "registry+https://github.com/rust-lang/crates.io-index"
263
- checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
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.2"
273
+ version = "0.29.3"
274
274
  source = "registry+https://github.com/rust-lang/crates.io-index"
275
- checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
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.0"
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 = "0.2.0"
4
- edition = "2024"
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 = ">=0.1.8"
21
- grep-regex = ">=0.1.14"
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/python.rs PyO3 classes and private core functions
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
- maturin develop
21
+ cargo develop
22
22
  pytest -q
23
23
  ```
24
24
 
25
- Python tests (`tests/`) run against the built extension. `cargo test` covers the Rust-only API, and takes no feature flags: `extension-module` stops pyo3 linking libpython, which a test binary needs. For a fast local loop use `maturin develop && pytest -q`. Run `cargo fmt --check` and `cargo check --all-features` for Rust-only edits. Run `chkstyle` after Python edits once tests pass.
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 `maturin develop && pytest -q`.
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.0
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"
@@ -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 crate::search::spans_for;
13
- use crate::walk::{find_iter_with, resolve_roots};
14
- use crate::{
15
- FindIter, FindOptions, NbCell, NbIter, NbOptions, RgIter, RgOptions, SearchBlock, SearchLine, StreamIter, WalkOptions, block_iter as block_iter_core,
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(py: Python<'_>, walk: WalkOptions, pattern: Option<String>, files: bool, dirs: bool, timeout_ms: Option<u64>, walk_root_links: bool) -> PyResult<(Vec<PathBuf>, bool)> {
201
- let opts = FindOptions { walk, pattern, files, dirs, ..FindOptions::default() };
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 = WalkOptions { roots: vec![root], ..WalkOptions::default() };
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
- features = ["extension-module"]
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 = "src/**/*.rs" }, { file = "Cargo.toml" }, { file = ".git/fastws-cargo-key" }]
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
- roots,
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
- if let Some(m) = c.get(1) { res.push(text[m.start()..m.end()].to_string()); }
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" { res.tools = tool_names(&src); return res; }
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
- roots,
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.line_number(true).line_terminator(LineTerminator::crlf()).before_context(before_context).after_context(after_context).binary_detection(BinaryDetection::quit(0));
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
- pub(crate) enum SearchError { Message(String), InvalidUtf8 }
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
- pub(crate) fn spans_for(matcher: &RegexMatcher, bytes: &[u8]) -> Result<Vec<MatchSpan>, SearchError> {
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(crate) fn find_iter_with(opts: &FindOptions, walk_root_links: bool) -> Result<FindIter, RgApiError> {
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
- roots,
96
- base,
97
- walk,
98
- filters,
99
- ready,
100
- move |dent, base, filters, tx, cancel| {
101
- if panic_probe { panic!("rgapi: deliberate panic for tests (panic_probe)"); }
102
- match find_entry(dent, base, filters, pattern.as_ref(), files, dirs, special_files, max_depth) {
103
- Ok(Some(path)) => {
104
- if cancel.load(Ordering::Relaxed) || tx.send(Ok(path)).is_err() { return WalkState::Quit; }
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) && !seen.lock().unwrap().insert(d.path().to_path_buf()) { return WalkState::Continue; }
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(crate) fn resolve_roots(walk: &WalkOptions, canonical: bool, walk_root_links: bool) -> Result<(Vec<PathBuf>, PathBuf), RgApiError> {
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 { let r = std::path::absolute(root)?; r.symlink_metadata()?; r };
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 = 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) });
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 [("*.py", "src/deep/app.py", "src/app.rs"), ("src/*", "src/app.py", "src/deep/app.py"),
420
- ("src/**", "src/deep/app.py", "other/src/app.py"), ("tests", "src/tests", "src/tests/app.py")] {
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