rgapi 0.1.1__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ['v*']
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+ - run: cargo test
15
+
16
+ linux:
17
+ needs: test
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+ - uses: PyO3/maturin-action@v1
22
+ with:
23
+ args: --release --out dist -i python3.10 -i python3.11 -i python3.12 -i python3.13
24
+ manylinux: auto
25
+ before-script-linux: |
26
+ python3.13 -m pip install 'fastship>=0.0.13'
27
+ python3.13 -m fastship.rs_prep --release
28
+ - uses: actions/upload-artifact@v4
29
+ with:
30
+ name: wheels-linux
31
+ path: dist
32
+
33
+ macos:
34
+ needs: test
35
+ runs-on: macos-latest
36
+ steps:
37
+ - uses: actions/checkout@v6
38
+ - uses: actions/setup-python@v6
39
+ with:
40
+ python-version: '3.13'
41
+ - run: python -m pip install 'fastship>=0.0.13'
42
+ - run: python -m fastship.rs_prep --release
43
+ - uses: PyO3/maturin-action@v1
44
+ with:
45
+ args: --release --out dist -i python3.10 -i python3.11 -i python3.12 -i python3.13
46
+ - uses: actions/upload-artifact@v4
47
+ with:
48
+ name: wheels-macos
49
+ path: dist
50
+
51
+ sdist:
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - uses: actions/checkout@v6
55
+ - uses: PyO3/maturin-action@v1
56
+ with:
57
+ command: sdist
58
+ args: -o dist
59
+ - uses: actions/upload-artifact@v4
60
+ with:
61
+ name: wheels-sdist
62
+ path: dist
63
+
64
+ publish:
65
+ if: startsWith(github.ref, 'refs/tags/v')
66
+ needs: [linux, macos, sdist]
67
+ runs-on: ubuntu-latest
68
+ permissions:
69
+ id-token: write
70
+ contents: write
71
+ steps:
72
+ - uses: actions/checkout@v6
73
+ - uses: actions/download-artifact@v4
74
+ with:
75
+ path: dist
76
+ merge-multiple: true
77
+ - uses: softprops/action-gh-release@v2
78
+ with:
79
+ files: dist/*
80
+ generate_release_notes: true
81
+ - uses: pypa/gh-action-pypi-publish@release/v1
82
+ with:
83
+ packages-dir: dist/
@@ -1,3 +1,4 @@
1
+ dist/
1
2
  Cargo.lock
2
3
  tags
3
4
  /target/
@@ -11,12 +11,6 @@ dependencies = [
11
11
  "memchr",
12
12
  ]
13
13
 
14
- [[package]]
15
- name = "autocfg"
16
- version = "1.5.1"
17
- source = "registry+https://github.com/rust-lang/crates.io-index"
18
- checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
19
-
20
14
  [[package]]
21
15
  name = "bstr"
22
16
  version = "1.12.1"
@@ -149,15 +143,6 @@ dependencies = [
149
143
  "winapi-util",
150
144
  ]
151
145
 
152
- [[package]]
153
- name = "indoc"
154
- version = "2.0.7"
155
- source = "registry+https://github.com/rust-lang/crates.io-index"
156
- checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
157
- dependencies = [
158
- "rustversion",
159
- ]
160
-
161
146
  [[package]]
162
147
  name = "libc"
163
148
  version = "0.2.186"
@@ -166,9 +151,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
166
151
 
167
152
  [[package]]
168
153
  name = "log"
169
- version = "0.4.32"
154
+ version = "0.4.33"
170
155
  source = "registry+https://github.com/rust-lang/crates.io-index"
171
- checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
156
+ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
172
157
 
173
158
  [[package]]
174
159
  name = "memchr"
@@ -185,15 +170,6 @@ dependencies = [
185
170
  "libc",
186
171
  ]
187
172
 
188
- [[package]]
189
- name = "memoffset"
190
- version = "0.9.1"
191
- source = "registry+https://github.com/rust-lang/crates.io-index"
192
- checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
193
- dependencies = [
194
- "autocfg",
195
- ]
196
-
197
173
  [[package]]
198
174
  name = "once_cell"
199
175
  version = "1.21.4"
@@ -217,37 +193,32 @@ dependencies = [
217
193
 
218
194
  [[package]]
219
195
  name = "pyo3"
220
- version = "0.23.5"
196
+ version = "0.29.0"
221
197
  source = "registry+https://github.com/rust-lang/crates.io-index"
222
- checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872"
198
+ checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
223
199
  dependencies = [
224
- "cfg-if",
225
- "indoc",
226
200
  "libc",
227
- "memoffset",
228
201
  "once_cell",
229
202
  "portable-atomic",
230
203
  "pyo3-build-config",
231
204
  "pyo3-ffi",
232
205
  "pyo3-macros",
233
- "unindent",
234
206
  ]
235
207
 
236
208
  [[package]]
237
209
  name = "pyo3-build-config"
238
- version = "0.23.5"
210
+ version = "0.29.0"
239
211
  source = "registry+https://github.com/rust-lang/crates.io-index"
240
- checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb"
212
+ checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
241
213
  dependencies = [
242
- "once_cell",
243
214
  "target-lexicon",
244
215
  ]
245
216
 
246
217
  [[package]]
247
218
  name = "pyo3-ffi"
248
- version = "0.23.5"
219
+ version = "0.29.0"
249
220
  source = "registry+https://github.com/rust-lang/crates.io-index"
250
- checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d"
221
+ checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
251
222
  dependencies = [
252
223
  "libc",
253
224
  "pyo3-build-config",
@@ -255,9 +226,9 @@ dependencies = [
255
226
 
256
227
  [[package]]
257
228
  name = "pyo3-macros"
258
- version = "0.23.5"
229
+ version = "0.29.0"
259
230
  source = "registry+https://github.com/rust-lang/crates.io-index"
260
- checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da"
231
+ checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
261
232
  dependencies = [
262
233
  "proc-macro2",
263
234
  "pyo3-macros-backend",
@@ -267,13 +238,12 @@ dependencies = [
267
238
 
268
239
  [[package]]
269
240
  name = "pyo3-macros-backend"
270
- version = "0.23.5"
241
+ version = "0.29.0"
271
242
  source = "registry+https://github.com/rust-lang/crates.io-index"
272
- checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028"
243
+ checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
273
244
  dependencies = [
274
245
  "heck",
275
246
  "proc-macro2",
276
- "pyo3-build-config",
277
247
  "quote",
278
248
  "syn",
279
249
  ]
@@ -306,7 +276,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
306
276
 
307
277
  [[package]]
308
278
  name = "rgapi"
309
- version = "0.1.1"
279
+ version = "0.1.4"
310
280
  dependencies = [
311
281
  "globset",
312
282
  "grep-matcher",
@@ -316,12 +286,6 @@ dependencies = [
316
286
  "pyo3",
317
287
  ]
318
288
 
319
- [[package]]
320
- name = "rustversion"
321
- version = "1.0.22"
322
- source = "registry+https://github.com/rust-lang/crates.io-index"
323
- checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
324
-
325
289
  [[package]]
326
290
  name = "same-file"
327
291
  version = "1.0.6"
@@ -373,9 +337,9 @@ dependencies = [
373
337
 
374
338
  [[package]]
375
339
  name = "target-lexicon"
376
- version = "0.12.16"
340
+ version = "0.13.5"
377
341
  source = "registry+https://github.com/rust-lang/crates.io-index"
378
- checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
342
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
379
343
 
380
344
  [[package]]
381
345
  name = "unicode-ident"
@@ -383,12 +347,6 @@ version = "1.0.24"
383
347
  source = "registry+https://github.com/rust-lang/crates.io-index"
384
348
  checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
385
349
 
386
- [[package]]
387
- name = "unindent"
388
- version = "0.2.4"
389
- source = "registry+https://github.com/rust-lang/crates.io-index"
390
- checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
391
-
392
350
  [[package]]
393
351
  name = "walkdir"
394
352
  version = "2.5.0"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rgapi"
3
- version = "0.1.1"
3
+ version = "0.1.4"
4
4
  edition = "2021"
5
5
  license = "MIT OR Apache-2.0"
6
6
  description = "Python API for ripgrep-style file walking and searching"
@@ -14,12 +14,12 @@ name = "rgapi"
14
14
  crate-type = ["cdylib", "rlib"]
15
15
 
16
16
  [dependencies]
17
- globset = "0.4.18"
18
- grep-matcher = "0.1.8"
19
- grep-regex = "0.1.14"
20
- grep-searcher = "0.1.16"
21
- ignore = "0.4.26"
22
- pyo3 = { version = "0.23", optional = true }
17
+ globset = ">=0.4.18"
18
+ grep-matcher = ">=0.1.8"
19
+ grep-regex = ">=0.1.14"
20
+ grep-searcher = ">=0.1.16"
21
+ ignore = ">=0.4.26"
22
+ pyo3 = { version = ">=0.28", optional = true }
23
23
 
24
24
  [features]
25
25
  extension-module = ["pyo3", "pyo3/extension-module"]
@@ -17,17 +17,23 @@ The public Python API lives in `python/rgapi/__init__.py`. The extension module
17
17
  ## Commands
18
18
 
19
19
  ```bash
20
- cargo fmt --check
21
- cargo check
22
- maturin develop
23
- pytest -q
20
+ ship-rs-test
24
21
  ```
25
22
 
26
- Run `cargo test` for Rust unit tests. Run `chkstyle` after Python edits once tests pass.
23
+ Run `cargo fmt --check` and `cargo check` for Rust-only edits. Run `chkstyle` after Python edits once tests pass.
27
24
 
28
25
  ## Release
29
26
 
30
- Release plumbing mirrors exhash. `tools/bump.sh` bumps the patch version in `pyproject.toml` and `Cargo.toml`; `tools/bump2.sh` bumps the minor version; `tools/release.sh` tags the current version and pushes `main` plus tags. The GitHub workflow builds wheels for Python 3.10-3.13 on Linux and macOS and publishes artifacts to GitHub Releases and PyPI when a `v*` tag is pushed.
27
+ The canonical version lives in `Cargo.toml`. `pyproject.toml` gets the Python package version from Cargo via `dynamic = ["version"]`.
28
+
29
+ Release flow is: release first, then bump.
30
+
31
+ 1. Run `ship-rs-test`.
32
+ 2. Confirm the release version in `Cargo.toml` (`[package].version`).
33
+ 3. Run `ship-rs-release`.
34
+ 4. After pushing the release tag, run `ship-rs-bump`, commit the `Cargo.toml` version bump, and push to `main` without a tag.
35
+
36
+ The GitHub workflow builds wheels for Python 3.10-3.13 on Linux and macOS and publishes artifacts to GitHub Releases and PyPI when a `v*` tag is pushed.
31
37
 
32
38
  ## Design notes
33
39
 
@@ -1,8 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rgapi
3
- Version: 0.1.1
3
+ Version: 0.1.4
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Requires-Dist: fastship>=0.0.12 ; extra == 'dev'
7
+ Requires-Dist: maturin>=1.0,<2.0 ; extra == 'dev'
8
+ Requires-Dist: pytest ; extra == 'dev'
9
+ Provides-Extra: dev
6
10
  Summary: Python API for ripgrep-style file walking and searching
7
11
  Home-Page: https://github.com/AnswerDotAI/rgapi
8
12
  Author-email: Jeremy Howard <j@fast.ai>
@@ -54,6 +58,7 @@ pip install rgapi
54
58
  ## Semantics
55
59
 
56
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.
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.
57
62
 
58
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.
59
64
 
@@ -73,12 +78,10 @@ matches list of (start, end) byte offsets for match rows
73
78
 
74
79
  `SearchLine` has a structured `repr`, an rg-style `str`, and `SearchLine.asdict()` returns row fields as a plain Python dict. `rg(..., paths=True)` returns unique matched paths, and `rg(..., count=True)` returns the total number of match spans. `paths` and `count` cannot both be set.
75
80
 
76
- `before_context`, `after_context`, and `context` match the shape of `rg -B`, `rg -A`, and `rg -C`. Files containing NUL bytes or invalid UTF-8 are skipped.
81
+ `before_context`, `after_context`, and `context` are like `rg -B`, `rg -A`, and `rg -C`. Files containing NUL bytes or invalid UTF-8 are skipped.
77
82
 
78
83
  Search is case-sensitive by default, matching `rg`. Use `smart_case=True` for `rg --smart-case` behavior, or `case_sensitive=False` to force case-insensitive matching.
79
84
 
80
- `rg` and `rg_iter` check Python signals while waiting for search results, so notebook interrupts and `KeyboardInterrupt` cancel traversal cooperatively. Cancellation is prompt between files and while results are streaming; a single huge file that produces no callbacks may continue until that file scan returns.
81
-
82
85
  ## Benchmarks
83
86
 
84
87
  `tools/bench.py` compares the `rg` CLI with in-process `rgapi`. Run it against a release build. One run on this machine, using best time from seven repeats:
@@ -39,6 +39,7 @@ pip install rgapi
39
39
  ## Semantics
40
40
 
41
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.
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.
42
43
 
43
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.
44
45
 
@@ -58,12 +59,10 @@ matches list of (start, end) byte offsets for match rows
58
59
 
59
60
  `SearchLine` has a structured `repr`, an rg-style `str`, and `SearchLine.asdict()` returns row fields as a plain Python dict. `rg(..., paths=True)` returns unique matched paths, and `rg(..., count=True)` returns the total number of match spans. `paths` and `count` cannot both be set.
60
61
 
61
- `before_context`, `after_context`, and `context` match the shape of `rg -B`, `rg -A`, and `rg -C`. Files containing NUL bytes or invalid UTF-8 are skipped.
62
+ `before_context`, `after_context`, and `context` are like `rg -B`, `rg -A`, and `rg -C`. Files containing NUL bytes or invalid UTF-8 are skipped.
62
63
 
63
64
  Search is case-sensitive by default, matching `rg`. Use `smart_case=True` for `rg --smart-case` behavior, or `case_sensitive=False` to force case-insensitive matching.
64
65
 
65
- `rg` and `rg_iter` check Python signals while waiting for search results, so notebook interrupts and `KeyboardInterrupt` cancel traversal cooperatively. Cancellation is prompt between files and while results are streaming; a single huge file that produces no callbacks may continue until that file scan returns.
66
-
67
66
  ## Benchmarks
68
67
 
69
68
  `tools/bench.py` compares the `rg` CLI with in-process `rgapi`. Run it against a release build. One run on this machine, using best time from seven repeats:
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "rgapi"
7
- version = "0.1.1"
7
+ dynamic = ["version"]
8
8
  description = "Python API for ripgrep-style file walking and searching"
9
9
  license = {text = "MIT OR Apache-2.0"}
10
10
  requires-python = ">=3.10"
@@ -15,6 +15,9 @@ classifiers = [
15
15
  "Programming Language :: Python :: Implementation :: CPython",
16
16
  ]
17
17
 
18
+ [project.optional-dependencies]
19
+ dev = ["fastship>=0.0.12", "maturin>=1.0,<2.0", "pytest"]
20
+
18
21
  [project.urls]
19
22
  Homepage = "https://github.com/AnswerDotAI/rgapi"
20
23
  Repository = "https://github.com/AnswerDotAI/rgapi"
@@ -24,3 +27,6 @@ Issues = "https://github.com/AnswerDotAI/rgapi/issues"
24
27
  features = ["extension-module"]
25
28
  python-source = "python"
26
29
  module-name = "rgapi._core"
30
+
31
+ [tool.fastship]
32
+ branch = "main"
@@ -0,0 +1,211 @@
1
+ from os import fspath
2
+ from pathlib import Path
3
+
4
+ from . import _core
5
+
6
+ Regex = _core.Regex
7
+ SearchLine = _core.SearchLine
8
+ RgIter = _core.RgIter
9
+ def compile(
10
+ pattern:str, # Regex pattern to compile
11
+ case_sensitive:bool|None=None, # True/False forces case; None allows `smart_case`
12
+ smart_case:bool=False # Match `rg --smart-case` behavior
13
+ ) -> Regex:
14
+ "Compile a regex matcher for `search_text`, `search_path`, and direct matching."
15
+ return _core.compile(pattern, case_sensitive=case_sensitive, smart_case=smart_case)
16
+
17
+ class SearchResults(list):
18
+ "List of `SearchLine` rows with rg-style text display."
19
+ def __str__(self): return "\n".join(map(str, self))
20
+ def _repr_pretty_(self, p, cycle): p.text("..." if cycle else str(self))
21
+
22
+
23
+ def _listify(value):
24
+ if value is None: return []
25
+ if isinstance(value, str): return [value]
26
+ return list(value)
27
+ def _fs_path(path): return str(Path(path).expanduser())
28
+ def _display_path(path): return None if path is None else fspath(path)
29
+ def _filters(glob=None, include=None, exclude=None, ext=None):
30
+ includes = _listify(include) + _listify(glob)
31
+ for suffix in _listify(ext):
32
+ suffix = str(suffix)
33
+ if suffix.startswith("."): suffix = suffix[1:]
34
+ includes.append(f"*.{suffix}")
35
+ return includes, _listify(exclude)
36
+
37
+
38
+ def _context(context, before_context, after_context):
39
+ if context: return context, context
40
+ return before_context, after_context
41
+
42
+
43
+ def walk(
44
+ root:str|Path=".", # Directory to walk (expands `~`)
45
+ hidden:bool=False, # Include hidden files and directories
46
+ ignore:bool=True, # Respect `.gitignore` and other ignore files
47
+ max_depth:int|None=None, # Maximum directory depth to descend
48
+ min_depth:int|None=None, # Minimum depth for returned paths
49
+ max_filesize:int|None=None, # Skip files larger than this many bytes
50
+ follow_links:bool=False, # Follow symbolic links while walking
51
+ same_file_system:bool=False, # Do not cross filesystem boundaries
52
+ path_re:str|None=None, # Regex that returned relative paths must match
53
+ skip_path_re:str|None=None, # Regex for relative paths to skip
54
+ skip_dir=None, # Directory glob or globs to prune
55
+ skip_dir_re:str|None=None, # Directory regex used to prune traversal
56
+ files:bool=True, # Include files in results
57
+ dirs:bool=False # Include directories in results
58
+ ) -> list[str]:
59
+ "Walk a directory and return relative file and/or directory paths."
60
+ return _core.walk(_fs_path(root), hidden, ignore, max_depth, min_depth, max_filesize, follow_links,
61
+ same_file_system, path_re, skip_path_re, _listify(skip_dir), skip_dir_re, files, dirs)
62
+
63
+
64
+ def fd(
65
+ root:str|Path=".", # Directory to walk (expands `~`)
66
+ pattern:str|None=None, # Substring that relative paths must contain
67
+ glob=None, # Include glob or globs; alias for `include`
68
+ include=None, # Include glob or globs, e.g. `*.py`
69
+ exclude=None, # Exclude glob or globs, e.g. `test_*.py`
70
+ ext=None, # Extension or extensions to include, without needing `*.`
71
+ hidden:bool=False, # Include hidden files and directories
72
+ ignore:bool=True, # Respect `.gitignore` and other ignore files
73
+ max_depth:int|None=None, # Maximum directory depth to descend
74
+ min_depth:int|None=None, # Minimum depth for returned paths
75
+ max_filesize:int|None=None, # Skip files larger than this many bytes
76
+ follow_links:bool=False, # Follow symbolic links while walking
77
+ same_file_system:bool=False, # Do not cross filesystem boundaries
78
+ path_re:str|None=None, # Regex that returned relative paths must match
79
+ skip_path_re:str|None=None, # Regex for relative paths to skip
80
+ skip_dir=None, # Directory glob or globs to prune
81
+ skip_dir_re:str|None=None, # Directory regex used to prune traversal
82
+ files:bool=True, # Include files in results
83
+ dirs:bool=False # Include directories in results
84
+ ) -> list[str]:
85
+ "Find paths with fd-style filters and gitignore support."
86
+ include, exclude = _filters(glob, include, exclude, ext)
87
+ return _core.find(_fs_path(root), pattern, include, exclude, hidden, ignore, max_depth, min_depth, max_filesize,
88
+ follow_links, same_file_system, path_re, skip_path_re, _listify(skip_dir), skip_dir_re, files, dirs)
89
+
90
+
91
+ def _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
92
+ follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
93
+ before_context, after_context, context):
94
+ include, exclude = _filters(glob, include, exclude, ext)
95
+ before_context, after_context = _context(context, before_context, after_context)
96
+ return (pattern, _fs_path(root), include, exclude, hidden, ignore, max_depth, min_depth, max_filesize, follow_links, same_file_system,
97
+ path_re, skip_path_re, _listify(skip_dir), skip_dir_re, case_sensitive, smart_case, before_context, after_context)
98
+
99
+
100
+ def rg(
101
+ pattern:str, # Regex pattern to search for
102
+ root:str|Path=".", # Directory to search (expands `~`)
103
+ glob=None, # Include glob or globs; alias for `include`
104
+ include=None, # Include glob or globs, e.g. `*.py`
105
+ exclude=None, # Exclude glob or globs, e.g. `test_*.py`
106
+ ext=None, # Extension or extensions to include, without needing `*.`
107
+ hidden:bool=False, # Include hidden files and directories
108
+ ignore:bool=True, # Respect `.gitignore` and other ignore files
109
+ max_depth:int|None=None, # Maximum directory depth to descend
110
+ min_depth:int|None=None, # Minimum depth for returned/searched files
111
+ max_filesize:int|None=None, # Skip files larger than this many bytes
112
+ follow_links:bool=False, # Follow symbolic links while walking
113
+ same_file_system:bool=False, # Do not cross filesystem boundaries
114
+ path_re:str|None=None, # Regex that searched relative paths must match
115
+ skip_path_re:str|None=None, # Regex for relative paths to skip
116
+ skip_dir=None, # Directory glob or globs to prune
117
+ skip_dir_re:str|None=None, # Directory regex used to prune traversal
118
+ case_sensitive:bool|None=None, # True/False forces case; None allows `smart_case`
119
+ smart_case:bool=False, # Match `rg --smart-case` behavior
120
+ before_context:int=0, # Lines of context before each match, like `rg -B`
121
+ after_context:int=0, # Lines of context after each match, like `rg -A`
122
+ context:int=0, # Sets both before and after context, like `rg -C`
123
+ paths:bool=False, # Return unique matched paths instead of rows
124
+ count:bool=False # Return total match span count instead of rows
125
+ ):
126
+ "Search files and return `SearchResults`, matched paths, or a count."
127
+ assert not (paths and count), "paths and count are mutually exclusive"
128
+ args = _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
129
+ follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
130
+ before_context, after_context, context)
131
+ if paths:
132
+ seen, res = set(), []
133
+ for row in _core.rg_iter(*args):
134
+ if row.kind != "match" or row.path in seen: continue
135
+ seen.add(row.path)
136
+ res.append(row.path)
137
+ return res
138
+ if count: return sum(len(row.matches) for row in _core.rg_iter(*args) if row.kind == "match")
139
+ return SearchResults(_core.rg(*args))
140
+
141
+
142
+ def rg_iter(
143
+ pattern:str, # Regex pattern to search for
144
+ root:str|Path=".", # Directory to search (expands `~`)
145
+ glob=None, # Include glob or globs; alias for `include`
146
+ include=None, # Include glob or globs, e.g. `*.py`
147
+ exclude=None, # Exclude glob or globs, e.g. `test_*.py`
148
+ ext=None, # Extension or extensions to include, without needing `*.`
149
+ hidden:bool=False, # Include hidden files and directories
150
+ ignore:bool=True, # Respect `.gitignore` and other ignore files
151
+ max_depth:int|None=None, # Maximum directory depth to descend
152
+ min_depth:int|None=None, # Minimum depth for returned/searched files
153
+ max_filesize:int|None=None, # Skip files larger than this many bytes
154
+ follow_links:bool=False, # Follow symbolic links while walking
155
+ same_file_system:bool=False, # Do not cross filesystem boundaries
156
+ path_re:str|None=None, # Regex that searched relative paths must match
157
+ skip_path_re:str|None=None, # Regex for relative paths to skip
158
+ skip_dir=None, # Directory glob or globs to prune
159
+ skip_dir_re:str|None=None, # Directory regex used to prune traversal
160
+ case_sensitive:bool|None=None, # True/False forces case; None allows `smart_case`
161
+ smart_case:bool=False, # Match `rg --smart-case` behavior
162
+ before_context:int=0, # Lines of context before each match, like `rg -B`
163
+ after_context:int=0, # Lines of context after each match, like `rg -A`
164
+ context:int=0 # Sets both before and after context, like `rg -C`
165
+ ) -> RgIter:
166
+ "Search files lazily, yielding `SearchLine` rows."
167
+ args = _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
168
+ follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
169
+ before_context, after_context, context)
170
+ return _core.rg_iter(*args)
171
+
172
+
173
+ def search_text(
174
+ matcher:Regex, # Compiled `Regex` from `compile()`
175
+ text:str, # Text to search
176
+ path:str|Path="<text>", # Path label stored in results
177
+ before_context:int=0, # Lines of context before each match
178
+ after_context:int=0, # Lines of context after each match
179
+ context:int=0 # Sets both before and after context, like `rg -C`
180
+ ) -> SearchResults:
181
+ "Search an in-memory string with a compiled matcher."
182
+ before_context, after_context = _context(context, before_context, after_context)
183
+ return SearchResults(_core.search_text(matcher, text, _display_path(path), before_context, after_context))
184
+
185
+
186
+ def search_path(
187
+ matcher:Regex, # Compiled `Regex` from `compile()`
188
+ path:str|Path, # File path to search (expands `~`)
189
+ display_path:str|Path|None=None, # Path stored in results; defaults to `path`
190
+ before_context:int=0, # Lines of context before each match
191
+ after_context:int=0, # Lines of context after each match
192
+ context:int=0 # Sets both before and after context, like `rg -C`
193
+ ) -> SearchResults:
194
+ "Search one file with a compiled matcher."
195
+ before_context, after_context = _context(context, before_context, after_context)
196
+ return SearchResults(_core.search_path(matcher, _fs_path(path), _display_path(display_path), before_context, after_context))
197
+
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
+ ]
@@ -13,7 +13,7 @@ use crate::{
13
13
  search_text as search_text_core, FindOptions, RgIter, RgOptions, SearchLine,
14
14
  };
15
15
 
16
- #[pyclass(name = "SearchLine", eq)]
16
+ #[pyclass(name = "SearchLine", eq, skip_from_py_object)]
17
17
  #[derive(Clone, PartialEq)]
18
18
  struct SearchLinePy {
19
19
  #[pyo3(get)]
@@ -105,7 +105,7 @@ fn check_signals_or_cancel(py: Python<'_>, iter: &RgIter) -> PyResult<()> {
105
105
  fn next_rg_line_py(py: Python<'_>, iter: &mut RgIter) -> PyResult<Option<SearchLinePy>> {
106
106
  loop {
107
107
  check_signals_or_cancel(py, iter)?;
108
- let res = py.allow_threads(|| iter.next_timeout(Duration::from_millis(50)));
108
+ let res = py.detach(|| iter.next_timeout(Duration::from_millis(50)));
109
109
  check_signals_or_cancel(py, iter)?;
110
110
  match res {
111
111
  Ok(Ok(line)) => return Ok(Some(SearchLinePy::from(line))),
@@ -123,7 +123,7 @@ fn collect_rg_py(py: Python<'_>, mut iter: RgIter) -> PyResult<Vec<SearchLinePy>
123
123
  }
124
124
  Ok(res)
125
125
  }
126
- #[pyclass(name = "Regex")]
126
+ #[pyclass(name = "Regex", skip_from_py_object)]
127
127
  #[derive(Clone)]
128
128
  struct RegexPy {
129
129
  #[pyo3(get)]
@@ -35,6 +35,21 @@ def test_fd_is_relative_and_respects_ignore_hidden_and_globs(tmp_path):
35
35
  assert set(fd(str(tmp_path), exclude="*.py")) == {"bad.txt", "bin.dat"}
36
36
  assert set(walk(str(tmp_path), files=True, dirs=False)) == found
37
37
 
38
+ def test_pathlike_arguments_and_expanduser(tmp_path, monkeypatch):
39
+ make_tree(tmp_path)
40
+ assert "src/app.py" in fd(tmp_path)
41
+ assert walk(tmp_path, path_re=r"\.py$") == ["src/app.py"]
42
+ assert [r.path for r in rg("TODO", tmp_path, include="*.py")] == ["src/app.py"]
43
+ assert list(rg_iter("TODO", tmp_path, include="*.py")) == rg("TODO", tmp_path, include="*.py")
44
+ matcher = compile("TODO")
45
+ text_label = tmp_path / "memory.txt"
46
+ assert search_text(matcher, "TODO\n", path=text_label)[0].path == str(text_label)
47
+ display = tmp_path / "display.py"
48
+ assert search_path(matcher, tmp_path / "src" / "app.py", display_path=display)[0].path == str(display)
49
+
50
+ monkeypatch.setenv("HOME", str(tmp_path))
51
+ assert fd("~", glob="*.py") == ["src/app.py"]
52
+
38
53
 
39
54
  def test_path_filters_prune_dirs_and_follow_links(tmp_path):
40
55
  (tmp_path / "src").mkdir()
@@ -1,62 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- tags: ['v*']
7
- pull_request:
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v6
14
- - run: cargo test
15
-
16
- build:
17
- needs: test
18
- strategy:
19
- matrix:
20
- os: [ubuntu-latest, macos-latest]
21
- python: ['3.10', '3.11', '3.12', '3.13']
22
- runs-on: ${{ matrix.os }}
23
- steps:
24
- - uses: actions/checkout@v6
25
- - uses: dtolnay/rust-toolchain@stable
26
- - uses: actions/setup-python@v5
27
- with:
28
- python-version: ${{ matrix.python }}
29
- - run: tools/build.sh release
30
- - uses: PyO3/maturin-action@v1
31
- with:
32
- command: build
33
- args: --release -o dist
34
- - uses: actions/upload-artifact@v4
35
- with:
36
- name: wheel-${{ matrix.os }}-py${{ matrix.python }}
37
- path: dist/*.whl
38
-
39
- publish:
40
- if: startsWith(github.ref, 'refs/tags/v')
41
- needs: build
42
- runs-on: ubuntu-latest
43
- permissions:
44
- id-token: write
45
- contents: write
46
- steps:
47
- - uses: actions/checkout@v6
48
- - uses: PyO3/maturin-action@v1
49
- with:
50
- command: sdist
51
- args: -o dist
52
- - uses: actions/download-artifact@v4
53
- with:
54
- path: dist
55
- merge-multiple: true
56
- - uses: softprops/action-gh-release@v2
57
- with:
58
- files: dist/*
59
- generate_release_notes: true
60
- - uses: pypa/gh-action-pypi-publish@release/v1
61
- with:
62
- packages-dir: dist/
@@ -1,208 +0,0 @@
1
- from . import _core
2
-
3
- Regex = _core.Regex
4
- SearchLine = _core.SearchLine
5
- RgIter = _core.RgIter
6
- def compile(
7
- pattern, # Regex pattern to compile
8
- case_sensitive=None, # True/False forces case; None allows `smart_case`
9
- smart_case=False # Match `rg --smart-case` behavior
10
- ):
11
- "Compile a regex matcher for `search_text`, `search_path`, and direct matching."
12
- return _core.compile(pattern, case_sensitive=case_sensitive, smart_case=smart_case)
13
-
14
- class SearchResults(list):
15
- "List of `SearchLine` rows with rg-style text display."
16
- def __str__(self): return "\n".join(map(str, self))
17
- def _repr_pretty_(self, p, cycle): p.text("..." if cycle else str(self))
18
-
19
-
20
- def _listify(value):
21
- if value is None: return []
22
- if isinstance(value, str): return [value]
23
- return list(value)
24
-
25
-
26
- def _filters(glob=None, include=None, exclude=None, ext=None):
27
- includes = _listify(include) + _listify(glob)
28
- for suffix in _listify(ext):
29
- suffix = str(suffix)
30
- if suffix.startswith("."): suffix = suffix[1:]
31
- includes.append(f"*.{suffix}")
32
- return includes, _listify(exclude)
33
-
34
-
35
- def _context(context, before_context, after_context):
36
- if context: return context, context
37
- return before_context, after_context
38
-
39
-
40
- def walk(
41
- root=".", # Directory to walk
42
- hidden=False, # Include hidden files and directories
43
- ignore=True, # Respect `.gitignore` and other ignore files
44
- max_depth=None, # Maximum directory depth to descend
45
- min_depth=None, # Minimum depth for returned paths
46
- max_filesize=None, # Skip files larger than this many bytes
47
- follow_links=False, # Follow symbolic links while walking
48
- same_file_system=False, # Do not cross filesystem boundaries
49
- path_re=None, # Regex that returned relative paths must match
50
- skip_path_re=None, # Regex for relative paths to skip
51
- skip_dir=None, # Directory glob or globs to prune
52
- skip_dir_re=None, # Directory regex used to prune traversal
53
- files=True, # Include files in results
54
- dirs=False # Include directories in results
55
- ):
56
- "Walk a directory and return relative file and/or directory paths."
57
- return _core.walk(root, hidden, ignore, max_depth, min_depth, max_filesize, follow_links,
58
- same_file_system, path_re, skip_path_re, _listify(skip_dir), skip_dir_re, files, dirs)
59
-
60
-
61
- def fd(
62
- root=".", # Directory to walk
63
- pattern=None, # Substring that relative paths must contain
64
- glob=None, # Include glob or globs; alias for `include`
65
- include=None, # Include glob or globs, e.g. `*.py`
66
- exclude=None, # Exclude glob or globs, e.g. `test_*.py`
67
- ext=None, # Extension or extensions to include, without needing `*.`
68
- hidden=False, # Include hidden files and directories
69
- ignore=True, # Respect `.gitignore` and other ignore files
70
- max_depth=None, # Maximum directory depth to descend
71
- min_depth=None, # Minimum depth for returned paths
72
- max_filesize=None, # Skip files larger than this many bytes
73
- follow_links=False, # Follow symbolic links while walking
74
- same_file_system=False, # Do not cross filesystem boundaries
75
- path_re=None, # Regex that returned relative paths must match
76
- skip_path_re=None, # Regex for relative paths to skip
77
- skip_dir=None, # Directory glob or globs to prune
78
- skip_dir_re=None, # Directory regex used to prune traversal
79
- files=True, # Include files in results
80
- dirs=False # Include directories in results
81
- ):
82
- "Find paths with fd-style filters and gitignore support."
83
- include, exclude = _filters(glob, include, exclude, ext)
84
- return _core.find(root, pattern, include, exclude, hidden, ignore, max_depth, min_depth, max_filesize,
85
- follow_links, same_file_system, path_re, skip_path_re, _listify(skip_dir), skip_dir_re, files, dirs)
86
-
87
-
88
- def _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
89
- follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
90
- before_context, after_context, context):
91
- include, exclude = _filters(glob, include, exclude, ext)
92
- before_context, after_context = _context(context, before_context, after_context)
93
- return (pattern, root, include, exclude, hidden, ignore, max_depth, min_depth, max_filesize, follow_links, same_file_system,
94
- path_re, skip_path_re, _listify(skip_dir), skip_dir_re, case_sensitive, smart_case, before_context, after_context)
95
-
96
-
97
- def rg(
98
- pattern, # Regex pattern to search for
99
- root=".", # Directory to search
100
- glob=None, # Include glob or globs; alias for `include`
101
- include=None, # Include glob or globs, e.g. `*.py`
102
- exclude=None, # Exclude glob or globs, e.g. `test_*.py`
103
- ext=None, # Extension or extensions to include, without needing `*.`
104
- hidden=False, # Include hidden files and directories
105
- ignore=True, # Respect `.gitignore` and other ignore files
106
- max_depth=None, # Maximum directory depth to descend
107
- min_depth=None, # Minimum depth for returned/searched files
108
- max_filesize=None, # Skip files larger than this many bytes
109
- follow_links=False, # Follow symbolic links while walking
110
- same_file_system=False, # Do not cross filesystem boundaries
111
- path_re=None, # Regex that searched relative paths must match
112
- skip_path_re=None, # Regex for relative paths to skip
113
- skip_dir=None, # Directory glob or globs to prune
114
- skip_dir_re=None, # Directory regex used to prune traversal
115
- case_sensitive=None, # True/False forces case; None allows `smart_case`
116
- smart_case=False, # Match `rg --smart-case` behavior
117
- before_context=0, # Lines of context before each match, like `rg -B`
118
- after_context=0, # Lines of context after each match, like `rg -A`
119
- context=0, # Sets both before and after context, like `rg -C`
120
- paths=False, # Return unique matched paths instead of rows
121
- count=False # Return total match span count instead of rows
122
- ):
123
- "Search files and return `SearchResults`, matched paths, or a count."
124
- assert not (paths and count), "paths and count are mutually exclusive"
125
- args = _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
126
- follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
127
- before_context, after_context, context)
128
- if paths:
129
- seen, res = set(), []
130
- for row in _core.rg_iter(*args):
131
- if row.kind != "match" or row.path in seen: continue
132
- seen.add(row.path)
133
- res.append(row.path)
134
- return res
135
- if count: return sum(len(row.matches) for row in _core.rg_iter(*args) if row.kind == "match")
136
- return SearchResults(_core.rg(*args))
137
-
138
-
139
- def rg_iter(
140
- pattern, # Regex pattern to search for
141
- root=".", # Directory to search
142
- glob=None, # Include glob or globs; alias for `include`
143
- include=None, # Include glob or globs, e.g. `*.py`
144
- exclude=None, # Exclude glob or globs, e.g. `test_*.py`
145
- ext=None, # Extension or extensions to include, without needing `*.`
146
- hidden=False, # Include hidden files and directories
147
- ignore=True, # Respect `.gitignore` and other ignore files
148
- max_depth=None, # Maximum directory depth to descend
149
- min_depth=None, # Minimum depth for returned/searched files
150
- max_filesize=None, # Skip files larger than this many bytes
151
- follow_links=False, # Follow symbolic links while walking
152
- same_file_system=False, # Do not cross filesystem boundaries
153
- path_re=None, # Regex that searched relative paths must match
154
- skip_path_re=None, # Regex for relative paths to skip
155
- skip_dir=None, # Directory glob or globs to prune
156
- skip_dir_re=None, # Directory regex used to prune traversal
157
- case_sensitive=None, # True/False forces case; None allows `smart_case`
158
- smart_case=False, # Match `rg --smart-case` behavior
159
- before_context=0, # Lines of context before each match, like `rg -B`
160
- after_context=0, # Lines of context after each match, like `rg -A`
161
- context=0 # Sets both before and after context, like `rg -C`
162
- ):
163
- "Search files lazily, yielding `SearchLine` rows."
164
- args = _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
165
- follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
166
- before_context, after_context, context)
167
- return _core.rg_iter(*args)
168
-
169
-
170
- def search_text(
171
- matcher, # Compiled `Regex` from `compile()`
172
- text, # Text to search
173
- path="<text>", # Path label stored in results
174
- before_context=0, # Lines of context before each match
175
- after_context=0, # Lines of context after each match
176
- context=0 # Sets both before and after context, like `rg -C`
177
- ):
178
- "Search an in-memory string with a compiled matcher."
179
- before_context, after_context = _context(context, before_context, after_context)
180
- return SearchResults(_core.search_text(matcher, text, path, before_context, after_context))
181
-
182
-
183
- def search_path(
184
- matcher, # Compiled `Regex` from `compile()`
185
- path, # File path to search
186
- display_path=None, # Path stored in results; defaults to `path`
187
- before_context=0, # Lines of context before each match
188
- after_context=0, # Lines of context after each match
189
- context=0 # Sets both before and after context, like `rg -C`
190
- ):
191
- "Search one file with a compiled matcher."
192
- before_context, after_context = _context(context, before_context, after_context)
193
- return SearchResults(_core.search_path(matcher, path, display_path, before_context, after_context))
194
-
195
-
196
- __all__ = [
197
- "Regex",
198
- "RgIter",
199
- "SearchLine",
200
- "SearchResults",
201
- "compile",
202
- "fd",
203
- "rg",
204
- "rg_iter",
205
- "search_path",
206
- "search_text",
207
- "walk",
208
- ]
@@ -1,5 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
- profile=${1:-debug}
4
- if [ "$profile" = "release" ]; then flags="--release"; else flags=""; fi
5
- cargo build $flags
rgapi-0.1.1/tools/bump.sh DELETED
@@ -1,6 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
- cur=$(grep '^version = ' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
4
- new=$(echo "$cur" | awk -F. '{print $1"."$2"."$3+1}')
5
- sed -i '' "s/^version = \"$cur\"/version = \"$new\"/" pyproject.toml Cargo.toml
6
- echo "$cur -> $new"
@@ -1,6 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
- cur=$(grep '^version = ' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
4
- new=$(echo "$cur" | awk -F. '{print $1"."$2+1"."0}')
5
- sed -i '' "s/^version = \"$cur\"/version = \"$new\"/" pyproject.toml Cargo.toml
6
- echo "$cur -> $new"
@@ -1,6 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
- v=$(grep '^version = ' pyproject.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
4
- git tag "v$v"
5
- git push origin main --tags
6
- echo "Released v$v"
rgapi-0.1.1/tools/test.sh DELETED
@@ -1,4 +0,0 @@
1
- #!/bin/bash
2
- set -e
3
- cargo test
4
- pytest -q
File without changes
File without changes
File without changes
File without changes