rgapi 0.1.26__tar.gz → 0.1.27__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rgapi-0.1.26 → rgapi-0.1.27}/Cargo.lock +9 -9
- {rgapi-0.1.26 → rgapi-0.1.27}/Cargo.toml +1 -1
- {rgapi-0.1.26 → rgapi-0.1.27}/DEV.md +3 -3
- {rgapi-0.1.26 → rgapi-0.1.27}/PKG-INFO +20 -8
- {rgapi-0.1.26 → rgapi-0.1.27}/README.md +19 -7
- {rgapi-0.1.26 → rgapi-0.1.27}/pyproject.toml +3 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/python/rgapi/__init__.py +50 -66
- {rgapi-0.1.26 → rgapi-0.1.27}/python/rgapi/nb.py +1 -1
- {rgapi-0.1.26 → rgapi-0.1.27}/python/rgapi/skill.py +4 -2
- {rgapi-0.1.26 → rgapi-0.1.27}/src/block.rs +1 -1
- {rgapi-0.1.26 → rgapi-0.1.27}/src/nb.rs +1 -1
- {rgapi-0.1.26 → rgapi-0.1.27}/src/python.rs +11 -30
- {rgapi-0.1.26 → rgapi-0.1.27}/src/search.rs +1 -1
- {rgapi-0.1.26 → rgapi-0.1.27}/src/walk.rs +53 -29
- {rgapi-0.1.26 → rgapi-0.1.27}/tests/test_async.py +5 -4
- {rgapi-0.1.26 → rgapi-0.1.27}/tests/test_rgapi.py +105 -79
- {rgapi-0.1.26 → rgapi-0.1.27}/.github/workflows/ci.yml +0 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/.gitignore +0 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/LICENSE +0 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/_config.yml +0 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/_layouts/default.html +0 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/python/rgapi/_cli.py +0 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/python/rgapi/block.py +0 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/rustfmt.toml +0 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/src/lib.rs +0 -0
- {rgapi-0.1.26 → rgapi-0.1.27}/tools/bench.py +0 -0
|
@@ -24,9 +24,9 @@ dependencies = [
|
|
|
24
24
|
|
|
25
25
|
[[package]]
|
|
26
26
|
name = "cfg-if"
|
|
27
|
-
version = "1.0.
|
|
27
|
+
version = "1.0.5"
|
|
28
28
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
29
|
-
checksum = "
|
|
29
|
+
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
|
30
30
|
|
|
31
31
|
[[package]]
|
|
32
32
|
name = "core_detect"
|
|
@@ -215,7 +215,7 @@ dependencies = [
|
|
|
215
215
|
"proc-macro2",
|
|
216
216
|
"quote",
|
|
217
217
|
"rustversion",
|
|
218
|
-
"syn 3.0.
|
|
218
|
+
"syn 3.0.6",
|
|
219
219
|
]
|
|
220
220
|
|
|
221
221
|
[[package]]
|
|
@@ -330,7 +330,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|
|
330
330
|
|
|
331
331
|
[[package]]
|
|
332
332
|
name = "rgapi"
|
|
333
|
-
version = "0.1.
|
|
333
|
+
version = "0.1.27"
|
|
334
334
|
dependencies = [
|
|
335
335
|
"crc32fast",
|
|
336
336
|
"globset",
|
|
@@ -391,7 +391,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|
|
391
391
|
dependencies = [
|
|
392
392
|
"proc-macro2",
|
|
393
393
|
"quote",
|
|
394
|
-
"syn 3.0.
|
|
394
|
+
"syn 3.0.6",
|
|
395
395
|
]
|
|
396
396
|
|
|
397
397
|
[[package]]
|
|
@@ -426,9 +426,9 @@ dependencies = [
|
|
|
426
426
|
|
|
427
427
|
[[package]]
|
|
428
428
|
name = "syn"
|
|
429
|
-
version = "3.0.
|
|
429
|
+
version = "3.0.6"
|
|
430
430
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
431
|
-
checksum = "
|
|
431
|
+
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
|
432
432
|
dependencies = [
|
|
433
433
|
"proc-macro2",
|
|
434
434
|
"quote",
|
|
@@ -443,9 +443,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
|
443
443
|
|
|
444
444
|
[[package]]
|
|
445
445
|
name = "unicode-ident"
|
|
446
|
-
version = "1.0.
|
|
446
|
+
version = "1.0.25"
|
|
447
447
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
448
|
-
checksum = "
|
|
448
|
+
checksum = "ab72a15cf68d77cb0987d3684aa8a45c5ef827e8cb49ee2f30bfd7ba2feb519f"
|
|
449
449
|
|
|
450
450
|
[[package]]
|
|
451
451
|
name = "walkdir"
|
|
@@ -38,15 +38,15 @@ The GitHub workflow builds wheels for Python 3.10-3.13 on Linux and macOS and pu
|
|
|
38
38
|
|
|
39
39
|
## Design notes
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Python discovery and `paths=True` results contain absolute `pathlib.Path` objects. Structured search rows retain root-relative string labels with `/` separators. Traversal uses `ignore::WalkParallel`, so result order is not part of the API contract. Search results are structured rows; collected result lists use rg-style `str()` and notebook display. `SearchLine.lnhash` is computed with the same CRC-32-based line-content hash format as exhash (`lineno|hash|`, low 16 bits of CRC-32 over the line's UTF-8 bytes); `lnhashs=True` only changes row display, not `line_number` or matching behavior. Path regexes filter returned/searched paths; `skip_dir` and `skip_dir_re` prune traversal through `ignore::WalkBuilder::filter_entry`. Depth, size, filesystem, hidden, and ignore options use `ignore::WalkBuilder` settings. Discovery checks root links with `symlink_metadata` and returns an unfollowed link through a ready stream without a worker. This also supports dangling roots, which the underlying walker would reject. Other discovery roots use absolute paths without canonicalizing. Content searches retain canonical root resolution. `rg_iter` exposes the same parallel search stream that `rg` collects by default; `paths=True` and `count=True` consume that stream with different reducers. Text search skips binary files and invalid UTF-8 content.
|
|
42
42
|
|
|
43
|
-
Streaming engine: `walk.rs` owns the generic machinery. `StreamIter<T>` is the worker-thread-plus-bounded-channel iterator (`sync_channel(8192)`, so producers block rather than buffer without limit when a consumer lags), and `spawn_walk` owns the shared scaffold: walker config, panic catching, cancel flag, and worker thread. `rg_iter` (`T = SearchLine`), `block_iter` (`T = SearchBlock`), `nb_iter` (`T = NbCell`), and `find_iter` (`T =
|
|
43
|
+
Streaming engine: `walk.rs` owns the generic machinery. `StreamIter<T>` is the worker-thread-plus-bounded-channel iterator (`sync_channel(8192)`, so producers block rather than buffer without limit when a consumer lags), and `spawn_walk` owns the shared scaffold: walker config, panic catching, cancel flag, and worker thread. `rg_iter` (`T = SearchLine`), `block_iter` (`T = SearchBlock`), `nb_iter` (`T = NbCell`), and `find_iter` (`T = PathBuf`, the path walk) plug entry closures into that engine. Block search reads each file once, searches it once, groups nonblank lines into blocks, maps matching lines to their blocks, and expands context by block index. Each `SearchBlock` carries numeric boundaries plus hashes for its first and last source lines. Python keeps both and chooses the displayed address without another file read.
|
|
44
44
|
|
|
45
45
|
Async API: `fda`, `fda_iter`, `rga`, `rga_iter`, `nbrga`, and `nbrga_iter` wrap the corresponding private core operations. `rga(summary=True)` uses `_core.block_search_async`; ordinary `rga` uses `_core.rg_async`. Each collected core function takes a Python callback, runs on Rust threads through the generic `stream_async` helper, and delivers with one GIL attach at the end. Iterator forms use `stream_iter_async` and attach once per batch. The Python side settles an `asyncio.Future` or feeds an `asyncio.Queue` via `loop.call_soon_threadsafe`; no Python thread blocks and `asyncio.to_thread` is not involved. `AsyncHandle.cancel()` sets the same atomic flag used by the Rust iterators.
|
|
46
46
|
|
|
47
47
|
Truncation is recorded on collected results: `max_results` sets `stop_reason="max_results"`, and `timeout_ms` on `rg`/`rga`/`nbrg`/`nbrga`/`fd`/`fda`/`walk`/`ls` sets `stop_reason="timeout"`. `SearchResults`, `BlockResults`, `PathResults`, and `NbResults` share this through `_Results`; `complete` means `stop_reason is None`. In block summary mode, `max_results` counts matching blocks and keeps their block context. `count=True` returns a plain int, so it rejects timeouts and block summary mode.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Rust discovery returns root-relative `PathBuf` values. Glob filters operate on native paths; only regex matching uses string labels. PyO3 converts discovery roots and results with its filesystem-path support. Python joins each result to its absolute base without resolving links. `PathResults` stores that display base and completion status, including across slices. Its `__repr__` uses `Path.lstat()` for an `ls -l`-style listing capped at `MAX_REPR` rows. `str()` returns one root-relative name per line. `ls` is `fd` with shell-style defaults (one level, dirs, ignore rules off), sorted in place.
|
|
50
50
|
|
|
51
51
|
Rust callers can consume a `StreamIter` with `cancel_and_join()` to cancel, drain queued results, and wait for the walk's workers to finish. `Drop` remains nonblocking. Since filesystem calls already in progress must return before joining completes, keep `cancel_and_join()` off async executors.
|
|
52
52
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rgapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.27
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Requires-Dist: fastcore>=1.14.6
|
|
@@ -77,17 +77,17 @@ pip install rgapi
|
|
|
77
77
|
|
|
78
78
|
## File discovery
|
|
79
79
|
|
|
80
|
-
`fd` and `walk` return
|
|
80
|
+
`fd` and `walk` return absolute `pathlib.Path` objects. Use them directly with `.read_text()`, `.open()`, `.stat()`, or other filesystem operations. Their collected results display names relative to `root`. Pass `root` as a `str` or `Path`. The sync and async APIs expand `~` and accept `.`, `./`, and paths containing `..`.
|
|
81
81
|
|
|
82
82
|
Discovery uses the `ignore` crate with ripgrep's default filters. It reads `.gitignore`, `.ignore`, and `.rgignore` files. `.rgignore` takes precedence over `.gitignore`. Pass `ignore=False` to disable all ignore-file filtering, including `.rgignore`.
|
|
83
83
|
|
|
84
|
-
Hidden files are skipped unless `hidden=True`.
|
|
84
|
+
Hidden files are skipped unless `hidden=True`. Discovery returns symlinks themselves, including explicitly named roots and dangling links. Set `follow_links=True` to traverse their targets. Paths retain the symlink spelling. Use `same_file_system=True` to avoid crossing filesystem boundaries.
|
|
85
85
|
|
|
86
86
|
Traversal runs in parallel without guaranteed result order. Use `sorted(...)` when order matters.
|
|
87
87
|
|
|
88
88
|
`fd` adds filename filters to `walk`. Its `pattern` is a smart-case regex matched against each basename. Lowercase patterns match case-insensitively. A pattern containing uppercase letters is case-sensitive. Use `path_re` to match the slash-separated relative path instead.
|
|
89
89
|
|
|
90
|
-
`include` and `exclude` use glob syntax. `glob=` is an alias for `include=`.
|
|
90
|
+
`include` and `exclude` use case-sensitive glob syntax. `glob=` is an alias for `include=`. Patterns without `/` match names at any depth, so `*.py` matches `src/app.py`. Patterns containing `/` are root-relative. `*` stays within a path component; `**` spans directories: `src/*` matches immediate children, whereas `src/**` also matches deeper descendants. Matching a directory with `exclude` prunes its entire subtree. Includes never prune traversal. Excludes always win, and includes do not override ignore rules.
|
|
91
91
|
|
|
92
92
|
Filter extensions with `ext="py"` or `ext=["py", "rs"]`. Extension and glob filters must both match. For example, `include="src/*", ext="py"` requires `src/*` and `*.py`, like combining `rg -g` with `-t`.
|
|
93
93
|
|
|
@@ -95,7 +95,7 @@ Set `min_depth` and `max_depth` to bound recursion. `max_filesize` skips files a
|
|
|
95
95
|
|
|
96
96
|
`ls` follows the shell command's listing conventions. It uses `fd` with `max_depth=1`, includes directories, disables ignore rules, and sorts by name. Set `hidden=True` for `ls -a` behaviour. All `fd` filters remain available.
|
|
97
97
|
|
|
98
|
-
`fd_iter` yields `
|
|
98
|
+
`fd_iter` yields absolute `Path` objects as the walk finds them. It accepts every `fd` filter. Stopping iteration ends the walk. It does not accept `timeout_ms`.
|
|
99
99
|
|
|
100
100
|
`path_re` and `skip_path_re` filter slash-separated relative paths using regexes. They select returned paths or searched files without changing traversal. To skip entire subtrees, use `skip_dir` with a glob or `skip_dir_re` with a regex.
|
|
101
101
|
|
|
@@ -126,11 +126,23 @@ For other result forms, use `rg(..., paths=True)` to return unique matched paths
|
|
|
126
126
|
|
|
127
127
|
### Path results
|
|
128
128
|
|
|
129
|
-
`fd`, `walk`, and `ls` return `PathResults
|
|
129
|
+
`fd`, `walk`, and `ls` return `PathResults`, a list of absolute `Path` objects. So do `rg` and `nbrg` with `paths=True`, and their async equivalents. Indexing or iterating returns ordinary Paths:
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
```python
|
|
132
|
+
for path in fd("src", ext="py"):
|
|
133
|
+
text = path.read_text()
|
|
134
|
+
size = path.stat().st_size
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Use `.name`, `.suffix`, and `.relative_to(root)` for path components. `.stat()` follows symlinks; `.lstat()` inspects the link itself. `.readlink()` returns a link's target. Discovery preserves native filenames, including literal backslashes on Unix and non-UTF-8 names on filesystems that support them.
|
|
138
|
+
|
|
139
|
+
`PathResults` displays as an `ls -l`-style listing of at most `rgapi.MAX_REPR` rows. A final `… N more` line reports omitted rows. The listing uses `.lstat()` only on displayed rows. `show_target=True` adds symlink targets. `str(res)` returns one root-relative name per line. Slices retain the display root and completion status. `list(res)` returns the absolute Paths without the custom display.
|
|
140
|
+
|
|
141
|
+
Structured text and notebook search rows retain root-relative string labels in their `path` fields. Content searches still follow explicitly named root links. This differs from discovery's default of returning the link itself.
|
|
142
|
+
|
|
143
|
+
The Rust `find` and `find_iter` APIs return native `PathBuf` values relative to the root. For an explicitly named file or unfollowed root link, the result is its basename.
|
|
132
144
|
|
|
133
|
-
|
|
145
|
+
Rust callers can set `FindOptions::special_files` to also discover FIFOs, sockets, and device nodes, for example to reject unsupported entries during archiving. This defaults to false; normal discovery returns regular files, directories when requested, and symlinks.
|
|
134
146
|
|
|
135
147
|
### Limits and timeouts
|
|
136
148
|
|
|
@@ -56,17 +56,17 @@ pip install rgapi
|
|
|
56
56
|
|
|
57
57
|
## File discovery
|
|
58
58
|
|
|
59
|
-
`fd` and `walk` return
|
|
59
|
+
`fd` and `walk` return absolute `pathlib.Path` objects. Use them directly with `.read_text()`, `.open()`, `.stat()`, or other filesystem operations. Their collected results display names relative to `root`. Pass `root` as a `str` or `Path`. The sync and async APIs expand `~` and accept `.`, `./`, and paths containing `..`.
|
|
60
60
|
|
|
61
61
|
Discovery uses the `ignore` crate with ripgrep's default filters. It reads `.gitignore`, `.ignore`, and `.rgignore` files. `.rgignore` takes precedence over `.gitignore`. Pass `ignore=False` to disable all ignore-file filtering, including `.rgignore`.
|
|
62
62
|
|
|
63
|
-
Hidden files are skipped unless `hidden=True`.
|
|
63
|
+
Hidden files are skipped unless `hidden=True`. Discovery returns symlinks themselves, including explicitly named roots and dangling links. Set `follow_links=True` to traverse their targets. Paths retain the symlink spelling. Use `same_file_system=True` to avoid crossing filesystem boundaries.
|
|
64
64
|
|
|
65
65
|
Traversal runs in parallel without guaranteed result order. Use `sorted(...)` when order matters.
|
|
66
66
|
|
|
67
67
|
`fd` adds filename filters to `walk`. Its `pattern` is a smart-case regex matched against each basename. Lowercase patterns match case-insensitively. A pattern containing uppercase letters is case-sensitive. Use `path_re` to match the slash-separated relative path instead.
|
|
68
68
|
|
|
69
|
-
`include` and `exclude` use glob syntax. `glob=` is an alias for `include=`.
|
|
69
|
+
`include` and `exclude` use case-sensitive glob syntax. `glob=` is an alias for `include=`. Patterns without `/` match names at any depth, so `*.py` matches `src/app.py`. Patterns containing `/` are root-relative. `*` stays within a path component; `**` spans directories: `src/*` matches immediate children, whereas `src/**` also matches deeper descendants. Matching a directory with `exclude` prunes its entire subtree. Includes never prune traversal. Excludes always win, and includes do not override ignore rules.
|
|
70
70
|
|
|
71
71
|
Filter extensions with `ext="py"` or `ext=["py", "rs"]`. Extension and glob filters must both match. For example, `include="src/*", ext="py"` requires `src/*` and `*.py`, like combining `rg -g` with `-t`.
|
|
72
72
|
|
|
@@ -74,7 +74,7 @@ Set `min_depth` and `max_depth` to bound recursion. `max_filesize` skips files a
|
|
|
74
74
|
|
|
75
75
|
`ls` follows the shell command's listing conventions. It uses `fd` with `max_depth=1`, includes directories, disables ignore rules, and sorts by name. Set `hidden=True` for `ls -a` behaviour. All `fd` filters remain available.
|
|
76
76
|
|
|
77
|
-
`fd_iter` yields `
|
|
77
|
+
`fd_iter` yields absolute `Path` objects as the walk finds them. It accepts every `fd` filter. Stopping iteration ends the walk. It does not accept `timeout_ms`.
|
|
78
78
|
|
|
79
79
|
`path_re` and `skip_path_re` filter slash-separated relative paths using regexes. They select returned paths or searched files without changing traversal. To skip entire subtrees, use `skip_dir` with a glob or `skip_dir_re` with a regex.
|
|
80
80
|
|
|
@@ -105,11 +105,23 @@ For other result forms, use `rg(..., paths=True)` to return unique matched paths
|
|
|
105
105
|
|
|
106
106
|
### Path results
|
|
107
107
|
|
|
108
|
-
`fd`, `walk`, and `ls` return `PathResults
|
|
108
|
+
`fd`, `walk`, and `ls` return `PathResults`, a list of absolute `Path` objects. So do `rg` and `nbrg` with `paths=True`, and their async equivalents. Indexing or iterating returns ordinary Paths:
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
```python
|
|
111
|
+
for path in fd("src", ext="py"):
|
|
112
|
+
text = path.read_text()
|
|
113
|
+
size = path.stat().st_size
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use `.name`, `.suffix`, and `.relative_to(root)` for path components. `.stat()` follows symlinks; `.lstat()` inspects the link itself. `.readlink()` returns a link's target. Discovery preserves native filenames, including literal backslashes on Unix and non-UTF-8 names on filesystems that support them.
|
|
117
|
+
|
|
118
|
+
`PathResults` displays as an `ls -l`-style listing of at most `rgapi.MAX_REPR` rows. A final `… N more` line reports omitted rows. The listing uses `.lstat()` only on displayed rows. `show_target=True` adds symlink targets. `str(res)` returns one root-relative name per line. Slices retain the display root and completion status. `list(res)` returns the absolute Paths without the custom display.
|
|
119
|
+
|
|
120
|
+
Structured text and notebook search rows retain root-relative string labels in their `path` fields. Content searches still follow explicitly named root links. This differs from discovery's default of returning the link itself.
|
|
121
|
+
|
|
122
|
+
The Rust `find` and `find_iter` APIs return native `PathBuf` values relative to the root. For an explicitly named file or unfollowed root link, the result is its basename.
|
|
111
123
|
|
|
112
|
-
|
|
124
|
+
Rust callers can set `FindOptions::special_files` to also discover FIFOs, sockets, and device nodes, for example to reject unsupported entries during archiving. This defaults to false; normal discovery returns regular files, directories when requested, and symlinks.
|
|
113
125
|
|
|
114
126
|
### Limits and timeouts
|
|
115
127
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import asyncio,
|
|
1
|
+
import asyncio, re
|
|
2
2
|
from contextlib import aclosing
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from
|
|
5
|
-
from stat import S_ISDIR, S_ISLNK, filemode
|
|
4
|
+
from stat import S_ISLNK, filemode
|
|
6
5
|
|
|
7
6
|
from os import fspath
|
|
8
7
|
from pathlib import Path
|
|
@@ -42,48 +41,32 @@ def _hsize(n):
|
|
|
42
41
|
n /= 1024
|
|
43
42
|
return f"{n:.0f}" if u == "B" else f"{n:.1f}{u}"
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
self = super().__new__(cls, path)
|
|
49
|
-
self.root,self.show_target = os.path.abspath(root),show_target
|
|
50
|
-
return self
|
|
51
|
-
@cached_property
|
|
52
|
-
def stat(self):
|
|
53
|
-
"Cached `os.lstat` result; `None` if the path has vanished"
|
|
54
|
-
try: return os.lstat(os.path.join(self.root, self))
|
|
55
|
-
except OSError: return None
|
|
56
|
-
@property
|
|
57
|
-
def size(self): return None if self.stat is None else self.stat.st_size
|
|
58
|
-
@property
|
|
59
|
-
def mtime(self): return None if self.stat is None else datetime.fromtimestamp(self.stat.st_mtime)
|
|
60
|
-
@property
|
|
61
|
-
def is_dir(self): return self.stat is not None and S_ISDIR(self.stat.st_mode)
|
|
62
|
-
@cached_property
|
|
63
|
-
def link_target(self):
|
|
64
|
-
"`os.readlink` result for a symlink; `None` otherwise"
|
|
65
|
-
if self.stat is None or not S_ISLNK(self.stat.st_mode): return None
|
|
66
|
-
try: return os.readlink(os.path.join(self.root, self))
|
|
67
|
-
except OSError: return None
|
|
68
|
-
def _line(self):
|
|
69
|
-
if self.stat is None: return f"{'?':10} {'?':>7} {'?':16} {self}"
|
|
70
|
-
tgt = f" -> {self.link_target}" if self.show_target and self.link_target is not None else ""
|
|
71
|
-
return f"{filemode(self.stat.st_mode)} {_hsize(self.stat.st_size):>7} {self.mtime:%Y-%m-%d %H:%M} {self}{tgt}"
|
|
72
|
-
def _repr_markdown_(self): return f"`{self._line()}`"
|
|
73
|
-
|
|
74
|
-
def _entry_root(root):
|
|
75
|
-
"Stat base for `FileEntry`: a file root's entries are named relative to its parent"
|
|
76
|
-
return os.path.dirname(root) if os.path.isfile(root) else root
|
|
77
|
-
|
|
78
|
-
def _fe(paths, root, show_target=False):
|
|
79
|
-
root = _entry_root(root)
|
|
80
|
-
return (FileEntry(p, root, show_target) for p in paths)
|
|
44
|
+
def _path_base(root, follow_links=True):
|
|
45
|
+
root = Path(root).absolute()
|
|
46
|
+
return root if (follow_links or not root.is_symlink()) and root.is_dir() else root.parent
|
|
81
47
|
|
|
82
48
|
class PathResults(_Results):
|
|
83
|
-
"
|
|
84
|
-
def
|
|
49
|
+
"Absolute `Path` objects with root-relative plain and `ls -l`-style displays"
|
|
50
|
+
def __init__(self, paths=(), root=".", show_target=False):
|
|
51
|
+
self.root,self.show_target = Path(root).absolute(),show_target
|
|
52
|
+
super().__init__(self.root/p for p in paths)
|
|
53
|
+
def __getitem__(self, key):
|
|
54
|
+
res = super().__getitem__(key)
|
|
55
|
+
if not isinstance(key, slice): return res
|
|
56
|
+
res = PathResults(res, self.root, self.show_target)
|
|
57
|
+
res.stop_reason = self.stop_reason
|
|
58
|
+
return res
|
|
59
|
+
def __str__(self): return "\n".join(p.relative_to(self.root).as_posix() for p in self)
|
|
60
|
+
def _line(self, path):
|
|
61
|
+
name = path.relative_to(self.root).as_posix()
|
|
62
|
+
try:
|
|
63
|
+
st = path.lstat()
|
|
64
|
+
tgt = f" -> {path.readlink()}" if self.show_target and S_ISLNK(st.st_mode) else ""
|
|
65
|
+
except OSError: return f"{'?':10} {'?':>7} {'?':16} {name}"
|
|
66
|
+
mtime = datetime.fromtimestamp(st.st_mtime)
|
|
67
|
+
return f"{filemode(st.st_mode)} {_hsize(st.st_size):>7} {mtime:%Y-%m-%d %H:%M} {name}{tgt}"
|
|
85
68
|
def __repr__(self):
|
|
86
|
-
res = [
|
|
69
|
+
res = [self._line(p) for p in self[:MAX_REPR]]
|
|
87
70
|
if len(self) > MAX_REPR: res.append(f"… {len(self)-MAX_REPR:,} more")
|
|
88
71
|
if self.stop_reason is not None: res.append(f"… truncated: {self.stop_reason}")
|
|
89
72
|
return "\n".join(res)
|
|
@@ -127,11 +110,11 @@ def walk(
|
|
|
127
110
|
dirs:bool=False, # Include directories in results
|
|
128
111
|
timeout_ms:int|None=None, # Cancel the walk after this long and return partial results
|
|
129
112
|
) -> PathResults:
|
|
130
|
-
"Walk a directory and return
|
|
131
|
-
rt =
|
|
113
|
+
"Walk a directory and return absolute file and/or directory Paths."
|
|
114
|
+
rt = Path(root).expanduser().absolute()
|
|
132
115
|
paths, timed_out = _core.walk(rt, hidden, ignore, max_depth, min_depth, max_filesize, follow_links,
|
|
133
116
|
same_file_system, path_re, skip_path_re, _listify(skip_dir), skip_dir_re, files, dirs, timeout_ms)
|
|
134
|
-
return _mk_results(PathResults,
|
|
117
|
+
return _mk_results(PathResults, paths, False, timed_out, root=_path_base(rt, follow_links))
|
|
135
118
|
|
|
136
119
|
|
|
137
120
|
def _walk_args(
|
|
@@ -166,10 +149,10 @@ def fd(
|
|
|
166
149
|
timeout_ms:int|None=None, # Cancel the walk after this long and return partial results
|
|
167
150
|
**kwargs
|
|
168
151
|
) -> PathResults:
|
|
169
|
-
"Find
|
|
170
|
-
rt =
|
|
152
|
+
"Find absolute Paths with fd-style filters and gitignore support."
|
|
153
|
+
rt = Path(root).expanduser().absolute()
|
|
171
154
|
paths, timed_out = _core.find(rt, pattern, *_walk_args(**kwargs), files, dirs, timeout_ms)
|
|
172
|
-
return _mk_results(PathResults,
|
|
155
|
+
return _mk_results(PathResults, paths, False, timed_out, root=_path_base(rt, kwargs.get('follow_links', False)), show_target=show_target)
|
|
173
156
|
|
|
174
157
|
|
|
175
158
|
@delegates(_walk_args)
|
|
@@ -180,9 +163,10 @@ def fd_iter(
|
|
|
180
163
|
dirs:bool=False, # Include directories in results
|
|
181
164
|
**kwargs
|
|
182
165
|
):
|
|
183
|
-
"Walk lazily, yielding
|
|
184
|
-
rt =
|
|
185
|
-
|
|
166
|
+
"Walk lazily, yielding absolute Paths; early exit stops the walk."
|
|
167
|
+
rt = Path(root).expanduser().absolute()
|
|
168
|
+
base = _path_base(rt, kwargs.get('follow_links', False))
|
|
169
|
+
return (base/p for p in _core.find_iter(rt, pattern, *_walk_args(**kwargs), files, dirs))
|
|
186
170
|
|
|
187
171
|
|
|
188
172
|
@delegates(fd)
|
|
@@ -197,9 +181,8 @@ def ls(
|
|
|
197
181
|
) -> PathResults:
|
|
198
182
|
"List a directory like `ls`: one level, directories included, ignore rules off, sorted by name."
|
|
199
183
|
res = fd(root, pattern, hidden=hidden, dirs=dirs, max_depth=max_depth, ignore=ignore, **kwargs)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return out
|
|
184
|
+
res.sort()
|
|
185
|
+
return res
|
|
203
186
|
|
|
204
187
|
async def _acall(fn, *args):
|
|
205
188
|
"Run a `_core` async op: settle a Future from its callback; cancel the op if abandoned"
|
|
@@ -228,9 +211,9 @@ async def fda(
|
|
|
228
211
|
**kwargs
|
|
229
212
|
) -> PathResults:
|
|
230
213
|
"Async `fd`: find paths on Rust threads without blocking the event loop."
|
|
231
|
-
rt =
|
|
214
|
+
rt = Path(root).expanduser().absolute()
|
|
232
215
|
paths, timed_out = await _acall(_core.find_async, rt, pattern, *_walk_args(**kwargs), files, dirs, timeout_ms)
|
|
233
|
-
return _mk_results(PathResults,
|
|
216
|
+
return _mk_results(PathResults, paths, False, timed_out, root=_path_base(rt, kwargs.get('follow_links', False)))
|
|
234
217
|
|
|
235
218
|
|
|
236
219
|
@delegates(_walk_args)
|
|
@@ -242,12 +225,13 @@ async def fda_iter(
|
|
|
242
225
|
batch_max:int=512, # Largest batch of paths delivered to the event loop at once
|
|
243
226
|
**kwargs
|
|
244
227
|
):
|
|
245
|
-
"Async `fd_iter`: yield
|
|
246
|
-
rt =
|
|
228
|
+
"Async `fd_iter`: yield absolute Paths; early exit stops the walk."
|
|
229
|
+
rt = Path(root).expanduser().absolute()
|
|
230
|
+
base = _path_base(rt, kwargs.get('follow_links', False))
|
|
247
231
|
async with aclosing(_abatches(_core.find_iter_async, batch_max, rt, pattern,
|
|
248
232
|
*_walk_args(**kwargs), files, dirs)) as batches:
|
|
249
233
|
async for paths in batches:
|
|
250
|
-
for p in
|
|
234
|
+
for p in paths: yield base/p
|
|
251
235
|
|
|
252
236
|
|
|
253
237
|
|
|
@@ -267,8 +251,8 @@ def _cap_rows(rows, n):
|
|
|
267
251
|
return res, False
|
|
268
252
|
|
|
269
253
|
|
|
270
|
-
def _mk_results(cls, items, capped, timed_out):
|
|
271
|
-
res = cls(items)
|
|
254
|
+
def _mk_results(cls, items, capped, timed_out, **kwargs):
|
|
255
|
+
res = cls(items, **kwargs)
|
|
272
256
|
if capped: res.stop_reason = "max_results"
|
|
273
257
|
elif timed_out: res.stop_reason = "timeout"
|
|
274
258
|
return res
|
|
@@ -276,15 +260,15 @@ def _mk_results(cls, items, capped, timed_out):
|
|
|
276
260
|
|
|
277
261
|
def _paths_reduce(rows, root, max_results, timed_out=False):
|
|
278
262
|
"Unique matched paths as `PathResults` from rows with `kind`/`path`, capped at `max_results`"
|
|
279
|
-
seen,res,capped
|
|
263
|
+
seen,res,capped = set(),[],False
|
|
280
264
|
for row in rows:
|
|
281
265
|
if row.kind != "match" or row.path in seen: continue
|
|
282
266
|
if max_results is not None and len(res) == max_results:
|
|
283
267
|
capped = True
|
|
284
268
|
break
|
|
285
269
|
seen.add(row.path)
|
|
286
|
-
res.append(
|
|
287
|
-
return _mk_results(PathResults, res, capped, timed_out)
|
|
270
|
+
res.append(row.path)
|
|
271
|
+
return _mk_results(PathResults, res, capped, timed_out, root=_path_base(Path(root).resolve()))
|
|
288
272
|
|
|
289
273
|
def _rg_post(rows, paths, count, max_results, timed_out, root):
|
|
290
274
|
"Reduce collected rows to the requested `rg`/`rga` result form"
|
|
@@ -324,8 +308,8 @@ def rg(
|
|
|
324
308
|
if summary:
|
|
325
309
|
rows,timed_out = _core.block_search(*args, timeout_ms)
|
|
326
310
|
return _block_post(rows, max_results, before_context, after_context, timed_out, maxlen, lnhashs)
|
|
327
|
-
if count: return sum(len(row.matches) for row in _core.rg_iter(*args) if row.kind == "match")
|
|
328
|
-
if paths and timeout_ms is None: return _paths_reduce(_core.rg_iter(*args), rt, max_results)
|
|
311
|
+
if count: return sum(len(row.matches) for row in _core.rg_iter(*args, False) if row.kind == "match")
|
|
312
|
+
if paths and timeout_ms is None: return _paths_reduce(_core.rg_iter(*args, False), rt, max_results)
|
|
329
313
|
rows, timed_out = _core.rg(*args, lnhashs, timeout_ms)
|
|
330
314
|
return _rg_post(rows, paths, False, max_results, timed_out, rt)
|
|
331
315
|
|
|
@@ -63,7 +63,7 @@ def search_nb(
|
|
|
63
63
|
"Search one `.ipynb` file's cell sources, returning matched cells."
|
|
64
64
|
disp = _display_path(path if display_path is None else display_path)
|
|
65
65
|
rows = _core.nb_search_file(pattern, _fs_path(path), disp, case_sensitive=case_sensitive,
|
|
66
|
-
smart_case=smart_case, cell_context=cell_context)
|
|
66
|
+
smart_case=smart_case, cell_context=cell_context, multiline=False)
|
|
67
67
|
res = NbResults(_rows_to_cells(rows, maxlen))
|
|
68
68
|
res.sort(key=lambda c: c.cell_index)
|
|
69
69
|
return res
|
|
@@ -12,9 +12,11 @@ For orientation, start with `rg(summary=True)`; use line-level results where nee
|
|
|
12
12
|
|
|
13
13
|
## Result fields and display
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Discovery and `paths=True` results contain absolute `pathlib.Path` objects. Use them directly: `p.read_text()`, `p.name`, `p.stat().st_size`, `p.stat().st_mtime`, and `p.is_dir()`. `.stat()` follows links; `.lstat()` inspects the link itself. Discovery returns symlinks themselves, including root links and dangling links, unless `follow_links=True`. `.readlink()` returns a link's target. `ls(hidden=True)` corresponds to `ls -a`.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
`PathResults` displays root-relative names in ls-style tables capped at `MAX_REPR`. `str(res)` returns one relative name per line; `list(res)` contains absolute Paths. Slices retain the display root and completion status. `show_target=True` appends link targets in the listing.
|
|
18
|
+
|
|
19
|
+
Search rows provide `asdict()`. Their `path` fields remain root-relative string labels. Content searches follow explicitly named root links:
|
|
18
20
|
|
|
19
21
|
| Row | Location | Content/matches | `kind` |
|
|
20
22
|
|---|---|---|---|
|
|
@@ -97,7 +97,7 @@ fn block_entry(
|
|
|
97
97
|
let Some(ft) = dent.file_type() else { return Ok(Vec::new()); };
|
|
98
98
|
if !ft.is_file() { return Ok(Vec::new()); }
|
|
99
99
|
let rel = rel_path(root, path);
|
|
100
|
-
if !filters.path_allowed(&rel) { return Ok(Vec::new()); }
|
|
100
|
+
if !filters.path_allowed(Path::new(&rel)) { return Ok(Vec::new()); }
|
|
101
101
|
let bytes = match std::fs::read(path) { Ok(bytes) => bytes, Err(_) => return Ok(Vec::new()) };
|
|
102
102
|
process_file(rel, &bytes, matcher, before_context, after_context)
|
|
103
103
|
}
|
|
@@ -174,7 +174,7 @@ fn nb_entry(
|
|
|
174
174
|
let Some(ft) = dent.file_type() else { return Ok(Vec::new()); };
|
|
175
175
|
if !ft.is_file() { return Ok(Vec::new()); }
|
|
176
176
|
let rel = rel_path(root, path);
|
|
177
|
-
if !filters.path_allowed(&rel) { return Ok(Vec::new()); }
|
|
177
|
+
if !filters.path_allowed(Path::new(&rel)) { return Ok(Vec::new()); }
|
|
178
178
|
let bytes = match std::fs::read(path) { Ok(b) => b, Err(_) => return Ok(Vec::new()) };
|
|
179
179
|
process_file(rel, &bytes, matcher, cell_context, multiline)
|
|
180
180
|
}
|