scry-run 0.1.0__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.
- scry_run/__init__.py +102 -0
- scry_run/backends/__init__.py +6 -0
- scry_run/backends/base.py +65 -0
- scry_run/backends/claude.py +404 -0
- scry_run/backends/frozen.py +85 -0
- scry_run/backends/registry.py +72 -0
- scry_run/cache.py +441 -0
- scry_run/cli/__init__.py +137 -0
- scry_run/cli/apps.py +396 -0
- scry_run/cli/cache.py +342 -0
- scry_run/cli/config_cmd.py +84 -0
- scry_run/cli/env.py +27 -0
- scry_run/cli/init.py +375 -0
- scry_run/cli/run.py +71 -0
- scry_run/config.py +141 -0
- scry_run/console.py +52 -0
- scry_run/context.py +298 -0
- scry_run/generator.py +698 -0
- scry_run/home.py +60 -0
- scry_run/logging.py +171 -0
- scry_run/meta.py +1852 -0
- scry_run/packages.py +175 -0
- scry_run-0.1.0.dist-info/METADATA +282 -0
- scry_run-0.1.0.dist-info/RECORD +26 -0
- scry_run-0.1.0.dist-info/WHEEL +4 -0
- scry_run-0.1.0.dist-info/entry_points.txt +2 -0
scry_run/context.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Context builder for collecting codebase and inserting hole markers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContextBuilder:
|
|
10
|
+
"""Builds context strings from the codebase for LLM prompts.
|
|
11
|
+
|
|
12
|
+
Collects all Python files in a project and formats them as a single
|
|
13
|
+
context string, with a "hole" marker indicating where code is missing.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Directories to always skip
|
|
17
|
+
SKIP_DIRS = {
|
|
18
|
+
"__pycache__",
|
|
19
|
+
".git",
|
|
20
|
+
".venv",
|
|
21
|
+
"venv",
|
|
22
|
+
".env",
|
|
23
|
+
"node_modules",
|
|
24
|
+
".pytest_cache",
|
|
25
|
+
".mypy_cache",
|
|
26
|
+
".ruff_cache",
|
|
27
|
+
"dist",
|
|
28
|
+
"build",
|
|
29
|
+
"*.egg-info",
|
|
30
|
+
".scry-run-cache",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# File patterns to skip
|
|
34
|
+
SKIP_FILES = {
|
|
35
|
+
"*.pyc",
|
|
36
|
+
"*.pyo",
|
|
37
|
+
"*.pyd",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def __init__(self, root_dir: str | Path | None = None):
|
|
41
|
+
"""Initialize context builder.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
root_dir: Root directory of the project. Defaults to current directory.
|
|
45
|
+
"""
|
|
46
|
+
self.root_dir = Path(root_dir) if root_dir else Path.cwd()
|
|
47
|
+
|
|
48
|
+
def _should_skip_dir(self, dir_path: Path) -> bool:
|
|
49
|
+
"""Check if a directory should be skipped."""
|
|
50
|
+
name = dir_path.name
|
|
51
|
+
return name in self.SKIP_DIRS or name.startswith(".")
|
|
52
|
+
|
|
53
|
+
def _should_skip_file(self, file_path: Path) -> bool:
|
|
54
|
+
"""Check if a file should be skipped."""
|
|
55
|
+
name = file_path.name
|
|
56
|
+
return any(
|
|
57
|
+
name.endswith(pattern.lstrip("*"))
|
|
58
|
+
for pattern in self.SKIP_FILES
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def collect_python_files(self) -> list[Path]:
|
|
62
|
+
"""Collect all Python files in the project.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of paths to Python files, relative to root_dir
|
|
66
|
+
"""
|
|
67
|
+
files = []
|
|
68
|
+
|
|
69
|
+
def walk(directory: Path) -> None:
|
|
70
|
+
try:
|
|
71
|
+
for item in sorted(directory.iterdir()):
|
|
72
|
+
if item.is_dir():
|
|
73
|
+
if not self._should_skip_dir(item):
|
|
74
|
+
walk(item)
|
|
75
|
+
elif item.is_file() and item.suffix == ".py":
|
|
76
|
+
if not self._should_skip_file(item):
|
|
77
|
+
files.append(item)
|
|
78
|
+
except PermissionError:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
walk(self.root_dir)
|
|
82
|
+
return files
|
|
83
|
+
|
|
84
|
+
def read_file_content(self, file_path: Path) -> str:
|
|
85
|
+
"""Read content of a file.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
file_path: Path to the file
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
File content as string
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
return file_path.read_text(encoding="utf-8")
|
|
95
|
+
except (UnicodeDecodeError, PermissionError, FileNotFoundError):
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
def build_context(
|
|
99
|
+
self,
|
|
100
|
+
class_name: str,
|
|
101
|
+
attr_name: str,
|
|
102
|
+
class_source: Optional[str] = None,
|
|
103
|
+
) -> str:
|
|
104
|
+
"""Build full context with hole marker.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
class_name: Name of the class needing the attribute
|
|
108
|
+
attr_name: Name of the missing attribute
|
|
109
|
+
class_source: Optional source code of the class itself
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Formatted context string with all files and hole marker
|
|
113
|
+
"""
|
|
114
|
+
files = self.collect_python_files()
|
|
115
|
+
|
|
116
|
+
sections = []
|
|
117
|
+
|
|
118
|
+
# Add header
|
|
119
|
+
sections.append(f"# Project: {self.root_dir.name}")
|
|
120
|
+
sections.append("")
|
|
121
|
+
|
|
122
|
+
# Add each file
|
|
123
|
+
for file_path in files:
|
|
124
|
+
relative_path = file_path.relative_to(self.root_dir)
|
|
125
|
+
content = self.read_file_content(file_path)
|
|
126
|
+
|
|
127
|
+
if content:
|
|
128
|
+
sections.append(f"# --- {relative_path} ---")
|
|
129
|
+
sections.append(content)
|
|
130
|
+
sections.append("")
|
|
131
|
+
|
|
132
|
+
# Add hole marker
|
|
133
|
+
sections.append("# --- MISSING CODE ---")
|
|
134
|
+
sections.append(f"# Class: {class_name}")
|
|
135
|
+
sections.append(f"# Missing attribute: {attr_name}")
|
|
136
|
+
sections.append("#")
|
|
137
|
+
sections.append("# Generate the code for this attribute based on:")
|
|
138
|
+
sections.append("# 1. The class name and its docstring")
|
|
139
|
+
sections.append("# 2. Existing methods in the class")
|
|
140
|
+
sections.append("# 3. How the class is used in the codebase")
|
|
141
|
+
sections.append("# 4. The attribute name (infer purpose from naming)")
|
|
142
|
+
|
|
143
|
+
if class_source:
|
|
144
|
+
sections.append("")
|
|
145
|
+
sections.append("# Current class definition:")
|
|
146
|
+
sections.append(class_source)
|
|
147
|
+
|
|
148
|
+
sections.append("# --- END MISSING CODE ---")
|
|
149
|
+
|
|
150
|
+
return "\n".join(sections)
|
|
151
|
+
|
|
152
|
+
def build_minimal_context(
|
|
153
|
+
self,
|
|
154
|
+
class_source: str,
|
|
155
|
+
class_name: str,
|
|
156
|
+
attr_name: str,
|
|
157
|
+
additional_context: Optional[str] = None,
|
|
158
|
+
) -> str:
|
|
159
|
+
"""Build minimal context with just the class and hole marker.
|
|
160
|
+
|
|
161
|
+
Useful for smaller, faster generation when full codebase isn't needed.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
class_source: Source code of the class
|
|
165
|
+
class_name: Name of the class
|
|
166
|
+
attr_name: Name of the missing attribute
|
|
167
|
+
additional_context: Optional additional context to include
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Formatted context string
|
|
171
|
+
"""
|
|
172
|
+
sections = [
|
|
173
|
+
"# Class definition:",
|
|
174
|
+
class_source,
|
|
175
|
+
"",
|
|
176
|
+
"# --- MISSING CODE ---",
|
|
177
|
+
f"# Class: {class_name}",
|
|
178
|
+
f"# Missing attribute: {attr_name}",
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
if additional_context:
|
|
182
|
+
sections.extend([
|
|
183
|
+
"",
|
|
184
|
+
"# Additional context:",
|
|
185
|
+
additional_context,
|
|
186
|
+
])
|
|
187
|
+
|
|
188
|
+
sections.append("# --- END MISSING CODE ---")
|
|
189
|
+
|
|
190
|
+
return "\n".join(sections)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def build_runtime_context() -> str:
|
|
194
|
+
"""Capture runtime context from the call stack.
|
|
195
|
+
|
|
196
|
+
Captures the call stack and local variables to give the LLM
|
|
197
|
+
context about how the missing method is being called.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Formatted string with call stack and variable info
|
|
201
|
+
"""
|
|
202
|
+
import sys
|
|
203
|
+
import traceback
|
|
204
|
+
|
|
205
|
+
lines = ["# === RUNTIME CONTEXT ===", ""]
|
|
206
|
+
|
|
207
|
+
# Get the current frame and walk up the stack
|
|
208
|
+
frame = sys._getframe()
|
|
209
|
+
|
|
210
|
+
# Collect relevant frames (skip scry_run internals)
|
|
211
|
+
relevant_frames = []
|
|
212
|
+
current = frame
|
|
213
|
+
while current and len(relevant_frames) < 50:
|
|
214
|
+
filename = current.f_code.co_filename
|
|
215
|
+
# Skip scry_run internal frames (but include test files)
|
|
216
|
+
if "scry_run" not in filename or "test" in filename:
|
|
217
|
+
relevant_frames.append(current)
|
|
218
|
+
current = current.f_back
|
|
219
|
+
|
|
220
|
+
if relevant_frames:
|
|
221
|
+
lines.append("# Call stack (most recent call first):")
|
|
222
|
+
lines.append("")
|
|
223
|
+
|
|
224
|
+
for i, f in enumerate(relevant_frames[:50]): # Limit to 50 frames
|
|
225
|
+
filename = f.f_code.co_filename
|
|
226
|
+
lineno = f.f_lineno
|
|
227
|
+
funcname = f.f_code.co_name
|
|
228
|
+
|
|
229
|
+
# Try to get the source line
|
|
230
|
+
try:
|
|
231
|
+
import linecache
|
|
232
|
+
source_line = linecache.getline(filename, lineno).strip()
|
|
233
|
+
except Exception:
|
|
234
|
+
source_line = "<source unavailable>"
|
|
235
|
+
|
|
236
|
+
lines.append(f"# Frame {i}: {funcname}() at {filename}:{lineno}")
|
|
237
|
+
lines.append(f"# {source_line}")
|
|
238
|
+
|
|
239
|
+
# Capture local variables (but not too many)
|
|
240
|
+
locals_dict = f.f_locals
|
|
241
|
+
if locals_dict:
|
|
242
|
+
# Filter out internal/large objects
|
|
243
|
+
interesting_vars = {}
|
|
244
|
+
for name, value in locals_dict.items():
|
|
245
|
+
if name.startswith("_"):
|
|
246
|
+
continue
|
|
247
|
+
# Skip self/cls as they're obvious
|
|
248
|
+
if name in ("self", "cls"):
|
|
249
|
+
continue
|
|
250
|
+
# Try to get a reasonable repr
|
|
251
|
+
try:
|
|
252
|
+
value_repr = repr(value)
|
|
253
|
+
if len(value_repr) > 200:
|
|
254
|
+
value_repr = value_repr[:200] + "..."
|
|
255
|
+
interesting_vars[name] = value_repr
|
|
256
|
+
except Exception:
|
|
257
|
+
interesting_vars[name] = "<unable to repr>"
|
|
258
|
+
|
|
259
|
+
if interesting_vars:
|
|
260
|
+
lines.append(f"# Local variables:")
|
|
261
|
+
for name, val in list(interesting_vars.items())[:5]:
|
|
262
|
+
lines.append(f"# {name} = {val}")
|
|
263
|
+
|
|
264
|
+
lines.append("")
|
|
265
|
+
|
|
266
|
+
# Add the immediate caller's arguments if this is a method call
|
|
267
|
+
if relevant_frames:
|
|
268
|
+
caller = relevant_frames[0]
|
|
269
|
+
caller_locals = caller.f_locals
|
|
270
|
+
|
|
271
|
+
# Get function arguments from the frame
|
|
272
|
+
code = caller.f_code
|
|
273
|
+
arg_names = code.co_varnames[:code.co_argcount]
|
|
274
|
+
|
|
275
|
+
args_info = {}
|
|
276
|
+
for arg_name in arg_names:
|
|
277
|
+
if arg_name in ("self", "cls"):
|
|
278
|
+
continue
|
|
279
|
+
if arg_name in caller_locals:
|
|
280
|
+
try:
|
|
281
|
+
val = repr(caller_locals[arg_name])
|
|
282
|
+
if len(val) > 200:
|
|
283
|
+
val = val[:200] + "..."
|
|
284
|
+
args_info[arg_name] = val
|
|
285
|
+
except Exception:
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
if args_info:
|
|
289
|
+
lines.append("# === CALL ARGUMENTS ===")
|
|
290
|
+
lines.append("# The method was called with these arguments:")
|
|
291
|
+
for name, val in args_info.items():
|
|
292
|
+
lines.append(f"# {name} = {val}")
|
|
293
|
+
lines.append("")
|
|
294
|
+
|
|
295
|
+
lines.append("# === END RUNTIME CONTEXT ===")
|
|
296
|
+
|
|
297
|
+
return "\n".join(lines)
|
|
298
|
+
|