rlmgrep 0.1.11__py3-none-any.whl → 0.1.13__py3-none-any.whl

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.
rlmgrep/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.1.11"
2
+ __version__ = "0.1.13"
rlmgrep/cli.py CHANGED
@@ -9,7 +9,13 @@ import dspy
9
9
  from . import __version__
10
10
  from .config import ensure_default_config, load_config
11
11
  from .file_map import build_file_map
12
- from .ingest import FileRecord, collect_candidates, load_files, resolve_type_exts
12
+ from .ingest import (
13
+ FileRecord,
14
+ build_gitignore_spec,
15
+ collect_candidates,
16
+ load_files,
17
+ resolve_type_exts,
18
+ )
13
19
  from .rlm import Match, build_lm, run_rlm
14
20
  from .render import render_matches
15
21
 
@@ -81,6 +87,8 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
81
87
  parser.add_argument("-B", dest="before", type=int, default=None, help="Context lines before")
82
88
  parser.add_argument("-m", dest="max_count", type=int, default=None, help="Max matching lines per file")
83
89
  parser.add_argument("-a", "--text", dest="binary_as_text", action="store_true", help="Search binary files as text")
90
+ parser.add_argument("--hidden", action="store_true", help="Include hidden files and directories")
91
+ parser.add_argument("--no-ignore", dest="no_ignore", action="store_true", help="Do not respect .gitignore")
84
92
  parser.add_argument("--answer", action="store_true", help="Print a narrative answer before grep output")
85
93
  parser.add_argument("-y", "--yes", action="store_true", help="Skip file count confirmation")
86
94
  parser.add_argument(
@@ -139,6 +147,13 @@ def _pick(cli_value, config: dict, key: str, default=None):
139
147
  return default
140
148
 
141
149
 
150
+ def _find_git_root(start: Path) -> Path | None:
151
+ for p in [start, *start.parents]:
152
+ if (p / ".git").is_dir():
153
+ return p
154
+ return None
155
+
156
+
142
157
  def _env_value(name: str) -> str | None:
143
158
  val = os.getenv(name)
144
159
  if val is None:
@@ -424,12 +439,21 @@ def main(argv: list[str] | None = None) -> int:
424
439
  if hard_max is not None and hard_max <= 0:
425
440
  hard_max = None
426
441
 
442
+ ignore_spec = None
443
+ ignore_root = None
444
+ if not args.no_ignore:
445
+ ignore_root = _find_git_root(cwd) or cwd
446
+ ignore_spec = build_gitignore_spec(ignore_root)
447
+
427
448
  candidates = collect_candidates(
428
449
  input_paths,
429
450
  cwd=cwd,
430
451
  recursive=args.recursive,
431
452
  include_globs=globs,
432
453
  type_exts=type_exts,
454
+ include_hidden=args.hidden,
455
+ ignore_spec=ignore_spec,
456
+ ignore_root=ignore_root,
433
457
  )
434
458
  candidate_count = len(candidates)
435
459
  if hard_max is not None and candidate_count > hard_max:
rlmgrep/ingest.py CHANGED
@@ -2,8 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from fnmatch import fnmatch
5
+ import os
5
6
  from pathlib import Path, PurePosixPath
6
- from typing import Iterable, Any, Callable
7
+ from typing import Any, Callable, Iterable
8
+
9
+ import pathspec
7
10
 
8
11
  from pypdf import PdfReader
9
12
 
@@ -161,6 +164,64 @@ def collect_files(paths: Iterable[str], recursive: bool = True) -> list[Path]:
161
164
  return files
162
165
 
163
166
 
167
+ def build_gitignore_spec(root: Path) -> "pathspec.PathSpec | None":
168
+ if pathspec is None:
169
+ return None
170
+ root = root.resolve()
171
+ gitignore_paths: list[Path] = []
172
+ for dirpath, dirnames, filenames in os.walk(root):
173
+ if ".git" in dirnames:
174
+ dirnames.remove(".git")
175
+ if ".gitignore" in filenames:
176
+ gitignore_paths.append(Path(dirpath) / ".gitignore")
177
+
178
+ if not gitignore_paths:
179
+ return None
180
+
181
+ def _sort_key(p: Path) -> tuple[int, str]:
182
+ try:
183
+ rel = p.parent.relative_to(root)
184
+ depth = len(rel.parts)
185
+ return depth, rel.as_posix()
186
+ except ValueError:
187
+ return 0, p.as_posix()
188
+
189
+ gitignore_paths.sort(key=_sort_key)
190
+
191
+ patterns: list[str] = []
192
+ for gi in gitignore_paths:
193
+ try:
194
+ rel_dir = gi.parent.relative_to(root).as_posix()
195
+ except ValueError:
196
+ rel_dir = ""
197
+ try:
198
+ raw_lines = gi.read_text(encoding="utf-8", errors="ignore").splitlines()
199
+ except Exception:
200
+ continue
201
+ for raw in raw_lines:
202
+ line = raw.rstrip("\n")
203
+ if not line:
204
+ continue
205
+ if line.startswith("\\#") or line.startswith("\\!"):
206
+ line = line[1:]
207
+ elif line.startswith("#"):
208
+ continue
209
+ negated = line.startswith("!")
210
+ if negated:
211
+ line = line[1:]
212
+ if line.startswith("/"):
213
+ line = line[1:]
214
+ if rel_dir:
215
+ line = f"{rel_dir}/{line}"
216
+ if negated:
217
+ line = "!" + line
218
+ patterns.append(line)
219
+
220
+ if not patterns:
221
+ return None
222
+ return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
223
+
224
+
164
225
  TYPE_EXTS = {
165
226
  "bash": {".bash"},
166
227
  "c": {".c", ".h"},
@@ -237,21 +298,46 @@ def _matches_globs(path: str, globs: list[str]) -> bool:
237
298
  return False
238
299
 
239
300
 
301
+ def _is_hidden_path(path: Path) -> bool:
302
+ return any(part.startswith(".") for part in path.parts if part)
303
+
304
+
240
305
  def collect_candidates(
241
306
  paths: Iterable[str],
242
307
  cwd: Path,
243
308
  recursive: bool = True,
244
309
  include_globs: list[str] | None = None,
245
310
  type_exts: set[str] | None = None,
311
+ include_hidden: bool = False,
312
+ ignore_spec: "pathspec.PathSpec | None" = None,
313
+ ignore_root: Path | None = None,
246
314
  ) -> list[Path]:
247
315
  files = collect_files(paths, recursive=recursive)
316
+ explicit_files: set[Path] = set()
317
+ for raw in paths:
318
+ p = Path(raw)
319
+ if p.exists() and p.is_file():
320
+ explicit_files.add(p.resolve())
248
321
  candidates: list[Path] = []
249
322
  for fp in files:
323
+ fp_resolved = fp.resolve()
324
+ is_explicit = fp_resolved in explicit_files
325
+ if not include_hidden and not is_explicit and _is_hidden_path(fp):
326
+ continue
327
+
250
328
  try:
251
329
  key = fp.relative_to(cwd).as_posix()
252
330
  except ValueError:
253
331
  key = fp.as_posix()
254
332
 
333
+ if ignore_spec is not None and ignore_root is not None and not is_explicit:
334
+ try:
335
+ rel = fp.relative_to(ignore_root).as_posix()
336
+ except ValueError:
337
+ rel = None
338
+ if rel and ignore_spec.match_file(rel):
339
+ continue
340
+
255
341
  if include_globs and not _matches_globs(key, include_globs):
256
342
  continue
257
343
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rlmgrep
3
- Version: 0.1.11
3
+ Version: 0.1.13
4
4
  Summary: Grep-shaped CLI search powered by DSPy RLM
5
5
  Author: rlmgrep
6
6
  License: MIT
@@ -8,11 +8,12 @@ Requires-Python: >=3.11
8
8
  Description-Content-Type: text/markdown
9
9
  Requires-Dist: dspy>=3.1.1
10
10
  Requires-Dist: markitdown[all]>=0.1.4
11
+ Requires-Dist: pathspec>=0.12.1
11
12
  Requires-Dist: pypdf>=4.0.0
12
13
 
13
14
  # rlmgrep
14
15
 
15
- Grep-shaped search powered by DSPy RLM. It accepts a natural-language query, scans the files you point at, and prints matching lines in a grep-like format.
16
+ Grep-shaped search powered by DSPy RLM. It accepts a natural-language query, scans the files you point at, and prints matching lines in a grep-like format. Use `--answer` to get a narrative response grounded in the selected files/directories.
16
17
 
17
18
  ## Quickstart
18
19
 
@@ -93,6 +94,8 @@ Common options:
93
94
  - `-m N` max matching lines per file
94
95
  - `-g GLOB` include files matching glob (repeatable, comma-separated)
95
96
  - `--type T` include file types (repeatable, comma-separated)
97
+ - `--hidden` include hidden files and directories
98
+ - `--no-ignore` do not respect `.gitignore`
96
99
  - `--no-recursive` do not recurse directories
97
100
  - `-a`, `--text` treat binary files as text
98
101
  - `-y`, `--yes` skip file count confirmation
@@ -125,6 +128,7 @@ rg -l "token" . | rlmgrep --files-from-stdin --answer "What does this token cont
125
128
  ## Input selection
126
129
 
127
130
  - Directories are searched recursively by default. Use `--no-recursive` to stop recursion.
131
+ - Hidden files and `.gitignore` rules are respected by default. Use `--hidden` or `--no-ignore` to include them.
128
132
  - `--type` uses built-in type mappings (e.g., `py`, `js`, `md`); unknown values are treated as file extensions.
129
133
  - `-g/--glob` matches path globs against normalized paths (forward slashes).
130
134
  - Paths are printed relative to the current working directory when possible.
@@ -0,0 +1,14 @@
1
+ rlmgrep/__init__.py,sha256=BrGyE0EGAAcw0ie-YqC8DUrNp9CZXcKoms53BtFY3f0,49
2
+ rlmgrep/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
3
+ rlmgrep/cli.py,sha256=sUtcvf-3U1fpUdFYmEE9j75xSgBAx9Uv3XY1lobHURk,21150
4
+ rlmgrep/config.py,sha256=u1iz-nI8dj-dZETbpIki3RQefHJEyi5oE5zE4_IR8kg,2399
5
+ rlmgrep/file_map.py,sha256=x2Ri1wzK8_87GUorsAV01K_nYLZcv30yIquDeTCcdEw,876
6
+ rlmgrep/ingest.py,sha256=Um4n0jvPaBhn_CieEu1RfdI0O-0k7N--sp2ncuwseqE,11816
7
+ rlmgrep/interpreter.py,sha256=s_nMRxLlAU9C0JmUzUBW5NbVbuH67doVWF54K54STlA,2478
8
+ rlmgrep/render.py,sha256=mCTT6yuKNv7HJ46LzOyLkCbyBedCWSNd7UeubyLXcyM,3356
9
+ rlmgrep/rlm.py,sha256=i3rCTp8OABByF60Un5gO7265gaW4spwU0OFKIz4surg,5750
10
+ rlmgrep-0.1.13.dist-info/METADATA,sha256=ZEzhJPnjR4oyq70tERS1XY5k-69e4FThLsY_1LX6GuQ,7936
11
+ rlmgrep-0.1.13.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
+ rlmgrep-0.1.13.dist-info/entry_points.txt,sha256=UV6QkEbkwBO1JJ53mm84_n35tVyOczPvOQ14ga7vrCI,45
13
+ rlmgrep-0.1.13.dist-info/top_level.txt,sha256=gTujSRsO58c80eN7aRH2cfe51FHxx8LJ1w1Y2YlHti0,8
14
+ rlmgrep-0.1.13.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- rlmgrep/__init__.py,sha256=eEU5vUkbBcAVKg20oMuOjMenVs64GjcSE_sBCcK_srU,49
2
- rlmgrep/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
3
- rlmgrep/cli.py,sha256=Jn7knAQq3Bnb578QK33RxDZ102yFrVSbNFjUKBGkb1o,20417
4
- rlmgrep/config.py,sha256=u1iz-nI8dj-dZETbpIki3RQefHJEyi5oE5zE4_IR8kg,2399
5
- rlmgrep/file_map.py,sha256=x2Ri1wzK8_87GUorsAV01K_nYLZcv30yIquDeTCcdEw,876
6
- rlmgrep/ingest.py,sha256=uCz2el9B-RIT9umFo-gFEdAsmWPP1IJOArFFQY0D_1A,9127
7
- rlmgrep/interpreter.py,sha256=s_nMRxLlAU9C0JmUzUBW5NbVbuH67doVWF54K54STlA,2478
8
- rlmgrep/render.py,sha256=mCTT6yuKNv7HJ46LzOyLkCbyBedCWSNd7UeubyLXcyM,3356
9
- rlmgrep/rlm.py,sha256=i3rCTp8OABByF60Un5gO7265gaW4spwU0OFKIz4surg,5750
10
- rlmgrep-0.1.11.dist-info/METADATA,sha256=ykp-GtmTqprVgTF1L0tV_Wc9CHI--3FwlYjAQVxcbF0,7610
11
- rlmgrep-0.1.11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
- rlmgrep-0.1.11.dist-info/entry_points.txt,sha256=UV6QkEbkwBO1JJ53mm84_n35tVyOczPvOQ14ga7vrCI,45
13
- rlmgrep-0.1.11.dist-info/top_level.txt,sha256=gTujSRsO58c80eN7aRH2cfe51FHxx8LJ1w1Y2YlHti0,8
14
- rlmgrep-0.1.11.dist-info/RECORD,,