rgapi 0.1.6__tar.gz → 0.1.8__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.
@@ -7,7 +7,19 @@ on:
7
7
  pull_request:
8
8
 
9
9
  jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+ - uses: dtolnay/rust-toolchain@stable
15
+ - uses: actions/setup-python@v6
16
+ with:
17
+ python-version: '3.12'
18
+ - run: pip install -e '.[dev]'
19
+ - run: pytest -q
20
+
10
21
  build:
22
+ needs: test
11
23
  strategy:
12
24
  matrix:
13
25
  os: [ubuntu-latest, macos-latest]
@@ -163,9 +163,9 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
163
163
 
164
164
  [[package]]
165
165
  name = "memmap2"
166
- version = "0.9.10"
166
+ version = "0.9.11"
167
167
  source = "registry+https://github.com/rust-lang/crates.io-index"
168
- checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
168
+ checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
169
169
  dependencies = [
170
170
  "libc",
171
171
  ]
@@ -250,9 +250,9 @@ dependencies = [
250
250
 
251
251
  [[package]]
252
252
  name = "quote"
253
- version = "1.0.45"
253
+ version = "1.0.46"
254
254
  source = "registry+https://github.com/rust-lang/crates.io-index"
255
- checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
255
+ checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
256
256
  dependencies = [
257
257
  "proc-macro2",
258
258
  ]
@@ -276,7 +276,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
276
276
 
277
277
  [[package]]
278
278
  name = "rgapi"
279
- version = "0.1.6"
279
+ version = "0.1.8"
280
280
  dependencies = [
281
281
  "globset",
282
282
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rgapi"
3
- version = "0.1.6"
3
+ version = "0.1.8"
4
4
  edition = "2021"
5
5
  license = "MIT OR Apache-2.0"
6
6
  description = "Python API for ripgrep-style file walking and searching"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rgapi
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Requires-Dist: fastship>=0.0.12 ; extra == 'dev'
@@ -57,7 +57,7 @@ pip install rgapi
57
57
 
58
58
  ## Semantics
59
59
 
60
- `fd` and `walk` return slash-separated paths relative to `root`. They use the `ignore` crate, so `.gitignore` and the usual ripgrep filters apply by default. Hidden files are skipped unless `hidden=True`. Pass `ignore=False` to disable ignore filtering. Symlinks are not followed unless `follow_links=True`; `same_file_system=True` avoids crossing filesystem boundaries. Traversal is parallel, and result order is not guaranteed; use `sorted(...)` if order matters.
60
+ `fd` and `walk` return slash-separated paths relative to `root`. They use the `ignore` crate, so `.gitignore`, `.ignore`, and the usual ripgrep filters apply by default. `.rgignore` files are also honored and take precedence over `.gitignore`. Hidden files are skipped unless `hidden=True`. Pass `ignore=False` to disable all ignore filtering (including `.rgignore`). Symlinks are not followed unless `follow_links=True`; `same_file_system=True` avoids crossing filesystem boundaries. Traversal is parallel, and result order is not guaranteed; use `sorted(...)` if order matters.
61
61
  `root` arguments accept `str` or `pathlib.Path` and expand `~`; `search_path` also accepts path-like file paths. Display labels such as `display_path` are stringified without expansion.
62
62
 
63
63
  `fd` adds fd-like filtering on top of `walk`: `pattern` is a substring match on the relative path, and `include`/`exclude` use glob syntax. `glob=` is accepted as an alias for `include=`. A basename glob such as `*.py` also matches recursively, so it finds `src/app.py`. Use `ext="py"` or `ext=["py", "rs"]` for extension filters, `min_depth=`/`max_depth=` to bound recursion, and `max_filesize=` to skip files above a byte limit.
@@ -38,7 +38,7 @@ pip install rgapi
38
38
 
39
39
  ## Semantics
40
40
 
41
- `fd` and `walk` return slash-separated paths relative to `root`. They use the `ignore` crate, so `.gitignore` and the usual ripgrep filters apply by default. Hidden files are skipped unless `hidden=True`. Pass `ignore=False` to disable ignore filtering. Symlinks are not followed unless `follow_links=True`; `same_file_system=True` avoids crossing filesystem boundaries. Traversal is parallel, and result order is not guaranteed; use `sorted(...)` if order matters.
41
+ `fd` and `walk` return slash-separated paths relative to `root`. They use the `ignore` crate, so `.gitignore`, `.ignore`, and the usual ripgrep filters apply by default. `.rgignore` files are also honored and take precedence over `.gitignore`. Hidden files are skipped unless `hidden=True`. Pass `ignore=False` to disable all ignore filtering (including `.rgignore`). Symlinks are not followed unless `follow_links=True`; `same_file_system=True` avoids crossing filesystem boundaries. Traversal is parallel, and result order is not guaranteed; use `sorted(...)` if order matters.
42
42
  `root` arguments accept `str` or `pathlib.Path` and expand `~`; `search_path` also accepts path-like file paths. Display labels such as `display_path` are stringified without expansion.
43
43
 
44
44
  `fd` adds fd-like filtering on top of `walk`: `pattern` is a substring match on the relative path, and `include`/`exclude` use glob syntax. `glob=` is accepted as an alias for `include=`. A basename glob such as `*.py` also matches recursively, so it finds `src/app.py`. Use `ext="py"` or `ext=["py", "rs"]` for extension filters, `min_depth=`/`max_depth=` to bound recursion, and `max_filesize=` to skip files above a byte limit.
@@ -23,6 +23,9 @@ Homepage = "https://github.com/AnswerDotAI/rgapi"
23
23
  Repository = "https://github.com/AnswerDotAI/rgapi"
24
24
  Issues = "https://github.com/AnswerDotAI/rgapi/issues"
25
25
 
26
+ [project.entry-points.pyskills]
27
+ rgapi = "rgapi.skill"
28
+
26
29
  [project.entry-points.fastaudit_safe_native]
27
30
  rgapi = "rgapi"
28
31
 
@@ -196,16 +196,4 @@ def search_path(
196
196
  return SearchResults(_core.search_path(matcher, _fs_path(path), _display_path(display_path), before_context, after_context))
197
197
 
198
198
 
199
- __all__ = [
200
- "Regex",
201
- "RgIter",
202
- "SearchLine",
203
- "SearchResults",
204
- "compile",
205
- "fd",
206
- "rg",
207
- "rg_iter",
208
- "search_path",
209
- "search_text",
210
- "walk",
211
- ]
199
+ __all__ = [ "RgIter", "fd", "rg", "rg_iter" ]
@@ -0,0 +1,24 @@
1
+ """Fast and flexible file discovery and search for Python. Use this when code needs `fd`-style file finding or `rg`-style searching.
2
+
3
+ rgapi wraps the same `ignore`, `grep-regex`, and `grep-searcher` crates ripgrep uses, so `.gitignore`/`.ignore`/`.rgignore`, hidden-file handling, glob/ext filters, and regex matching all behave like `rg`. Walking and searching run in parallel and most work stays in Rust, so results come back as structured Python objects instead of CLI text to parse. Prefer rgapi over shelling out to `rg`/`fd` or scanning files by hand: you get typed rows, byte-offset match spans, and lazy iteration.
4
+
5
+ Core APIs:
6
+ - `fd(root=".", ...)` finds paths with fd-style filters (`pattern` substring, `include`/`exclude`/`glob`, `ext`); returns slash-separated relative paths.
7
+ - `rg(pattern, root=".", ...)` searches and returns `SearchResults` (or `paths=True` for unique matched paths, `count=True` for a match-span total). NB: The `SearchResults` repr shows an rg-style multiline string, which is usually the most ergonomic approach.
8
+
9
+ SearchLine rows:
10
+ kind 'match', 'before', 'after', or 'context'
11
+ path path relative to root
12
+ line_number 1-based line number
13
+ line line text without the trailing newline
14
+ matches list of (start, end) byte offsets, for 'match' rows
15
+ asdict() returns the row fields as a plain dict
16
+
17
+ Important:
18
+ Traversal is parallel and result order is NOT guaranteed; wrap in `sorted(...)` if you need stable order. `path_re`/`skip_path_re` filter the returned/searched paths but do not prune traversal; use `skip_dir`/`skip_dir_re` to prune whole subtrees for speed. Run `doc(func)` for full parameter docments.
19
+ """
20
+
21
+ from . import Regex, RgIter, SearchLine, SearchResults, compile, fd, rg, rg_iter, search_path, search_text, walk
22
+
23
+ __all__ = ["Regex", "RgIter", "SearchLine", "SearchResults", "compile", "fd", "rg", "rg_iter", "search_path", "search_text", "walk"]
24
+
@@ -234,6 +234,7 @@ fn walk_py(
234
234
  same_file_system,
235
235
  files,
236
236
  dirs,
237
+ panic_probe: false,
237
238
  };
238
239
  find(&opts).map_err(|e| PyValueError::new_err(e.to_string()))
239
240
  }
@@ -277,6 +278,7 @@ fn find_py(
277
278
  same_file_system,
278
279
  files,
279
280
  dirs,
281
+ panic_probe: false,
280
282
  };
281
283
  find(&opts).map_err(|e| PyValueError::new_err(e.to_string()))
282
284
  }
@@ -365,6 +367,7 @@ fn rg_py(
365
367
  smart_case,
366
368
  before_context,
367
369
  after_context,
370
+ panic_probe: false,
368
371
  };
369
372
  let iter = rg_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
370
373
  collect_rg_py(py, iter)
@@ -412,12 +415,37 @@ fn rg_iter_py(
412
415
  smart_case,
413
416
  before_context,
414
417
  after_context,
418
+ panic_probe: false,
415
419
  };
416
420
  rg_iter_core(&opts)
417
421
  .map(|inner| RgIterPy { inner })
418
422
  .map_err(|e| PyValueError::new_err(e.to_string()))
419
423
  }
420
424
 
425
+ #[pyfunction(name = "panic_probe")]
426
+ #[pyo3(signature = (root=".", walk=false))]
427
+ fn panic_probe_py(py: Python<'_>, root: &str, walk: bool) -> PyResult<()> {
428
+ let root = PathBuf::from(root);
429
+ if walk {
430
+ let opts = FindOptions {
431
+ root,
432
+ panic_probe: true,
433
+ ..FindOptions::default()
434
+ };
435
+ find(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
436
+ } else {
437
+ let opts = RgOptions {
438
+ root,
439
+ pattern: "panic_probe".to_string(),
440
+ panic_probe: true,
441
+ ..RgOptions::default()
442
+ };
443
+ let iter = rg_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
444
+ collect_rg_py(py, iter)?;
445
+ }
446
+ Ok(())
447
+ }
448
+
421
449
  impl From<SearchLine> for SearchLinePy {
422
450
  fn from(line: SearchLine) -> Self {
423
451
  Self {
@@ -442,5 +470,6 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
442
470
  m.add_function(wrap_pyfunction!(rg_iter_py, m)?)?;
443
471
  m.add_function(wrap_pyfunction!(search_text_py, m)?)?;
444
472
  m.add_function(wrap_pyfunction!(search_path_py, m)?)?;
473
+ m.add_function(wrap_pyfunction!(panic_probe_py, m)?)?;
445
474
  Ok(())
446
475
  }
@@ -1,3 +1,4 @@
1
+ use std::panic::{catch_unwind, AssertUnwindSafe};
1
2
  use std::path::{Path, PathBuf};
2
3
  use std::sync::{
3
4
  atomic::{AtomicBool, Ordering},
@@ -69,6 +70,7 @@ pub struct RgOptions {
69
70
  pub smart_case: bool,
70
71
  pub before_context: usize,
71
72
  pub after_context: usize,
73
+ pub panic_probe: bool,
72
74
  }
73
75
 
74
76
  impl Default for RgOptions {
@@ -93,6 +95,7 @@ impl Default for RgOptions {
93
95
  smart_case: false,
94
96
  before_context: 0,
95
97
  after_context: 0,
98
+ panic_probe: false,
96
99
  }
97
100
  }
98
101
  }
@@ -179,6 +182,7 @@ fn run_parallel_search(
179
182
  filter_dirs(&mut walker, &root, filters.clone());
180
183
  let before_context = opts.before_context;
181
184
  let after_context = opts.after_context;
185
+ let panic_probe = opts.panic_probe;
182
186
  walker.build_parallel().run(|| {
183
187
  let tx = tx.clone();
184
188
  let root = root.clone();
@@ -188,16 +192,27 @@ fn run_parallel_search(
188
192
  let after_context = after_context;
189
193
  let cancel = cancel.clone();
190
194
  Box::new(move |entry| {
191
- search_entry(
192
- entry,
193
- &root,
194
- &filters,
195
- &matcher,
196
- before_context,
197
- after_context,
198
- &tx,
199
- &cancel,
200
- )
195
+ catch_unwind(AssertUnwindSafe(|| {
196
+ if panic_probe {
197
+ panic!("rgapi: deliberate panic for tests (panic_probe)");
198
+ }
199
+ search_entry(
200
+ entry,
201
+ &root,
202
+ &filters,
203
+ &matcher,
204
+ before_context,
205
+ after_context,
206
+ &tx,
207
+ &cancel,
208
+ )
209
+ }))
210
+ .unwrap_or_else(|_| {
211
+ let _ = tx.send(Err(RgApiError::new(
212
+ "internal error during search (this is a bug, please report it)",
213
+ )));
214
+ WalkState::Quit
215
+ })
201
216
  })
202
217
  });
203
218
  }
@@ -1,3 +1,4 @@
1
+ use std::panic::{catch_unwind, AssertUnwindSafe};
1
2
  use std::path::{Path, PathBuf};
2
3
  use std::sync::{mpsc, Arc};
3
4
 
@@ -27,6 +28,7 @@ pub struct FindOptions {
27
28
  pub same_file_system: bool,
28
29
  pub files: bool,
29
30
  pub dirs: bool,
31
+ pub panic_probe: bool,
30
32
  }
31
33
 
32
34
  impl Default for FindOptions {
@@ -49,6 +51,7 @@ impl Default for FindOptions {
49
51
  same_file_system: false,
50
52
  files: true,
51
53
  dirs: false,
54
+ panic_probe: false,
52
55
  }
53
56
  }
54
57
  }
@@ -79,13 +82,25 @@ pub fn find(opts: &FindOptions) -> Result<Vec<String>, RgApiError> {
79
82
  let pattern = opts.pattern.clone();
80
83
  let files = opts.files;
81
84
  let dirs = opts.dirs;
85
+ let panic_probe = opts.panic_probe;
82
86
  walker.build_parallel().run(|| {
83
87
  let tx = tx.clone();
84
88
  let root = root.clone();
85
89
  let filters = filters.clone();
86
90
  let pattern = pattern.clone();
87
91
  Box::new(move |entry| {
88
- match find_entry(entry, &root, &filters, pattern.as_deref(), files, dirs) {
92
+ let outcome = catch_unwind(AssertUnwindSafe(|| {
93
+ if panic_probe {
94
+ panic!("rgapi: deliberate panic for tests (panic_probe)");
95
+ }
96
+ find_entry(entry, &root, &filters, pattern.as_deref(), files, dirs)
97
+ }))
98
+ .unwrap_or_else(|_| {
99
+ Err(RgApiError::new(
100
+ "internal error during walk (this is a bug, please report it)",
101
+ ))
102
+ });
103
+ match outcome {
89
104
  Ok(Some(path)) => {
90
105
  if tx.send(Ok(path)).is_err() {
91
106
  return WalkState::Quit;
@@ -170,6 +185,9 @@ pub(crate) fn configure_walker(
170
185
  same_file_system: bool,
171
186
  ) {
172
187
  walker.standard_filters(ignore);
188
+ if ignore {
189
+ walker.add_custom_ignore_filename(".rgignore");
190
+ }
173
191
  walker.hidden(!hidden);
174
192
  walker.require_git(false);
175
193
  walker.max_depth(max_depth);
@@ -2,6 +2,7 @@ import _thread, threading
2
2
 
3
3
  import pytest
4
4
 
5
+ from rgapi import _core
5
6
  from rgapi import Regex, SearchResults, compile, fd, rg, rg_iter, search_path, search_text, walk
6
7
 
7
8
 
@@ -72,6 +73,20 @@ def test_path_filters_prune_dirs_and_follow_links(tmp_path):
72
73
  assert fd(str(tmp_path), path_re=r"linked/.*\.py$", follow_links=False) == []
73
74
  assert fd(str(tmp_path), path_re=r"linked/.*\.py$", follow_links=True) == ["linked/app.py"]
74
75
 
76
+ def test_rgignore_is_honored(tmp_path):
77
+ (tmp_path / ".rgignore").write_text("only_rg.txt\n")
78
+ (tmp_path / "only_rg.txt").write_text("hi\n")
79
+ (tmp_path / "keep.txt").write_text("hi\n")
80
+ assert set(fd(str(tmp_path))) == {"keep.txt"}
81
+ assert set(fd(str(tmp_path), ignore=False)) == {"keep.txt", "only_rg.txt"}
82
+
83
+ def test_rgignore_can_override_gitignore(tmp_path):
84
+ (tmp_path / ".gitignore").write_text("*/\n")
85
+ (tmp_path / ".rgignore").write_text("!*/\n")
86
+ (tmp_path / "sub").mkdir()
87
+ (tmp_path / "sub" / "app.py").write_text("hi\n")
88
+ assert "sub/app.py" in set(fd(str(tmp_path)))
89
+
75
90
  def test_depth_size_and_filesystem_options(tmp_path):
76
91
  (tmp_path / "top.txt").write_text("TODO\n")
77
92
  sub = tmp_path / "sub"
@@ -121,6 +136,15 @@ def test_rg_returns_structured_matches_context_and_relative_paths(tmp_path):
121
136
  assert str(stream) == repr(stream)
122
137
 
123
138
 
139
+ def test_worker_panic_surfaces_as_error_not_truncation(tmp_path):
140
+ # A panic inside a parallel search/walk worker must raise, not silently end the
141
+ # result stream (which would look like "no matches"). `_core.panic_probe` arms the
142
+ # panic flag and runs the real search/walk machinery so the catch_unwind path is exercised.
143
+ (tmp_path / "a.py").write_text("TODO here\n")
144
+ with pytest.raises(Exception): _core.panic_probe(str(tmp_path)) # search workers
145
+ with pytest.raises(Exception): _core.panic_probe(str(tmp_path), walk=True) # walk workers
146
+
147
+
124
148
  def test_search_path_skips_binary_and_invalid_utf8(tmp_path):
125
149
  (tmp_path / "bin.dat").write_bytes(b"TODO before\n\0TODO after\n")
126
150
  (tmp_path / "bad.txt").write_bytes(b"TODO\xff\n")
File without changes
File without changes
File without changes
File without changes