Skip to content

Commit 065345f

Browse files
committed
Finish re2
1 parent 0289a21 commit 065345f

22 files changed

Lines changed: 390 additions & 32 deletions

.github/workflows/ci.yaml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,24 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
os: [ubuntu, macos, windows]
19-
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy-3.9", "pypy-3.10", "pypy-3.11"]
20-
backend: [base, hyperscan]
18+
os:
19+
- ubuntu
20+
- macos
21+
- windows
22+
python:
23+
- "3.9"
24+
- "3.10"
25+
- "3.11"
26+
- "3.12"
27+
- "3.13"
28+
- "3.14"
29+
- "pypy-3.9"
30+
- "pypy-3.10"
31+
- "pypy-3.11"
32+
backend:
33+
- base
34+
- hyperscan
35+
- re2
2136
exclude:
2237
# Hyperscan does hot have wheels for Python 3.9 on Windows.
2338
- os: windows

CHANGES.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,17 @@ Major changes:
1010

1111
- `Issue #91`_: Dropped support of EoL Python 3.8.
1212

13+
- Added concept of backends to allow for faster regex matching. The backend can be controlled using the `backend` argument to `PathSpec()`, `PathSpec.from_lines()`, `GitIgnoreSpec()`, and `GitIgnoreSpec.from_lines()`.
14+
15+
API changes:
16+
17+
- Protected method `pathspec.pathspec.PathSpec._match_file()` (with a leading underscore) has been removed and replaced by backends. This does not affect normal usage of `PathSpec()` and `GitIgnoreSpec()`. Only custom subclasses will be affected. If this breaks your usage, `open an issue <https://github.com/cpburnz/python-pathspec/issues>`_.
18+
1319
New features:
1420

15-
- Added optional `hyperscan`_ backend for faster regex matching. It will automatically be used when installed. The backend can be controlled using the `backend` argument to `PathSpec()`, `PathSpec.from_lines()`, `GitIgnoreSpec()`, or `GitIgnoreSpec.from_lines()`.
21+
- Added optional `hyperscan`_ backend. It will automatically be used when installed. This dependency can be installed with ``pip install 'pathspec[hyperscan]'``.
22+
23+
- Added optional "re2" backend using the `google-re2`_ library. It will automatically be used when installed. This dependency can be installed with ``pip install 'pathspec[google-re2]'``.
1624

1725
Bug fixes:
1826

@@ -23,9 +31,12 @@ Improvements:
2331
- Mark Python 3.13 and 3.14 as supported.
2432
- No-op patterns are now filtered out when matching files, slightly improving performance.
2533

26-
.. _`hyperscan`: https://pypi.org/project/hyperscan/
34+
35+
.. _`Issue #38`: https://github.com/cpburnz/python-pathspec/issues/38
2736
.. _`Issue #91`: https://github.com/cpburnz/python-pathspec/issues/91
2837
.. _`Issue #98`: https://github.com/cpburnz/python-pathspec/issues/98
38+
.. _`google-re2`: https://pypi.org/project/google-re2/
39+
.. _`hyperscan`: https://pypi.org/project/hyperscan/
2940

3041

3142
0.12.1 (2023-12-10)

benchmarks/bench_gitignore_many_to_many.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
GROUP = "GitIgnore.match_files(): 180 lines, 6.5k files"
2727

2828

29+
# Hyperscan backend.
30+
2931
@pytest.mark.benchmark(group=GROUP)
3032
def bench_hs_r1_block_closure(
3133
benchmark: BenchmarkFixture,
@@ -138,6 +140,23 @@ def bench_hs_v1(
138140
benchmark(run_match, spec, cpython_files)
139141

140142

143+
# Re2 backend.
144+
145+
@pytest.mark.benchmark(group=GROUP)
146+
def bench_re2_v1(
147+
benchmark: BenchmarkFixture,
148+
cpython_files: set[str],
149+
cpython_gi_lines_all: list[str],
150+
):
151+
spec = GitIgnoreSpec.from_lines(
152+
cpython_gi_lines_all,
153+
backend='re2',
154+
)
155+
benchmark(run_match, spec, cpython_files)
156+
157+
158+
# Simple backend.
159+
141160
@pytest.mark.benchmark(group=GROUP)
142161
def bench_sm_filtered(
143162
benchmark: BenchmarkFixture,

benchmarks/bench_gitignore_many_to_one_end.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
GROUP = "GitIgnore.match_file(): 180 lines, one file (end)"
2626

2727

28+
# Hyperscan backend.
29+
2830
@pytest.mark.benchmark(group=GROUP)
2931
def bench_hs_r1_block_closure(
3032
benchmark: BenchmarkFixture,
@@ -122,6 +124,23 @@ def bench_hs_v1(
122124
benchmark(run_match, spec, cpython_file_match_end)
123125

124126

127+
# Re2 backend.
128+
129+
@pytest.mark.benchmark(group=GROUP)
130+
def bench_re2_v1(
131+
benchmark: BenchmarkFixture,
132+
cpython_file_match_end: str,
133+
cpython_gi_lines_all: list[str],
134+
):
135+
spec = GitIgnoreSpec.from_lines(
136+
cpython_gi_lines_all,
137+
backend='re2',
138+
)
139+
benchmark(run_match, spec, cpython_file_match_end)
140+
141+
142+
# Simple backend.
143+
125144
@pytest.mark.benchmark(group=GROUP)
126145
def bench_sm_filtered(
127146
benchmark: BenchmarkFixture,

benchmarks/bench_gitignore_many_to_one_middle.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
GROUP = "GitIgnore.match_file(): 180 lines, one file (middle)"
2626

2727

28+
# Hyperscan backend.
29+
2830
@pytest.mark.benchmark(group=GROUP)
2931
def bench_hs_r1_block_closure(
3032
benchmark: BenchmarkFixture,
@@ -122,6 +124,23 @@ def bench_hs_v1(
122124
benchmark(run_match, spec, cpython_file_match_middle)
123125

124126

127+
# Re2 backend.
128+
129+
@pytest.mark.benchmark(group=GROUP)
130+
def bench_re2_v1(
131+
benchmark: BenchmarkFixture,
132+
cpython_file_match_middle: str,
133+
cpython_gi_lines_all: list[str],
134+
):
135+
spec = GitIgnoreSpec.from_lines(
136+
cpython_gi_lines_all,
137+
backend='re2',
138+
)
139+
benchmark(run_match, spec, cpython_file_match_middle)
140+
141+
142+
# Simple backend.
143+
125144
@pytest.mark.benchmark(group=GROUP)
126145
def bench_sm_filtered(
127146
benchmark: BenchmarkFixture,

benchmarks/bench_gitignore_many_to_one_none.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
GROUP = "GitIgnore.match_file(): 180 lines, one file (none)"
2525

2626

27+
# Hyperscan backend.
28+
2729
@pytest.mark.benchmark(group=GROUP)
2830
def bench_hs_r1_block_closure(
2931
benchmark: BenchmarkFixture,
@@ -121,6 +123,23 @@ def bench_hs_v1(
121123
benchmark(run_match, spec, cpython_file_match_none)
122124

123125

126+
# Re2 backend.
127+
128+
@pytest.mark.benchmark(group=GROUP)
129+
def bench_re2_v1(
130+
benchmark: BenchmarkFixture,
131+
cpython_file_match_none: str,
132+
cpython_gi_lines_all: list[str],
133+
):
134+
spec = GitIgnoreSpec.from_lines(
135+
cpython_gi_lines_all,
136+
backend='re2',
137+
)
138+
benchmark(run_match, spec, cpython_file_match_none)
139+
140+
141+
# Simple backend.
142+
124143
@pytest.mark.benchmark(group=GROUP)
125144
def bench_sm_filtered(
126145
benchmark: BenchmarkFixture,

benchmarks/bench_gitignore_many_to_one_start.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
GROUP = "GitIgnore.match_file(): 180 lines, one file (start)"
2626

2727

28+
# Hyperscan backend.
29+
2830
@pytest.mark.benchmark(group=GROUP)
2931
def bench_hs_r1_block_closure(
3032
benchmark: BenchmarkFixture,
@@ -122,6 +124,23 @@ def bench_hs_v1(
122124
benchmark(run_match, spec, cpython_file_match_start)
123125

124126

127+
# Re2 backend.
128+
129+
@pytest.mark.benchmark(group=GROUP)
130+
def bench_re2_v1(
131+
benchmark: BenchmarkFixture,
132+
cpython_file_match_start: str,
133+
cpython_gi_lines_all: list[str],
134+
):
135+
spec = GitIgnoreSpec.from_lines(
136+
cpython_gi_lines_all,
137+
backend='re2',
138+
)
139+
benchmark(run_match, spec, cpython_file_match_start)
140+
141+
142+
# Simple backend.
143+
125144
@pytest.mark.benchmark(group=GROUP)
126145
def bench_sm_filtered(
127146
benchmark: BenchmarkFixture,

benchmarks/match_gitignore.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def on_match(
109109
if out_index == -1:
110110
out_index = None
111111

112-
return out_include, out_index
112+
return (out_include, out_index)
113113

114114

115115
class HyperscanGiR1BlockStateBackend(_HyperscanGiR1BlockBaseBackend):
@@ -136,7 +136,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
136136
if out_index == -1:
137137
out_index = None
138138

139-
return out_include, out_index
139+
return (out_include, out_index)
140140

141141
@override
142142
def __on_match(
@@ -242,7 +242,7 @@ def on_match(
242242
if out_index == -1:
243243
out_index = None
244244

245-
return out_include, out_index
245+
return (out_include, out_index)
246246

247247

248248
# WARNING: This segfaults.
@@ -269,7 +269,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
269269
if out_index == -1:
270270
out_index = None
271271

272-
return out_include, out_index
272+
return (out_include, out_index)
273273

274274
@override
275275
def __on_match(
@@ -452,7 +452,7 @@ def on_match(
452452
if out_index == -1:
453453
out_index = None
454454

455-
return out_include, out_index
455+
return (out_include, out_index)
456456

457457

458458
class HyperscanGiR2BlockStateBackend(_HyperscanGiR2BlockBaseBackend):
@@ -477,7 +477,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
477477
if out_index == -1:
478478
out_index = None
479479

480-
return out_include, out_index
480+
return (out_include, out_index)
481481

482482
@override
483483
def __on_match(
@@ -571,4 +571,4 @@ def on_match(
571571
if out_index == -1:
572572
out_index = None
573573

574-
return out_include, out_index
574+
return (out_include, out_index)

benchmarks/match_pathspec.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def on_match(
124124
if out_index == -1:
125125
out_index = None
126126

127-
return out_include, out_index
127+
return (out_include, out_index)
128128

129129

130130
class HyperscanPsR1BlockStateBackend(_HyperscanPsR1BlockBaseBackend):
@@ -146,7 +146,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
146146
if out_index == -1:
147147
out_index = None
148148

149-
return out_include, out_index
149+
return (out_include, out_index)
150150

151151
@override
152152
def __on_match(
@@ -205,7 +205,7 @@ def on_match(
205205
if out_index == -1:
206206
out_index = None
207207

208-
return out_include, out_index
208+
return (out_include, out_index)
209209

210210

211211
# WARNING: This segfaults.
@@ -230,7 +230,7 @@ def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]:
230230
if out_index == -1:
231231
out_index = None
232232

233-
return out_include, out_index
233+
return (out_include, out_index)
234234

235235
@override
236236
def __on_match(

pathspec/_backends/agg.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
HyperscanPsBackend)
2727
from .re2.base import (
2828
re2_error)
29+
from .re2.gitignore import (
30+
Re2GiBackend)
2931
from .re2.pathspec import (
3032
Re2PsBackend)
3133
from .simple.gitignore import (
@@ -64,7 +66,9 @@ def make_gitignore_backend(
6466
if name == 'best':
6567
name = _BEST_BACKEND
6668

67-
if name == 'hyperscan':
69+
if name == 're2':
70+
return Re2GiBackend(cast(Sequence[RegexPattern], patterns))
71+
elif name == 'hyperscan':
6872
return HyperscanGiBackend(cast(Sequence[RegexPattern], patterns))
6973
elif name == 'simple':
7074
return SimpleGiBackend(cast(Sequence[RegexPattern], patterns))
@@ -90,10 +94,10 @@ def make_pathspec_backend(
9094
if name == 'best':
9195
name = _BEST_BACKEND
9296

93-
if name == 'hyperscan':
94-
return HyperscanPsBackend(cast(Sequence[RegexPattern], patterns))
95-
elif name == 're2':
97+
if name == 're2':
9698
return Re2PsBackend(cast(Sequence[RegexPattern], patterns))
99+
elif name == 'hyperscan':
100+
return HyperscanPsBackend(cast(Sequence[RegexPattern], patterns))
97101
elif name == 'simple':
98102
return SimplePsBackend(patterns)
99103
else:

0 commit comments

Comments
 (0)