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/meta.py
ADDED
|
@@ -0,0 +1,1852 @@
|
|
|
1
|
+
"""Metaclass and base class for LLM-powered dynamic code generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from scry_run.console import (
|
|
12
|
+
status, info, success, warning, error,
|
|
13
|
+
generating, generated, using_cached, err_console
|
|
14
|
+
)
|
|
15
|
+
from scry_run.packages import install_packages, get_installed_packages
|
|
16
|
+
|
|
17
|
+
# Thread-local guard to prevent infinite recursion during repr() in context building
|
|
18
|
+
# When this is active, LLMMethodProxy.__repr__ returns a safe string instead of resolving
|
|
19
|
+
_context_build_guard = threading.local()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from scry_run.cache import ScryCache
|
|
24
|
+
from scry_run.generator import CodeGenerator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# Dunder (Magic Method) Configuration
|
|
29
|
+
# =============================================================================
|
|
30
|
+
|
|
31
|
+
# All known Python dunder methods from the data model
|
|
32
|
+
ALL_KNOWN_DUNDERS = {
|
|
33
|
+
# Basic customization
|
|
34
|
+
'__new__', '__init__', '__del__',
|
|
35
|
+
'__repr__', '__str__', '__bytes__', '__format__',
|
|
36
|
+
|
|
37
|
+
# Rich comparison
|
|
38
|
+
'__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__',
|
|
39
|
+
'__hash__', '__bool__',
|
|
40
|
+
|
|
41
|
+
# Attribute access
|
|
42
|
+
'__getattr__', '__getattribute__', '__setattr__', '__delattr__', '__dir__',
|
|
43
|
+
|
|
44
|
+
# Descriptors
|
|
45
|
+
'__get__', '__set__', '__delete__', '__set_name__',
|
|
46
|
+
|
|
47
|
+
# Class creation
|
|
48
|
+
'__init_subclass__', '__prepare__', '__class_getitem__',
|
|
49
|
+
|
|
50
|
+
# Instance and subclass checks
|
|
51
|
+
'__instancecheck__', '__subclasscheck__',
|
|
52
|
+
|
|
53
|
+
# Callable objects
|
|
54
|
+
'__call__',
|
|
55
|
+
|
|
56
|
+
# Container emulation
|
|
57
|
+
'__len__', '__length_hint__', '__getitem__', '__setitem__', '__delitem__',
|
|
58
|
+
'__missing__', '__iter__', '__next__', '__reversed__', '__contains__',
|
|
59
|
+
|
|
60
|
+
# Numeric types - binary operations
|
|
61
|
+
'__add__', '__sub__', '__mul__', '__matmul__', '__truediv__', '__floordiv__',
|
|
62
|
+
'__mod__', '__divmod__', '__pow__', '__lshift__', '__rshift__',
|
|
63
|
+
'__and__', '__xor__', '__or__',
|
|
64
|
+
|
|
65
|
+
# Numeric types - reflected (swapped) operations
|
|
66
|
+
'__radd__', '__rsub__', '__rmul__', '__rmatmul__', '__rtruediv__',
|
|
67
|
+
'__rfloordiv__', '__rmod__', '__rdivmod__', '__rpow__',
|
|
68
|
+
'__rlshift__', '__rrshift__', '__rand__', '__rxor__', '__ror__',
|
|
69
|
+
|
|
70
|
+
# Numeric types - in-place operations
|
|
71
|
+
'__iadd__', '__isub__', '__imul__', '__imatmul__', '__itruediv__',
|
|
72
|
+
'__ifloordiv__', '__imod__', '__ipow__', '__ilshift__', '__irshift__',
|
|
73
|
+
'__iand__', '__ixor__', '__ior__',
|
|
74
|
+
|
|
75
|
+
# Unary operations
|
|
76
|
+
'__neg__', '__pos__', '__abs__', '__invert__',
|
|
77
|
+
|
|
78
|
+
# Type conversion
|
|
79
|
+
'__complex__', '__int__', '__float__', '__index__',
|
|
80
|
+
'__round__', '__trunc__', '__floor__', '__ceil__',
|
|
81
|
+
|
|
82
|
+
# Context managers
|
|
83
|
+
'__enter__', '__exit__',
|
|
84
|
+
|
|
85
|
+
# Async context managers and coroutines
|
|
86
|
+
'__aenter__', '__aexit__', '__await__', '__aiter__', '__anext__',
|
|
87
|
+
|
|
88
|
+
# Buffer protocol
|
|
89
|
+
'__buffer__', '__release_buffer__',
|
|
90
|
+
|
|
91
|
+
# Pickling
|
|
92
|
+
'__getnewargs_ex__', '__getnewargs__', '__getstate__', '__setstate__',
|
|
93
|
+
'__reduce__', '__reduce_ex__',
|
|
94
|
+
|
|
95
|
+
# Copy
|
|
96
|
+
'__copy__', '__deepcopy__',
|
|
97
|
+
|
|
98
|
+
# Introspection
|
|
99
|
+
'__sizeof__', '__fspath__',
|
|
100
|
+
|
|
101
|
+
# Match statement (3.10+)
|
|
102
|
+
'__match_args__',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Dunders that should NEVER be auto-generated (would break Python or scry-run)
|
|
106
|
+
FORBIDDEN_DUNDERS = {
|
|
107
|
+
'__getattribute__', # Would break all attribute lookup
|
|
108
|
+
'__setattr__', # Would break attribute assignment
|
|
109
|
+
'__delattr__', # Would break attribute deletion
|
|
110
|
+
'__getattr__', # Already used by ScryClass for lazy generation
|
|
111
|
+
'__class__', # Would break type system
|
|
112
|
+
'__dict__', # Would break attribute storage
|
|
113
|
+
'__init_subclass__', # Would break class creation hooks
|
|
114
|
+
'__prepare__', # Would break metaclass protocol
|
|
115
|
+
'__mro__', # Would break method resolution order
|
|
116
|
+
'__new__', # Handled specially, dangerous to auto-generate
|
|
117
|
+
'__init__', # Handled specially by metaclass __call__
|
|
118
|
+
'__del__', # Destructor - dangerous to auto-generate
|
|
119
|
+
'__subclasshook__', # ABC hook - dangerous
|
|
120
|
+
'__instancecheck__', # isinstance() - dangerous
|
|
121
|
+
'__subclasscheck__', # issubclass() - dangerous
|
|
122
|
+
# Note: __repr__ and __str__ are now generatable with recursion guard
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Dunders that are safe to lazily generate via trampolines
|
|
126
|
+
GENERATABLE_DUNDERS = ALL_KNOWN_DUNDERS - FORBIDDEN_DUNDERS
|
|
127
|
+
|
|
128
|
+
# Thread-local guard to prevent recursion when generating __repr__/__str__
|
|
129
|
+
_generation_guard = threading.local()
|
|
130
|
+
|
|
131
|
+
# Per-attribute generation locks to prevent concurrent generation of the same attribute
|
|
132
|
+
# Key: (class_name, attr_name), Value: threading.Lock
|
|
133
|
+
_generation_locks: dict[tuple[str, str], threading.Lock] = {}
|
|
134
|
+
_generation_locks_lock = threading.Lock() # Lock for accessing the locks dict
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _get_generation_lock(class_name: str, attr_name: str) -> threading.Lock:
|
|
138
|
+
"""Get or create a lock for generating a specific class attribute."""
|
|
139
|
+
key = (class_name, attr_name)
|
|
140
|
+
with _generation_locks_lock:
|
|
141
|
+
if key not in _generation_locks:
|
|
142
|
+
_generation_locks[key] = threading.Lock()
|
|
143
|
+
return _generation_locks[key]
|
|
144
|
+
|
|
145
|
+
# Metadata for known dunders to provide better generation context
|
|
146
|
+
DUNDER_INFO = {
|
|
147
|
+
# String representation
|
|
148
|
+
'__str__': {
|
|
149
|
+
'signature': 'def __str__(self) -> str',
|
|
150
|
+
'returns': 'str',
|
|
151
|
+
'purpose': 'Human-readable string representation',
|
|
152
|
+
'description': 'Called by str(obj) and print(obj). Return a friendly, readable string.',
|
|
153
|
+
},
|
|
154
|
+
'__repr__': {
|
|
155
|
+
'signature': 'def __repr__(self) -> str',
|
|
156
|
+
'returns': 'str',
|
|
157
|
+
'purpose': 'Unambiguous string representation',
|
|
158
|
+
'description': 'Called by repr(obj) and in REPL. Ideally returns valid Python to recreate the object.',
|
|
159
|
+
},
|
|
160
|
+
'__bytes__': {
|
|
161
|
+
'signature': 'def __bytes__(self) -> bytes',
|
|
162
|
+
'returns': 'bytes',
|
|
163
|
+
'purpose': 'Byte representation',
|
|
164
|
+
'description': 'Called by bytes(obj). Return a bytes representation.',
|
|
165
|
+
},
|
|
166
|
+
'__format__': {
|
|
167
|
+
'signature': 'def __format__(self, format_spec: str) -> str',
|
|
168
|
+
'returns': 'str',
|
|
169
|
+
'purpose': 'Custom formatting',
|
|
170
|
+
'description': 'Called by format(obj, spec) and f-strings. Return formatted string.',
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
# Comparison
|
|
174
|
+
'__lt__': {
|
|
175
|
+
'signature': 'def __lt__(self, other) -> bool',
|
|
176
|
+
'returns': 'bool',
|
|
177
|
+
'purpose': 'Less than comparison',
|
|
178
|
+
'description': 'Called by obj < other. Return True if self is less than other.',
|
|
179
|
+
},
|
|
180
|
+
'__le__': {
|
|
181
|
+
'signature': 'def __le__(self, other) -> bool',
|
|
182
|
+
'returns': 'bool',
|
|
183
|
+
'purpose': 'Less than or equal comparison',
|
|
184
|
+
'description': 'Called by obj <= other. Return True if self is less than or equal to other.',
|
|
185
|
+
},
|
|
186
|
+
'__eq__': {
|
|
187
|
+
'signature': 'def __eq__(self, other) -> bool',
|
|
188
|
+
'returns': 'bool',
|
|
189
|
+
'purpose': 'Equality comparison',
|
|
190
|
+
'description': 'Called by obj == other. Return True if equal. Should be consistent with __hash__.',
|
|
191
|
+
},
|
|
192
|
+
'__ne__': {
|
|
193
|
+
'signature': 'def __ne__(self, other) -> bool',
|
|
194
|
+
'returns': 'bool',
|
|
195
|
+
'purpose': 'Inequality comparison',
|
|
196
|
+
'description': 'Called by obj != other. Return True if not equal.',
|
|
197
|
+
},
|
|
198
|
+
'__gt__': {
|
|
199
|
+
'signature': 'def __gt__(self, other) -> bool',
|
|
200
|
+
'returns': 'bool',
|
|
201
|
+
'purpose': 'Greater than comparison',
|
|
202
|
+
'description': 'Called by obj > other. Return True if self is greater than other.',
|
|
203
|
+
},
|
|
204
|
+
'__ge__': {
|
|
205
|
+
'signature': 'def __ge__(self, other) -> bool',
|
|
206
|
+
'returns': 'bool',
|
|
207
|
+
'purpose': 'Greater than or equal comparison',
|
|
208
|
+
'description': 'Called by obj >= other. Return True if self is greater than or equal to other.',
|
|
209
|
+
},
|
|
210
|
+
'__hash__': {
|
|
211
|
+
'signature': 'def __hash__(self) -> int',
|
|
212
|
+
'returns': 'int',
|
|
213
|
+
'purpose': 'Hash value for dict keys and sets',
|
|
214
|
+
'description': 'Called by hash(obj). Return integer hash. Must be consistent with __eq__.',
|
|
215
|
+
},
|
|
216
|
+
'__bool__': {
|
|
217
|
+
'signature': 'def __bool__(self) -> bool',
|
|
218
|
+
'returns': 'bool',
|
|
219
|
+
'purpose': 'Truth value testing',
|
|
220
|
+
'description': 'Called by bool(obj) and in if statements. Return True or False.',
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
# Container methods
|
|
224
|
+
'__len__': {
|
|
225
|
+
'signature': 'def __len__(self) -> int',
|
|
226
|
+
'returns': 'int',
|
|
227
|
+
'purpose': 'Return the length/count of items',
|
|
228
|
+
'description': 'Called by len(obj). Return non-negative integer count.',
|
|
229
|
+
},
|
|
230
|
+
'__length_hint__': {
|
|
231
|
+
'signature': 'def __length_hint__(self) -> int',
|
|
232
|
+
'returns': 'int',
|
|
233
|
+
'purpose': 'Estimated length hint',
|
|
234
|
+
'description': 'Called by operator.length_hint(). Return estimated length.',
|
|
235
|
+
},
|
|
236
|
+
'__getitem__': {
|
|
237
|
+
'signature': 'def __getitem__(self, key)',
|
|
238
|
+
'returns': 'Any',
|
|
239
|
+
'purpose': 'Get item by key/index',
|
|
240
|
+
'description': 'Called by obj[key]. Return the item at the given key or index.',
|
|
241
|
+
},
|
|
242
|
+
'__setitem__': {
|
|
243
|
+
'signature': 'def __setitem__(self, key, value) -> None',
|
|
244
|
+
'returns': 'None',
|
|
245
|
+
'purpose': 'Set item by key/index',
|
|
246
|
+
'description': 'Called by obj[key] = value. Store value at the given key or index.',
|
|
247
|
+
},
|
|
248
|
+
'__delitem__': {
|
|
249
|
+
'signature': 'def __delitem__(self, key) -> None',
|
|
250
|
+
'returns': 'None',
|
|
251
|
+
'purpose': 'Delete item by key/index',
|
|
252
|
+
'description': 'Called by del obj[key]. Remove the item at the given key or index.',
|
|
253
|
+
},
|
|
254
|
+
'__missing__': {
|
|
255
|
+
'signature': 'def __missing__(self, key)',
|
|
256
|
+
'returns': 'Any',
|
|
257
|
+
'purpose': 'Handle missing dict keys',
|
|
258
|
+
'description': 'Called by dict subclasses when key is not found. Return default or raise KeyError.',
|
|
259
|
+
},
|
|
260
|
+
'__iter__': {
|
|
261
|
+
'signature': 'def __iter__(self)',
|
|
262
|
+
'returns': 'Iterator',
|
|
263
|
+
'purpose': 'Return an iterator',
|
|
264
|
+
'description': 'Called by iter(obj) and for loops. Return an iterator object.',
|
|
265
|
+
},
|
|
266
|
+
'__next__': {
|
|
267
|
+
'signature': 'def __next__(self)',
|
|
268
|
+
'returns': 'Any',
|
|
269
|
+
'purpose': 'Get next item from iterator',
|
|
270
|
+
'description': 'Called by next(obj). Return next item or raise StopIteration.',
|
|
271
|
+
},
|
|
272
|
+
'__reversed__': {
|
|
273
|
+
'signature': 'def __reversed__(self)',
|
|
274
|
+
'returns': 'Iterator',
|
|
275
|
+
'purpose': 'Return reversed iterator',
|
|
276
|
+
'description': 'Called by reversed(obj). Return iterator over items in reverse order.',
|
|
277
|
+
},
|
|
278
|
+
'__contains__': {
|
|
279
|
+
'signature': 'def __contains__(self, item) -> bool',
|
|
280
|
+
'returns': 'bool',
|
|
281
|
+
'purpose': 'Membership test',
|
|
282
|
+
'description': 'Called by "item in obj". Return True if item is contained.',
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
# Callable
|
|
286
|
+
'__call__': {
|
|
287
|
+
'signature': 'def __call__(self, *args, **kwargs)',
|
|
288
|
+
'returns': 'Any',
|
|
289
|
+
'purpose': 'Make instance callable like a function',
|
|
290
|
+
'description': 'Called by obj(*args, **kwargs). Makes the instance work like a function.',
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
# Context managers
|
|
294
|
+
'__enter__': {
|
|
295
|
+
'signature': 'def __enter__(self)',
|
|
296
|
+
'returns': 'Any',
|
|
297
|
+
'purpose': 'Context manager entry',
|
|
298
|
+
'description': 'Called at start of "with obj as x:" block. Return value is assigned to x.',
|
|
299
|
+
},
|
|
300
|
+
'__exit__': {
|
|
301
|
+
'signature': 'def __exit__(self, exc_type, exc_val, exc_tb)',
|
|
302
|
+
'returns': 'bool | None',
|
|
303
|
+
'purpose': 'Context manager exit',
|
|
304
|
+
'description': 'Called at end of with block. Return True to suppress exceptions.',
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
# Async context managers
|
|
308
|
+
'__aenter__': {
|
|
309
|
+
'signature': 'async def __aenter__(self)',
|
|
310
|
+
'returns': 'Any',
|
|
311
|
+
'purpose': 'Async context manager entry',
|
|
312
|
+
'description': 'Called at start of "async with obj as x:" block.',
|
|
313
|
+
},
|
|
314
|
+
'__aexit__': {
|
|
315
|
+
'signature': 'async def __aexit__(self, exc_type, exc_val, exc_tb)',
|
|
316
|
+
'returns': 'bool | None',
|
|
317
|
+
'purpose': 'Async context manager exit',
|
|
318
|
+
'description': 'Called at end of async with block.',
|
|
319
|
+
},
|
|
320
|
+
'__aiter__': {
|
|
321
|
+
'signature': 'def __aiter__(self)',
|
|
322
|
+
'returns': 'AsyncIterator',
|
|
323
|
+
'purpose': 'Return async iterator',
|
|
324
|
+
'description': 'Called by "async for x in obj". Return async iterator.',
|
|
325
|
+
},
|
|
326
|
+
'__anext__': {
|
|
327
|
+
'signature': 'async def __anext__(self)',
|
|
328
|
+
'returns': 'Any',
|
|
329
|
+
'purpose': 'Get next item from async iterator',
|
|
330
|
+
'description': 'Called by async iteration. Return next item or raise StopAsyncIteration.',
|
|
331
|
+
},
|
|
332
|
+
'__await__': {
|
|
333
|
+
'signature': 'def __await__(self)',
|
|
334
|
+
'returns': 'Iterator',
|
|
335
|
+
'purpose': 'Make object awaitable',
|
|
336
|
+
'description': 'Called by "await obj". Return iterator for await protocol.',
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
# Numeric - binary
|
|
340
|
+
'__add__': {
|
|
341
|
+
'signature': 'def __add__(self, other)',
|
|
342
|
+
'returns': 'Any',
|
|
343
|
+
'purpose': 'Addition',
|
|
344
|
+
'description': 'Called by obj + other. Return the sum.',
|
|
345
|
+
},
|
|
346
|
+
'__sub__': {
|
|
347
|
+
'signature': 'def __sub__(self, other)',
|
|
348
|
+
'returns': 'Any',
|
|
349
|
+
'purpose': 'Subtraction',
|
|
350
|
+
'description': 'Called by obj - other. Return the difference.',
|
|
351
|
+
},
|
|
352
|
+
'__mul__': {
|
|
353
|
+
'signature': 'def __mul__(self, other)',
|
|
354
|
+
'returns': 'Any',
|
|
355
|
+
'purpose': 'Multiplication',
|
|
356
|
+
'description': 'Called by obj * other. Return the product.',
|
|
357
|
+
},
|
|
358
|
+
'__matmul__': {
|
|
359
|
+
'signature': 'def __matmul__(self, other)',
|
|
360
|
+
'returns': 'Any',
|
|
361
|
+
'purpose': 'Matrix multiplication',
|
|
362
|
+
'description': 'Called by obj @ other. Return matrix product.',
|
|
363
|
+
},
|
|
364
|
+
'__truediv__': {
|
|
365
|
+
'signature': 'def __truediv__(self, other)',
|
|
366
|
+
'returns': 'Any',
|
|
367
|
+
'purpose': 'True division',
|
|
368
|
+
'description': 'Called by obj / other. Return quotient.',
|
|
369
|
+
},
|
|
370
|
+
'__floordiv__': {
|
|
371
|
+
'signature': 'def __floordiv__(self, other)',
|
|
372
|
+
'returns': 'Any',
|
|
373
|
+
'purpose': 'Floor division',
|
|
374
|
+
'description': 'Called by obj // other. Return floor of quotient.',
|
|
375
|
+
},
|
|
376
|
+
'__mod__': {
|
|
377
|
+
'signature': 'def __mod__(self, other)',
|
|
378
|
+
'returns': 'Any',
|
|
379
|
+
'purpose': 'Modulo',
|
|
380
|
+
'description': 'Called by obj % other. Return remainder.',
|
|
381
|
+
},
|
|
382
|
+
'__divmod__': {
|
|
383
|
+
'signature': 'def __divmod__(self, other)',
|
|
384
|
+
'returns': 'tuple',
|
|
385
|
+
'purpose': 'Division with remainder',
|
|
386
|
+
'description': 'Called by divmod(obj, other). Return (quotient, remainder).',
|
|
387
|
+
},
|
|
388
|
+
'__pow__': {
|
|
389
|
+
'signature': 'def __pow__(self, other, mod=None)',
|
|
390
|
+
'returns': 'Any',
|
|
391
|
+
'purpose': 'Exponentiation',
|
|
392
|
+
'description': 'Called by obj ** other or pow(). Return power.',
|
|
393
|
+
},
|
|
394
|
+
'__lshift__': {
|
|
395
|
+
'signature': 'def __lshift__(self, other)',
|
|
396
|
+
'returns': 'Any',
|
|
397
|
+
'purpose': 'Left bit shift',
|
|
398
|
+
'description': 'Called by obj << other. Return shifted value.',
|
|
399
|
+
},
|
|
400
|
+
'__rshift__': {
|
|
401
|
+
'signature': 'def __rshift__(self, other)',
|
|
402
|
+
'returns': 'Any',
|
|
403
|
+
'purpose': 'Right bit shift',
|
|
404
|
+
'description': 'Called by obj >> other. Return shifted value.',
|
|
405
|
+
},
|
|
406
|
+
'__and__': {
|
|
407
|
+
'signature': 'def __and__(self, other)',
|
|
408
|
+
'returns': 'Any',
|
|
409
|
+
'purpose': 'Bitwise AND',
|
|
410
|
+
'description': 'Called by obj & other. Return bitwise AND.',
|
|
411
|
+
},
|
|
412
|
+
'__xor__': {
|
|
413
|
+
'signature': 'def __xor__(self, other)',
|
|
414
|
+
'returns': 'Any',
|
|
415
|
+
'purpose': 'Bitwise XOR',
|
|
416
|
+
'description': 'Called by obj ^ other. Return bitwise XOR.',
|
|
417
|
+
},
|
|
418
|
+
'__or__': {
|
|
419
|
+
'signature': 'def __or__(self, other)',
|
|
420
|
+
'returns': 'Any',
|
|
421
|
+
'purpose': 'Bitwise OR',
|
|
422
|
+
'description': 'Called by obj | other. Return bitwise OR.',
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
# Numeric - reflected (when left operand doesn't support the operation)
|
|
426
|
+
'__radd__': {
|
|
427
|
+
'signature': 'def __radd__(self, other)',
|
|
428
|
+
'returns': 'Any',
|
|
429
|
+
'purpose': 'Reflected addition',
|
|
430
|
+
'description': 'Called by other + obj when other lacks __add__.',
|
|
431
|
+
},
|
|
432
|
+
'__rsub__': {
|
|
433
|
+
'signature': 'def __rsub__(self, other)',
|
|
434
|
+
'returns': 'Any',
|
|
435
|
+
'purpose': 'Reflected subtraction',
|
|
436
|
+
'description': 'Called by other - obj when other lacks __sub__.',
|
|
437
|
+
},
|
|
438
|
+
'__rmul__': {
|
|
439
|
+
'signature': 'def __rmul__(self, other)',
|
|
440
|
+
'returns': 'Any',
|
|
441
|
+
'purpose': 'Reflected multiplication',
|
|
442
|
+
'description': 'Called by other * obj when other lacks __mul__.',
|
|
443
|
+
},
|
|
444
|
+
'__rmatmul__': {
|
|
445
|
+
'signature': 'def __rmatmul__(self, other)',
|
|
446
|
+
'returns': 'Any',
|
|
447
|
+
'purpose': 'Reflected matrix multiplication',
|
|
448
|
+
'description': 'Called by other @ obj when other lacks __matmul__.',
|
|
449
|
+
},
|
|
450
|
+
'__rtruediv__': {
|
|
451
|
+
'signature': 'def __rtruediv__(self, other)',
|
|
452
|
+
'returns': 'Any',
|
|
453
|
+
'purpose': 'Reflected true division',
|
|
454
|
+
'description': 'Called by other / obj when other lacks __truediv__.',
|
|
455
|
+
},
|
|
456
|
+
'__rfloordiv__': {
|
|
457
|
+
'signature': 'def __rfloordiv__(self, other)',
|
|
458
|
+
'returns': 'Any',
|
|
459
|
+
'purpose': 'Reflected floor division',
|
|
460
|
+
'description': 'Called by other // obj when other lacks __floordiv__.',
|
|
461
|
+
},
|
|
462
|
+
'__rmod__': {
|
|
463
|
+
'signature': 'def __rmod__(self, other)',
|
|
464
|
+
'returns': 'Any',
|
|
465
|
+
'purpose': 'Reflected modulo',
|
|
466
|
+
'description': 'Called by other % obj when other lacks __mod__.',
|
|
467
|
+
},
|
|
468
|
+
'__rdivmod__': {
|
|
469
|
+
'signature': 'def __rdivmod__(self, other)',
|
|
470
|
+
'returns': 'tuple',
|
|
471
|
+
'purpose': 'Reflected divmod',
|
|
472
|
+
'description': 'Called by divmod(other, obj) when other lacks __divmod__.',
|
|
473
|
+
},
|
|
474
|
+
'__rpow__': {
|
|
475
|
+
'signature': 'def __rpow__(self, other, mod=None)',
|
|
476
|
+
'returns': 'Any',
|
|
477
|
+
'purpose': 'Reflected power',
|
|
478
|
+
'description': 'Called by other ** obj when other lacks __pow__.',
|
|
479
|
+
},
|
|
480
|
+
'__rlshift__': {
|
|
481
|
+
'signature': 'def __rlshift__(self, other)',
|
|
482
|
+
'returns': 'Any',
|
|
483
|
+
'purpose': 'Reflected left shift',
|
|
484
|
+
'description': 'Called by other << obj when other lacks __lshift__.',
|
|
485
|
+
},
|
|
486
|
+
'__rrshift__': {
|
|
487
|
+
'signature': 'def __rrshift__(self, other)',
|
|
488
|
+
'returns': 'Any',
|
|
489
|
+
'purpose': 'Reflected right shift',
|
|
490
|
+
'description': 'Called by other >> obj when other lacks __rshift__.',
|
|
491
|
+
},
|
|
492
|
+
'__rand__': {
|
|
493
|
+
'signature': 'def __rand__(self, other)',
|
|
494
|
+
'returns': 'Any',
|
|
495
|
+
'purpose': 'Reflected bitwise AND',
|
|
496
|
+
'description': 'Called by other & obj when other lacks __and__.',
|
|
497
|
+
},
|
|
498
|
+
'__rxor__': {
|
|
499
|
+
'signature': 'def __rxor__(self, other)',
|
|
500
|
+
'returns': 'Any',
|
|
501
|
+
'purpose': 'Reflected bitwise XOR',
|
|
502
|
+
'description': 'Called by other ^ obj when other lacks __xor__.',
|
|
503
|
+
},
|
|
504
|
+
'__ror__': {
|
|
505
|
+
'signature': 'def __ror__(self, other)',
|
|
506
|
+
'returns': 'Any',
|
|
507
|
+
'purpose': 'Reflected bitwise OR',
|
|
508
|
+
'description': 'Called by other | obj when other lacks __or__.',
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
# Numeric - in-place
|
|
512
|
+
'__iadd__': {
|
|
513
|
+
'signature': 'def __iadd__(self, other)',
|
|
514
|
+
'returns': 'Self',
|
|
515
|
+
'purpose': 'In-place addition',
|
|
516
|
+
'description': 'Called by obj += other. Modify self and return self.',
|
|
517
|
+
},
|
|
518
|
+
'__isub__': {
|
|
519
|
+
'signature': 'def __isub__(self, other)',
|
|
520
|
+
'returns': 'Self',
|
|
521
|
+
'purpose': 'In-place subtraction',
|
|
522
|
+
'description': 'Called by obj -= other. Modify self and return self.',
|
|
523
|
+
},
|
|
524
|
+
'__imul__': {
|
|
525
|
+
'signature': 'def __imul__(self, other)',
|
|
526
|
+
'returns': 'Self',
|
|
527
|
+
'purpose': 'In-place multiplication',
|
|
528
|
+
'description': 'Called by obj *= other. Modify self and return self.',
|
|
529
|
+
},
|
|
530
|
+
'__imatmul__': {
|
|
531
|
+
'signature': 'def __imatmul__(self, other)',
|
|
532
|
+
'returns': 'Self',
|
|
533
|
+
'purpose': 'In-place matrix multiplication',
|
|
534
|
+
'description': 'Called by obj @= other. Modify self and return self.',
|
|
535
|
+
},
|
|
536
|
+
'__itruediv__': {
|
|
537
|
+
'signature': 'def __itruediv__(self, other)',
|
|
538
|
+
'returns': 'Self',
|
|
539
|
+
'purpose': 'In-place true division',
|
|
540
|
+
'description': 'Called by obj /= other. Modify self and return self.',
|
|
541
|
+
},
|
|
542
|
+
'__ifloordiv__': {
|
|
543
|
+
'signature': 'def __ifloordiv__(self, other)',
|
|
544
|
+
'returns': 'Self',
|
|
545
|
+
'purpose': 'In-place floor division',
|
|
546
|
+
'description': 'Called by obj //= other. Modify self and return self.',
|
|
547
|
+
},
|
|
548
|
+
'__imod__': {
|
|
549
|
+
'signature': 'def __imod__(self, other)',
|
|
550
|
+
'returns': 'Self',
|
|
551
|
+
'purpose': 'In-place modulo',
|
|
552
|
+
'description': 'Called by obj %= other. Modify self and return self.',
|
|
553
|
+
},
|
|
554
|
+
'__ipow__': {
|
|
555
|
+
'signature': 'def __ipow__(self, other)',
|
|
556
|
+
'returns': 'Self',
|
|
557
|
+
'purpose': 'In-place power',
|
|
558
|
+
'description': 'Called by obj **= other. Modify self and return self.',
|
|
559
|
+
},
|
|
560
|
+
'__ilshift__': {
|
|
561
|
+
'signature': 'def __ilshift__(self, other)',
|
|
562
|
+
'returns': 'Self',
|
|
563
|
+
'purpose': 'In-place left shift',
|
|
564
|
+
'description': 'Called by obj <<= other. Modify self and return self.',
|
|
565
|
+
},
|
|
566
|
+
'__irshift__': {
|
|
567
|
+
'signature': 'def __irshift__(self, other)',
|
|
568
|
+
'returns': 'Self',
|
|
569
|
+
'purpose': 'In-place right shift',
|
|
570
|
+
'description': 'Called by obj >>= other. Modify self and return self.',
|
|
571
|
+
},
|
|
572
|
+
'__iand__': {
|
|
573
|
+
'signature': 'def __iand__(self, other)',
|
|
574
|
+
'returns': 'Self',
|
|
575
|
+
'purpose': 'In-place bitwise AND',
|
|
576
|
+
'description': 'Called by obj &= other. Modify self and return self.',
|
|
577
|
+
},
|
|
578
|
+
'__ixor__': {
|
|
579
|
+
'signature': 'def __ixor__(self, other)',
|
|
580
|
+
'returns': 'Self',
|
|
581
|
+
'purpose': 'In-place bitwise XOR',
|
|
582
|
+
'description': 'Called by obj ^= other. Modify self and return self.',
|
|
583
|
+
},
|
|
584
|
+
'__ior__': {
|
|
585
|
+
'signature': 'def __ior__(self, other)',
|
|
586
|
+
'returns': 'Self',
|
|
587
|
+
'purpose': 'In-place bitwise OR',
|
|
588
|
+
'description': 'Called by obj |= other. Modify self and return self.',
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
# Unary operations
|
|
592
|
+
'__neg__': {
|
|
593
|
+
'signature': 'def __neg__(self)',
|
|
594
|
+
'returns': 'Any',
|
|
595
|
+
'purpose': 'Unary negation',
|
|
596
|
+
'description': 'Called by -obj. Return negated value.',
|
|
597
|
+
},
|
|
598
|
+
'__pos__': {
|
|
599
|
+
'signature': 'def __pos__(self)',
|
|
600
|
+
'returns': 'Any',
|
|
601
|
+
'purpose': 'Unary positive',
|
|
602
|
+
'description': 'Called by +obj. Return positive value.',
|
|
603
|
+
},
|
|
604
|
+
'__abs__': {
|
|
605
|
+
'signature': 'def __abs__(self)',
|
|
606
|
+
'returns': 'Any',
|
|
607
|
+
'purpose': 'Absolute value',
|
|
608
|
+
'description': 'Called by abs(obj). Return absolute value.',
|
|
609
|
+
},
|
|
610
|
+
'__invert__': {
|
|
611
|
+
'signature': 'def __invert__(self)',
|
|
612
|
+
'returns': 'Any',
|
|
613
|
+
'purpose': 'Bitwise inversion',
|
|
614
|
+
'description': 'Called by ~obj. Return bitwise NOT.',
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
# Type conversion
|
|
618
|
+
'__complex__': {
|
|
619
|
+
'signature': 'def __complex__(self) -> complex',
|
|
620
|
+
'returns': 'complex',
|
|
621
|
+
'purpose': 'Complex number conversion',
|
|
622
|
+
'description': 'Called by complex(obj). Return complex number.',
|
|
623
|
+
},
|
|
624
|
+
'__int__': {
|
|
625
|
+
'signature': 'def __int__(self) -> int',
|
|
626
|
+
'returns': 'int',
|
|
627
|
+
'purpose': 'Integer conversion',
|
|
628
|
+
'description': 'Called by int(obj). Return integer.',
|
|
629
|
+
},
|
|
630
|
+
'__float__': {
|
|
631
|
+
'signature': 'def __float__(self) -> float',
|
|
632
|
+
'returns': 'float',
|
|
633
|
+
'purpose': 'Float conversion',
|
|
634
|
+
'description': 'Called by float(obj). Return float.',
|
|
635
|
+
},
|
|
636
|
+
'__index__': {
|
|
637
|
+
'signature': 'def __index__(self) -> int',
|
|
638
|
+
'returns': 'int',
|
|
639
|
+
'purpose': 'Lossless integer conversion',
|
|
640
|
+
'description': 'Called when integer is needed (slicing, bin(), hex(), oct()). Must return exact int.',
|
|
641
|
+
},
|
|
642
|
+
'__round__': {
|
|
643
|
+
'signature': 'def __round__(self, ndigits=None)',
|
|
644
|
+
'returns': 'Any',
|
|
645
|
+
'purpose': 'Rounding',
|
|
646
|
+
'description': 'Called by round(obj, ndigits). Return rounded value.',
|
|
647
|
+
},
|
|
648
|
+
'__trunc__': {
|
|
649
|
+
'signature': 'def __trunc__(self)',
|
|
650
|
+
'returns': 'int',
|
|
651
|
+
'purpose': 'Truncation',
|
|
652
|
+
'description': 'Called by math.trunc(obj). Return truncated integer.',
|
|
653
|
+
},
|
|
654
|
+
'__floor__': {
|
|
655
|
+
'signature': 'def __floor__(self)',
|
|
656
|
+
'returns': 'int',
|
|
657
|
+
'purpose': 'Floor',
|
|
658
|
+
'description': 'Called by math.floor(obj). Return floor integer.',
|
|
659
|
+
},
|
|
660
|
+
'__ceil__': {
|
|
661
|
+
'signature': 'def __ceil__(self)',
|
|
662
|
+
'returns': 'int',
|
|
663
|
+
'purpose': 'Ceiling',
|
|
664
|
+
'description': 'Called by math.ceil(obj). Return ceiling integer.',
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
# Descriptors
|
|
668
|
+
'__get__': {
|
|
669
|
+
'signature': 'def __get__(self, obj, objtype=None)',
|
|
670
|
+
'returns': 'Any',
|
|
671
|
+
'purpose': 'Descriptor get',
|
|
672
|
+
'description': 'Called when descriptor attribute is accessed. Return value.',
|
|
673
|
+
},
|
|
674
|
+
'__set__': {
|
|
675
|
+
'signature': 'def __set__(self, obj, value) -> None',
|
|
676
|
+
'returns': 'None',
|
|
677
|
+
'purpose': 'Descriptor set',
|
|
678
|
+
'description': 'Called when descriptor attribute is assigned. Set value.',
|
|
679
|
+
},
|
|
680
|
+
'__delete__': {
|
|
681
|
+
'signature': 'def __delete__(self, obj) -> None',
|
|
682
|
+
'returns': 'None',
|
|
683
|
+
'purpose': 'Descriptor delete',
|
|
684
|
+
'description': 'Called when descriptor attribute is deleted.',
|
|
685
|
+
},
|
|
686
|
+
'__set_name__': {
|
|
687
|
+
'signature': 'def __set_name__(self, owner, name) -> None',
|
|
688
|
+
'returns': 'None',
|
|
689
|
+
'purpose': 'Descriptor naming',
|
|
690
|
+
'description': 'Called at class creation time with the attribute name.',
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
# Attribute access
|
|
694
|
+
'__dir__': {
|
|
695
|
+
'signature': 'def __dir__(self) -> list',
|
|
696
|
+
'returns': 'list',
|
|
697
|
+
'purpose': 'List attributes',
|
|
698
|
+
'description': 'Called by dir(obj). Return list of attribute names.',
|
|
699
|
+
},
|
|
700
|
+
|
|
701
|
+
# Introspection
|
|
702
|
+
'__sizeof__': {
|
|
703
|
+
'signature': 'def __sizeof__(self) -> int',
|
|
704
|
+
'returns': 'int',
|
|
705
|
+
'purpose': 'Size in bytes',
|
|
706
|
+
'description': 'Called by sys.getsizeof(obj). Return memory size in bytes.',
|
|
707
|
+
},
|
|
708
|
+
'__fspath__': {
|
|
709
|
+
'signature': 'def __fspath__(self) -> str | bytes',
|
|
710
|
+
'returns': 'str | bytes',
|
|
711
|
+
'purpose': 'Filesystem path',
|
|
712
|
+
'description': 'Called by os.fspath(obj). Return filesystem path.',
|
|
713
|
+
},
|
|
714
|
+
|
|
715
|
+
# Pickling
|
|
716
|
+
'__getstate__': {
|
|
717
|
+
'signature': 'def __getstate__(self)',
|
|
718
|
+
'returns': 'Any',
|
|
719
|
+
'purpose': 'Pickle state',
|
|
720
|
+
'description': 'Called by pickle. Return object state for serialization.',
|
|
721
|
+
},
|
|
722
|
+
'__setstate__': {
|
|
723
|
+
'signature': 'def __setstate__(self, state) -> None',
|
|
724
|
+
'returns': 'None',
|
|
725
|
+
'purpose': 'Restore pickle state',
|
|
726
|
+
'description': 'Called by pickle. Restore state from serialization.',
|
|
727
|
+
},
|
|
728
|
+
'__reduce__': {
|
|
729
|
+
'signature': 'def __reduce__(self)',
|
|
730
|
+
'returns': 'tuple',
|
|
731
|
+
'purpose': 'Pickle reduction',
|
|
732
|
+
'description': 'Called by pickle. Return (callable, args) to recreate object.',
|
|
733
|
+
},
|
|
734
|
+
'__reduce_ex__': {
|
|
735
|
+
'signature': 'def __reduce_ex__(self, protocol)',
|
|
736
|
+
'returns': 'tuple',
|
|
737
|
+
'purpose': 'Pickle reduction with protocol',
|
|
738
|
+
'description': 'Called by pickle with protocol version.',
|
|
739
|
+
},
|
|
740
|
+
|
|
741
|
+
# Copy
|
|
742
|
+
'__copy__': {
|
|
743
|
+
'signature': 'def __copy__(self)',
|
|
744
|
+
'returns': 'Self',
|
|
745
|
+
'purpose': 'Shallow copy',
|
|
746
|
+
'description': 'Called by copy.copy(obj). Return shallow copy.',
|
|
747
|
+
},
|
|
748
|
+
'__deepcopy__': {
|
|
749
|
+
'signature': 'def __deepcopy__(self, memo)',
|
|
750
|
+
'returns': 'Self',
|
|
751
|
+
'purpose': 'Deep copy',
|
|
752
|
+
'description': 'Called by copy.deepcopy(obj). Return deep copy.',
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
# Class-level
|
|
756
|
+
'__class_getitem__': {
|
|
757
|
+
'signature': 'def __class_getitem__(cls, item)',
|
|
758
|
+
'returns': 'Any',
|
|
759
|
+
'purpose': 'Generic type parameterization',
|
|
760
|
+
'description': 'Called by Class[item]. Return parameterized generic.',
|
|
761
|
+
},
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
# =============================================================================
|
|
766
|
+
# Dunder Trampoline System
|
|
767
|
+
# =============================================================================
|
|
768
|
+
|
|
769
|
+
def _get_dunder_info(dunder_name: str) -> dict | None:
|
|
770
|
+
"""Get metadata about a known dunder method."""
|
|
771
|
+
return DUNDER_INFO.get(dunder_name)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _build_dunder_context(dunder_name: str, args: tuple, kwargs: dict) -> str:
|
|
775
|
+
"""Build context for dunder generation, even for unknown ones."""
|
|
776
|
+
info = _get_dunder_info(dunder_name)
|
|
777
|
+
|
|
778
|
+
args_desc = []
|
|
779
|
+
if args:
|
|
780
|
+
args_desc.append(f"Positional arguments: {repr(args)}")
|
|
781
|
+
if kwargs:
|
|
782
|
+
args_desc.append(f"Keyword arguments: {repr(kwargs)}")
|
|
783
|
+
|
|
784
|
+
context = f"""
|
|
785
|
+
## Magic Method Generation
|
|
786
|
+
|
|
787
|
+
You are generating the magic method: `{dunder_name}`
|
|
788
|
+
|
|
789
|
+
**What happened:**
|
|
790
|
+
This method was called with:
|
|
791
|
+
{chr(10).join(args_desc) if args_desc else "No arguments (besides self)"}
|
|
792
|
+
|
|
793
|
+
**What you need to do:**
|
|
794
|
+
"""
|
|
795
|
+
|
|
796
|
+
if info:
|
|
797
|
+
context += f"""
|
|
798
|
+
{info['description']}
|
|
799
|
+
|
|
800
|
+
**Required signature**: `{info['signature']}`
|
|
801
|
+
**Expected return type**: `{info['returns']}`
|
|
802
|
+
**Purpose**: {info['purpose']}
|
|
803
|
+
"""
|
|
804
|
+
else:
|
|
805
|
+
# Unknown dunder - provide general guidance
|
|
806
|
+
context += f"""
|
|
807
|
+
This is an UNKNOWN or CUSTOM magic method. Based on the name `{dunder_name}`:
|
|
808
|
+
|
|
809
|
+
1. It's a dunder method (surrounded by double underscores)
|
|
810
|
+
2. It was called with {len(args)} argument(s): {repr(args) if args else 'none'}
|
|
811
|
+
3. You should infer the purpose from:
|
|
812
|
+
- The name itself (what does "{dunder_name[2:-2]}" suggest?)
|
|
813
|
+
- The class docstring and context
|
|
814
|
+
- The arguments passed
|
|
815
|
+
|
|
816
|
+
**Signature**: Based on the call, use: `def {dunder_name}(self{', ' + ', '.join(f'arg{i}' for i in range(len(args))) if args else ''})`
|
|
817
|
+
**Return**: Return an appropriate value based on what this method should do
|
|
818
|
+
"""
|
|
819
|
+
|
|
820
|
+
context += f"""
|
|
821
|
+
|
|
822
|
+
**Critical requirements:**
|
|
823
|
+
1. Use the EXACT name: `{dunder_name}`
|
|
824
|
+
2. Don't call this method recursively (e.g., don't call str(self) inside __str__)
|
|
825
|
+
3. Follow Python's magic method protocols
|
|
826
|
+
4. Be consistent with the class's purpose (read the docstring!)
|
|
827
|
+
|
|
828
|
+
Reference: https://docs.python.org/3/reference/datamodel.html#special-method-names
|
|
829
|
+
"""
|
|
830
|
+
|
|
831
|
+
return context
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _make_dunder_trampoline(dunder_name: str):
|
|
835
|
+
"""Create a trampoline function that lazily generates a magic method on first call.
|
|
836
|
+
|
|
837
|
+
The trampoline is installed on the class at class-creation time. When called:
|
|
838
|
+
1. Check if we've already generated and cached the real implementation
|
|
839
|
+
2. If not, generate it using the LLM
|
|
840
|
+
3. Replace the trampoline with the real method on the class
|
|
841
|
+
4. Call the real method with the original arguments
|
|
842
|
+
|
|
843
|
+
This works because Python looks up magic methods on type(obj), not obj itself.
|
|
844
|
+
By injecting the trampoline at class creation time, it's found by Python's
|
|
845
|
+
special method lookup, and then lazily generates the real implementation.
|
|
846
|
+
"""
|
|
847
|
+
|
|
848
|
+
def trampoline(self, *args, **kwargs):
|
|
849
|
+
"""Trampoline that generates the real dunder on first invocation."""
|
|
850
|
+
# Get the actual class (handles inheritance correctly)
|
|
851
|
+
actual_cls = type(self)
|
|
852
|
+
|
|
853
|
+
# Recursion guard for __repr__/__str__ to prevent infinite loops
|
|
854
|
+
# during generation (when logging/debugging calls repr())
|
|
855
|
+
if dunder_name in ('__repr__', '__str__'):
|
|
856
|
+
guard_key = (id(self), dunder_name)
|
|
857
|
+
active_guards = getattr(_generation_guard, 'active', set())
|
|
858
|
+
if guard_key in active_guards:
|
|
859
|
+
# Recursion detected - return safe placeholder
|
|
860
|
+
return f"<{actual_cls.__name__} (generating...)>"
|
|
861
|
+
# Mark as active
|
|
862
|
+
if not hasattr(_generation_guard, 'active'):
|
|
863
|
+
_generation_guard.active = set()
|
|
864
|
+
_generation_guard.active.add(guard_key)
|
|
865
|
+
|
|
866
|
+
# Check if LLM generation is disabled
|
|
867
|
+
if not getattr(actual_cls, '_llm_enabled', True):
|
|
868
|
+
if dunder_name in ('__repr__', '__str__'):
|
|
869
|
+
_generation_guard.active.discard((id(self), dunder_name))
|
|
870
|
+
raise TypeError(f"'{actual_cls.__name__}' object does not support {dunder_name}")
|
|
871
|
+
|
|
872
|
+
# Check cache first
|
|
873
|
+
cache = actual_cls._get_cache()
|
|
874
|
+
cached = cache.get(actual_cls.__name__, dunder_name)
|
|
875
|
+
|
|
876
|
+
if cached:
|
|
877
|
+
# Already generated, execute it
|
|
878
|
+
quiet = getattr(actual_cls, '_llm_quiet', False)
|
|
879
|
+
if not quiet:
|
|
880
|
+
using_cached(actual_cls.__name__, dunder_name)
|
|
881
|
+
real_method = actual_cls._execute_code(
|
|
882
|
+
cached.code,
|
|
883
|
+
dunder_name,
|
|
884
|
+
cached.dependencies
|
|
885
|
+
)
|
|
886
|
+
else:
|
|
887
|
+
# Need to generate it
|
|
888
|
+
quiet = getattr(actual_cls, '_llm_quiet', False)
|
|
889
|
+
if not quiet:
|
|
890
|
+
info(f"{dunder_name} called on {actual_cls.__name__} - generating...")
|
|
891
|
+
|
|
892
|
+
# Build context with signature info
|
|
893
|
+
context_hint = _build_dunder_context(dunder_name, args, kwargs)
|
|
894
|
+
|
|
895
|
+
# Generate using standard flow
|
|
896
|
+
real_method = actual_cls._llm_generate(
|
|
897
|
+
dunder_name,
|
|
898
|
+
is_classmethod=False,
|
|
899
|
+
additional_context=context_hint
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
# Replace the trampoline with the real method on the class
|
|
903
|
+
# This ensures subsequent calls go directly to the real implementation
|
|
904
|
+
setattr(actual_cls, dunder_name, real_method)
|
|
905
|
+
|
|
906
|
+
# Clear recursion guard for __repr__/__str__
|
|
907
|
+
if dunder_name in ('__repr__', '__str__'):
|
|
908
|
+
_generation_guard.active.discard((id(self), dunder_name))
|
|
909
|
+
|
|
910
|
+
# Now call the real method
|
|
911
|
+
return real_method(self, *args, **kwargs)
|
|
912
|
+
|
|
913
|
+
# Set helpful attributes for introspection
|
|
914
|
+
trampoline.__name__ = dunder_name
|
|
915
|
+
trampoline.__qualname__ = f"ScryClass.{dunder_name}"
|
|
916
|
+
trampoline._llm_is_trampoline = True
|
|
917
|
+
trampoline._llm_dunder_name = dunder_name
|
|
918
|
+
|
|
919
|
+
return trampoline
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def _has_meaningful_parent_implementation(cls, dunder_name: str, llm_class: type) -> bool:
|
|
923
|
+
"""Check if a dunder is inherited from a meaningful parent class.
|
|
924
|
+
|
|
925
|
+
Returns True if the dunder is defined in a parent class other than:
|
|
926
|
+
- object (Python's base object)
|
|
927
|
+
- ScryClass (our base class)
|
|
928
|
+
|
|
929
|
+
This prevents us from overwriting user-defined dunders from parent classes.
|
|
930
|
+
"""
|
|
931
|
+
for base in cls.__mro__[1:]: # Skip cls itself
|
|
932
|
+
if base is object:
|
|
933
|
+
continue
|
|
934
|
+
if base is llm_class:
|
|
935
|
+
continue
|
|
936
|
+
if dunder_name in base.__dict__:
|
|
937
|
+
return True
|
|
938
|
+
return False
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
# =============================================================================
|
|
942
|
+
# GenerationProxy (for non-dunder attributes)
|
|
943
|
+
# =============================================================================
|
|
944
|
+
|
|
945
|
+
class GenerationProxy:
|
|
946
|
+
"""Lazy proxy that triggers generation upon use (call or attribute access)."""
|
|
947
|
+
|
|
948
|
+
def __init__(self, cls_or_inst: Any, name: str, is_classmethod: bool):
|
|
949
|
+
self._target = cls_or_inst
|
|
950
|
+
self._name = name
|
|
951
|
+
self._is_classmethod = is_classmethod
|
|
952
|
+
self._cls = cls_or_inst if is_classmethod else type(cls_or_inst)
|
|
953
|
+
|
|
954
|
+
def __call__(self, *args, **kwargs) -> Any:
|
|
955
|
+
"""Called when the proxy is used as a function."""
|
|
956
|
+
# We now know the call arguments, so we can generate the method with this context!
|
|
957
|
+
|
|
958
|
+
# Describe args for the prompt
|
|
959
|
+
args_desc = []
|
|
960
|
+
if args:
|
|
961
|
+
args_desc.append(f"Positional args: {repr(args)}")
|
|
962
|
+
if kwargs:
|
|
963
|
+
args_desc.append(f"Keyword args: {repr(kwargs)}")
|
|
964
|
+
|
|
965
|
+
# Include instance attributes in context (for instance methods)
|
|
966
|
+
instance_attrs_desc = ""
|
|
967
|
+
if not self._is_classmethod and hasattr(self._target, '__dict__'):
|
|
968
|
+
instance_attrs = {}
|
|
969
|
+
for k, v in self._target.__dict__.items():
|
|
970
|
+
if not k.startswith('_llm_'):
|
|
971
|
+
# Truncate long repr values
|
|
972
|
+
v_repr = repr(v)
|
|
973
|
+
if len(v_repr) > 100:
|
|
974
|
+
v_repr = v_repr[:100] + "..."
|
|
975
|
+
instance_attrs[k] = v_repr
|
|
976
|
+
if instance_attrs:
|
|
977
|
+
instance_attrs_desc = f"\n\nInstance attributes (self.__dict__):\n{instance_attrs}"
|
|
978
|
+
|
|
979
|
+
call_context = f"""
|
|
980
|
+
## Runtime Call Context
|
|
981
|
+
|
|
982
|
+
The attribute `{self._name}` is being CALLED with these arguments:
|
|
983
|
+
{chr(10).join(args_desc)}{instance_attrs_desc}
|
|
984
|
+
|
|
985
|
+
Requirements:
|
|
986
|
+
1. Generate a METHOD (not property)
|
|
987
|
+
2. Ensure signature matches these arguments
|
|
988
|
+
3. If arguments are ambiguous, use *args, **kwargs
|
|
989
|
+
"""
|
|
990
|
+
|
|
991
|
+
quiet = getattr(self._cls, '_llm_quiet', False)
|
|
992
|
+
if not quiet:
|
|
993
|
+
info(f"Calling {self._cls.__name__}.{self._name}(...) - resolving...")
|
|
994
|
+
|
|
995
|
+
MAX_RETRIES = 3
|
|
996
|
+
last_error = None
|
|
997
|
+
current_context = call_context
|
|
998
|
+
|
|
999
|
+
for attempt in range(MAX_RETRIES + 1):
|
|
1000
|
+
# Generate code
|
|
1001
|
+
code_obj = self._cls._llm_generate(
|
|
1002
|
+
self._name,
|
|
1003
|
+
is_classmethod=self._is_classmethod,
|
|
1004
|
+
additional_context=current_context
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
# Bind to instance/class
|
|
1008
|
+
bound_obj = self._bind_and_cache(code_obj)
|
|
1009
|
+
|
|
1010
|
+
# Try to execute
|
|
1011
|
+
try:
|
|
1012
|
+
return bound_obj(*args, **kwargs)
|
|
1013
|
+
except Exception as e:
|
|
1014
|
+
import traceback
|
|
1015
|
+
|
|
1016
|
+
# Check for signature mismatch (TypeError)
|
|
1017
|
+
is_signature_error = isinstance(e, TypeError) and ("argument" in str(e) or "takes" in str(e))
|
|
1018
|
+
|
|
1019
|
+
# Check if the error originated from the generated code
|
|
1020
|
+
# We look for the specific filename pattern we used in _execute_code
|
|
1021
|
+
generated_filename = f"<generated: {self._cls.__name__}.{self._name}>"
|
|
1022
|
+
is_generated_error = False
|
|
1023
|
+
if e.__traceback__:
|
|
1024
|
+
for frame in traceback.extract_tb(e.__traceback__):
|
|
1025
|
+
if frame.filename == generated_filename:
|
|
1026
|
+
is_generated_error = True
|
|
1027
|
+
break
|
|
1028
|
+
|
|
1029
|
+
if (is_signature_error or is_generated_error) and attempt < MAX_RETRIES:
|
|
1030
|
+
if not quiet:
|
|
1031
|
+
reason = f"Signature mismatch: {e}" if is_signature_error else f"Runtime error: {type(e).__name__}: {e}"
|
|
1032
|
+
warning(f"{reason}. Regenerating (attempt {attempt + 1}/{MAX_RETRIES})...")
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
# Invalidate cache so _llm_generate actually generates new code
|
|
1036
|
+
self._cls._get_cache().prune(self._cls.__name__, self._name)
|
|
1037
|
+
|
|
1038
|
+
# Update context with error feedback
|
|
1039
|
+
if is_signature_error:
|
|
1040
|
+
error_feedback = f"""
|
|
1041
|
+
## CRITICAL ERROR - SIGNATURE MISMATCH
|
|
1042
|
+
|
|
1043
|
+
You previously generated code that caused this error when called:
|
|
1044
|
+
TypeError: {e}
|
|
1045
|
+
|
|
1046
|
+
The code was called with:
|
|
1047
|
+
{chr(10).join(args_desc) if args_desc else "No arguments ()"}
|
|
1048
|
+
|
|
1049
|
+
You MUST fix the signature to match exactly how it is called.
|
|
1050
|
+
"""
|
|
1051
|
+
else:
|
|
1052
|
+
error_feedback = f"""
|
|
1053
|
+
## CRITICAL ERROR - RUNTIME EXCEPTION
|
|
1054
|
+
|
|
1055
|
+
You previously generated code that crashed with this error when executed:
|
|
1056
|
+
{type(e).__name__}: {e}
|
|
1057
|
+
|
|
1058
|
+
This error occurred INSIDE your generated code.
|
|
1059
|
+
Please analyze the error and FIX the code to handle this case.
|
|
1060
|
+
"""
|
|
1061
|
+
|
|
1062
|
+
current_context = call_context + error_feedback
|
|
1063
|
+
continue
|
|
1064
|
+
|
|
1065
|
+
# If we get here, it's either not a retryable error OR retries exhausted.
|
|
1066
|
+
# Print helpful debugging info if not already improved.
|
|
1067
|
+
if not getattr(e, "_scry_run_handled", False):
|
|
1068
|
+
error(f"Unhandled exception in generated code: {self._cls.__name__}.{self._name}")
|
|
1069
|
+
warning(f"To inspect: scry-run cache show {self._cls.__name__} {self._name}")
|
|
1070
|
+
warning(f"To remove: scry-run cache rm {self._cls.__name__}.{self._name}")
|
|
1071
|
+
e._scry_run_handled = True
|
|
1072
|
+
|
|
1073
|
+
raise
|
|
1074
|
+
|
|
1075
|
+
# If we exhausted retries (loop completed normally without return)
|
|
1076
|
+
# Wait, if we 'continue' on attempt == MAX_RETRIES-1?
|
|
1077
|
+
# Range is range(MAX_RETRIES + 1) -> 0, 1, 2, 3.
|
|
1078
|
+
# If attempt 3 fails, `attempt < 3` is False. We fall through to raise.
|
|
1079
|
+
# So we never exit the loop normally unless we return.
|
|
1080
|
+
# The code below is unreachable if logic is correct?
|
|
1081
|
+
# Actually if `pass` was used... but here we `raise`.
|
|
1082
|
+
pass
|
|
1083
|
+
|
|
1084
|
+
def _bind_and_cache(self, code_obj: Any) -> Any:
|
|
1085
|
+
"""Bind the generated object to the instance/class and replace the proxy."""
|
|
1086
|
+
if self._is_classmethod:
|
|
1087
|
+
setattr(self._target, self._name, code_obj)
|
|
1088
|
+
return code_obj
|
|
1089
|
+
else:
|
|
1090
|
+
# Instance access
|
|
1091
|
+
if isinstance(code_obj, property):
|
|
1092
|
+
# Properties MUST be set on the class to work as descriptors
|
|
1093
|
+
setattr(type(self._target), self._name, code_obj)
|
|
1094
|
+
# Now access the property on the instance to trigger the getter and return the value
|
|
1095
|
+
try:
|
|
1096
|
+
return getattr(self._target, self._name)
|
|
1097
|
+
except Exception as e:
|
|
1098
|
+
if not getattr(e, "_scry_run_handled", False):
|
|
1099
|
+
error(f"Unhandled exception in generated property: {self._cls.__name__}.{self._name}")
|
|
1100
|
+
warning(f"To inspect: scry-run cache show {self._cls.__name__} {self._name}")
|
|
1101
|
+
warning(f"To remove: scry-run cache rm {self._cls.__name__}.{self._name}")
|
|
1102
|
+
e._scry_run_handled = True
|
|
1103
|
+
raise
|
|
1104
|
+
elif callable(code_obj):
|
|
1105
|
+
# Methods should be set on the class, not the instance.
|
|
1106
|
+
# This ensures they work as descriptors (bound methods) and can overwrite
|
|
1107
|
+
# existing (broken) properties on the class.
|
|
1108
|
+
setattr(self._cls, self._name, code_obj)
|
|
1109
|
+
# Return the bound method accessed from the instance
|
|
1110
|
+
return getattr(self._target, self._name)
|
|
1111
|
+
else:
|
|
1112
|
+
# Regular value (not a method or property)
|
|
1113
|
+
object.__setattr__(self._target, self._name, code_obj)
|
|
1114
|
+
return code_obj
|
|
1115
|
+
|
|
1116
|
+
def _resolve(self) -> Any:
|
|
1117
|
+
"""Resolve the proxy for non-call usage (properties)."""
|
|
1118
|
+
quiet = getattr(self._cls, '_llm_quiet', False)
|
|
1119
|
+
if not quiet:
|
|
1120
|
+
info(f"Accessing {self._cls.__name__}.{self._name} - resolving...")
|
|
1121
|
+
|
|
1122
|
+
# Include instance attributes in context (for instance properties)
|
|
1123
|
+
instance_attrs_desc = ""
|
|
1124
|
+
if not self._is_classmethod and hasattr(self._target, '__dict__'):
|
|
1125
|
+
instance_attrs = {}
|
|
1126
|
+
for k, v in self._target.__dict__.items():
|
|
1127
|
+
if not k.startswith('_llm_'):
|
|
1128
|
+
v_repr = repr(v)
|
|
1129
|
+
if len(v_repr) > 100:
|
|
1130
|
+
v_repr = v_repr[:100] + "..."
|
|
1131
|
+
instance_attrs[k] = v_repr
|
|
1132
|
+
if instance_attrs:
|
|
1133
|
+
instance_attrs_desc = f"\n\nInstance attributes (self.__dict__):\n{instance_attrs}"
|
|
1134
|
+
|
|
1135
|
+
context = f"## Runtime Context\n\nAttribute accessed as a value (property), NOT called as a function.{instance_attrs_desc}"
|
|
1136
|
+
|
|
1137
|
+
code_obj = self._cls._llm_generate(
|
|
1138
|
+
self._name,
|
|
1139
|
+
is_classmethod=self._is_classmethod,
|
|
1140
|
+
additional_context=context
|
|
1141
|
+
)
|
|
1142
|
+
return self._bind_and_cache(code_obj)
|
|
1143
|
+
|
|
1144
|
+
# Magic methods to trigger resolution on value usage
|
|
1145
|
+
def __str__(self): return str(self._resolve())
|
|
1146
|
+
def __repr__(self):
|
|
1147
|
+
# Prevent infinite recursion if we are building runtime context
|
|
1148
|
+
if getattr(_context_build_guard, 'active', False):
|
|
1149
|
+
return f"<Unresolved Proxy: {self._cls.__name__}.{self._name}>"
|
|
1150
|
+
return repr(self._resolve())
|
|
1151
|
+
|
|
1152
|
+
def __bool__(self): return bool(self._resolve())
|
|
1153
|
+
def __int__(self): return int(self._resolve())
|
|
1154
|
+
def __float__(self): return float(self._resolve())
|
|
1155
|
+
def __add__(self, other): return self._resolve() + other
|
|
1156
|
+
def __radd__(self, other): return other + self._resolve()
|
|
1157
|
+
def __sub__(self, other): return self._resolve() - other
|
|
1158
|
+
def __mul__(self, other): return self._resolve() * other
|
|
1159
|
+
def __getitem__(self, item): return self._resolve()[item]
|
|
1160
|
+
def __iter__(self): return iter(self._resolve())
|
|
1161
|
+
def __getattr__(self, name): return getattr(self._resolve(), name)
|
|
1162
|
+
def __eq__(self, other): return self._resolve() == other
|
|
1163
|
+
def __hash__(self): return hash(self._resolve())
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
# =============================================================================
|
|
1167
|
+
# Metaclass
|
|
1168
|
+
# =============================================================================
|
|
1169
|
+
|
|
1170
|
+
class ScryMeta(type):
|
|
1171
|
+
"""Metaclass that intercepts attribute access and object creation.
|
|
1172
|
+
|
|
1173
|
+
Capabilities:
|
|
1174
|
+
1. __new__: Injects dunder trampolines at class creation time
|
|
1175
|
+
2. __call__: Generates __init__ when creating objects if not defined
|
|
1176
|
+
3. __getattr__: Generates missing class-level attributes/methods
|
|
1177
|
+
4. __getattribute__: Catches explicit access to unknown dunders
|
|
1178
|
+
|
|
1179
|
+
When a class-level attribute is not found, this metaclass will
|
|
1180
|
+
attempt to generate the code using an LLM and cache it.
|
|
1181
|
+
"""
|
|
1182
|
+
|
|
1183
|
+
def __new__(mcs, name, bases, namespace, **kwargs):
|
|
1184
|
+
"""Create a new class and inject dunder trampolines."""
|
|
1185
|
+
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
1186
|
+
|
|
1187
|
+
# Only inject trampolines if LLM is enabled for this class
|
|
1188
|
+
# (check if it will be enabled, default is True)
|
|
1189
|
+
if namespace.get('_llm_enabled', True):
|
|
1190
|
+
mcs._inject_dunder_trampolines(cls)
|
|
1191
|
+
|
|
1192
|
+
return cls
|
|
1193
|
+
|
|
1194
|
+
@staticmethod
|
|
1195
|
+
def _inject_dunder_trampolines(cls):
|
|
1196
|
+
"""Inject trampoline methods for all generatable dunders.
|
|
1197
|
+
|
|
1198
|
+
Only inject if:
|
|
1199
|
+
1. The class inherits from ScryClass (has LLM infrastructure)
|
|
1200
|
+
2. The dunder doesn't already exist in this class's __dict__
|
|
1201
|
+
3. The dunder isn't inherited with a custom implementation from a real parent
|
|
1202
|
+
"""
|
|
1203
|
+
# We need to import ScryClass here to avoid circular reference
|
|
1204
|
+
# at module load time. The class will be defined below.
|
|
1205
|
+
llm_class = None
|
|
1206
|
+
for base in cls.__mro__:
|
|
1207
|
+
if base.__name__ == 'ScryClass' and base is not cls:
|
|
1208
|
+
llm_class = base
|
|
1209
|
+
break
|
|
1210
|
+
|
|
1211
|
+
# Don't inject trampolines if this class doesn't inherit from ScryClass
|
|
1212
|
+
# (it lacks the necessary infrastructure like _get_cache, _llm_generate, etc.)
|
|
1213
|
+
if llm_class is None:
|
|
1214
|
+
return
|
|
1215
|
+
|
|
1216
|
+
for dunder_name in GENERATABLE_DUNDERS:
|
|
1217
|
+
# Skip if this class explicitly defines it
|
|
1218
|
+
if dunder_name in cls.__dict__:
|
|
1219
|
+
continue
|
|
1220
|
+
|
|
1221
|
+
# Skip if meaningfully inherited (not from object/ScryClass)
|
|
1222
|
+
if llm_class and _has_meaningful_parent_implementation(cls, dunder_name, llm_class):
|
|
1223
|
+
continue
|
|
1224
|
+
|
|
1225
|
+
# Inject the trampoline
|
|
1226
|
+
trampoline = _make_dunder_trampoline(dunder_name)
|
|
1227
|
+
setattr(cls, dunder_name, trampoline)
|
|
1228
|
+
|
|
1229
|
+
def __call__(cls, *args, **kwargs):
|
|
1230
|
+
"""Intercept object creation to generate __init__ if needed.
|
|
1231
|
+
|
|
1232
|
+
This allows the LLM to generate constructors based on the class
|
|
1233
|
+
docstring and any arguments passed to the constructor.
|
|
1234
|
+
"""
|
|
1235
|
+
# Check if __init__ is defined in the class itself (not inherited from ScryClass/object)
|
|
1236
|
+
has_custom_init = '__init__' in cls.__dict__
|
|
1237
|
+
|
|
1238
|
+
# Check if LLM generation is enabled
|
|
1239
|
+
llm_enabled = getattr(cls, '_llm_enabled', True)
|
|
1240
|
+
|
|
1241
|
+
# Generate __init__ if not custom-defined (even without args)
|
|
1242
|
+
if not has_custom_init and llm_enabled:
|
|
1243
|
+
quiet = getattr(cls, '_llm_quiet', False)
|
|
1244
|
+
if not quiet:
|
|
1245
|
+
if args or kwargs:
|
|
1246
|
+
info(f"Creating {cls.__name__} with args - generating __init__...")
|
|
1247
|
+
else:
|
|
1248
|
+
info(f"Creating {cls.__name__} - generating __init__...")
|
|
1249
|
+
|
|
1250
|
+
# Generate __init__ using the LLM
|
|
1251
|
+
init_code = cls._llm_generate_init(args, kwargs)
|
|
1252
|
+
|
|
1253
|
+
# Set it on the class
|
|
1254
|
+
setattr(cls, '__init__', init_code)
|
|
1255
|
+
|
|
1256
|
+
# Now create the instance normally
|
|
1257
|
+
return super().__call__(*args, **kwargs)
|
|
1258
|
+
|
|
1259
|
+
def __getattr__(cls, name: str) -> Any:
|
|
1260
|
+
"""Called when a class-level attribute is not found.
|
|
1261
|
+
|
|
1262
|
+
Args:
|
|
1263
|
+
name: Name of the missing attribute
|
|
1264
|
+
|
|
1265
|
+
Returns:
|
|
1266
|
+
A GenerationProxy that triggers generation on use
|
|
1267
|
+
"""
|
|
1268
|
+
# Check if this class has LLM infrastructure (inherits from ScryClass)
|
|
1269
|
+
# We check __dict__ directly to avoid recursion through __getattr__
|
|
1270
|
+
has_llm_infrastructure = any(
|
|
1271
|
+
'_get_cache' in base.__dict__ and '_llm_generate' in base.__dict__
|
|
1272
|
+
for base in cls.__mro__
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
# For dunders, trampolines should have been injected at class creation.
|
|
1276
|
+
# If we get here for a dunder, it means:
|
|
1277
|
+
# 1. It's a forbidden dunder, or
|
|
1278
|
+
# 2. The class doesn't inherit from ScryClass, or
|
|
1279
|
+
# 3. Something went wrong
|
|
1280
|
+
if name.startswith("__") and name.endswith("__"):
|
|
1281
|
+
if name in FORBIDDEN_DUNDERS:
|
|
1282
|
+
raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")
|
|
1283
|
+
# If class doesn't have LLM infrastructure, don't try to generate
|
|
1284
|
+
if not has_llm_infrastructure:
|
|
1285
|
+
raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")
|
|
1286
|
+
# For generatable dunders that somehow weren't injected, inject now
|
|
1287
|
+
if name in GENERATABLE_DUNDERS:
|
|
1288
|
+
trampoline = _make_dunder_trampoline(name)
|
|
1289
|
+
setattr(cls, name, trampoline)
|
|
1290
|
+
return trampoline
|
|
1291
|
+
# Unknown dunder - try to inject a trampoline anyway
|
|
1292
|
+
if getattr(cls, "_llm_enabled", True):
|
|
1293
|
+
info(f"Unknown dunder {name} accessed - injecting trampoline...")
|
|
1294
|
+
trampoline = _make_dunder_trampoline(name)
|
|
1295
|
+
setattr(cls, name, trampoline)
|
|
1296
|
+
return trampoline
|
|
1297
|
+
raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")
|
|
1298
|
+
|
|
1299
|
+
# Skip our internal config namespace
|
|
1300
|
+
if name.startswith("_llm_"):
|
|
1301
|
+
raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")
|
|
1302
|
+
|
|
1303
|
+
# Skip known Python internals
|
|
1304
|
+
if name.startswith(("_abc_", "_v_")):
|
|
1305
|
+
raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")
|
|
1306
|
+
|
|
1307
|
+
# Check if LLM generation is enabled
|
|
1308
|
+
if not getattr(cls, "_llm_enabled", True):
|
|
1309
|
+
raise AttributeError(f"type object '{cls.__name__}' has no attribute '{name}'")
|
|
1310
|
+
|
|
1311
|
+
return GenerationProxy(cls, name, is_classmethod=True)
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
# =============================================================================
|
|
1315
|
+
# Base Class
|
|
1316
|
+
# =============================================================================
|
|
1317
|
+
|
|
1318
|
+
class ScryClass(metaclass=ScryMeta):
|
|
1319
|
+
"""Base class for LLM-powered dynamic code generation.
|
|
1320
|
+
|
|
1321
|
+
Inherit from this class to enable automatic code generation for
|
|
1322
|
+
missing methods and properties. The LLM will use the class docstring,
|
|
1323
|
+
existing methods, and the full codebase as context.
|
|
1324
|
+
|
|
1325
|
+
Features:
|
|
1326
|
+
- Instance methods: app.method() generates method if missing
|
|
1327
|
+
- Class methods: MyClass.method() generates class method if missing
|
|
1328
|
+
- Constructor: MyClass(args) generates __init__ if missing and args provided
|
|
1329
|
+
- Magic methods: str(obj), len(obj), obj[key], etc. generate dunders
|
|
1330
|
+
|
|
1331
|
+
Example:
|
|
1332
|
+
```python
|
|
1333
|
+
class TodoApp(ScryClass):
|
|
1334
|
+
'''A minimal web todo app with task management.'''
|
|
1335
|
+
pass
|
|
1336
|
+
|
|
1337
|
+
app = TodoApp()
|
|
1338
|
+
# This will trigger LLM generation of add_task method
|
|
1339
|
+
app.add_task("Buy groceries")
|
|
1340
|
+
|
|
1341
|
+
# Magic methods work too!
|
|
1342
|
+
print(app) # Generates __str__
|
|
1343
|
+
len(app) # Generates __len__
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
Configuration:
|
|
1347
|
+
- _llm_enabled: Set to False to disable LLM generation
|
|
1348
|
+
- _llm_cache: Custom ScryCache instance
|
|
1349
|
+
- _llm_generator: Custom CodeGenerator instance
|
|
1350
|
+
- _llm_use_full_context: Whether to use full codebase context (default: True)
|
|
1351
|
+
- _llm_quiet: Set to True to suppress generation messages
|
|
1352
|
+
"""
|
|
1353
|
+
|
|
1354
|
+
# Class-level configuration
|
|
1355
|
+
_llm_enabled: bool = True
|
|
1356
|
+
_llm_cache: Optional["ScryCache"] = None
|
|
1357
|
+
_llm_generator: Optional["CodeGenerator"] = None
|
|
1358
|
+
_llm_model: Optional[str] = None # Model override (e.g., "opus" for Claude)
|
|
1359
|
+
_llm_backend: Optional[str] = None # Backend override (e.g., "frozen" for baked apps)
|
|
1360
|
+
_llm_use_full_context: bool = True
|
|
1361
|
+
_llm_quiet: bool = False # Set to True to suppress generation messages
|
|
1362
|
+
|
|
1363
|
+
# Note: __repr__ and __str__ are now generated via trampolines with recursion guard
|
|
1364
|
+
|
|
1365
|
+
def __getattr__(self, name: str) -> Any:
|
|
1366
|
+
"""Called when an instance-level attribute is not found.
|
|
1367
|
+
|
|
1368
|
+
Args:
|
|
1369
|
+
name: Name of the missing attribute
|
|
1370
|
+
|
|
1371
|
+
Returns:
|
|
1372
|
+
The generated and cached attribute
|
|
1373
|
+
"""
|
|
1374
|
+
# For dunders, trampolines should handle them at the class level.
|
|
1375
|
+
# If we get here for a dunder, something is wrong or it's forbidden.
|
|
1376
|
+
if name.startswith("__") and name.endswith("__"):
|
|
1377
|
+
if name in FORBIDDEN_DUNDERS:
|
|
1378
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
1379
|
+
# This shouldn't normally happen (trampolines should catch it)
|
|
1380
|
+
# but we can try generating it anyway for robustness
|
|
1381
|
+
warning(f"{name} accessed via __getattr__ (trampoline missing?)")
|
|
1382
|
+
|
|
1383
|
+
# Skip our internal config namespace
|
|
1384
|
+
if name.startswith("_llm_"):
|
|
1385
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
1386
|
+
|
|
1387
|
+
# Skip known Python internals
|
|
1388
|
+
if name.startswith(("_abc_", "_v_")):
|
|
1389
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
1390
|
+
|
|
1391
|
+
# Check if LLM generation is enabled
|
|
1392
|
+
if not getattr(type(self), "_llm_enabled", True):
|
|
1393
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
1394
|
+
|
|
1395
|
+
return GenerationProxy(self, name, is_classmethod=False)
|
|
1396
|
+
|
|
1397
|
+
@classmethod
|
|
1398
|
+
def _get_cache(cls) -> "ScryCache":
|
|
1399
|
+
"""Get or create the cache instance.
|
|
1400
|
+
|
|
1401
|
+
Cache is stored as cache.json next to the source file that defines the class.
|
|
1402
|
+
"""
|
|
1403
|
+
if cls._llm_cache is None:
|
|
1404
|
+
from pathlib import Path
|
|
1405
|
+
from scry_run.cache import ScryCache
|
|
1406
|
+
|
|
1407
|
+
# Find cache.json next to the source file
|
|
1408
|
+
try:
|
|
1409
|
+
source_file = Path(inspect.getfile(cls))
|
|
1410
|
+
cache_path = source_file.parent / "cache.json"
|
|
1411
|
+
except (OSError, TypeError):
|
|
1412
|
+
# Fallback to current directory
|
|
1413
|
+
cache_path = Path.cwd() / "cache.json"
|
|
1414
|
+
|
|
1415
|
+
# Enable read-only mode if the backend is frozen
|
|
1416
|
+
read_only = cls._llm_backend == "frozen"
|
|
1417
|
+
cls._llm_cache = ScryCache(cache_path, read_only=read_only)
|
|
1418
|
+
return cls._llm_cache
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
@classmethod
|
|
1422
|
+
def _get_app_dir(cls) -> Path | None:
|
|
1423
|
+
"""Get the app directory for this class, if running in an app context."""
|
|
1424
|
+
from pathlib import Path
|
|
1425
|
+
try:
|
|
1426
|
+
source_file = Path(inspect.getfile(cls))
|
|
1427
|
+
# Check if this looks like an app directory (has .venv or is under ~/.scry-run/apps/)
|
|
1428
|
+
app_dir = source_file.parent
|
|
1429
|
+
if (app_dir / ".venv").exists():
|
|
1430
|
+
return app_dir
|
|
1431
|
+
if ".scry-run/apps/" in str(app_dir):
|
|
1432
|
+
return app_dir
|
|
1433
|
+
except (OSError, TypeError):
|
|
1434
|
+
pass
|
|
1435
|
+
return None
|
|
1436
|
+
|
|
1437
|
+
@classmethod
|
|
1438
|
+
def _get_generator(cls) -> "CodeGenerator":
|
|
1439
|
+
"""Get or create the generator instance."""
|
|
1440
|
+
if cls._llm_generator is None:
|
|
1441
|
+
from scry_run.generator import CodeGenerator
|
|
1442
|
+
cls._llm_generator = CodeGenerator(
|
|
1443
|
+
model=cls._llm_model,
|
|
1444
|
+
backend=cls._llm_backend,
|
|
1445
|
+
)
|
|
1446
|
+
return cls._llm_generator
|
|
1447
|
+
|
|
1448
|
+
@classmethod
|
|
1449
|
+
def _get_class_source(cls) -> str:
|
|
1450
|
+
"""Get the source code of this class."""
|
|
1451
|
+
try:
|
|
1452
|
+
return inspect.getsource(cls)
|
|
1453
|
+
except (OSError, TypeError):
|
|
1454
|
+
# Fallback: construct minimal representation
|
|
1455
|
+
lines = [f"class {cls.__name__}:"]
|
|
1456
|
+
if cls.__doc__:
|
|
1457
|
+
lines.append(f' """{cls.__doc__}"""')
|
|
1458
|
+
|
|
1459
|
+
# List existing methods
|
|
1460
|
+
for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
|
|
1461
|
+
if not name.startswith("_"):
|
|
1462
|
+
sig = inspect.signature(method)
|
|
1463
|
+
lines.append(f" def {name}{sig}: ...")
|
|
1464
|
+
|
|
1465
|
+
return "\n".join(lines)
|
|
1466
|
+
|
|
1467
|
+
@classmethod
|
|
1468
|
+
def _llm_generate(
|
|
1469
|
+
cls,
|
|
1470
|
+
attr_name: str,
|
|
1471
|
+
is_classmethod: bool = False,
|
|
1472
|
+
additional_context: Optional[str] = None
|
|
1473
|
+
) -> Any:
|
|
1474
|
+
"""Generate code for a missing attribute using LLM.
|
|
1475
|
+
|
|
1476
|
+
Args:
|
|
1477
|
+
attr_name: Name of the attribute to generate
|
|
1478
|
+
is_classmethod: Whether this is a class-level attribute
|
|
1479
|
+
additional_context: Optional extra context (e.g. runtime arguments)
|
|
1480
|
+
|
|
1481
|
+
Returns:
|
|
1482
|
+
The generated code object (function, property, etc.)
|
|
1483
|
+
"""
|
|
1484
|
+
from scry_run.logging import get_logger
|
|
1485
|
+
logger = get_logger()
|
|
1486
|
+
|
|
1487
|
+
# Set up app-specific logging if in app context
|
|
1488
|
+
app_dir = cls._get_app_dir()
|
|
1489
|
+
if app_dir:
|
|
1490
|
+
logger.set_app_context(app_dir)
|
|
1491
|
+
|
|
1492
|
+
cache = cls._get_cache()
|
|
1493
|
+
class_name = cls.__name__
|
|
1494
|
+
|
|
1495
|
+
logger.info(f"LLM generate: {class_name}.{attr_name} (is_classmethod={is_classmethod})")
|
|
1496
|
+
|
|
1497
|
+
# Check cache first (fast path, no lock needed)
|
|
1498
|
+
# We always check cache. If the cached code is invalid (e.g. signature mismatch),
|
|
1499
|
+
# the caller (GenerationProxy) handles TypeError, invalidates cache, and retries.
|
|
1500
|
+
cached = cache.get(class_name, attr_name)
|
|
1501
|
+
if cached:
|
|
1502
|
+
logger.info(f"Cache HIT: {class_name}.{attr_name}")
|
|
1503
|
+
logger.debug(f"Cached code for {class_name}.{attr_name}", f"CODE:\n{cached.code}\n\nDEPENDENCIES:\n{cached.dependencies}")
|
|
1504
|
+
if not cls._llm_quiet:
|
|
1505
|
+
using_cached(class_name, attr_name)
|
|
1506
|
+
return cls._execute_code(cached.code, attr_name, cached.dependencies)
|
|
1507
|
+
|
|
1508
|
+
# Cache miss - acquire per-attribute lock to prevent concurrent generation
|
|
1509
|
+
# Multiple threads might hit cache miss simultaneously; only one should generate
|
|
1510
|
+
generation_lock = _get_generation_lock(class_name, attr_name)
|
|
1511
|
+
with generation_lock:
|
|
1512
|
+
# Double-check cache after acquiring lock (another thread may have filled it)
|
|
1513
|
+
cached = cache.get(class_name, attr_name)
|
|
1514
|
+
if cached:
|
|
1515
|
+
logger.info(f"Cache HIT (after lock): {class_name}.{attr_name}")
|
|
1516
|
+
if not cls._llm_quiet:
|
|
1517
|
+
using_cached(class_name, attr_name)
|
|
1518
|
+
return cls._execute_code(cached.code, attr_name, cached.dependencies)
|
|
1519
|
+
|
|
1520
|
+
logger.info(f"Cache MISS: {class_name}.{attr_name} - generating...")
|
|
1521
|
+
|
|
1522
|
+
# Generate new code
|
|
1523
|
+
if not cls._llm_quiet:
|
|
1524
|
+
generating(class_name, attr_name)
|
|
1525
|
+
generator = cls._get_generator()
|
|
1526
|
+
|
|
1527
|
+
# Capture runtime context (call stack, variables)
|
|
1528
|
+
# Capture runtime context (call stack, variables)
|
|
1529
|
+
from scry_run.context import ContextBuilder
|
|
1530
|
+
|
|
1531
|
+
# Set guard to prevent repr() on proxies from triggering recursive generation
|
|
1532
|
+
_context_build_guard.active = True
|
|
1533
|
+
try:
|
|
1534
|
+
runtime_context = ContextBuilder.build_runtime_context()
|
|
1535
|
+
finally:
|
|
1536
|
+
_context_build_guard.active = False
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
if additional_context:
|
|
1540
|
+
runtime_context = additional_context + "\n\n" + runtime_context
|
|
1541
|
+
|
|
1542
|
+
# Log truncated runtime context
|
|
1543
|
+
if len(runtime_context) > 1200:
|
|
1544
|
+
ctx_preview = runtime_context[:500] + f"\n\n... [{len(runtime_context) - 1000} chars truncated] ...\n\n" + runtime_context[-500:]
|
|
1545
|
+
else:
|
|
1546
|
+
ctx_preview = runtime_context
|
|
1547
|
+
logger.debug(f"Runtime context for {class_name}.{attr_name}", f"CONTEXT:\n{ctx_preview}")
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
if cls._llm_use_full_context:
|
|
1551
|
+
builder = ContextBuilder()
|
|
1552
|
+
context = builder.build_context(
|
|
1553
|
+
class_name=class_name,
|
|
1554
|
+
attr_name=attr_name,
|
|
1555
|
+
class_source=cls._get_class_source(),
|
|
1556
|
+
)
|
|
1557
|
+
# Append runtime context
|
|
1558
|
+
context = context + "\n\n" + runtime_context
|
|
1559
|
+
else:
|
|
1560
|
+
builder = ContextBuilder()
|
|
1561
|
+
context = builder.build_minimal_context(
|
|
1562
|
+
class_source=cls._get_class_source(),
|
|
1563
|
+
class_name=class_name,
|
|
1564
|
+
attr_name=attr_name,
|
|
1565
|
+
additional_context=runtime_context,
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
# Log truncated context
|
|
1569
|
+
if len(context) > 1200:
|
|
1570
|
+
context_preview = context[:500] + f"\n\n... [{len(context) - 1000} chars truncated] ...\n\n" + context[-500:]
|
|
1571
|
+
else:
|
|
1572
|
+
context_preview = context
|
|
1573
|
+
logger.debug(f"Full context for {class_name}.{attr_name} ({len(context)} chars)", f"CONTEXT:\n{context_preview}")
|
|
1574
|
+
|
|
1575
|
+
# Get app directory for package management
|
|
1576
|
+
app_dir = cls._get_app_dir()
|
|
1577
|
+
installed_packages = get_installed_packages(app_dir) if app_dir else None
|
|
1578
|
+
|
|
1579
|
+
result = generator.generate(
|
|
1580
|
+
context=context,
|
|
1581
|
+
class_name=class_name,
|
|
1582
|
+
attr_name=attr_name,
|
|
1583
|
+
is_classmethod=is_classmethod,
|
|
1584
|
+
installed_packages=installed_packages,
|
|
1585
|
+
)
|
|
1586
|
+
|
|
1587
|
+
logger.info(f"Generation complete: {class_name}.{attr_name} (type={result.code_type})")
|
|
1588
|
+
logger.debug(f"Generated result for {class_name}.{attr_name}", f"CODE:\n{result.code}\n\nDOCSTRING:\n{result.docstring}\n\nDEPENDENCIES:\n{result.dependencies}")
|
|
1589
|
+
|
|
1590
|
+
# Install any required packages
|
|
1591
|
+
if result.packages and app_dir:
|
|
1592
|
+
install_packages(app_dir, result.packages)
|
|
1593
|
+
|
|
1594
|
+
# Cache the result
|
|
1595
|
+
cache.set(
|
|
1596
|
+
class_name=class_name,
|
|
1597
|
+
attr_name=attr_name,
|
|
1598
|
+
code=result.code,
|
|
1599
|
+
code_type=result.code_type,
|
|
1600
|
+
docstring=result.docstring,
|
|
1601
|
+
dependencies=result.dependencies,
|
|
1602
|
+
packages=result.packages,
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
if not cls._llm_quiet:
|
|
1606
|
+
generated(class_name, attr_name)
|
|
1607
|
+
|
|
1608
|
+
return cls._execute_code(result.code, attr_name, result.dependencies)
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
@classmethod
|
|
1612
|
+
def _llm_generate_init(cls, args: tuple, kwargs: dict) -> Any:
|
|
1613
|
+
"""Generate __init__ method based on constructor arguments.
|
|
1614
|
+
|
|
1615
|
+
This is called by the metaclass __call__ when creating an object
|
|
1616
|
+
with arguments but no custom __init__ defined.
|
|
1617
|
+
|
|
1618
|
+
Args:
|
|
1619
|
+
args: Positional arguments passed to constructor
|
|
1620
|
+
kwargs: Keyword arguments passed to constructor
|
|
1621
|
+
|
|
1622
|
+
Returns:
|
|
1623
|
+
The generated __init__ function
|
|
1624
|
+
"""
|
|
1625
|
+
cache = cls._get_cache()
|
|
1626
|
+
class_name = cls.__name__
|
|
1627
|
+
|
|
1628
|
+
# Check cache first
|
|
1629
|
+
cached = cache.get(class_name, "__init__")
|
|
1630
|
+
if cached:
|
|
1631
|
+
if not cls._llm_quiet:
|
|
1632
|
+
using_cached(class_name, "__init__")
|
|
1633
|
+
return cls._execute_code(cached.code, "__init__", cached.dependencies)
|
|
1634
|
+
|
|
1635
|
+
# Generate new __init__
|
|
1636
|
+
if not cls._llm_quiet:
|
|
1637
|
+
generating(class_name, "__init__")
|
|
1638
|
+
|
|
1639
|
+
generator = cls._get_generator()
|
|
1640
|
+
|
|
1641
|
+
# Build context with constructor arguments info
|
|
1642
|
+
from scry_run.context import ContextBuilder
|
|
1643
|
+
|
|
1644
|
+
# Describe the constructor call
|
|
1645
|
+
if args or kwargs:
|
|
1646
|
+
args_desc = []
|
|
1647
|
+
if args:
|
|
1648
|
+
args_desc.append(f"Positional args: {repr(args)}")
|
|
1649
|
+
if kwargs:
|
|
1650
|
+
args_desc.append(f"Keyword args: {repr(kwargs)}")
|
|
1651
|
+
constructor_context = f"""
|
|
1652
|
+
## Constructor Call Context
|
|
1653
|
+
|
|
1654
|
+
This __init__ is being generated because the class was instantiated with arguments:
|
|
1655
|
+
{chr(10).join(args_desc)}
|
|
1656
|
+
|
|
1657
|
+
The __init__ should:
|
|
1658
|
+
1. Accept these arguments (infer parameter names from values/keys)
|
|
1659
|
+
2. Store them as instance attributes (self.xxx = xxx)
|
|
1660
|
+
3. Initialize any other state the class needs based on its docstring
|
|
1661
|
+
"""
|
|
1662
|
+
else:
|
|
1663
|
+
constructor_context = """
|
|
1664
|
+
## Constructor Call Context
|
|
1665
|
+
|
|
1666
|
+
This __init__ is being generated for a no-argument construction: `MyClass()`.
|
|
1667
|
+
|
|
1668
|
+
Based on the class docstring, initialize sensible default state:
|
|
1669
|
+
1. Create any instance attributes the class needs to function
|
|
1670
|
+
2. Set reasonable default values based on the class's purpose
|
|
1671
|
+
3. If the class represents a collection/container, initialize it as empty
|
|
1672
|
+
4. If the class needs configuration, use sensible defaults
|
|
1673
|
+
"""
|
|
1674
|
+
|
|
1675
|
+
if cls._llm_use_full_context:
|
|
1676
|
+
builder = ContextBuilder()
|
|
1677
|
+
context = builder.build_context(
|
|
1678
|
+
class_name=class_name,
|
|
1679
|
+
attr_name="__init__",
|
|
1680
|
+
class_source=cls._get_class_source(),
|
|
1681
|
+
)
|
|
1682
|
+
context = context + "\n\n" + constructor_context
|
|
1683
|
+
else:
|
|
1684
|
+
builder = ContextBuilder()
|
|
1685
|
+
context = builder.build_minimal_context(
|
|
1686
|
+
class_source=cls._get_class_source(),
|
|
1687
|
+
class_name=class_name,
|
|
1688
|
+
attr_name="__init__",
|
|
1689
|
+
additional_context=constructor_context,
|
|
1690
|
+
)
|
|
1691
|
+
|
|
1692
|
+
# Get app directory for package management
|
|
1693
|
+
app_dir = cls._get_app_dir()
|
|
1694
|
+
installed_packages = get_installed_packages(app_dir) if app_dir else None
|
|
1695
|
+
|
|
1696
|
+
result = generator.generate(
|
|
1697
|
+
context=context,
|
|
1698
|
+
class_name=class_name,
|
|
1699
|
+
attr_name="__init__",
|
|
1700
|
+
is_classmethod=False,
|
|
1701
|
+
installed_packages=installed_packages,
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
# Install any required packages
|
|
1705
|
+
if result.packages and app_dir:
|
|
1706
|
+
install_packages(app_dir, result.packages)
|
|
1707
|
+
|
|
1708
|
+
# Cache the result
|
|
1709
|
+
cache.set(
|
|
1710
|
+
class_name=class_name,
|
|
1711
|
+
attr_name="__init__",
|
|
1712
|
+
code=result.code,
|
|
1713
|
+
code_type=result.code_type,
|
|
1714
|
+
docstring=result.docstring,
|
|
1715
|
+
dependencies=result.dependencies,
|
|
1716
|
+
packages=result.packages,
|
|
1717
|
+
)
|
|
1718
|
+
|
|
1719
|
+
if not cls._llm_quiet:
|
|
1720
|
+
generated(class_name, "__init__")
|
|
1721
|
+
|
|
1722
|
+
return cls._execute_code(result.code, "__init__", result.dependencies)
|
|
1723
|
+
|
|
1724
|
+
@classmethod
|
|
1725
|
+
def _execute_code(
|
|
1726
|
+
cls,
|
|
1727
|
+
code: str,
|
|
1728
|
+
attr_name: str,
|
|
1729
|
+
dependencies: list[str],
|
|
1730
|
+
) -> Any:
|
|
1731
|
+
"""Execute generated code and extract the attribute.
|
|
1732
|
+
|
|
1733
|
+
Args:
|
|
1734
|
+
code: The Python code to execute
|
|
1735
|
+
attr_name: Name of the attribute being generated
|
|
1736
|
+
dependencies: List of import statements
|
|
1737
|
+
|
|
1738
|
+
Returns:
|
|
1739
|
+
The extracted code object
|
|
1740
|
+
"""
|
|
1741
|
+
# Build execution namespace with imports
|
|
1742
|
+
namespace: dict[str, Any] = {}
|
|
1743
|
+
|
|
1744
|
+
# Execute imports
|
|
1745
|
+
for dep in dependencies:
|
|
1746
|
+
try:
|
|
1747
|
+
exec(dep, namespace)
|
|
1748
|
+
except Exception:
|
|
1749
|
+
pass # Ignore import errors, let the main code fail if needed
|
|
1750
|
+
|
|
1751
|
+
# Execute the generated code
|
|
1752
|
+
# Compile with descriptive filename for clearer stack traces
|
|
1753
|
+
filename = f"<generated: {cls.__name__}.{attr_name}>"
|
|
1754
|
+
compiled = compile(code, filename, "exec")
|
|
1755
|
+
exec(compiled, namespace)
|
|
1756
|
+
|
|
1757
|
+
# Extract the generated attribute
|
|
1758
|
+
obj = None
|
|
1759
|
+
if attr_name in namespace:
|
|
1760
|
+
obj = namespace[attr_name]
|
|
1761
|
+
else:
|
|
1762
|
+
# Try sanitized name (replace invalid chars with _)
|
|
1763
|
+
sanitized = re.sub(r'[^a-zA-Z0-9_]', '_', attr_name)
|
|
1764
|
+
if sanitized in namespace:
|
|
1765
|
+
obj = namespace[sanitized]
|
|
1766
|
+
else:
|
|
1767
|
+
# If exact/sanitized name not found, look for functions defined in the generated block
|
|
1768
|
+
# We identify them by checking if their code object's filename matches what we compiled
|
|
1769
|
+
for name, o in namespace.items():
|
|
1770
|
+
# Check for function with matching filename
|
|
1771
|
+
if hasattr(o, "__code__") and getattr(o.__code__, "co_filename", "") == filename:
|
|
1772
|
+
obj = o
|
|
1773
|
+
break
|
|
1774
|
+
|
|
1775
|
+
# Fallback: look for the first callable that isn't a dunder
|
|
1776
|
+
if obj is None:
|
|
1777
|
+
for name, o in namespace.items():
|
|
1778
|
+
if name.startswith("__") and name.endswith("__"):
|
|
1779
|
+
continue
|
|
1780
|
+
# Avoid picking up imports (which we don't have a perfect way to distinguish
|
|
1781
|
+
# without the filename check above, but this is a last resort)
|
|
1782
|
+
if callable(o) and not name.startswith("_"):
|
|
1783
|
+
obj = o
|
|
1784
|
+
break
|
|
1785
|
+
|
|
1786
|
+
if obj is None:
|
|
1787
|
+
raise RuntimeError(f"Generated code did not define '{attr_name}'")
|
|
1788
|
+
|
|
1789
|
+
# Watermark the object as generated
|
|
1790
|
+
marker_name = "_llm_is_generated"
|
|
1791
|
+
|
|
1792
|
+
def mark(target: Any) -> None:
|
|
1793
|
+
try:
|
|
1794
|
+
setattr(target, marker_name, True)
|
|
1795
|
+
setattr(target, "_llm_source", f"{cls.__name__}.{attr_name}")
|
|
1796
|
+
except (AttributeError, TypeError):
|
|
1797
|
+
# Cannot mark some objects (e.g. built-ins, but generated code is unlikely to be built-in)
|
|
1798
|
+
pass
|
|
1799
|
+
|
|
1800
|
+
if isinstance(obj, property):
|
|
1801
|
+
if obj.fget: mark(obj.fget)
|
|
1802
|
+
if obj.fset: mark(obj.fset)
|
|
1803
|
+
if obj.fdel: mark(obj.fdel)
|
|
1804
|
+
elif isinstance(obj, (classmethod, staticmethod)):
|
|
1805
|
+
mark(obj.__func__)
|
|
1806
|
+
else:
|
|
1807
|
+
mark(obj)
|
|
1808
|
+
|
|
1809
|
+
return obj
|
|
1810
|
+
|
|
1811
|
+
@classmethod
|
|
1812
|
+
def scry_export_cache(cls, output_path: Optional[str] = None) -> dict:
|
|
1813
|
+
"""Export the cache for this class.
|
|
1814
|
+
|
|
1815
|
+
Args:
|
|
1816
|
+
output_path: If provided, write to this file as Python code
|
|
1817
|
+
|
|
1818
|
+
Returns:
|
|
1819
|
+
Dictionary of cached entries
|
|
1820
|
+
"""
|
|
1821
|
+
cache = cls._get_cache()
|
|
1822
|
+
|
|
1823
|
+
if output_path:
|
|
1824
|
+
cache.export_to_file(output_path)
|
|
1825
|
+
|
|
1826
|
+
return cache.export()
|
|
1827
|
+
|
|
1828
|
+
@classmethod
|
|
1829
|
+
def scry_prune_cache(
|
|
1830
|
+
cls,
|
|
1831
|
+
attr_name: Optional[str] = None,
|
|
1832
|
+
) -> int:
|
|
1833
|
+
"""Prune cache entries.
|
|
1834
|
+
|
|
1835
|
+
Args:
|
|
1836
|
+
attr_name: If provided, only prune this specific attribute
|
|
1837
|
+
|
|
1838
|
+
Returns:
|
|
1839
|
+
Number of entries pruned
|
|
1840
|
+
"""
|
|
1841
|
+
cache = cls._get_cache()
|
|
1842
|
+
return cache.prune(class_name=cls.__name__, attr_name=attr_name)
|
|
1843
|
+
|
|
1844
|
+
@classmethod
|
|
1845
|
+
def scry_disable(cls) -> None:
|
|
1846
|
+
"""Disable LLM generation for this class."""
|
|
1847
|
+
cls._llm_enabled = False
|
|
1848
|
+
|
|
1849
|
+
@classmethod
|
|
1850
|
+
def scry_enable(cls) -> None:
|
|
1851
|
+
"""Enable LLM generation for this class."""
|
|
1852
|
+
cls._llm_enabled = True
|