codetool-explore 0.5.0__py3-none-win_amd64.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.
Files changed (33) hide show
  1. codetool_explore/__init__.py +35 -0
  2. codetool_explore/_bin/codetool-explore-rust-windows-x86_64.exe +0 -0
  3. codetool_explore/api.py +266 -0
  4. codetool_explore/cli.py +188 -0
  5. codetool_explore/compression.py +150 -0
  6. codetool_explore/cursor.py +71 -0
  7. codetool_explore/errors.py +23 -0
  8. codetool_explore/explorer.py +497 -0
  9. codetool_explore/ignore.py +222 -0
  10. codetool_explore/py.typed +0 -0
  11. codetool_explore/python_backend/__init__.py +154 -0
  12. codetool_explore/python_backend/case.py +19 -0
  13. codetool_explore/python_backend/config.py +35 -0
  14. codetool_explore/python_backend/constants.py +39 -0
  15. codetool_explore/python_backend/file_search.py +51 -0
  16. codetool_explore/python_backend/ignore_rules.py +40 -0
  17. codetool_explore/python_backend/literal.py +79 -0
  18. codetool_explore/python_backend/matcher.py +79 -0
  19. codetool_explore/python_backend/models.py +49 -0
  20. codetool_explore/python_backend/output.py +82 -0
  21. codetool_explore/python_backend/regex_search.py +63 -0
  22. codetool_explore/python_backend/search.py +327 -0
  23. codetool_explore/python_backend/text.py +39 -0
  24. codetool_explore/python_backend/walker.py +119 -0
  25. codetool_explore/ranking.py +384 -0
  26. codetool_explore/roots.py +148 -0
  27. codetool_explore/rust_backend.py +308 -0
  28. codetool_explore/text_output.py +475 -0
  29. codetool_explore-0.5.0.dist-info/METADATA +240 -0
  30. codetool_explore-0.5.0.dist-info/RECORD +33 -0
  31. codetool_explore-0.5.0.dist-info/WHEEL +4 -0
  32. codetool_explore-0.5.0.dist-info/entry_points.txt +2 -0
  33. codetool_explore-0.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,308 @@
1
+ """Rust CLI backend discovery and subprocess wrapper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import platform
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from collections.abc import Iterable, Iterator
12
+ from pathlib import Path
13
+
14
+ from .cursor import normalize_limit
15
+ from .errors import (
16
+ ExploreArgumentError,
17
+ ExploreBackendError,
18
+ ExplorePatternError,
19
+ )
20
+ from .ignore import normalize_patterns
21
+ from .python_backend import normalize_mode, normalize_path_scope, normalize_target, resolve_case
22
+ from .roots import RootInput, normalize_search_roots
23
+
24
+ ENV_BINARY = "CODETOOL_EXPLORE_RUST_BINARY"
25
+ TARGET_RUNTIME_ENVS = (
26
+ "CODETOOL_EXPLORE_TARGET_RUNTIME",
27
+ "CODETOOL_TARGET_RUNTIME",
28
+ )
29
+ SUPPORTED_RUNTIME_KEYS = (
30
+ "linux-x86_64",
31
+ "linux-aarch64",
32
+ "macos-x86_64",
33
+ "macos-arm64",
34
+ "windows-x86_64",
35
+ "windows-arm64",
36
+ )
37
+ RUNTIME_ALIASES = {
38
+ "linux-amd64": "linux-x86_64",
39
+ "linux-x64": "linux-x86_64",
40
+ "linux-arm64": "linux-aarch64",
41
+ "darwin-x86_64": "macos-x86_64",
42
+ "darwin-amd64": "macos-x86_64",
43
+ "darwin-arm64": "macos-arm64",
44
+ "darwin-aarch64": "macos-arm64",
45
+ "macos-aarch64": "macos-arm64",
46
+ "macosx-x86_64": "macos-x86_64",
47
+ "macosx-arm64": "macos-arm64",
48
+ "windows-amd64": "windows-x86_64",
49
+ "windows-x64": "windows-x86_64",
50
+ "windows-aarch64": "windows-arm64",
51
+ "win-x86_64": "windows-x86_64",
52
+ "win-amd64": "windows-x86_64",
53
+ "win-arm64": "windows-arm64",
54
+ }
55
+ BINARY_NAMES: tuple[str, ...] = (
56
+ "codetool-explore-rust",
57
+ "codetool_explore_rust",
58
+ )
59
+ BASE_BINARY = "codetool-explore-rust"
60
+
61
+
62
+ class RustBackendUnavailable(ExploreBackendError):
63
+ """Raised when an explicit Rust backend request cannot be satisfied."""
64
+
65
+
66
+ def _normalize_machine(machine: str | None = None) -> str:
67
+ raw = (machine or platform.machine() or "unknown").lower().replace("-", "_")
68
+ aliases = {
69
+ "amd64": "x86_64",
70
+ "x64": "x86_64",
71
+ "i386": "x86",
72
+ "i686": "x86",
73
+ "arm64": "arm64",
74
+ "aarch64": "aarch64",
75
+ }
76
+ return aliases.get(raw, raw)
77
+
78
+
79
+ def normalize_runtime_key(value: str) -> str:
80
+ """Normalize user/platform spelling to a stable packaged-binary runtime key."""
81
+
82
+ key = value.strip().lower().replace("_", "-")
83
+ key = key.replace("linux-x86-64", "linux-x86_64")
84
+ key = key.replace("macos-x86-64", "macos-x86_64")
85
+ key = key.replace("windows-x86-64", "windows-x86_64")
86
+ key = key.replace("darwin-x86-64", "darwin-x86_64")
87
+ key = RUNTIME_ALIASES.get(key, key)
88
+ if key not in SUPPORTED_RUNTIME_KEYS:
89
+ supported = ", ".join(SUPPORTED_RUNTIME_KEYS)
90
+ raise ExploreArgumentError(
91
+ f"unsupported target runtime {value!r}; expected one of: {supported}"
92
+ )
93
+ return key
94
+
95
+
96
+ def _host_runtime_key() -> str:
97
+ if os.name == "nt":
98
+ os_tag = "windows"
99
+ elif sys.platform == "darwin":
100
+ os_tag = "macos"
101
+ elif sys.platform.startswith("linux"):
102
+ os_tag = "linux"
103
+ else:
104
+ os_tag = sys.platform.replace("-", "_")
105
+
106
+ arch = _normalize_machine()
107
+ if os_tag == "linux" and arch == "arm64":
108
+ arch = "aarch64"
109
+ elif os_tag in {"macos", "windows"} and arch == "aarch64":
110
+ arch = "arm64"
111
+ return normalize_runtime_key(f"{os_tag}-{arch}")
112
+
113
+
114
+ def _target_runtime_key() -> str:
115
+ for env_name in TARGET_RUNTIME_ENVS:
116
+ if value := os.environ.get(env_name):
117
+ return normalize_runtime_key(value)
118
+ return _host_runtime_key()
119
+
120
+
121
+ def _platform_tag() -> str:
122
+ """Return the stable runtime key used for packaged Rust helper binaries."""
123
+
124
+ return _target_runtime_key()
125
+
126
+
127
+ def packaged_binary_names(runtime_key: str | None = None) -> tuple[str, ...]:
128
+ """Return target-specific packaged binary names and generic binary names."""
129
+
130
+ tag = normalize_runtime_key(runtime_key) if runtime_key else _platform_tag()
131
+ names = [f"{BASE_BINARY}-{tag}"]
132
+ if tag == "linux-x86_64":
133
+ names.append(f"{BASE_BINARY}-linux-x86_64")
134
+ if tag.endswith("arm64"):
135
+ names.append(f"{BASE_BINARY}-{tag.replace('arm64', 'aarch64')}")
136
+ names.extend(BINARY_NAMES)
137
+ return tuple(dict.fromkeys(names))
138
+
139
+
140
+ def _with_platform_suffix(name: str) -> tuple[str, ...]:
141
+ if os.name == "nt" and not name.endswith(".exe"):
142
+ return (name + ".exe", name)
143
+ return (name,)
144
+
145
+
146
+ def _candidate_names() -> Iterator[str]:
147
+ for name in BINARY_NAMES:
148
+ yield from _with_platform_suffix(name)
149
+
150
+
151
+ def _packaged_candidate_names() -> Iterator[str]:
152
+ for name in packaged_binary_names():
153
+ yield name
154
+ if "windows-" in name and not name.endswith(".exe"):
155
+ yield name + ".exe"
156
+ elif os.name == "nt" and not name.endswith(".exe"):
157
+ yield name + ".exe"
158
+
159
+
160
+ def _is_executable_file(path: Path) -> bool:
161
+ return path.is_file() and os.access(path, os.X_OK)
162
+
163
+
164
+ def candidate_binary_paths() -> Iterator[Path]:
165
+ """Yield environment, packaged, and development Rust binary candidates."""
166
+
167
+ env_value = os.environ.get(ENV_BINARY)
168
+ if env_value:
169
+ yield Path(env_value).expanduser()
170
+
171
+ package_dir = Path(__file__).resolve().parent
172
+ for name in _packaged_candidate_names():
173
+ yield package_dir / "_bin" / name
174
+
175
+ # Development checkout: src/codetool_explore/rust_backend.py -> repo root
176
+ repo_root = package_dir.parents[1]
177
+ for profile in ("release", "debug"):
178
+ for name in _candidate_names():
179
+ yield repo_root / "rust" / "target" / profile / name
180
+
181
+
182
+ def find_rust_binary() -> str | None:
183
+ """Find the optional Rust CLI binary, if available."""
184
+
185
+ for path in candidate_binary_paths():
186
+ if _is_executable_file(path):
187
+ return str(path)
188
+
189
+ for name in _candidate_names():
190
+ found = shutil.which(name)
191
+ if found:
192
+ return found
193
+
194
+ return None
195
+
196
+
197
+ def _append_patterns(args: list[str], flag: str, patterns: Iterable[str]) -> None:
198
+ for pattern in patterns:
199
+ args.extend((flag, pattern))
200
+
201
+
202
+ def search_rust(
203
+ pattern: str,
204
+ root: RootInput = ".",
205
+ *,
206
+ regex: bool = False,
207
+ target: str = "content",
208
+ path_scope: str = "path",
209
+ glob: str | Iterable[str] | None = None,
210
+ exclude: str | Iterable[str] | None = None,
211
+ case: str = "smart",
212
+ mode: str = "files",
213
+ context_lines: int = 0,
214
+ limit: int = 50,
215
+ cursor: str | int | None = None,
216
+ binary: str | None = None,
217
+ ) -> dict[str, object]:
218
+ """Search workspace content, paths, or content/path union via Rust CLI."""
219
+
220
+ if not isinstance(pattern, str):
221
+ raise ExploreArgumentError("pattern must be a string")
222
+ normalised_target = normalize_target(target)
223
+ if pattern == "" and normalised_target in {"content", "content_or_path"}:
224
+ raise ExplorePatternError("pattern must not be empty")
225
+ root_set = normalize_search_roots(root)
226
+
227
+ normalised_mode = normalize_mode(mode)
228
+ if normalised_mode == "snippets" and normalised_target == "path":
229
+ raise ExploreArgumentError(
230
+ "mode='snippets' is not supported for target='path'; "
231
+ "use target='content' or target='content_or_path'"
232
+ )
233
+ normalised_path_scope = normalize_path_scope(path_scope)
234
+ resolve_case(str(case or "smart").lower(), pattern)
235
+ safe_limit = normalize_limit(limit)
236
+ try:
237
+ safe_context_lines = max(0, int(context_lines or 0))
238
+ except (TypeError, ValueError) as exc:
239
+ raise ExploreArgumentError(
240
+ "context_lines must be a non-negative integer"
241
+ ) from exc
242
+ glob_patterns = normalize_patterns(glob)
243
+ exclude_patterns = normalize_patterns(exclude)
244
+
245
+ executable = binary or find_rust_binary()
246
+ if not executable:
247
+ raise RustBackendUnavailable(
248
+ f"Rust backend unavailable; set {ENV_BINARY} or build rust/ with cargo"
249
+ )
250
+
251
+ args = [
252
+ executable,
253
+ "--pattern",
254
+ pattern,
255
+ "--mode",
256
+ normalised_mode,
257
+ "--target",
258
+ normalised_target,
259
+ "--path-scope",
260
+ normalised_path_scope,
261
+ "--case",
262
+ str(case or "smart"),
263
+ "--limit",
264
+ str(safe_limit),
265
+ "--context-lines",
266
+ str(safe_context_lines),
267
+ ]
268
+ for search_root in root_set.roots:
269
+ args.extend(("--root", search_root.raw))
270
+ if regex:
271
+ args.append("--regex")
272
+ if cursor not in (None, ""):
273
+ args.extend(("--cursor", str(cursor)))
274
+ _append_patterns(args, "--glob", glob_patterns)
275
+ _append_patterns(args, "--exclude", exclude_patterns)
276
+
277
+ try:
278
+ completed = subprocess.run(
279
+ args,
280
+ check=False,
281
+ capture_output=True,
282
+ text=True,
283
+ encoding="utf-8",
284
+ timeout=60,
285
+ )
286
+ except (OSError, subprocess.TimeoutExpired) as exc:
287
+ raise ExploreBackendError(f"Rust backend failed: {exc}") from exc
288
+ if completed.returncode != 0:
289
+ message = (
290
+ completed.stderr.strip()
291
+ or completed.stdout.strip()
292
+ or "Rust backend failed"
293
+ )
294
+ if "invalid regex" in message or "empty spans" in message:
295
+ raise ExplorePatternError(message)
296
+ raise ExploreBackendError(message)
297
+
298
+ try:
299
+ result = json.loads(completed.stdout)
300
+ except json.JSONDecodeError as exc:
301
+ raise ExploreBackendError("Rust backend returned invalid JSON") from exc
302
+
303
+ if not isinstance(result, dict):
304
+ raise ExploreBackendError("Rust backend returned a non-object JSON response")
305
+
306
+ result["backend"] = "rust"
307
+ result["root"] = root_set.display
308
+ return result