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/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