rgapi 0.1.0__tar.gz → 0.1.1__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.0 → rgapi-0.1.1}/.github/workflows/ci.yml +3 -3
- {rgapi-0.1.0 → rgapi-0.1.1}/Cargo.lock +1 -1
- {rgapi-0.1.0 → rgapi-0.1.1}/Cargo.toml +1 -1
- {rgapi-0.1.0 → rgapi-0.1.1}/PKG-INFO +3 -1
- {rgapi-0.1.0 → rgapi-0.1.1}/README.md +2 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/pyproject.toml +1 -1
- rgapi-0.1.1/python/rgapi/__init__.py +208 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/src/python.rs +40 -10
- {rgapi-0.1.0 → rgapi-0.1.1}/src/search.rs +91 -7
- {rgapi-0.1.0 → rgapi-0.1.1}/tests/test_rgapi.py +13 -0
- rgapi-0.1.0/python/rgapi/__init__.py +0 -106
- {rgapi-0.1.0 → rgapi-0.1.1}/.gitignore +0 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/DEV.md +0 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/src/lib.rs +0 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/src/walk.rs +0 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/tools/bench.py +0 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/tools/build.sh +0 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/tools/bump.sh +0 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/tools/bump2.sh +0 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/tools/release.sh +0 -0
- {rgapi-0.1.0 → rgapi-0.1.1}/tools/test.sh +0 -0
|
@@ -10,7 +10,7 @@ jobs:
|
|
|
10
10
|
test:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
14
|
- run: cargo test
|
|
15
15
|
|
|
16
16
|
build:
|
|
@@ -21,7 +21,7 @@ jobs:
|
|
|
21
21
|
python: ['3.10', '3.11', '3.12', '3.13']
|
|
22
22
|
runs-on: ${{ matrix.os }}
|
|
23
23
|
steps:
|
|
24
|
-
- uses: actions/checkout@
|
|
24
|
+
- uses: actions/checkout@v6
|
|
25
25
|
- uses: dtolnay/rust-toolchain@stable
|
|
26
26
|
- uses: actions/setup-python@v5
|
|
27
27
|
with:
|
|
@@ -44,7 +44,7 @@ jobs:
|
|
|
44
44
|
id-token: write
|
|
45
45
|
contents: write
|
|
46
46
|
steps:
|
|
47
|
-
- uses: actions/checkout@
|
|
47
|
+
- uses: actions/checkout@v6
|
|
48
48
|
- uses: PyO3/maturin-action@v1
|
|
49
49
|
with:
|
|
50
50
|
command: sdist
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rgapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Summary: Python API for ripgrep-style file walking and searching
|
|
@@ -77,6 +77,8 @@ matches list of (start, end) byte offsets for match rows
|
|
|
77
77
|
|
|
78
78
|
Search is case-sensitive by default, matching `rg`. Use `smart_case=True` for `rg --smart-case` behavior, or `case_sensitive=False` to force case-insensitive matching.
|
|
79
79
|
|
|
80
|
+
`rg` and `rg_iter` check Python signals while waiting for search results, so notebook interrupts and `KeyboardInterrupt` cancel traversal cooperatively. Cancellation is prompt between files and while results are streaming; a single huge file that produces no callbacks may continue until that file scan returns.
|
|
81
|
+
|
|
80
82
|
## Benchmarks
|
|
81
83
|
|
|
82
84
|
`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:
|
|
@@ -62,6 +62,8 @@ matches list of (start, end) byte offsets for match rows
|
|
|
62
62
|
|
|
63
63
|
Search is case-sensitive by default, matching `rg`. Use `smart_case=True` for `rg --smart-case` behavior, or `case_sensitive=False` to force case-insensitive matching.
|
|
64
64
|
|
|
65
|
+
`rg` and `rg_iter` check Python signals while waiting for search results, so notebook interrupts and `KeyboardInterrupt` cancel traversal cooperatively. Cancellation is prompt between files and while results are streaming; a single huge file that produces no callbacks may continue until that file scan returns.
|
|
66
|
+
|
|
65
67
|
## Benchmarks
|
|
66
68
|
|
|
67
69
|
`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:
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from . import _core
|
|
2
|
+
|
|
3
|
+
Regex = _core.Regex
|
|
4
|
+
SearchLine = _core.SearchLine
|
|
5
|
+
RgIter = _core.RgIter
|
|
6
|
+
def compile(
|
|
7
|
+
pattern, # Regex pattern to compile
|
|
8
|
+
case_sensitive=None, # True/False forces case; None allows `smart_case`
|
|
9
|
+
smart_case=False # Match `rg --smart-case` behavior
|
|
10
|
+
):
|
|
11
|
+
"Compile a regex matcher for `search_text`, `search_path`, and direct matching."
|
|
12
|
+
return _core.compile(pattern, case_sensitive=case_sensitive, smart_case=smart_case)
|
|
13
|
+
|
|
14
|
+
class SearchResults(list):
|
|
15
|
+
"List of `SearchLine` rows with rg-style text display."
|
|
16
|
+
def __str__(self): return "\n".join(map(str, self))
|
|
17
|
+
def _repr_pretty_(self, p, cycle): p.text("..." if cycle else str(self))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _listify(value):
|
|
21
|
+
if value is None: return []
|
|
22
|
+
if isinstance(value, str): return [value]
|
|
23
|
+
return list(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _filters(glob=None, include=None, exclude=None, ext=None):
|
|
27
|
+
includes = _listify(include) + _listify(glob)
|
|
28
|
+
for suffix in _listify(ext):
|
|
29
|
+
suffix = str(suffix)
|
|
30
|
+
if suffix.startswith("."): suffix = suffix[1:]
|
|
31
|
+
includes.append(f"*.{suffix}")
|
|
32
|
+
return includes, _listify(exclude)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _context(context, before_context, after_context):
|
|
36
|
+
if context: return context, context
|
|
37
|
+
return before_context, after_context
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def walk(
|
|
41
|
+
root=".", # Directory to walk
|
|
42
|
+
hidden=False, # Include hidden files and directories
|
|
43
|
+
ignore=True, # Respect `.gitignore` and other ignore files
|
|
44
|
+
max_depth=None, # Maximum directory depth to descend
|
|
45
|
+
min_depth=None, # Minimum depth for returned paths
|
|
46
|
+
max_filesize=None, # Skip files larger than this many bytes
|
|
47
|
+
follow_links=False, # Follow symbolic links while walking
|
|
48
|
+
same_file_system=False, # Do not cross filesystem boundaries
|
|
49
|
+
path_re=None, # Regex that returned relative paths must match
|
|
50
|
+
skip_path_re=None, # Regex for relative paths to skip
|
|
51
|
+
skip_dir=None, # Directory glob or globs to prune
|
|
52
|
+
skip_dir_re=None, # Directory regex used to prune traversal
|
|
53
|
+
files=True, # Include files in results
|
|
54
|
+
dirs=False # Include directories in results
|
|
55
|
+
):
|
|
56
|
+
"Walk a directory and return relative file and/or directory paths."
|
|
57
|
+
return _core.walk(root, hidden, ignore, max_depth, min_depth, max_filesize, follow_links,
|
|
58
|
+
same_file_system, path_re, skip_path_re, _listify(skip_dir), skip_dir_re, files, dirs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def fd(
|
|
62
|
+
root=".", # Directory to walk
|
|
63
|
+
pattern=None, # Substring that relative paths must contain
|
|
64
|
+
glob=None, # Include glob or globs; alias for `include`
|
|
65
|
+
include=None, # Include glob or globs, e.g. `*.py`
|
|
66
|
+
exclude=None, # Exclude glob or globs, e.g. `test_*.py`
|
|
67
|
+
ext=None, # Extension or extensions to include, without needing `*.`
|
|
68
|
+
hidden=False, # Include hidden files and directories
|
|
69
|
+
ignore=True, # Respect `.gitignore` and other ignore files
|
|
70
|
+
max_depth=None, # Maximum directory depth to descend
|
|
71
|
+
min_depth=None, # Minimum depth for returned paths
|
|
72
|
+
max_filesize=None, # Skip files larger than this many bytes
|
|
73
|
+
follow_links=False, # Follow symbolic links while walking
|
|
74
|
+
same_file_system=False, # Do not cross filesystem boundaries
|
|
75
|
+
path_re=None, # Regex that returned relative paths must match
|
|
76
|
+
skip_path_re=None, # Regex for relative paths to skip
|
|
77
|
+
skip_dir=None, # Directory glob or globs to prune
|
|
78
|
+
skip_dir_re=None, # Directory regex used to prune traversal
|
|
79
|
+
files=True, # Include files in results
|
|
80
|
+
dirs=False # Include directories in results
|
|
81
|
+
):
|
|
82
|
+
"Find paths with fd-style filters and gitignore support."
|
|
83
|
+
include, exclude = _filters(glob, include, exclude, ext)
|
|
84
|
+
return _core.find(root, pattern, include, exclude, hidden, ignore, max_depth, min_depth, max_filesize,
|
|
85
|
+
follow_links, same_file_system, path_re, skip_path_re, _listify(skip_dir), skip_dir_re, files, dirs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
|
|
89
|
+
follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
|
|
90
|
+
before_context, after_context, context):
|
|
91
|
+
include, exclude = _filters(glob, include, exclude, ext)
|
|
92
|
+
before_context, after_context = _context(context, before_context, after_context)
|
|
93
|
+
return (pattern, root, include, exclude, hidden, ignore, max_depth, min_depth, max_filesize, follow_links, same_file_system,
|
|
94
|
+
path_re, skip_path_re, _listify(skip_dir), skip_dir_re, case_sensitive, smart_case, before_context, after_context)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def rg(
|
|
98
|
+
pattern, # Regex pattern to search for
|
|
99
|
+
root=".", # Directory to search
|
|
100
|
+
glob=None, # Include glob or globs; alias for `include`
|
|
101
|
+
include=None, # Include glob or globs, e.g. `*.py`
|
|
102
|
+
exclude=None, # Exclude glob or globs, e.g. `test_*.py`
|
|
103
|
+
ext=None, # Extension or extensions to include, without needing `*.`
|
|
104
|
+
hidden=False, # Include hidden files and directories
|
|
105
|
+
ignore=True, # Respect `.gitignore` and other ignore files
|
|
106
|
+
max_depth=None, # Maximum directory depth to descend
|
|
107
|
+
min_depth=None, # Minimum depth for returned/searched files
|
|
108
|
+
max_filesize=None, # Skip files larger than this many bytes
|
|
109
|
+
follow_links=False, # Follow symbolic links while walking
|
|
110
|
+
same_file_system=False, # Do not cross filesystem boundaries
|
|
111
|
+
path_re=None, # Regex that searched relative paths must match
|
|
112
|
+
skip_path_re=None, # Regex for relative paths to skip
|
|
113
|
+
skip_dir=None, # Directory glob or globs to prune
|
|
114
|
+
skip_dir_re=None, # Directory regex used to prune traversal
|
|
115
|
+
case_sensitive=None, # True/False forces case; None allows `smart_case`
|
|
116
|
+
smart_case=False, # Match `rg --smart-case` behavior
|
|
117
|
+
before_context=0, # Lines of context before each match, like `rg -B`
|
|
118
|
+
after_context=0, # Lines of context after each match, like `rg -A`
|
|
119
|
+
context=0, # Sets both before and after context, like `rg -C`
|
|
120
|
+
paths=False, # Return unique matched paths instead of rows
|
|
121
|
+
count=False # Return total match span count instead of rows
|
|
122
|
+
):
|
|
123
|
+
"Search files and return `SearchResults`, matched paths, or a count."
|
|
124
|
+
assert not (paths and count), "paths and count are mutually exclusive"
|
|
125
|
+
args = _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
|
|
126
|
+
follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
|
|
127
|
+
before_context, after_context, context)
|
|
128
|
+
if paths:
|
|
129
|
+
seen, res = set(), []
|
|
130
|
+
for row in _core.rg_iter(*args):
|
|
131
|
+
if row.kind != "match" or row.path in seen: continue
|
|
132
|
+
seen.add(row.path)
|
|
133
|
+
res.append(row.path)
|
|
134
|
+
return res
|
|
135
|
+
if count: return sum(len(row.matches) for row in _core.rg_iter(*args) if row.kind == "match")
|
|
136
|
+
return SearchResults(_core.rg(*args))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def rg_iter(
|
|
140
|
+
pattern, # Regex pattern to search for
|
|
141
|
+
root=".", # Directory to search
|
|
142
|
+
glob=None, # Include glob or globs; alias for `include`
|
|
143
|
+
include=None, # Include glob or globs, e.g. `*.py`
|
|
144
|
+
exclude=None, # Exclude glob or globs, e.g. `test_*.py`
|
|
145
|
+
ext=None, # Extension or extensions to include, without needing `*.`
|
|
146
|
+
hidden=False, # Include hidden files and directories
|
|
147
|
+
ignore=True, # Respect `.gitignore` and other ignore files
|
|
148
|
+
max_depth=None, # Maximum directory depth to descend
|
|
149
|
+
min_depth=None, # Minimum depth for returned/searched files
|
|
150
|
+
max_filesize=None, # Skip files larger than this many bytes
|
|
151
|
+
follow_links=False, # Follow symbolic links while walking
|
|
152
|
+
same_file_system=False, # Do not cross filesystem boundaries
|
|
153
|
+
path_re=None, # Regex that searched relative paths must match
|
|
154
|
+
skip_path_re=None, # Regex for relative paths to skip
|
|
155
|
+
skip_dir=None, # Directory glob or globs to prune
|
|
156
|
+
skip_dir_re=None, # Directory regex used to prune traversal
|
|
157
|
+
case_sensitive=None, # True/False forces case; None allows `smart_case`
|
|
158
|
+
smart_case=False, # Match `rg --smart-case` behavior
|
|
159
|
+
before_context=0, # Lines of context before each match, like `rg -B`
|
|
160
|
+
after_context=0, # Lines of context after each match, like `rg -A`
|
|
161
|
+
context=0 # Sets both before and after context, like `rg -C`
|
|
162
|
+
):
|
|
163
|
+
"Search files lazily, yielding `SearchLine` rows."
|
|
164
|
+
args = _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
|
|
165
|
+
follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
|
|
166
|
+
before_context, after_context, context)
|
|
167
|
+
return _core.rg_iter(*args)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def search_text(
|
|
171
|
+
matcher, # Compiled `Regex` from `compile()`
|
|
172
|
+
text, # Text to search
|
|
173
|
+
path="<text>", # Path label stored in results
|
|
174
|
+
before_context=0, # Lines of context before each match
|
|
175
|
+
after_context=0, # Lines of context after each match
|
|
176
|
+
context=0 # Sets both before and after context, like `rg -C`
|
|
177
|
+
):
|
|
178
|
+
"Search an in-memory string with a compiled matcher."
|
|
179
|
+
before_context, after_context = _context(context, before_context, after_context)
|
|
180
|
+
return SearchResults(_core.search_text(matcher, text, path, before_context, after_context))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def search_path(
|
|
184
|
+
matcher, # Compiled `Regex` from `compile()`
|
|
185
|
+
path, # File path to search
|
|
186
|
+
display_path=None, # Path stored in results; defaults to `path`
|
|
187
|
+
before_context=0, # Lines of context before each match
|
|
188
|
+
after_context=0, # Lines of context after each match
|
|
189
|
+
context=0 # Sets both before and after context, like `rg -C`
|
|
190
|
+
):
|
|
191
|
+
"Search one file with a compiled matcher."
|
|
192
|
+
before_context, after_context = _context(context, before_context, after_context)
|
|
193
|
+
return SearchResults(_core.search_path(matcher, path, display_path, before_context, after_context))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = [
|
|
197
|
+
"Regex",
|
|
198
|
+
"RgIter",
|
|
199
|
+
"SearchLine",
|
|
200
|
+
"SearchResults",
|
|
201
|
+
"compile",
|
|
202
|
+
"fd",
|
|
203
|
+
"rg",
|
|
204
|
+
"rg_iter",
|
|
205
|
+
"search_path",
|
|
206
|
+
"search_text",
|
|
207
|
+
"walk",
|
|
208
|
+
]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
use grep_matcher::Matcher;
|
|
2
2
|
use std::path::PathBuf;
|
|
3
|
+
use std::sync::mpsc::RecvTimeoutError;
|
|
4
|
+
use std::time::Duration;
|
|
3
5
|
|
|
4
6
|
use pyo3::exceptions::PyValueError;
|
|
5
7
|
use pyo3::prelude::*;
|
|
@@ -7,7 +9,7 @@ use pyo3::types::{PyAny, PyDict};
|
|
|
7
9
|
|
|
8
10
|
use crate::search::spans_for;
|
|
9
11
|
use crate::{
|
|
10
|
-
compile_regex, find,
|
|
12
|
+
compile_regex, find, rg_iter as rg_iter_core, search_path as search_path_core,
|
|
11
13
|
search_text as search_text_core, FindOptions, RgIter, RgOptions, SearchLine,
|
|
12
14
|
};
|
|
13
15
|
|
|
@@ -70,12 +72,11 @@ impl RgIterPy {
|
|
|
70
72
|
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
|
|
71
73
|
slf
|
|
72
74
|
}
|
|
73
|
-
fn __next__(mut slf: PyRefMut<'_, Self>) -> PyResult<Option<SearchLinePy>> {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
75
|
+
fn __next__(mut slf: PyRefMut<'_, Self>, py: Python<'_>) -> PyResult<Option<SearchLinePy>> {
|
|
76
|
+
next_rg_line_py(py, &mut slf.inner)
|
|
77
|
+
}
|
|
78
|
+
fn cancel(&self) {
|
|
79
|
+
self.inner.cancel();
|
|
79
80
|
}
|
|
80
81
|
fn __repr__(&self) -> String {
|
|
81
82
|
"RgIter(SearchLine stream)".to_string()
|
|
@@ -93,6 +94,35 @@ impl RgIterPy {
|
|
|
93
94
|
Ok(())
|
|
94
95
|
}
|
|
95
96
|
}
|
|
97
|
+
fn check_signals_or_cancel(py: Python<'_>, iter: &RgIter) -> PyResult<()> {
|
|
98
|
+
if let Err(err) = py.check_signals() {
|
|
99
|
+
iter.cancel();
|
|
100
|
+
return Err(err);
|
|
101
|
+
}
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn next_rg_line_py(py: Python<'_>, iter: &mut RgIter) -> PyResult<Option<SearchLinePy>> {
|
|
106
|
+
loop {
|
|
107
|
+
check_signals_or_cancel(py, iter)?;
|
|
108
|
+
let res = py.allow_threads(|| iter.next_timeout(Duration::from_millis(50)));
|
|
109
|
+
check_signals_or_cancel(py, iter)?;
|
|
110
|
+
match res {
|
|
111
|
+
Ok(Ok(line)) => return Ok(Some(SearchLinePy::from(line))),
|
|
112
|
+
Ok(Err(err)) => return Err(PyValueError::new_err(err.to_string())),
|
|
113
|
+
Err(RecvTimeoutError::Disconnected) => return Ok(None),
|
|
114
|
+
Err(RecvTimeoutError::Timeout) => continue,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn collect_rg_py(py: Python<'_>, mut iter: RgIter) -> PyResult<Vec<SearchLinePy>> {
|
|
120
|
+
let mut res = Vec::new();
|
|
121
|
+
while let Some(line) = next_rg_line_py(py, &mut iter)? {
|
|
122
|
+
res.push(line);
|
|
123
|
+
}
|
|
124
|
+
Ok(res)
|
|
125
|
+
}
|
|
96
126
|
#[pyclass(name = "Regex")]
|
|
97
127
|
#[derive(Clone)]
|
|
98
128
|
struct RegexPy {
|
|
@@ -294,6 +324,7 @@ fn search_path_py(
|
|
|
294
324
|
#[pyfunction(name = "rg")]
|
|
295
325
|
#[pyo3(signature = (pattern, root=".", include=None, exclude=None, hidden=false, ignore=true, max_depth=None, min_depth=None, max_filesize=None, follow_links=false, same_file_system=false, path_re=None, skip_path_re=None, skip_dir=None, skip_dir_re=None, case_sensitive=None, smart_case=false, before_context=0, after_context=0))]
|
|
296
326
|
fn rg_py(
|
|
327
|
+
py: Python<'_>,
|
|
297
328
|
pattern: String,
|
|
298
329
|
root: &str,
|
|
299
330
|
include: Option<Vec<String>>,
|
|
@@ -335,9 +366,8 @@ fn rg_py(
|
|
|
335
366
|
before_context,
|
|
336
367
|
after_context,
|
|
337
368
|
};
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
.map_err(|e| PyValueError::new_err(e.to_string()))
|
|
369
|
+
let iter = rg_iter_core(&opts).map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
370
|
+
collect_rg_py(py, iter)
|
|
341
371
|
}
|
|
342
372
|
#[pyfunction(name = "rg_iter")]
|
|
343
373
|
#[pyo3(signature = (pattern, root=".", include=None, exclude=None, hidden=false, ignore=true, max_depth=None, min_depth=None, max_filesize=None, follow_links=false, same_file_system=false, path_re=None, skip_path_re=None, skip_dir=None, skip_dir_re=None, case_sensitive=None, smart_case=false, before_context=0, after_context=0))]
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
use std::path::{Path, PathBuf};
|
|
2
|
-
use std::sync::
|
|
3
|
-
|
|
2
|
+
use std::sync::{
|
|
3
|
+
atomic::{AtomicBool, Ordering},
|
|
4
|
+
mpsc::{self, Receiver, RecvTimeoutError, Sender},
|
|
5
|
+
Arc,
|
|
6
|
+
};
|
|
7
|
+
use std::time::Duration;
|
|
4
8
|
|
|
5
9
|
use grep_matcher::Matcher;
|
|
6
10
|
use grep_regex::{RegexMatcher, RegexMatcherBuilder};
|
|
@@ -109,25 +113,49 @@ pub fn rg_iter(opts: &RgOptions) -> Result<RgIter, RgApiError> {
|
|
|
109
113
|
)?);
|
|
110
114
|
let matcher = compile_regex(&opts.pattern, opts.case_sensitive, opts.smart_case)?;
|
|
111
115
|
let opts = opts.clone();
|
|
116
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
117
|
+
let worker_cancel = cancel.clone();
|
|
112
118
|
let (tx, rx) = mpsc::channel();
|
|
113
|
-
let worker = std::thread::spawn(move ||
|
|
119
|
+
let worker = std::thread::spawn(move || {
|
|
120
|
+
run_parallel_search(root, opts, filters, matcher, tx, worker_cancel)
|
|
121
|
+
});
|
|
114
122
|
Ok(RgIter {
|
|
115
123
|
rx,
|
|
124
|
+
cancel,
|
|
116
125
|
_worker: worker,
|
|
117
126
|
})
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
pub struct RgIter {
|
|
121
130
|
rx: Receiver<Result<SearchLine, RgApiError>>,
|
|
131
|
+
cancel: Arc<AtomicBool>,
|
|
122
132
|
_worker: std::thread::JoinHandle<()>,
|
|
123
133
|
}
|
|
124
134
|
|
|
135
|
+
impl RgIter {
|
|
136
|
+
pub fn cancel(&self) {
|
|
137
|
+
self.cancel.store(true, Ordering::Relaxed);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pub fn next_timeout(
|
|
141
|
+
&mut self,
|
|
142
|
+
timeout: Duration,
|
|
143
|
+
) -> Result<Result<SearchLine, RgApiError>, RecvTimeoutError> {
|
|
144
|
+
self.rx.recv_timeout(timeout)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
125
148
|
impl Iterator for RgIter {
|
|
126
149
|
type Item = Result<SearchLine, RgApiError>;
|
|
127
150
|
fn next(&mut self) -> Option<Self::Item> {
|
|
128
151
|
self.rx.recv().ok()
|
|
129
152
|
}
|
|
130
153
|
}
|
|
154
|
+
impl Drop for RgIter {
|
|
155
|
+
fn drop(&mut self) {
|
|
156
|
+
self.cancel();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
131
159
|
|
|
132
160
|
fn run_parallel_search(
|
|
133
161
|
root: PathBuf,
|
|
@@ -135,6 +163,7 @@ fn run_parallel_search(
|
|
|
135
163
|
filters: Arc<PathFilters>,
|
|
136
164
|
matcher: RegexMatcher,
|
|
137
165
|
tx: Sender<Result<SearchLine, RgApiError>>,
|
|
166
|
+
cancel: Arc<AtomicBool>,
|
|
138
167
|
) {
|
|
139
168
|
let mut walker = WalkBuilder::new(&root);
|
|
140
169
|
configure_walker(
|
|
@@ -157,6 +186,7 @@ fn run_parallel_search(
|
|
|
157
186
|
let matcher = matcher.clone();
|
|
158
187
|
let before_context = before_context;
|
|
159
188
|
let after_context = after_context;
|
|
189
|
+
let cancel = cancel.clone();
|
|
160
190
|
Box::new(move |entry| {
|
|
161
191
|
search_entry(
|
|
162
192
|
entry,
|
|
@@ -166,6 +196,7 @@ fn run_parallel_search(
|
|
|
166
196
|
before_context,
|
|
167
197
|
after_context,
|
|
168
198
|
&tx,
|
|
199
|
+
&cancel,
|
|
169
200
|
)
|
|
170
201
|
})
|
|
171
202
|
});
|
|
@@ -179,7 +210,11 @@ fn search_entry(
|
|
|
179
210
|
before_context: usize,
|
|
180
211
|
after_context: usize,
|
|
181
212
|
tx: &Sender<Result<SearchLine, RgApiError>>,
|
|
213
|
+
cancel: &Arc<AtomicBool>,
|
|
182
214
|
) -> WalkState {
|
|
215
|
+
if is_cancelled(cancel) {
|
|
216
|
+
return WalkState::Quit;
|
|
217
|
+
}
|
|
183
218
|
let dent = match entry {
|
|
184
219
|
Ok(dent) => dent,
|
|
185
220
|
Err(err) => return send_search_error(tx, RgApiError::new(err.to_string())),
|
|
@@ -198,10 +233,20 @@ fn search_entry(
|
|
|
198
233
|
if !filters.path_allowed(&rel) {
|
|
199
234
|
return WalkState::Continue;
|
|
200
235
|
}
|
|
201
|
-
match
|
|
236
|
+
match search_path_cancelable(
|
|
237
|
+
path,
|
|
238
|
+
rel,
|
|
239
|
+
matcher.clone(),
|
|
240
|
+
before_context,
|
|
241
|
+
after_context,
|
|
242
|
+
Some(cancel.clone()),
|
|
243
|
+
) {
|
|
202
244
|
Ok(lines) => {
|
|
245
|
+
if is_cancelled(cancel) {
|
|
246
|
+
return WalkState::Quit;
|
|
247
|
+
}
|
|
203
248
|
for line in lines {
|
|
204
|
-
if tx.send(Ok(line)).is_err() {
|
|
249
|
+
if is_cancelled(cancel) || tx.send(Ok(line)).is_err() {
|
|
205
250
|
return WalkState::Quit;
|
|
206
251
|
}
|
|
207
252
|
}
|
|
@@ -211,6 +256,10 @@ fn search_entry(
|
|
|
211
256
|
}
|
|
212
257
|
}
|
|
213
258
|
|
|
259
|
+
fn is_cancelled(cancel: &Arc<AtomicBool>) -> bool {
|
|
260
|
+
cancel.load(Ordering::Relaxed)
|
|
261
|
+
}
|
|
262
|
+
|
|
214
263
|
fn send_search_error(tx: &Sender<Result<SearchLine, RgApiError>>, err: RgApiError) -> WalkState {
|
|
215
264
|
let _ = tx.send(Err(err));
|
|
216
265
|
WalkState::Quit
|
|
@@ -250,6 +299,24 @@ pub fn search_path(
|
|
|
250
299
|
matcher: RegexMatcher,
|
|
251
300
|
before_context: usize,
|
|
252
301
|
after_context: usize,
|
|
302
|
+
) -> Result<Vec<SearchLine>, RgApiError> {
|
|
303
|
+
search_path_cancelable(
|
|
304
|
+
path,
|
|
305
|
+
display_path,
|
|
306
|
+
matcher,
|
|
307
|
+
before_context,
|
|
308
|
+
after_context,
|
|
309
|
+
None,
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fn search_path_cancelable(
|
|
314
|
+
path: &Path,
|
|
315
|
+
display_path: String,
|
|
316
|
+
matcher: RegexMatcher,
|
|
317
|
+
before_context: usize,
|
|
318
|
+
after_context: usize,
|
|
319
|
+
cancel: Option<Arc<AtomicBool>>,
|
|
253
320
|
) -> Result<Vec<SearchLine>, RgApiError> {
|
|
254
321
|
let mut builder = SearcherBuilder::new();
|
|
255
322
|
builder
|
|
@@ -264,6 +331,7 @@ pub fn search_path(
|
|
|
264
331
|
path: display_path,
|
|
265
332
|
matcher,
|
|
266
333
|
lines: &mut out,
|
|
334
|
+
cancel,
|
|
267
335
|
};
|
|
268
336
|
match searcher.search_path(search_matcher, path, sink) {
|
|
269
337
|
Ok(()) => Ok(out),
|
|
@@ -305,6 +373,7 @@ fn search_bytes(
|
|
|
305
373
|
path: display_path,
|
|
306
374
|
matcher,
|
|
307
375
|
lines: &mut out,
|
|
376
|
+
cancel: None,
|
|
308
377
|
};
|
|
309
378
|
searcher
|
|
310
379
|
.search_slice(search_matcher, bytes, sink)
|
|
@@ -316,6 +385,15 @@ struct CollectSink<'a> {
|
|
|
316
385
|
path: String,
|
|
317
386
|
matcher: RegexMatcher,
|
|
318
387
|
lines: &'a mut Vec<SearchLine>,
|
|
388
|
+
cancel: Option<Arc<AtomicBool>>,
|
|
389
|
+
}
|
|
390
|
+
impl CollectSink<'_> {
|
|
391
|
+
fn cancelled(&self) -> bool {
|
|
392
|
+
match &self.cancel {
|
|
393
|
+
Some(cancel) => is_cancelled(cancel),
|
|
394
|
+
None => false,
|
|
395
|
+
}
|
|
396
|
+
}
|
|
319
397
|
}
|
|
320
398
|
|
|
321
399
|
impl Sink for CollectSink<'_> {
|
|
@@ -326,6 +404,9 @@ impl Sink for CollectSink<'_> {
|
|
|
326
404
|
_searcher: &grep_searcher::Searcher,
|
|
327
405
|
mat: &SinkMatch<'_>,
|
|
328
406
|
) -> Result<bool, Self::Error> {
|
|
407
|
+
if self.cancelled() {
|
|
408
|
+
return Ok(false);
|
|
409
|
+
}
|
|
329
410
|
let line = bytes_to_line(mat.bytes())?;
|
|
330
411
|
let spans = spans_for(&self.matcher, mat.bytes())?;
|
|
331
412
|
self.lines.push(SearchLine {
|
|
@@ -335,7 +416,7 @@ impl Sink for CollectSink<'_> {
|
|
|
335
416
|
line,
|
|
336
417
|
matches: spans,
|
|
337
418
|
});
|
|
338
|
-
Ok(
|
|
419
|
+
Ok(!self.cancelled())
|
|
339
420
|
}
|
|
340
421
|
|
|
341
422
|
fn context(
|
|
@@ -343,6 +424,9 @@ impl Sink for CollectSink<'_> {
|
|
|
343
424
|
_searcher: &grep_searcher::Searcher,
|
|
344
425
|
ctx: &SinkContext<'_>,
|
|
345
426
|
) -> Result<bool, Self::Error> {
|
|
427
|
+
if self.cancelled() {
|
|
428
|
+
return Ok(false);
|
|
429
|
+
}
|
|
346
430
|
let kind = match ctx.kind() {
|
|
347
431
|
SinkContextKind::Before => SearchKind::Before,
|
|
348
432
|
SinkContextKind::After => SearchKind::After,
|
|
@@ -355,7 +439,7 @@ impl Sink for CollectSink<'_> {
|
|
|
355
439
|
line: bytes_to_line(ctx.bytes())?,
|
|
356
440
|
matches: Vec::new(),
|
|
357
441
|
});
|
|
358
|
-
Ok(
|
|
442
|
+
Ok(!self.cancelled())
|
|
359
443
|
}
|
|
360
444
|
fn binary_data(
|
|
361
445
|
&mut self,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import _thread, threading
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
1
5
|
from rgapi import Regex, SearchResults, compile, fd, rg, rg_iter, search_path, search_text, walk
|
|
2
6
|
|
|
3
7
|
|
|
@@ -102,6 +106,15 @@ def test_rg_returns_structured_matches_context_and_relative_paths(tmp_path):
|
|
|
102
106
|
assert str(stream) == repr(stream)
|
|
103
107
|
|
|
104
108
|
|
|
109
|
+
def test_rg_keyboard_interrupt_cancels(tmp_path):
|
|
110
|
+
(tmp_path / "big.txt").write_text("alpha beta gamma\n" * 1_000_000)
|
|
111
|
+
timer = threading.Timer(0.001, _thread.interrupt_main)
|
|
112
|
+
try:
|
|
113
|
+
with pytest.raises(KeyboardInterrupt):
|
|
114
|
+
timer.start()
|
|
115
|
+
rg("needle_that_is_not_present", str(tmp_path))
|
|
116
|
+
finally: timer.cancel()
|
|
117
|
+
|
|
105
118
|
def test_direct_regex_and_search_apis(tmp_path):
|
|
106
119
|
make_tree(tmp_path)
|
|
107
120
|
matcher = compile("todo")
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
from . import _core
|
|
2
|
-
|
|
3
|
-
Regex = _core.Regex
|
|
4
|
-
SearchLine = _core.SearchLine
|
|
5
|
-
RgIter = _core.RgIter
|
|
6
|
-
compile = _core.compile
|
|
7
|
-
|
|
8
|
-
class SearchResults(list):
|
|
9
|
-
def __str__(self): return "\n".join(map(str, self))
|
|
10
|
-
def _repr_pretty_(self, p, cycle): p.text("..." if cycle else str(self))
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _listify(value):
|
|
14
|
-
if value is None: return []
|
|
15
|
-
if isinstance(value, str): return [value]
|
|
16
|
-
return list(value)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _filters(glob=None, include=None, exclude=None, ext=None):
|
|
20
|
-
includes = _listify(include) + _listify(glob)
|
|
21
|
-
for suffix in _listify(ext):
|
|
22
|
-
suffix = str(suffix)
|
|
23
|
-
if suffix.startswith("."): suffix = suffix[1:]
|
|
24
|
-
includes.append(f"*.{suffix}")
|
|
25
|
-
return includes, _listify(exclude)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _context(context, before_context, after_context):
|
|
29
|
-
if context: return context, context
|
|
30
|
-
return before_context, after_context
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def walk(root=".", hidden=False, ignore=True, max_depth=None, min_depth=None, max_filesize=None, follow_links=False,
|
|
34
|
-
same_file_system=False, path_re=None, skip_path_re=None, skip_dir=None, skip_dir_re=None, files=True, dirs=False):
|
|
35
|
-
return _core.walk(root, hidden, ignore, max_depth, min_depth, max_filesize, follow_links,
|
|
36
|
-
same_file_system, path_re, skip_path_re, _listify(skip_dir), skip_dir_re, files, dirs)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def fd(root=".", pattern=None, glob=None, include=None, exclude=None, ext=None, hidden=False, ignore=True, max_depth=None,
|
|
40
|
-
min_depth=None, max_filesize=None, follow_links=False, same_file_system=False, path_re=None,
|
|
41
|
-
skip_path_re=None, skip_dir=None, skip_dir_re=None, files=True, dirs=False):
|
|
42
|
-
include, exclude = _filters(glob, include, exclude, ext)
|
|
43
|
-
return _core.find(root, pattern, include, exclude, hidden, ignore, max_depth, min_depth, max_filesize,
|
|
44
|
-
follow_links, same_file_system, path_re, skip_path_re, _listify(skip_dir), skip_dir_re, files, dirs)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
|
|
48
|
-
follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
|
|
49
|
-
before_context, after_context, context):
|
|
50
|
-
include, exclude = _filters(glob, include, exclude, ext)
|
|
51
|
-
before_context, after_context = _context(context, before_context, after_context)
|
|
52
|
-
return (pattern, root, include, exclude, hidden, ignore, max_depth, min_depth, max_filesize, follow_links, same_file_system,
|
|
53
|
-
path_re, skip_path_re, _listify(skip_dir), skip_dir_re, case_sensitive, smart_case, before_context, after_context)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def rg(pattern, root=".", glob=None, include=None, exclude=None, ext=None, hidden=False, ignore=True, max_depth=None,
|
|
57
|
-
min_depth=None, max_filesize=None, follow_links=False, same_file_system=False, path_re=None,
|
|
58
|
-
skip_path_re=None, skip_dir=None, skip_dir_re=None, case_sensitive=None, smart_case=False, before_context=0, after_context=0,
|
|
59
|
-
context=0, paths=False, count=False):
|
|
60
|
-
assert not (paths and count), "paths and count are mutually exclusive"
|
|
61
|
-
args = _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
|
|
62
|
-
follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
|
|
63
|
-
before_context, after_context, context)
|
|
64
|
-
if paths:
|
|
65
|
-
seen, res = set(), []
|
|
66
|
-
for row in _core.rg_iter(*args):
|
|
67
|
-
if row.kind != "match" or row.path in seen: continue
|
|
68
|
-
seen.add(row.path)
|
|
69
|
-
res.append(row.path)
|
|
70
|
-
return res
|
|
71
|
-
if count: return sum(len(row.matches) for row in _core.rg_iter(*args) if row.kind == "match")
|
|
72
|
-
return SearchResults(_core.rg(*args))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def rg_iter(pattern, root=".", glob=None, include=None, exclude=None, ext=None, hidden=False, ignore=True, max_depth=None,
|
|
76
|
-
min_depth=None, max_filesize=None, follow_links=False, same_file_system=False, path_re=None, skip_path_re=None,
|
|
77
|
-
skip_dir=None, skip_dir_re=None, case_sensitive=None, smart_case=False, before_context=0, after_context=0, context=0):
|
|
78
|
-
args = _rg_args(pattern, root, glob, include, exclude, ext, hidden, ignore, max_depth, min_depth, max_filesize,
|
|
79
|
-
follow_links, same_file_system, path_re, skip_path_re, skip_dir, skip_dir_re, case_sensitive, smart_case,
|
|
80
|
-
before_context, after_context, context)
|
|
81
|
-
return _core.rg_iter(*args)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def search_text(matcher, text, path="<text>", before_context=0, after_context=0, context=0):
|
|
85
|
-
before_context, after_context = _context(context, before_context, after_context)
|
|
86
|
-
return SearchResults(_core.search_text(matcher, text, path, before_context, after_context))
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def search_path(matcher, path, display_path=None, before_context=0, after_context=0, context=0):
|
|
90
|
-
before_context, after_context = _context(context, before_context, after_context)
|
|
91
|
-
return SearchResults(_core.search_path(matcher, path, display_path, before_context, after_context))
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
__all__ = [
|
|
95
|
-
"Regex",
|
|
96
|
-
"RgIter",
|
|
97
|
-
"SearchLine",
|
|
98
|
-
"SearchResults",
|
|
99
|
-
"compile",
|
|
100
|
-
"fd",
|
|
101
|
-
"rg",
|
|
102
|
-
"rg_iter",
|
|
103
|
-
"search_path",
|
|
104
|
-
"search_text",
|
|
105
|
-
"walk",
|
|
106
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|