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.
@@ -24,9 +24,9 @@ dependencies = [
24
24
 
25
25
  [[package]]
26
26
  name = "cfg-if"
27
- version = "1.0.4"
27
+ version = "1.0.5"
28
28
  source = "registry+https://github.com/rust-lang/crates.io-index"
29
- checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
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.5",
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.26"
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.5",
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.5"
429
+ version = "3.0.6"
430
430
  source = "registry+https://github.com/rust-lang/crates.io-index"
431
- checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
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.24"
446
+ version = "1.0.25"
447
447
  source = "registry+https://github.com/rust-lang/crates.io-index"
448
- checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
448
+ checksum = "ab72a15cf68d77cb0987d3684aa8a45c5ef827e8cb49ee2f30bfd7ba2feb519f"
449
449
 
450
450
  [[package]]
451
451
  name = "walkdir"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rgapi"
3
- version = "0.1.26"
3
+ version = "0.1.27"
4
4
  edition = "2024"
5
5
  rust-version = "1.91"
6
6
  license = "Apache-2.0"
@@ -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
- Paths in `fd`, `walk`, `rg`, and `rg_iter` results are relative to the requested root and use `/` 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, symlink, filesystem, hidden, and ignore options are direct `ignore::WalkBuilder` settings. `rg_iter` exposes the same parallel search stream that `rg` collects by default; `paths=True` and `count=True` consume that stream with different reducers. Binary files and invalid UTF-8 are skipped for now.
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 = String`, 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.
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
- Path results are `FileEntry` rows: a `str` subclass carrying the walk root, so paths stay plain strings for compatibility while stat info loads lazily (one cached `os.lstat` per entry, read only on attribute access). The wrapping happens at result construction on the Python side; Rust still streams plain strings. `PathResults.__repr__` shows an `ls -l`-style listing capped at `MAX_REPR` rows, so a huge result never stats everything, while `str()` stays one plain path per line. `ls` is `fd` with shell-style defaults (one level, dirs, ignore rules off), re-sorted with `stop_reason` preserved.
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.26
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 slash-separated paths relative to `root`. Pass `root` as a `str` or `pathlib.Path`. The sync and async APIs expand `~` and accept `.`, `./`, and paths containing `..`.
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`. Symlinks are followed only with `follow_links=True`. Use `same_file_system=True` to avoid crossing filesystem boundaries.
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=`. A basename glob such as `*.py` also matches nested paths such as `src/app.py`.
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 `FileEntry` paths as the walk finds them. It accepts every `fd` filter. Stopping iteration ends the walk. It does not accept `timeout_ms`.
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`. So do `rg` and `nbrg` with `paths=True`. This is a list of `FileEntry` rows.
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
- A `FileEntry` is a `str` subclass containing a relative path. Its `stat` property calls `os.lstat` on first access and caches the result. It returns `None` if the path has vanished. `size`, `mtime`, and `is_dir` use the cached stat result. The object also supports ordinary string operations.
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
- `PathResults` displays as an `ls -l`-style listing of at most `rgapi.MAX_REPR` rows. A final `… N more` line reports omitted rows. Only displayed rows need stat calls. `str()` returns one plain path per line. `list(res)` also displays plain paths.
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 slash-separated paths relative to `root`. Pass `root` as a `str` or `pathlib.Path`. The sync and async APIs expand `~` and accept `.`, `./`, and paths containing `..`.
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`. Symlinks are followed only with `follow_links=True`. Use `same_file_system=True` to avoid crossing filesystem boundaries.
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=`. A basename glob such as `*.py` also matches nested paths such as `src/app.py`.
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 `FileEntry` paths as the walk finds them. It accepts every `fd` filter. Stopping iteration ends the walk. It does not accept `timeout_ms`.
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`. So do `rg` and `nbrg` with `paths=True`. This is a list of `FileEntry` rows.
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
- A `FileEntry` is a `str` subclass containing a relative path. Its `stat` property calls `os.lstat` on first access and caches the result. It returns `None` if the path has vanished. `size`, `mtime`, and `is_dir` use the cached stat result. The object also supports ordinary string operations.
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
- `PathResults` displays as an `ls -l`-style listing of at most `rgapi.MAX_REPR` rows. A final `… N more` line reports omitted rows. Only displayed rows need stat calls. `str()` returns one plain path per line. `list(res)` also displays plain paths.
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
 
@@ -43,3 +43,6 @@ cache-keys = [{ file = "pyproject.toml" }, { file = "src/**/*.rs" }, { file = "C
43
43
 
44
44
  [tool.fastship]
45
45
  branch = "main"
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
@@ -1,8 +1,7 @@
1
- import asyncio, os, re
1
+ import asyncio, re
2
2
  from contextlib import aclosing
3
3
  from datetime import datetime
4
- from functools import cached_property
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
- class FileEntry(str):
46
- "Relative path that lazily stats itself for `size`/`mtime`/`is_dir`/`link_target` and `ls -l`-style display"
47
- def __new__(cls, path, root=".", show_target=False):
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
- "List of relative `FileEntry` paths; repr is `ls -l`-style, `str()` is line-per-path"
84
- def __str__(self): return "\n".join(self)
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 = [p._line() if isinstance(p, FileEntry) else str(p) for p in self[:MAX_REPR]]
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 relative file and/or directory paths."
131
- rt = _fs_path(root)
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, _fe(paths, rt), False, timed_out)
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 paths with fd-style filters and gitignore support."
170
- rt = _fs_path(root)
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, _fe(paths, rt, show_target), False, timed_out)
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 `FileEntry` paths as they are found; early exit stops the walk."
184
- rt = _fs_path(root)
185
- return _fe(_core.find_iter(rt, pattern, *_walk_args(**kwargs), files, dirs), rt)
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
- out = PathResults(sorted(res))
201
- out.stop_reason = res.stop_reason
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 = _fs_path(root)
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, _fe(paths, rt), False, timed_out)
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 `FileEntry` paths as they are found; early exit stops the walk."
246
- rt = _fs_path(root)
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 _fe(paths, rt): yield p
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,er = set(),[],False,_entry_root(root)
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(FileEntry(row.path, er))
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
- `FileEntry` is a slash-separated relative-path `str` with lazy `size`/`mtime`/`is_dir`/`stat`. Path lists render as ls-style tables capped at `MAX_REPR`; `str(res)`/`list(res)` yield plain paths. Unfollowed symlinks remain, marked `l`; `link_target` is their target or `None` for non-links. `ls(hidden=True)` corresponds to `ls -a`.
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
- Search rows provide `asdict()`. All paths are relative to the search root:
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
  }