rgapi 0.1.6__tar.gz → 0.1.9__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.6 → rgapi-0.1.9}/.github/workflows/ci.yml +12 -0
- {rgapi-0.1.6 → rgapi-0.1.9}/Cargo.lock +8 -17
- {rgapi-0.1.6 → rgapi-0.1.9}/Cargo.toml +1 -1
- {rgapi-0.1.6 → rgapi-0.1.9}/PKG-INFO +39 -2
- {rgapi-0.1.6 → rgapi-0.1.9}/README.md +38 -1
- {rgapi-0.1.6 → rgapi-0.1.9}/pyproject.toml +3 -0
- {rgapi-0.1.6 → rgapi-0.1.9}/python/rgapi/__init__.py +3 -13
- rgapi-0.1.9/python/rgapi/nb.py +96 -0
- rgapi-0.1.9/python/rgapi/skill.py +34 -0
- {rgapi-0.1.6 → rgapi-0.1.9}/src/python.rs +29 -0
- {rgapi-0.1.6 → rgapi-0.1.9}/src/search.rs +25 -10
- {rgapi-0.1.6 → rgapi-0.1.9}/src/walk.rs +19 -1
- {rgapi-0.1.6 → rgapi-0.1.9}/tests/test_rgapi.py +129 -1
- {rgapi-0.1.6 → rgapi-0.1.9}/.gitignore +0 -0
- {rgapi-0.1.6 → rgapi-0.1.9}/DEV.md +0 -0
- {rgapi-0.1.6 → rgapi-0.1.9}/src/lib.rs +0 -0
- {rgapi-0.1.6 → rgapi-0.1.9}/tools/bench.py +0 -0
|
@@ -7,7 +7,19 @@ on:
|
|
|
7
7
|
pull_request:
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
15
|
+
- uses: actions/setup-python@v6
|
|
16
|
+
with:
|
|
17
|
+
python-version: '3.12'
|
|
18
|
+
- run: pip install -e '.[dev]'
|
|
19
|
+
- run: pytest -q
|
|
20
|
+
|
|
10
21
|
build:
|
|
22
|
+
needs: test
|
|
11
23
|
strategy:
|
|
12
24
|
matrix:
|
|
13
25
|
os: [ubuntu-latest, macos-latest]
|
|
@@ -13,13 +13,13 @@ dependencies = [
|
|
|
13
13
|
|
|
14
14
|
[[package]]
|
|
15
15
|
name = "bstr"
|
|
16
|
-
version = "1.12.
|
|
16
|
+
version = "1.12.2"
|
|
17
17
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
|
-
checksum = "
|
|
18
|
+
checksum = "a4e308e67087593070530a666f7fe8815cbd2e61d8f5b7313b71b7d721d1dfde"
|
|
19
19
|
dependencies = [
|
|
20
20
|
"memchr",
|
|
21
21
|
"regex-automata",
|
|
22
|
-
"
|
|
22
|
+
"serde_core",
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
[[package]]
|
|
@@ -163,9 +163,9 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
|
|
163
163
|
|
|
164
164
|
[[package]]
|
|
165
165
|
name = "memmap2"
|
|
166
|
-
version = "0.9.
|
|
166
|
+
version = "0.9.11"
|
|
167
167
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
168
|
-
checksum = "
|
|
168
|
+
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
|
|
169
169
|
dependencies = [
|
|
170
170
|
"libc",
|
|
171
171
|
]
|
|
@@ -250,9 +250,9 @@ dependencies = [
|
|
|
250
250
|
|
|
251
251
|
[[package]]
|
|
252
252
|
name = "quote"
|
|
253
|
-
version = "1.0.
|
|
253
|
+
version = "1.0.46"
|
|
254
254
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
255
|
-
checksum = "
|
|
255
|
+
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
|
256
256
|
dependencies = [
|
|
257
257
|
"proc-macro2",
|
|
258
258
|
]
|
|
@@ -276,7 +276,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|
|
276
276
|
|
|
277
277
|
[[package]]
|
|
278
278
|
name = "rgapi"
|
|
279
|
-
version = "0.1.
|
|
279
|
+
version = "0.1.9"
|
|
280
280
|
dependencies = [
|
|
281
281
|
"globset",
|
|
282
282
|
"grep-matcher",
|
|
@@ -295,15 +295,6 @@ dependencies = [
|
|
|
295
295
|
"winapi-util",
|
|
296
296
|
]
|
|
297
297
|
|
|
298
|
-
[[package]]
|
|
299
|
-
name = "serde"
|
|
300
|
-
version = "1.0.228"
|
|
301
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
302
|
-
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
|
303
|
-
dependencies = [
|
|
304
|
-
"serde_core",
|
|
305
|
-
]
|
|
306
|
-
|
|
307
298
|
[[package]]
|
|
308
299
|
name = "serde_core"
|
|
309
300
|
version = "1.0.228"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rgapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Requires-Dist: fastship>=0.0.12 ; extra == 'dev'
|
|
@@ -35,6 +35,14 @@ for row in rg_iter("TODO", ".", include="*.py", context=2): print(row.asdict())
|
|
|
35
35
|
rg("TODO", ".", ext="py", skip_dir=".venv", paths=True)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
For cell-aware search of Jupyter notebooks (see [Notebooks](#notebooks)):
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from rgapi import nbrg
|
|
42
|
+
|
|
43
|
+
nbrg("read_csv", ".", cell_context=1)
|
|
44
|
+
```
|
|
45
|
+
|
|
38
46
|
For direct access to the regex, search, and walk pieces:
|
|
39
47
|
|
|
40
48
|
```python
|
|
@@ -57,7 +65,7 @@ pip install rgapi
|
|
|
57
65
|
|
|
58
66
|
## Semantics
|
|
59
67
|
|
|
60
|
-
`fd` and `walk` return slash-separated paths relative to `root`. They use the `ignore` crate, so `.gitignore
|
|
68
|
+
`fd` and `walk` return slash-separated paths relative to `root`. They use the `ignore` crate, so `.gitignore`, `.ignore`, and the usual ripgrep filters apply by default. `.rgignore` files are also honored and take precedence over `.gitignore`. Hidden files are skipped unless `hidden=True`. Pass `ignore=False` to disable all ignore filtering (including `.rgignore`). Symlinks are not followed unless `follow_links=True`; `same_file_system=True` avoids crossing filesystem boundaries. Traversal is parallel, and result order is not guaranteed; use `sorted(...)` if order matters.
|
|
61
69
|
`root` arguments accept `str` or `pathlib.Path` and expand `~`; `search_path` also accepts path-like file paths. Display labels such as `display_path` are stringified without expansion.
|
|
62
70
|
|
|
63
71
|
`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.
|
|
@@ -82,6 +90,35 @@ matches list of (start, end) byte offsets for match rows
|
|
|
82
90
|
|
|
83
91
|
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.
|
|
84
92
|
|
|
93
|
+
## Notebooks
|
|
94
|
+
|
|
95
|
+
`nbrg` searches Jupyter `.ipynb` files cell-by-cell, so results are *cells* rather than raw JSON lines. Searching a notebook with plain `rg` matches the escaped JSON text (including outputs and metadata) and reports JSON line numbers; `nbrg` instead searches each cell's reconstructed **source** and returns the cells that matched.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from rgapi import nbrg
|
|
99
|
+
|
|
100
|
+
nbrg("read_csv", ".") # cells whose source matches, across all notebooks under "."
|
|
101
|
+
nbrg("read_csv", ".", cell_context=1) # also include neighbouring cells as context
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It finds notebooks with the fast parallel Rust walker (`fd`), then matches each cell's source through the same Rust engine as `rg` (via `search_text`), so regex behaviour and the `case_sensitive`/`smart_case` flags match `rg`. Only cell `source` is searched, not outputs or metadata. `nbrg` accepts the same discovery filters as `fd`/`rg` (`include`, `exclude`, `glob`, `hidden`, `max_depth`, `skip_dir`, …).
|
|
105
|
+
|
|
106
|
+
`nbrg` returns `NbResults`, a list of `NbCell`. Each `NbCell` has:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
path notebook path relative to root
|
|
110
|
+
cell_index 0-based position of the cell in the notebook
|
|
111
|
+
cell_id nbformat cell id (falls back to the cell index for notebooks without ids)
|
|
112
|
+
cell_type 'code', 'markdown', or 'raw'
|
|
113
|
+
kind 'match' or 'context'
|
|
114
|
+
source full cell source
|
|
115
|
+
matches list of SearchLine rows for the matched lines within the cell
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`NbCell.asdict()` returns those fields as a plain dict (with `matches` as `SearchLine` dicts). `str()`/pretty display is one truncated, newline-escaped line per cell: `path:cell_id:source` for matches and `path:cell_id-source` for context cells. A cell with several matches appears once, with every hit collected in `matches`.
|
|
119
|
+
|
|
120
|
+
`cell_context=N` includes the `N` cells before and after each matching cell as `kind="context"` rows (deduplicated per notebook). `prefilter=True` first narrows candidate notebooks with `rg` before the per-cell search; it is faster on notebook-heavy trees but can miss matches whose pattern is affected by JSON escaping (quotes, backslashes, regex metacharacters straddling escapes), so it is off by default.
|
|
121
|
+
|
|
85
122
|
## Benchmarks
|
|
86
123
|
|
|
87
124
|
`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:
|
|
@@ -16,6 +16,14 @@ for row in rg_iter("TODO", ".", include="*.py", context=2): print(row.asdict())
|
|
|
16
16
|
rg("TODO", ".", ext="py", skip_dir=".venv", paths=True)
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
For cell-aware search of Jupyter notebooks (see [Notebooks](#notebooks)):
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from rgapi import nbrg
|
|
23
|
+
|
|
24
|
+
nbrg("read_csv", ".", cell_context=1)
|
|
25
|
+
```
|
|
26
|
+
|
|
19
27
|
For direct access to the regex, search, and walk pieces:
|
|
20
28
|
|
|
21
29
|
```python
|
|
@@ -38,7 +46,7 @@ pip install rgapi
|
|
|
38
46
|
|
|
39
47
|
## Semantics
|
|
40
48
|
|
|
41
|
-
`fd` and `walk` return slash-separated paths relative to `root`. They use the `ignore` crate, so `.gitignore
|
|
49
|
+
`fd` and `walk` return slash-separated paths relative to `root`. They use the `ignore` crate, so `.gitignore`, `.ignore`, and the usual ripgrep filters apply by default. `.rgignore` files are also honored and take precedence over `.gitignore`. Hidden files are skipped unless `hidden=True`. Pass `ignore=False` to disable all ignore filtering (including `.rgignore`). Symlinks are not followed unless `follow_links=True`; `same_file_system=True` avoids crossing filesystem boundaries. Traversal is parallel, and result order is not guaranteed; use `sorted(...)` if order matters.
|
|
42
50
|
`root` arguments accept `str` or `pathlib.Path` and expand `~`; `search_path` also accepts path-like file paths. Display labels such as `display_path` are stringified without expansion.
|
|
43
51
|
|
|
44
52
|
`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.
|
|
@@ -63,6 +71,35 @@ matches list of (start, end) byte offsets for match rows
|
|
|
63
71
|
|
|
64
72
|
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.
|
|
65
73
|
|
|
74
|
+
## Notebooks
|
|
75
|
+
|
|
76
|
+
`nbrg` searches Jupyter `.ipynb` files cell-by-cell, so results are *cells* rather than raw JSON lines. Searching a notebook with plain `rg` matches the escaped JSON text (including outputs and metadata) and reports JSON line numbers; `nbrg` instead searches each cell's reconstructed **source** and returns the cells that matched.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from rgapi import nbrg
|
|
80
|
+
|
|
81
|
+
nbrg("read_csv", ".") # cells whose source matches, across all notebooks under "."
|
|
82
|
+
nbrg("read_csv", ".", cell_context=1) # also include neighbouring cells as context
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It finds notebooks with the fast parallel Rust walker (`fd`), then matches each cell's source through the same Rust engine as `rg` (via `search_text`), so regex behaviour and the `case_sensitive`/`smart_case` flags match `rg`. Only cell `source` is searched, not outputs or metadata. `nbrg` accepts the same discovery filters as `fd`/`rg` (`include`, `exclude`, `glob`, `hidden`, `max_depth`, `skip_dir`, …).
|
|
86
|
+
|
|
87
|
+
`nbrg` returns `NbResults`, a list of `NbCell`. Each `NbCell` has:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
path notebook path relative to root
|
|
91
|
+
cell_index 0-based position of the cell in the notebook
|
|
92
|
+
cell_id nbformat cell id (falls back to the cell index for notebooks without ids)
|
|
93
|
+
cell_type 'code', 'markdown', or 'raw'
|
|
94
|
+
kind 'match' or 'context'
|
|
95
|
+
source full cell source
|
|
96
|
+
matches list of SearchLine rows for the matched lines within the cell
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`NbCell.asdict()` returns those fields as a plain dict (with `matches` as `SearchLine` dicts). `str()`/pretty display is one truncated, newline-escaped line per cell: `path:cell_id:source` for matches and `path:cell_id-source` for context cells. A cell with several matches appears once, with every hit collected in `matches`.
|
|
100
|
+
|
|
101
|
+
`cell_context=N` includes the `N` cells before and after each matching cell as `kind="context"` rows (deduplicated per notebook). `prefilter=True` first narrows candidate notebooks with `rg` before the per-cell search; it is faster on notebook-heavy trees but can miss matches whose pattern is affected by JSON escaping (quotes, backslashes, regex metacharacters straddling escapes), so it is off by default.
|
|
102
|
+
|
|
66
103
|
## Benchmarks
|
|
67
104
|
|
|
68
105
|
`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:
|
|
@@ -23,6 +23,9 @@ Homepage = "https://github.com/AnswerDotAI/rgapi"
|
|
|
23
23
|
Repository = "https://github.com/AnswerDotAI/rgapi"
|
|
24
24
|
Issues = "https://github.com/AnswerDotAI/rgapi/issues"
|
|
25
25
|
|
|
26
|
+
[project.entry-points.pyskills]
|
|
27
|
+
rgapi = "rgapi.skill"
|
|
28
|
+
|
|
26
29
|
[project.entry-points.fastaudit_safe_native]
|
|
27
30
|
rgapi = "rgapi"
|
|
28
31
|
|
|
@@ -196,16 +196,6 @@ def search_path(
|
|
|
196
196
|
return SearchResults(_core.search_path(matcher, _fs_path(path), _display_path(display_path), before_context, after_context))
|
|
197
197
|
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"SearchLine",
|
|
203
|
-
"SearchResults",
|
|
204
|
-
"compile",
|
|
205
|
-
"fd",
|
|
206
|
-
"rg",
|
|
207
|
-
"rg_iter",
|
|
208
|
-
"search_path",
|
|
209
|
-
"search_text",
|
|
210
|
-
"walk",
|
|
211
|
-
]
|
|
199
|
+
from .nb import NbCell, NbResults, nbrg, search_nb
|
|
200
|
+
|
|
201
|
+
__all__ = [ "RgIter", "fd", "rg", "rg_iter", "nbrg" ]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"Notebook-aware search: run `rg`-style search over `.ipynb` files, returning matched cells instead of raw JSON lines."
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from fastcore.meta import delegates
|
|
6
|
+
|
|
7
|
+
from . import compile, fd, rg, search_text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _cell_source(cell):
|
|
11
|
+
src = cell.get("source", "")
|
|
12
|
+
return "".join(src) if isinstance(src, list) else src
|
|
13
|
+
|
|
14
|
+
def _preview(text, width=120):
|
|
15
|
+
text = text.rstrip("\n").replace("\n", "\\n")
|
|
16
|
+
return text if len(text) <= width else text[:width] + "…"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NbCell:
|
|
20
|
+
"A notebook cell that matched (or provides context for) a search."
|
|
21
|
+
def __init__(self, path, cell_index, cell_id, cell_type, kind, source, matches):
|
|
22
|
+
self.path,self.cell_index,self.cell_id = path,cell_index,cell_id
|
|
23
|
+
self.cell_type,self.kind,self.source,self.matches = cell_type,kind,source,matches
|
|
24
|
+
|
|
25
|
+
def asdict(self):
|
|
26
|
+
return dict(path=self.path, cell_index=self.cell_index, cell_id=self.cell_id,
|
|
27
|
+
cell_type=self.cell_type, kind=self.kind, source=self.source,
|
|
28
|
+
matches=[m.asdict() for m in self.matches])
|
|
29
|
+
|
|
30
|
+
def __repr__(self):
|
|
31
|
+
return (f"NbCell(path={self.path!r}, cell_id={self.cell_id!r}, cell_type={self.cell_type!r}, "
|
|
32
|
+
f"kind={self.kind!r}, matches={len(self.matches)})")
|
|
33
|
+
|
|
34
|
+
def __str__(self):
|
|
35
|
+
sep = ":" if self.kind == "match" else "-"
|
|
36
|
+
return f"{self.path}:{self.cell_id}{sep}{_preview(self.source)}"
|
|
37
|
+
|
|
38
|
+
def _repr_pretty_(self, p, cycle): p.text("..." if cycle else str(self))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NbResults(list):
|
|
42
|
+
"List of `NbCell` rows with rg-style text display."
|
|
43
|
+
def __str__(self): return "\n".join(map(str, self))
|
|
44
|
+
def _repr_pretty_(self, p, cycle): p.text("..." if cycle else str(self))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def search_nb(
|
|
48
|
+
matcher, # Compiled `Regex` from `compile()`
|
|
49
|
+
path, # Notebook file to search (expands `~`)
|
|
50
|
+
cell_context:int=0, # Cells of context to include before/after each matching cell
|
|
51
|
+
display_path:str=None # Path stored in results; defaults to `path`
|
|
52
|
+
) -> NbResults:
|
|
53
|
+
"Search one `.ipynb` file's cell sources with a compiled matcher."
|
|
54
|
+
disp = display_path if display_path is not None else str(path)
|
|
55
|
+
try: nb = json.loads(Path(path).expanduser().read_text())
|
|
56
|
+
except (ValueError, OSError, UnicodeDecodeError): return NbResults()
|
|
57
|
+
cells = nb.get("cells")
|
|
58
|
+
if not isinstance(cells, list): return NbResults()
|
|
59
|
+
info,matched = {},{}
|
|
60
|
+
for i,cell in enumerate(cells):
|
|
61
|
+
if not isinstance(cell, dict): continue
|
|
62
|
+
src = _cell_source(cell)
|
|
63
|
+
info[i] = (str(cell.get("id", i)), cell.get("cell_type", ""), src)
|
|
64
|
+
hits = [m for m in search_text(matcher, src, path=disp) if m.kind == "match"]
|
|
65
|
+
if hits: matched[i] = hits
|
|
66
|
+
if not matched: return NbResults()
|
|
67
|
+
emit = {i:"match" for i in matched}
|
|
68
|
+
if cell_context:
|
|
69
|
+
for i in matched:
|
|
70
|
+
for j in range(max(0, i-cell_context), min(len(cells), i+cell_context+1)): emit.setdefault(j, "context")
|
|
71
|
+
res = NbResults()
|
|
72
|
+
for i in sorted(emit):
|
|
73
|
+
if i not in info: continue
|
|
74
|
+
cid,ctype,src = info[i]
|
|
75
|
+
res.append(NbCell(disp, i, cid, ctype, emit[i], src, matched.get(i, [])))
|
|
76
|
+
return res
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@delegates(fd, but=["ext", "files", "dirs"])
|
|
80
|
+
def nbrg(
|
|
81
|
+
pattern:str, # Regex pattern to search for
|
|
82
|
+
root:str=".", # Directory to search (expands `~`)
|
|
83
|
+
prefilter:bool=False, # Pre-narrow notebooks with `rg` before per-cell search (may miss escape-affected patterns)
|
|
84
|
+
cell_context:int=0, # Cells of context to include before/after each matching cell
|
|
85
|
+
case_sensitive:bool|None=None,# True/False forces case; None allows `smart_case`
|
|
86
|
+
smart_case:bool=False, # Match `rg --smart-case` behavior
|
|
87
|
+
**kwargs
|
|
88
|
+
) -> NbResults:
|
|
89
|
+
"Search `.ipynb` cell sources under `root`, returning matched cells."
|
|
90
|
+
matcher = compile(pattern, case_sensitive=case_sensitive, smart_case=smart_case)
|
|
91
|
+
if prefilter: paths = rg(pattern, root, ext="ipynb", case_sensitive=case_sensitive, smart_case=smart_case, paths=True, **kwargs)
|
|
92
|
+
else: paths = fd(root, ext="ipynb", **kwargs)
|
|
93
|
+
base = Path(root).expanduser()
|
|
94
|
+
res = NbResults()
|
|
95
|
+
for rel in sorted(paths): res += search_nb(matcher, base/rel, cell_context=cell_context, display_path=rel)
|
|
96
|
+
return res
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Fast and flexible file discovery and search for Python. Use this when code needs `fd`-style file finding or `rg`-style searching.
|
|
2
|
+
|
|
3
|
+
rgapi wraps the same `ignore`, `grep-regex`, and `grep-searcher` crates ripgrep uses, so `.gitignore`/`.ignore`/`.rgignore`, hidden-file handling, glob/ext filters, and regex matching all behave like `rg`. Walking and searching run in parallel and most work stays in Rust, so results come back as structured Python objects instead of CLI text to parse. Prefer rgapi over shelling out to `rg`/`fd` or scanning files by hand: you get typed rows, byte-offset match spans, and lazy iteration.
|
|
4
|
+
|
|
5
|
+
Core APIs:
|
|
6
|
+
- `fd(root=".", ...)` finds paths with fd-style filters (`pattern` substring, `include`/`exclude`/`glob`, `ext`); returns slash-separated relative paths.
|
|
7
|
+
- `rg(pattern, root=".", ...)` searches and returns `SearchResults` (or `paths=True` for unique matched paths, `count=True` for a match-span total). NB: The `SearchResults` repr shows an rg-style multiline string, which is usually the most ergonomic approach.
|
|
8
|
+
- `nbrg(pattern, root=".", cell_context=0, ...)` searches Jupyter `.ipynb` files (cell source only) and returns matched cells as `NbResults`/`NbCell`, which have a nice repr like rg(). Use this for notebooks rather than `rg`, to avoid escaping problems, and to get back message IDs.
|
|
9
|
+
|
|
10
|
+
SearchLine rows:
|
|
11
|
+
kind 'match', 'before', 'after', or 'context'
|
|
12
|
+
path path relative to root
|
|
13
|
+
line_number 1-based line number
|
|
14
|
+
line line text without the trailing newline
|
|
15
|
+
matches list of (start, end) byte offsets, for 'match' rows
|
|
16
|
+
asdict() returns the row fields as a plain dict
|
|
17
|
+
|
|
18
|
+
NbCell rows (from `nbrg`):
|
|
19
|
+
path/cell_index/cell_id/cell_type locate the cell ('code'/'markdown'/'raw')
|
|
20
|
+
kind 'match' or 'context'
|
|
21
|
+
source full cell source
|
|
22
|
+
matches list of SearchLine rows for the matched lines within the cell
|
|
23
|
+
asdict() returns the cell fields as a plain dict
|
|
24
|
+
`cell_context=N` adds N neighbour cells as 'context'. `prefilter=True` narrows notebooks with `rg` first (faster, but can miss patterns affected by JSON escaping; off by default).
|
|
25
|
+
|
|
26
|
+
Important:
|
|
27
|
+
Traversal is parallel and result order is NOT guaranteed; wrap in `sorted(...)` if you need stable order. `path_re`/`skip_path_re` filter the returned/searched paths but do not prune traversal; use `skip_dir`/`skip_dir_re` to prune whole subtrees for speed. Run `doc(func)` for full parameter docments.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from . import Regex, RgIter, SearchLine, SearchResults, compile, fd, rg, rg_iter, search_path, search_text, walk
|
|
31
|
+
from . import NbCell, NbResults, nbrg, search_nb
|
|
32
|
+
|
|
33
|
+
__all__ = [ "RgIter", "fd", "rg", "rg_iter", "nbrg" ]
|
|
34
|
+
|
|
@@ -234,6 +234,7 @@ fn walk_py(
|
|
|
234
234
|
same_file_system,
|
|
235
235
|
files,
|
|
236
236
|
dirs,
|
|
237
|
+
panic_probe: false,
|
|
237
238
|
};
|
|
238
239
|
find(&opts).map_err(|e| PyValueError::new_err(e.to_string()))
|
|
239
240
|
}
|
|
@@ -277,6 +278,7 @@ fn find_py(
|
|
|
277
278
|
same_file_system,
|
|
278
279
|
files,
|
|
279
280
|
dirs,
|
|
281
|
+
panic_probe: false,
|
|
280
282
|
};
|
|
281
283
|
find(&opts).map_err(|e| PyValueError::new_err(e.to_string()))
|
|
282
284
|
}
|
|
@@ -365,6 +367,7 @@ fn rg_py(
|
|
|
365
367
|
smart_case,
|
|
366
368
|
before_context,
|
|
367
369
|
after_context,
|
|
370
|
+
panic_probe: false,
|
|
368
371
|
};
|
|
369
372
|
let iter = rg_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
370
373
|
collect_rg_py(py, iter)
|
|
@@ -412,12 +415,37 @@ fn rg_iter_py(
|
|
|
412
415
|
smart_case,
|
|
413
416
|
before_context,
|
|
414
417
|
after_context,
|
|
418
|
+
panic_probe: false,
|
|
415
419
|
};
|
|
416
420
|
rg_iter_core(&opts)
|
|
417
421
|
.map(|inner| RgIterPy { inner })
|
|
418
422
|
.map_err(|e| PyValueError::new_err(e.to_string()))
|
|
419
423
|
}
|
|
420
424
|
|
|
425
|
+
#[pyfunction(name = "panic_probe")]
|
|
426
|
+
#[pyo3(signature = (root=".", walk=false))]
|
|
427
|
+
fn panic_probe_py(py: Python<'_>, root: &str, walk: bool) -> PyResult<()> {
|
|
428
|
+
let root = PathBuf::from(root);
|
|
429
|
+
if walk {
|
|
430
|
+
let opts = FindOptions {
|
|
431
|
+
root,
|
|
432
|
+
panic_probe: true,
|
|
433
|
+
..FindOptions::default()
|
|
434
|
+
};
|
|
435
|
+
find(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
436
|
+
} else {
|
|
437
|
+
let opts = RgOptions {
|
|
438
|
+
root,
|
|
439
|
+
pattern: "panic_probe".to_string(),
|
|
440
|
+
panic_probe: true,
|
|
441
|
+
..RgOptions::default()
|
|
442
|
+
};
|
|
443
|
+
let iter = rg_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
444
|
+
collect_rg_py(py, iter)?;
|
|
445
|
+
}
|
|
446
|
+
Ok(())
|
|
447
|
+
}
|
|
448
|
+
|
|
421
449
|
impl From<SearchLine> for SearchLinePy {
|
|
422
450
|
fn from(line: SearchLine) -> Self {
|
|
423
451
|
Self {
|
|
@@ -442,5 +470,6 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
442
470
|
m.add_function(wrap_pyfunction!(rg_iter_py, m)?)?;
|
|
443
471
|
m.add_function(wrap_pyfunction!(search_text_py, m)?)?;
|
|
444
472
|
m.add_function(wrap_pyfunction!(search_path_py, m)?)?;
|
|
473
|
+
m.add_function(wrap_pyfunction!(panic_probe_py, m)?)?;
|
|
445
474
|
Ok(())
|
|
446
475
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
1
2
|
use std::path::{Path, PathBuf};
|
|
2
3
|
use std::sync::{
|
|
3
4
|
atomic::{AtomicBool, Ordering},
|
|
@@ -69,6 +70,7 @@ pub struct RgOptions {
|
|
|
69
70
|
pub smart_case: bool,
|
|
70
71
|
pub before_context: usize,
|
|
71
72
|
pub after_context: usize,
|
|
73
|
+
pub panic_probe: bool,
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
impl Default for RgOptions {
|
|
@@ -93,6 +95,7 @@ impl Default for RgOptions {
|
|
|
93
95
|
smart_case: false,
|
|
94
96
|
before_context: 0,
|
|
95
97
|
after_context: 0,
|
|
98
|
+
panic_probe: false,
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
}
|
|
@@ -179,6 +182,7 @@ fn run_parallel_search(
|
|
|
179
182
|
filter_dirs(&mut walker, &root, filters.clone());
|
|
180
183
|
let before_context = opts.before_context;
|
|
181
184
|
let after_context = opts.after_context;
|
|
185
|
+
let panic_probe = opts.panic_probe;
|
|
182
186
|
walker.build_parallel().run(|| {
|
|
183
187
|
let tx = tx.clone();
|
|
184
188
|
let root = root.clone();
|
|
@@ -188,16 +192,27 @@ fn run_parallel_search(
|
|
|
188
192
|
let after_context = after_context;
|
|
189
193
|
let cancel = cancel.clone();
|
|
190
194
|
Box::new(move |entry| {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
catch_unwind(AssertUnwindSafe(|| {
|
|
196
|
+
if panic_probe {
|
|
197
|
+
panic!("rgapi: deliberate panic for tests (panic_probe)");
|
|
198
|
+
}
|
|
199
|
+
search_entry(
|
|
200
|
+
entry,
|
|
201
|
+
&root,
|
|
202
|
+
&filters,
|
|
203
|
+
&matcher,
|
|
204
|
+
before_context,
|
|
205
|
+
after_context,
|
|
206
|
+
&tx,
|
|
207
|
+
&cancel,
|
|
208
|
+
)
|
|
209
|
+
}))
|
|
210
|
+
.unwrap_or_else(|_| {
|
|
211
|
+
let _ = tx.send(Err(RgApiError::new(
|
|
212
|
+
"internal error during search (this is a bug, please report it)",
|
|
213
|
+
)));
|
|
214
|
+
WalkState::Quit
|
|
215
|
+
})
|
|
201
216
|
})
|
|
202
217
|
});
|
|
203
218
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
1
2
|
use std::path::{Path, PathBuf};
|
|
2
3
|
use std::sync::{mpsc, Arc};
|
|
3
4
|
|
|
@@ -27,6 +28,7 @@ pub struct FindOptions {
|
|
|
27
28
|
pub same_file_system: bool,
|
|
28
29
|
pub files: bool,
|
|
29
30
|
pub dirs: bool,
|
|
31
|
+
pub panic_probe: bool,
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
impl Default for FindOptions {
|
|
@@ -49,6 +51,7 @@ impl Default for FindOptions {
|
|
|
49
51
|
same_file_system: false,
|
|
50
52
|
files: true,
|
|
51
53
|
dirs: false,
|
|
54
|
+
panic_probe: false,
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -79,13 +82,25 @@ pub fn find(opts: &FindOptions) -> Result<Vec<String>, RgApiError> {
|
|
|
79
82
|
let pattern = opts.pattern.clone();
|
|
80
83
|
let files = opts.files;
|
|
81
84
|
let dirs = opts.dirs;
|
|
85
|
+
let panic_probe = opts.panic_probe;
|
|
82
86
|
walker.build_parallel().run(|| {
|
|
83
87
|
let tx = tx.clone();
|
|
84
88
|
let root = root.clone();
|
|
85
89
|
let filters = filters.clone();
|
|
86
90
|
let pattern = pattern.clone();
|
|
87
91
|
Box::new(move |entry| {
|
|
88
|
-
|
|
92
|
+
let outcome = catch_unwind(AssertUnwindSafe(|| {
|
|
93
|
+
if panic_probe {
|
|
94
|
+
panic!("rgapi: deliberate panic for tests (panic_probe)");
|
|
95
|
+
}
|
|
96
|
+
find_entry(entry, &root, &filters, pattern.as_deref(), files, dirs)
|
|
97
|
+
}))
|
|
98
|
+
.unwrap_or_else(|_| {
|
|
99
|
+
Err(RgApiError::new(
|
|
100
|
+
"internal error during walk (this is a bug, please report it)",
|
|
101
|
+
))
|
|
102
|
+
});
|
|
103
|
+
match outcome {
|
|
89
104
|
Ok(Some(path)) => {
|
|
90
105
|
if tx.send(Ok(path)).is_err() {
|
|
91
106
|
return WalkState::Quit;
|
|
@@ -170,6 +185,9 @@ pub(crate) fn configure_walker(
|
|
|
170
185
|
same_file_system: bool,
|
|
171
186
|
) {
|
|
172
187
|
walker.standard_filters(ignore);
|
|
188
|
+
if ignore {
|
|
189
|
+
walker.add_custom_ignore_filename(".rgignore");
|
|
190
|
+
}
|
|
173
191
|
walker.hidden(!hidden);
|
|
174
192
|
walker.require_git(false);
|
|
175
193
|
walker.max_depth(max_depth);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import _thread, threading
|
|
1
|
+
import _thread, json, threading
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
+
from rgapi import _core
|
|
5
6
|
from rgapi import Regex, SearchResults, compile, fd, rg, rg_iter, search_path, search_text, walk
|
|
6
7
|
|
|
7
8
|
|
|
@@ -72,6 +73,20 @@ def test_path_filters_prune_dirs_and_follow_links(tmp_path):
|
|
|
72
73
|
assert fd(str(tmp_path), path_re=r"linked/.*\.py$", follow_links=False) == []
|
|
73
74
|
assert fd(str(tmp_path), path_re=r"linked/.*\.py$", follow_links=True) == ["linked/app.py"]
|
|
74
75
|
|
|
76
|
+
def test_rgignore_is_honored(tmp_path):
|
|
77
|
+
(tmp_path / ".rgignore").write_text("only_rg.txt\n")
|
|
78
|
+
(tmp_path / "only_rg.txt").write_text("hi\n")
|
|
79
|
+
(tmp_path / "keep.txt").write_text("hi\n")
|
|
80
|
+
assert set(fd(str(tmp_path))) == {"keep.txt"}
|
|
81
|
+
assert set(fd(str(tmp_path), ignore=False)) == {"keep.txt", "only_rg.txt"}
|
|
82
|
+
|
|
83
|
+
def test_rgignore_can_override_gitignore(tmp_path):
|
|
84
|
+
(tmp_path / ".gitignore").write_text("*/\n")
|
|
85
|
+
(tmp_path / ".rgignore").write_text("!*/\n")
|
|
86
|
+
(tmp_path / "sub").mkdir()
|
|
87
|
+
(tmp_path / "sub" / "app.py").write_text("hi\n")
|
|
88
|
+
assert "sub/app.py" in set(fd(str(tmp_path)))
|
|
89
|
+
|
|
75
90
|
def test_depth_size_and_filesystem_options(tmp_path):
|
|
76
91
|
(tmp_path / "top.txt").write_text("TODO\n")
|
|
77
92
|
sub = tmp_path / "sub"
|
|
@@ -121,6 +136,15 @@ def test_rg_returns_structured_matches_context_and_relative_paths(tmp_path):
|
|
|
121
136
|
assert str(stream) == repr(stream)
|
|
122
137
|
|
|
123
138
|
|
|
139
|
+
def test_worker_panic_surfaces_as_error_not_truncation(tmp_path):
|
|
140
|
+
# A panic inside a parallel search/walk worker must raise, not silently end the
|
|
141
|
+
# result stream (which would look like "no matches"). `_core.panic_probe` arms the
|
|
142
|
+
# panic flag and runs the real search/walk machinery so the catch_unwind path is exercised.
|
|
143
|
+
(tmp_path / "a.py").write_text("TODO here\n")
|
|
144
|
+
with pytest.raises(Exception): _core.panic_probe(str(tmp_path)) # search workers
|
|
145
|
+
with pytest.raises(Exception): _core.panic_probe(str(tmp_path), walk=True) # walk workers
|
|
146
|
+
|
|
147
|
+
|
|
124
148
|
def test_search_path_skips_binary_and_invalid_utf8(tmp_path):
|
|
125
149
|
(tmp_path / "bin.dat").write_bytes(b"TODO before\n\0TODO after\n")
|
|
126
150
|
(tmp_path / "bad.txt").write_bytes(b"TODO\xff\n")
|
|
@@ -164,3 +188,107 @@ def test_direct_regex_and_search_apis(tmp_path):
|
|
|
164
188
|
assert [(r.kind, r.path, r.line_number, r.line, r.matches) for r in path_res] == [
|
|
165
189
|
("match", "display.py", 2, "TODO here", [(0, 4)])]
|
|
166
190
|
assert path_res[0].asdict() == dict(kind="match", path="display.py", line_number=2, line="TODO here", matches=[(0, 4)])
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def write_nb(path, cells):
|
|
194
|
+
nb = {"cells": cells, "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
|
195
|
+
path.write_text(json.dumps(nb))
|
|
196
|
+
|
|
197
|
+
def _cell(cell_type, source, cid=None, outputs=None):
|
|
198
|
+
c = {"cell_type": cell_type, "metadata": {}, "source": source}
|
|
199
|
+
if cell_type == "code":
|
|
200
|
+
c["execution_count"] = None
|
|
201
|
+
c["outputs"] = outputs or []
|
|
202
|
+
if cid is not None: c["id"] = cid
|
|
203
|
+
return c
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_nbrg_source_only_with_cells(tmp_path):
|
|
207
|
+
from rgapi import nbrg
|
|
208
|
+
write_nb(tmp_path / "nb.ipynb", [
|
|
209
|
+
_cell("code", ["import os\n", "def foo():\n", " return 1\n"], cid="c1",
|
|
210
|
+
outputs=[{"output_type": "stream", "name": "stdout", "text": ["foo ran\n"]}]),
|
|
211
|
+
_cell("code", ["print('hi')\n"], cid="c2",
|
|
212
|
+
outputs=[{"output_type": "stream", "name": "stdout", "text": ["foo in output\n"]}]),
|
|
213
|
+
_cell("markdown", ["# Title\n", "use foo here\n"], cid="m1"),
|
|
214
|
+
])
|
|
215
|
+
res = nbrg("foo", str(tmp_path))
|
|
216
|
+
# c2 has 'foo' only in its output, so source-only search must skip it
|
|
217
|
+
assert {c.cell_id for c in res} == {"c1", "m1"}
|
|
218
|
+
c1 = next(c for c in res if c.cell_id == "c1")
|
|
219
|
+
assert c1.cell_type == "code" and c1.kind == "match"
|
|
220
|
+
assert [m.line_number for m in c1.matches] == [2]
|
|
221
|
+
assert c1.matches[0].line == "def foo():"
|
|
222
|
+
assert c1.matches[0].matches == [(4, 7)]
|
|
223
|
+
m1 = next(c for c in res if c.cell_id == "m1")
|
|
224
|
+
assert m1.cell_type == "markdown"
|
|
225
|
+
assert [m.line for m in m1.matches] == ["use foo here"]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_nbrg_string_source_and_missing_id(tmp_path):
|
|
229
|
+
from rgapi import nbrg
|
|
230
|
+
write_nb(tmp_path / "nb.ipynb", [_cell("code", "x = 1\nfoo = 2\n")])
|
|
231
|
+
res = nbrg("foo", str(tmp_path))
|
|
232
|
+
assert len(res) == 1
|
|
233
|
+
assert res[0].cell_id == "0"
|
|
234
|
+
assert res[0].matches[0].line == "foo = 2"
|
|
235
|
+
assert res[0].matches[0].line_number == 2
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_nbrg_multiple_matches_single_cell_appears_once(tmp_path):
|
|
239
|
+
from rgapi import nbrg
|
|
240
|
+
write_nb(tmp_path / "nb.ipynb", [
|
|
241
|
+
_cell("code", ["foo = 1\n", "bar = 2\n", "baz = foo + foo\n"], cid="c1"),
|
|
242
|
+
])
|
|
243
|
+
res = nbrg("foo", str(tmp_path))
|
|
244
|
+
assert len(res) == 1 # cell appears once, not once per match
|
|
245
|
+
assert [c.cell_id for c in res] == ["c1"]
|
|
246
|
+
cell = res[0]
|
|
247
|
+
assert [m.line_number for m in cell.matches] == [1, 3] # both matching lines kept
|
|
248
|
+
assert cell.matches[1].matches == [(6, 9), (12, 15)] # two spans on the same line
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_nbrg_cell_context(tmp_path):
|
|
252
|
+
from rgapi import nbrg
|
|
253
|
+
write_nb(tmp_path / "nb.ipynb", [
|
|
254
|
+
_cell("code", ["a = 1\n"], cid="c0"),
|
|
255
|
+
_cell("code", ["target = 2\n"], cid="c1"),
|
|
256
|
+
_cell("code", ["b = 3\n"], cid="c2"),
|
|
257
|
+
_cell("code", ["c = 4\n"], cid="c3"),
|
|
258
|
+
])
|
|
259
|
+
res = nbrg("target", str(tmp_path), cell_context=1)
|
|
260
|
+
kinds = {c.cell_id: c.kind for c in res}
|
|
261
|
+
assert kinds == {"c0": "context", "c1": "match", "c2": "context"}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_nbrg_skips_bad_json_and_prefilter_parity(tmp_path):
|
|
265
|
+
from rgapi import nbrg
|
|
266
|
+
write_nb(tmp_path / "good.ipynb", [_cell("code", ["read_csv(x)\n"], cid="g1")])
|
|
267
|
+
(tmp_path / "bad.ipynb").write_text("{not valid json")
|
|
268
|
+
a = nbrg("read_csv", str(tmp_path))
|
|
269
|
+
b = nbrg("read_csv", str(tmp_path), prefilter=True)
|
|
270
|
+
assert {c.cell_id for c in a} == {"g1"}
|
|
271
|
+
assert {c.cell_id for c in b} == {"g1"}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_search_nb_single_and_asdict_str(tmp_path):
|
|
275
|
+
from rgapi import compile, search_nb
|
|
276
|
+
p = tmp_path / "nb.ipynb"
|
|
277
|
+
write_nb(p, [_cell("code", ["def foo():\n", " return 1\n"], cid="c1")])
|
|
278
|
+
res = search_nb(compile("foo"), p, display_path="nb.ipynb")
|
|
279
|
+
assert len(res) == 1
|
|
280
|
+
cell = res[0]
|
|
281
|
+
assert str(cell) == "nb.ipynb:c1:def foo():\\n return 1" # cell-oriented: whole cell, newlines escaped
|
|
282
|
+
d = cell.asdict()
|
|
283
|
+
assert d["cell_id"] == "c1" and d["cell_type"] == "code" and d["kind"] == "match"
|
|
284
|
+
assert d["matches"][0]["line"] == "def foo():"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_nbcell_str_truncates_and_escapes(tmp_path):
|
|
288
|
+
from rgapi import compile, search_nb
|
|
289
|
+
p = tmp_path / "nb.ipynb"
|
|
290
|
+
write_nb(p, [_cell("code", ["x = '" + "a" * 500 + "' # foo\n"], cid="c1")])
|
|
291
|
+
s = str(search_nb(compile("foo"), p, display_path="nb.ipynb")[0])
|
|
292
|
+
assert "\n" not in s # newlines escaped to a single display line
|
|
293
|
+
assert s.endswith("…") # long cell is truncated
|
|
294
|
+
assert s.startswith("nb.ipynb:c1:")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|