edsl 0.1.54__py3-none-any.whl → 0.1.56__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.
Files changed (105) hide show
  1. edsl/__init__.py +8 -1
  2. edsl/__init__original.py +134 -0
  3. edsl/__version__.py +1 -1
  4. edsl/agents/agent.py +29 -0
  5. edsl/agents/agent_list.py +36 -1
  6. edsl/base/base_class.py +281 -151
  7. edsl/base/data_transfer_models.py +15 -4
  8. edsl/buckets/__init__.py +8 -3
  9. edsl/buckets/bucket_collection.py +9 -3
  10. edsl/buckets/model_buckets.py +4 -2
  11. edsl/buckets/token_bucket.py +2 -2
  12. edsl/buckets/token_bucket_client.py +5 -3
  13. edsl/caching/cache.py +131 -62
  14. edsl/caching/cache_entry.py +70 -58
  15. edsl/caching/sql_dict.py +17 -0
  16. edsl/cli.py +99 -0
  17. edsl/config/config_class.py +16 -0
  18. edsl/conversation/__init__.py +31 -0
  19. edsl/coop/coop.py +276 -242
  20. edsl/coop/coop_jobs_objects.py +59 -0
  21. edsl/coop/coop_objects.py +29 -0
  22. edsl/coop/coop_regular_objects.py +26 -0
  23. edsl/coop/utils.py +24 -19
  24. edsl/dataset/dataset.py +338 -101
  25. edsl/dataset/dataset_operations_mixin.py +216 -180
  26. edsl/db_list/sqlite_list.py +349 -0
  27. edsl/inference_services/__init__.py +40 -5
  28. edsl/inference_services/exceptions.py +11 -0
  29. edsl/inference_services/services/anthropic_service.py +5 -2
  30. edsl/inference_services/services/aws_bedrock.py +6 -2
  31. edsl/inference_services/services/azure_ai.py +6 -2
  32. edsl/inference_services/services/google_service.py +7 -3
  33. edsl/inference_services/services/mistral_ai_service.py +6 -2
  34. edsl/inference_services/services/open_ai_service.py +6 -2
  35. edsl/inference_services/services/perplexity_service.py +6 -2
  36. edsl/inference_services/services/test_service.py +94 -5
  37. edsl/interviews/answering_function.py +167 -59
  38. edsl/interviews/interview.py +124 -72
  39. edsl/interviews/interview_task_manager.py +10 -0
  40. edsl/interviews/request_token_estimator.py +8 -0
  41. edsl/invigilators/invigilators.py +35 -13
  42. edsl/jobs/async_interview_runner.py +146 -104
  43. edsl/jobs/data_structures.py +6 -4
  44. edsl/jobs/decorators.py +61 -0
  45. edsl/jobs/fetch_invigilator.py +61 -18
  46. edsl/jobs/html_table_job_logger.py +14 -2
  47. edsl/jobs/jobs.py +180 -104
  48. edsl/jobs/jobs_component_constructor.py +2 -2
  49. edsl/jobs/jobs_interview_constructor.py +2 -0
  50. edsl/jobs/jobs_pricing_estimation.py +154 -113
  51. edsl/jobs/jobs_remote_inference_logger.py +4 -0
  52. edsl/jobs/jobs_runner_status.py +30 -25
  53. edsl/jobs/progress_bar_manager.py +79 -0
  54. edsl/jobs/remote_inference.py +35 -1
  55. edsl/key_management/key_lookup_builder.py +6 -1
  56. edsl/language_models/language_model.py +110 -12
  57. edsl/language_models/model.py +10 -3
  58. edsl/language_models/price_manager.py +176 -71
  59. edsl/language_models/registry.py +5 -0
  60. edsl/notebooks/notebook.py +77 -10
  61. edsl/questions/VALIDATION_README.md +134 -0
  62. edsl/questions/__init__.py +24 -1
  63. edsl/questions/exceptions.py +21 -0
  64. edsl/questions/question_dict.py +201 -16
  65. edsl/questions/question_multiple_choice_with_other.py +624 -0
  66. edsl/questions/question_registry.py +2 -1
  67. edsl/questions/templates/multiple_choice_with_other/__init__.py +0 -0
  68. edsl/questions/templates/multiple_choice_with_other/answering_instructions.jinja +15 -0
  69. edsl/questions/templates/multiple_choice_with_other/question_presentation.jinja +17 -0
  70. edsl/questions/validation_analysis.py +185 -0
  71. edsl/questions/validation_cli.py +131 -0
  72. edsl/questions/validation_html_report.py +404 -0
  73. edsl/questions/validation_logger.py +136 -0
  74. edsl/results/result.py +115 -46
  75. edsl/results/results.py +702 -171
  76. edsl/scenarios/construct_download_link.py +16 -3
  77. edsl/scenarios/directory_scanner.py +226 -226
  78. edsl/scenarios/file_methods.py +5 -0
  79. edsl/scenarios/file_store.py +150 -9
  80. edsl/scenarios/handlers/__init__.py +5 -1
  81. edsl/scenarios/handlers/mp4_file_store.py +104 -0
  82. edsl/scenarios/handlers/webm_file_store.py +104 -0
  83. edsl/scenarios/scenario.py +120 -101
  84. edsl/scenarios/scenario_list.py +800 -727
  85. edsl/scenarios/scenario_list_gc_test.py +146 -0
  86. edsl/scenarios/scenario_list_memory_test.py +214 -0
  87. edsl/scenarios/scenario_list_source_refactor.md +35 -0
  88. edsl/scenarios/scenario_selector.py +5 -4
  89. edsl/scenarios/scenario_source.py +1990 -0
  90. edsl/scenarios/tests/test_scenario_list_sources.py +52 -0
  91. edsl/surveys/survey.py +22 -0
  92. edsl/tasks/__init__.py +4 -2
  93. edsl/tasks/task_history.py +198 -36
  94. edsl/tests/scenarios/test_ScenarioSource.py +51 -0
  95. edsl/tests/scenarios/test_scenario_list_sources.py +51 -0
  96. edsl/utilities/__init__.py +2 -1
  97. edsl/utilities/decorators.py +121 -0
  98. edsl/utilities/memory_debugger.py +1010 -0
  99. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/METADATA +51 -76
  100. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/RECORD +103 -79
  101. edsl/jobs/jobs_runner_asyncio.py +0 -281
  102. edsl/language_models/unused/fake_openai_service.py +0 -60
  103. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/LICENSE +0 -0
  104. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/WHEEL +0 -0
  105. {edsl-0.1.54.dist-info → edsl-0.1.56.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1010 @@
1
+ """
2
+ Memory debugging utilities for analyzing object references and memory leaks.
3
+ """
4
+ import gc
5
+ import os
6
+ import sys
7
+ import time
8
+ import types
9
+ import inspect
10
+ import webbrowser
11
+ from pathlib import Path
12
+ import base64
13
+ import html
14
+ from datetime import datetime
15
+ from typing import Any, Set, List, Dict, Optional, Tuple, Union
16
+
17
+ # Try to import objgraph, which is only available as a dev dependency
18
+ try:
19
+ import objgraph
20
+ OBJGRAPH_AVAILABLE = True
21
+ except ImportError:
22
+ OBJGRAPH_AVAILABLE = False
23
+
24
+ class MemoryDebugger:
25
+ """
26
+ A class for debugging memory issues and analyzing object references.
27
+
28
+ This class provides utilities to:
29
+ - Inspect objects referring to a target object
30
+ - Detect reference cycles
31
+ - Visualize object dependencies
32
+ - Analyze memory usage patterns
33
+ """
34
+
35
+ def __init__(self, target_obj: Any):
36
+ """
37
+ Initialize the debugger with a target object to analyze.
38
+
39
+ Args:
40
+ target_obj: The object to inspect for memory issues
41
+ """
42
+ self.target_obj = target_obj
43
+
44
+ def _get_source_info(self, obj: Any) -> Dict[str, Any]:
45
+ """Get source code information for an object if available."""
46
+ result = {
47
+ 'module': getattr(obj, '__module__', 'unknown'),
48
+ 'file': 'unknown',
49
+ 'line': 0,
50
+ 'source': None,
51
+ 'has_source': False,
52
+ 'type_str': str(type(obj))
53
+ }
54
+
55
+ try:
56
+ # Try to get the source file and line number
57
+ if hasattr(obj, '__class__'):
58
+ module = inspect.getmodule(obj.__class__)
59
+ if module:
60
+ result['module'] = module.__name__
61
+ try:
62
+ file = inspect.getsourcefile(obj.__class__)
63
+ if file:
64
+ result['file'] = file
65
+ try:
66
+ _, line = inspect.getsourcelines(obj.__class__)
67
+ result['line'] = line
68
+ result['has_source'] = True
69
+ except Exception:
70
+ pass
71
+ except Exception:
72
+ pass
73
+ except Exception:
74
+ # Silently fail if we can't get source info
75
+ pass
76
+
77
+ return result
78
+
79
+ def _generate_html_report(self, prefix: str = "", output_dir: str = None) -> Tuple[str, str, str]:
80
+ """
81
+ Generate a comprehensive HTML memory debugging report.
82
+
83
+ Args:
84
+ prefix: Optional prefix for output files. If empty, uses target object type.
85
+ output_dir: Optional directory to write files to. If None, uses tempdir or current directory.
86
+
87
+ Returns:
88
+ A tuple containing (html_filename, refs_graph_filename, backrefs_graph_filename)
89
+ """
90
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
91
+ if not prefix:
92
+ prefix = type(self.target_obj).__name__.lower()
93
+
94
+ # Determine output directory
95
+ if output_dir is None:
96
+ output_dir = os.environ.get("EDSL_MEMORY_DEBUG_DIR", "")
97
+
98
+ if output_dir:
99
+ # Ensure directory exists
100
+ os.makedirs(output_dir, exist_ok=True)
101
+
102
+ # Prepare filenames
103
+ html_filename = os.path.join(output_dir, f"{prefix}_memory_debug_{timestamp}.html") if output_dir else f"{prefix}_memory_debug_{timestamp}.html"
104
+ refs_graph_filename = os.path.join(output_dir, f"{prefix}_outgoing_refs_{timestamp}.png") if output_dir else f"{prefix}_outgoing_refs_{timestamp}.png"
105
+ backrefs_graph_filename = os.path.join(output_dir, f"{prefix}_incoming_refs_{timestamp}.png") if output_dir else f"{prefix}_incoming_refs_{timestamp}.png"
106
+
107
+ # Generate object graphs - both directions
108
+ graph_exists = False
109
+ backrefs_graph_exists = False
110
+
111
+ if OBJGRAPH_AVAILABLE:
112
+ try:
113
+ # Use objgraph's backup function to filter frames and functions
114
+ def skip_frames(obj):
115
+ return not isinstance(obj, (types.FrameType, types.FunctionType))
116
+
117
+ # Outgoing references (what this object references)
118
+ objgraph.show_refs(
119
+ self.target_obj,
120
+ filename=refs_graph_filename,
121
+ max_depth=3,
122
+ extra_ignore=[id(obj) for obj in gc.get_objects()
123
+ if isinstance(obj, (types.FrameType, types.FunctionType))]
124
+ )
125
+ graph_exists = True
126
+
127
+ # Incoming references (what references this object)
128
+ objgraph.show_backrefs(
129
+ self.target_obj,
130
+ filename=backrefs_graph_filename,
131
+ max_depth=3,
132
+ extra_ignore=[id(obj) for obj in gc.get_objects()
133
+ if isinstance(obj, (types.FrameType, types.FunctionType))]
134
+ )
135
+ backrefs_graph_exists = True
136
+ except Exception as e:
137
+ print(f"Warning: Could not generate object graph visualization: {e}")
138
+ graph_exists = False
139
+ else:
140
+ print("Warning: objgraph package is not available. Install it with 'pip install objgraph' to enable visualizations.")
141
+
142
+ # Get all reference cycle information
143
+ import io
144
+ from contextlib import redirect_stdout
145
+
146
+ captured_output = io.StringIO()
147
+ with redirect_stdout(captured_output):
148
+ cycles = self.detect_reference_cycles()
149
+
150
+ cycle_output = captured_output.getvalue()
151
+
152
+ # Get referrers and referents with source info
153
+ referrers = gc.get_referrers(self.target_obj)
154
+ referents = gc.get_referents(self.target_obj)
155
+
156
+ # Skip frames and function objects for referrers
157
+ filtered_referrers = [ref for ref in referrers if not isinstance(ref, (types.FrameType, types.FunctionType))]
158
+
159
+ # Get source info for all objects
160
+ referrer_info = [self._get_source_info(ref) for ref in filtered_referrers]
161
+ referent_info = [self._get_source_info(ref) for ref in referents]
162
+
163
+ # Get target object info
164
+ target_info = self._get_source_info(self.target_obj)
165
+
166
+ # Generate HTML content
167
+ html_content = f"""<!DOCTYPE html>
168
+ <html lang="en">
169
+ <head>
170
+ <meta charset="UTF-8">
171
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
172
+ <title>Memory Debug Report - {type(self.target_obj).__name__}</title>
173
+ <style>
174
+ body {{
175
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
176
+ line-height: 1.6;
177
+ color: #333;
178
+ max-width: 1200px;
179
+ margin: 0 auto;
180
+ padding: 20px;
181
+ }}
182
+ h1, h2, h3 {{
183
+ color: #2c3e50;
184
+ }}
185
+ h1 {{
186
+ border-bottom: 2px solid #eaecef;
187
+ padding-bottom: 10px;
188
+ }}
189
+ h2 {{
190
+ margin-top: 30px;
191
+ border-bottom: 1px solid #eaecef;
192
+ padding-bottom: 5px;
193
+ }}
194
+ .info-box {{
195
+ background-color: #f8f9fa;
196
+ border: 1px solid #e9ecef;
197
+ border-radius: 5px;
198
+ padding: 15px;
199
+ margin: 20px 0;
200
+ }}
201
+ .warning {{
202
+ background-color: #fff3cd;
203
+ border-color: #ffeeba;
204
+ }}
205
+ .object {{
206
+ margin-bottom: 10px;
207
+ padding: 10px;
208
+ background-color: #f8f9fa;
209
+ border-left: 4px solid #007bff;
210
+ border-radius: 3px;
211
+ }}
212
+ .object-header {{
213
+ display: flex;
214
+ justify-content: space-between;
215
+ font-weight: bold;
216
+ }}
217
+ .object-details {{
218
+ margin-top: 5px;
219
+ font-size: 0.9em;
220
+ }}
221
+ .file-link {{
222
+ color: #007bff;
223
+ text-decoration: none;
224
+ }}
225
+ .file-link:hover {{
226
+ text-decoration: underline;
227
+ }}
228
+ .unhashable {{
229
+ border-left-color: #dc3545;
230
+ }}
231
+ .cycle {{
232
+ border-left-color: #fd7e14;
233
+ }}
234
+ .tab {{
235
+ overflow: hidden;
236
+ border: 1px solid #ccc;
237
+ background-color: #f1f1f1;
238
+ border-radius: 5px 5px 0 0;
239
+ }}
240
+ .tab button {{
241
+ background-color: inherit;
242
+ float: left;
243
+ border: none;
244
+ outline: none;
245
+ cursor: pointer;
246
+ padding: 10px 16px;
247
+ transition: 0.3s;
248
+ }}
249
+ .tab button:hover {{
250
+ background-color: #ddd;
251
+ }}
252
+ .tab button.active {{
253
+ background-color: #ccc;
254
+ }}
255
+ .tabcontent {{
256
+ display: none;
257
+ padding: 20px;
258
+ border: 1px solid #ccc;
259
+ border-top: none;
260
+ border-radius: 0 0 5px 5px;
261
+ }}
262
+ .code {{
263
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
264
+ background-color: #f6f8fa;
265
+ padding: 10px;
266
+ border-radius: 3px;
267
+ overflow-x: auto;
268
+ }}
269
+ .graph-container {{
270
+ text-align: center;
271
+ margin: 20px 0;
272
+ }}
273
+ table {{
274
+ border-collapse: collapse;
275
+ width: 100%;
276
+ margin: 20px 0;
277
+ }}
278
+ th, td {{
279
+ border: 1px solid #ddd;
280
+ padding: 8px;
281
+ text-align: left;
282
+ }}
283
+ th {{
284
+ background-color: #f2f2f2;
285
+ }}
286
+ tr:nth-child(even) {{
287
+ background-color: #f9f9f9;
288
+ }}
289
+ .toggle-button {{
290
+ background-color: #4CAF50;
291
+ border: none;
292
+ color: white;
293
+ padding: 5px 10px;
294
+ text-align: center;
295
+ text-decoration: none;
296
+ display: inline-block;
297
+ font-size: 12px;
298
+ margin: 4px 2px;
299
+ cursor: pointer;
300
+ border-radius: 3px;
301
+ }}
302
+ </style>
303
+ </head>
304
+ <body>
305
+ <h1>Memory Debug Report - {type(self.target_obj).__name__}</h1>
306
+ <p>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
307
+
308
+ <div class="info-box">
309
+ <h3>Target Object Information</h3>
310
+ <p><strong>Type:</strong> {type(self.target_obj)}</p>
311
+ <p><strong>ID:</strong> {id(self.target_obj)}</p>
312
+ <p><strong>Module:</strong> {target_info['module']}</p>
313
+ <p><strong>Reference Count:</strong> {sys.getrefcount(self.target_obj)}</p>
314
+ {f'<p><strong>Source File:</strong> <a href="file://{target_info["file"]}" class="file-link">{os.path.basename(target_info["file"])}</a> (line {target_info["line"]})</p>' if target_info["has_source"] else ''}
315
+ </div>
316
+
317
+ <div class="tab">
318
+ <button class="tablinks" onclick="openTab(event, 'Overview')" id="defaultOpen">Overview</button>
319
+ <button class="tablinks" onclick="openTab(event, 'Referrers')">Referrers ({len(filtered_referrers)})</button>
320
+ <button class="tablinks" onclick="openTab(event, 'Referents')">Referents ({len(referents)})</button>
321
+ <button class="tablinks" onclick="openTab(event, 'Cycles')" {'style="color: red;"' if cycles else ''}>Cycles {f"({len(cycles)})" if cycles else ""}</button>
322
+ <button class="tablinks" onclick="openTab(event, 'Graph')">Reference Visualizations</button>
323
+ </div>
324
+
325
+ <div id="Overview" class="tabcontent">
326
+ <h2>Memory Analysis Overview</h2>
327
+ <div class="info-box">
328
+ <p>This report analyzes the memory references to and from the target object. It helps identify potential memory leaks by showing object relationships.</p>
329
+ <p><strong>Total referrers:</strong> {len(filtered_referrers)}</p>
330
+ <p><strong>Total referents:</strong> {len(referents)}</p>
331
+ <p><strong>Potential reference cycles:</strong> {len(cycles) if cycles else "None detected"}</p>
332
+ </div>
333
+
334
+ <h3>Common Types</h3>
335
+ <table>
336
+ <tr>
337
+ <th>Type</th>
338
+ <th>Count as Referrer</th>
339
+ <th>Count as Referent</th>
340
+ </tr>
341
+ """
342
+
343
+ # Count types
344
+ referrer_types = {}
345
+ for ref in filtered_referrers:
346
+ type_name = type(ref).__name__
347
+ referrer_types[type_name] = referrer_types.get(type_name, 0) + 1
348
+
349
+ referent_types = {}
350
+ for ref in referents:
351
+ type_name = type(ref).__name__
352
+ referent_types[type_name] = referent_types.get(type_name, 0) + 1
353
+
354
+ # Get all unique types
355
+ all_types = set(list(referrer_types.keys()) + list(referent_types.keys()))
356
+
357
+ # Add type rows to the table
358
+ for type_name in sorted(all_types):
359
+ html_content += f"""
360
+ <tr>
361
+ <td>{type_name}</td>
362
+ <td>{referrer_types.get(type_name, 0)}</td>
363
+ <td>{referent_types.get(type_name, 0)}</td>
364
+ </tr>"""
365
+
366
+ html_content += """
367
+ </table>
368
+ </div>
369
+
370
+ <div id="Referrers" class="tabcontent">
371
+ <h2>Objects Referring to Target</h2>
372
+ <p>These objects hold references to the target object:</p>
373
+ """
374
+
375
+ # Add referrers
376
+ for i, (ref, info) in enumerate(zip(filtered_referrers, referrer_info)):
377
+ object_id = id(ref)
378
+ object_type = type(ref).__name__
379
+ is_unhashable = not self._is_hashable(ref)
380
+
381
+ html_content += f"""
382
+ <div class="object {'unhashable' if is_unhashable else ''}">
383
+ <div class="object-header">
384
+ <span>[{i}] {object_type}{' (unhashable)' if is_unhashable else ''}</span>
385
+ <span>ID: {object_id}</span>
386
+ </div>
387
+ <div class="object-details">
388
+ <p><strong>Module:</strong> {info['module']}</p>
389
+ {f'<p><strong>Source:</strong> <a href="file://{info["file"]}" class="file-link">{os.path.basename(info["file"])}</a> (line {info["line"]})</p>' if info["has_source"] else ''}"""
390
+
391
+ # Add more specific information based on type
392
+ if isinstance(ref, dict):
393
+ html_content += f"""
394
+ <p><strong>Dict size:</strong> {len(ref)} items</p>
395
+ <button class="toggle-button" onclick="toggleDetails('referrer-{i}')">Show/Hide Contents</button>
396
+ <div id="referrer-{i}" style="display: none;">
397
+ <table>
398
+ <tr><th>Key</th><th>Value Type</th></tr>"""
399
+
400
+ for k, v in list(ref.items())[:20]: # Limit to 20 items
401
+ # Skip showing the target object itself
402
+ if v is self.target_obj:
403
+ key_html = html.escape(str(k))
404
+ html_content += f"""
405
+ <tr><td>{key_html}</td><td><strong>TARGET OBJECT</strong></td></tr>"""
406
+ else:
407
+ key_html = html.escape(str(k))
408
+ html_content += f"""
409
+ <tr><td>{key_html}</td><td>{type(v).__name__}</td></tr>"""
410
+
411
+ if len(ref) > 20:
412
+ html_content += f"""
413
+ <tr><td colspan="2">... and {len(ref) - 20} more items</td></tr>"""
414
+
415
+ html_content += """
416
+ </table>
417
+ </div>"""
418
+ elif isinstance(ref, (list, tuple)):
419
+ html_content += f"""
420
+ <p><strong>{type(ref).__name__} size:</strong> {len(ref)} items</p>
421
+ <button class="toggle-button" onclick="toggleDetails('referrer-{i}')">Show/Hide Contents</button>
422
+ <div id="referrer-{i}" style="display: none;">
423
+ <table>
424
+ <tr><th>Index</th><th>Value Type</th></tr>"""
425
+
426
+ for idx, item in enumerate(ref[:20]): # Limit to 20 items
427
+ if item is self.target_obj:
428
+ html_content += f"""
429
+ <tr><td>{idx}</td><td><strong>TARGET OBJECT</strong></td></tr>"""
430
+ else:
431
+ html_content += f"""
432
+ <tr><td>{idx}</td><td>{type(item).__name__}</td></tr>"""
433
+
434
+ if len(ref) > 20:
435
+ html_content += f"""
436
+ <tr><td colspan="2">... and {len(ref) - 20} more items</td></tr>"""
437
+
438
+ html_content += """
439
+ </table>
440
+ </div>"""
441
+
442
+ html_content += """
443
+ </div>
444
+ </div>"""
445
+
446
+ html_content += """
447
+ </div>
448
+
449
+ <div id="Referents" class="tabcontent">
450
+ <h2>Objects Referenced by Target</h2>
451
+ <p>These objects are referenced by the target object:</p>
452
+ """
453
+
454
+ # Add referents
455
+ for i, (ref, info) in enumerate(zip(referents, referent_info)):
456
+ object_id = id(ref)
457
+ object_type = type(ref).__name__
458
+ is_unhashable = not self._is_hashable(ref)
459
+
460
+ html_content += f"""
461
+ <div class="object {'unhashable' if is_unhashable else ''}">
462
+ <div class="object-header">
463
+ <span>[{i}] {object_type}{' (unhashable)' if is_unhashable else ''}</span>
464
+ <span>ID: {object_id}</span>
465
+ </div>
466
+ <div class="object-details">
467
+ <p><strong>Module:</strong> {info['module']}</p>
468
+ {f'<p><strong>Source:</strong> <a href="file://{info["file"]}" class="file-link">{os.path.basename(info["file"])}</a> (line {info["line"]})</p>' if info["has_source"] else ''}"""
469
+
470
+ # Add specific information for dicts and sequences
471
+ if isinstance(ref, dict):
472
+ html_content += f"""
473
+ <p><strong>Dict size:</strong> {len(ref)} items</p>
474
+ <button class="toggle-button" onclick="toggleDetails('referent-{i}')">Show/Hide Contents</button>
475
+ <div id="referent-{i}" style="display: none;">
476
+ <table>
477
+ <tr><th>Key</th><th>Value Type</th></tr>"""
478
+
479
+ for k, v in list(ref.items())[:20]: # Limit to 20 items
480
+ key_html = html.escape(str(k))
481
+ html_content += f"""
482
+ <tr><td>{key_html}</td><td>{type(v).__name__}</td></tr>"""
483
+
484
+ if len(ref) > 20:
485
+ html_content += f"""
486
+ <tr><td colspan="2">... and {len(ref) - 20} more items</td></tr>"""
487
+
488
+ html_content += """
489
+ </table>
490
+ </div>"""
491
+ elif isinstance(ref, (list, tuple)):
492
+ html_content += f"""
493
+ <p><strong>{type(ref).__name__} size:</strong> {len(ref)} items</p>
494
+ <button class="toggle-button" onclick="toggleDetails('referent-{i}')">Show/Hide Contents</button>
495
+ <div id="referent-{i}" style="display: none;">
496
+ <table>
497
+ <tr><th>Index</th><th>Value Type</th></tr>"""
498
+
499
+ for idx, item in enumerate(ref[:20]): # Limit to 20 items
500
+ html_content += f"""
501
+ <tr><td>{idx}</td><td>{type(item).__name__}</td></tr>"""
502
+
503
+ if len(ref) > 20:
504
+ html_content += f"""
505
+ <tr><td colspan="2">... and {len(ref) - 20} more items</td></tr>"""
506
+
507
+ html_content += """
508
+ </table>
509
+ </div>"""
510
+
511
+ html_content += """
512
+ </div>
513
+ </div>"""
514
+
515
+ html_content += """
516
+ </div>
517
+
518
+ <div id="Cycles" class="tabcontent">
519
+ <h2>Reference Cycles</h2>
520
+ """
521
+
522
+ # Add reference cycle output
523
+ if cycles:
524
+ html_content += f"""
525
+ <div class="info-box warning">
526
+ <p>Found {len(cycles)} potential reference cycles. These objects might be causing memory leaks.</p>
527
+ </div>
528
+ """
529
+ # Add cycle details
530
+ for i, obj in enumerate(cycles):
531
+ object_type = type(obj).__name__
532
+ object_id = id(obj)
533
+ info = self._get_source_info(obj)
534
+
535
+ html_content += f"""
536
+ <div class="object cycle">
537
+ <div class="object-header">
538
+ <span>[{i}] {object_type}</span>
539
+ <span>ID: {object_id}</span>
540
+ </div>
541
+ <div class="object-details">
542
+ <p><strong>Module:</strong> {info['module']}</p>
543
+ {f'<p><strong>Source:</strong> <a href="file://{info["file"]}" class="file-link">{os.path.basename(info["file"])}</a> (line {info["line"]})</p>' if info["has_source"] else ''}
544
+ </div>
545
+ </div>"""
546
+ else:
547
+ html_content += """
548
+ <p>No reference cycles detected among hashable objects.</p>
549
+ """
550
+
551
+ # Add unhashable object analysis
552
+ html_content += """
553
+ <h3>Unhashable Object Analysis</h3>
554
+ <div class="code">
555
+ """
556
+ for line in cycle_output.splitlines():
557
+ html_content += f"{html.escape(line)}<br>"
558
+
559
+ html_content += """
560
+ </div>
561
+ </div>
562
+
563
+ <div id="Graph" class="tabcontent">
564
+ <h2>Object Graph Visualization</h2>
565
+
566
+ <h3>Outgoing References (Objects Referenced By Target)</h3>
567
+ <p>This graph shows what objects are referenced by the target object:</p>
568
+ """
569
+
570
+ if graph_exists:
571
+ try:
572
+ with open(refs_graph_filename, 'rb') as f:
573
+ img_data = base64.b64encode(f.read()).decode('utf-8')
574
+ html_content += f"""
575
+ <div class="graph-container">
576
+ <img src="data:image/png;base64,{img_data}" alt="Objects referenced by target" style="max-width: 100%;">
577
+ </div>
578
+ <p><a href="file://{os.path.abspath(refs_graph_filename)}" target="_blank">Open full-size outgoing references graph</a></p>
579
+ """
580
+ except Exception as e:
581
+ html_content += f"""
582
+ <div class="info-box warning">
583
+ <p>Could not embed outgoing references graph image: {e}</p>
584
+ <p><a href="file://{os.path.abspath(refs_graph_filename)}" target="_blank">Open outgoing references graph image</a></p>
585
+ </div>
586
+ """
587
+ else:
588
+ html_content += """
589
+ <div class="info-box warning">
590
+ <p>Could not generate outgoing references graph visualization.</p>
591
+ </div>
592
+ """
593
+
594
+ html_content += """
595
+ <h3>Incoming References (Objects Referencing Target)</h3>
596
+ <p>This graph shows what objects have a strong reference to the target object:</p>
597
+ """
598
+
599
+ if backrefs_graph_exists:
600
+ try:
601
+ with open(backrefs_graph_filename, 'rb') as f:
602
+ img_data = base64.b64encode(f.read()).decode('utf-8')
603
+ html_content += f"""
604
+ <div class="graph-container">
605
+ <img src="data:image/png;base64,{img_data}" alt="Objects referencing the target" style="max-width: 100%;">
606
+ </div>
607
+ <p><a href="file://{os.path.abspath(backrefs_graph_filename)}" target="_blank">Open full-size incoming references graph</a></p>
608
+ """
609
+ except Exception as e:
610
+ html_content += f"""
611
+ <div class="info-box warning">
612
+ <p>Could not embed incoming references graph image: {e}</p>
613
+ <p><a href="file://{os.path.abspath(backrefs_graph_filename)}" target="_blank">Open incoming references graph image</a></p>
614
+ </div>
615
+ """
616
+ else:
617
+ html_content += """
618
+ <div class="info-box warning">
619
+ <p>Could not generate incoming references graph visualization.</p>
620
+ </div>
621
+ """
622
+
623
+ html_content += """
624
+ </div>
625
+
626
+ <script>
627
+ function openTab(evt, tabName) {
628
+ var i, tabcontent, tablinks;
629
+ tabcontent = document.getElementsByClassName("tabcontent");
630
+ for (i = 0; i < tabcontent.length; i++) {
631
+ tabcontent[i].style.display = "none";
632
+ }
633
+ tablinks = document.getElementsByClassName("tablinks");
634
+ for (i = 0; i < tablinks.length; i++) {
635
+ tablinks[i].className = tablinks[i].className.replace(" active", "");
636
+ }
637
+ document.getElementById(tabName).style.display = "block";
638
+ evt.currentTarget.className += " active";
639
+ }
640
+
641
+ function toggleDetails(id) {
642
+ var element = document.getElementById(id);
643
+ if (element.style.display === "none") {
644
+ element.style.display = "block";
645
+ } else {
646
+ element.style.display = "none";
647
+ }
648
+ }
649
+
650
+ // Open the default tab
651
+ document.getElementById("defaultOpen").click();
652
+ </script>
653
+ </body>
654
+ </html>
655
+ """
656
+
657
+ # Write HTML to file
658
+ with open(html_filename, 'w') as f:
659
+ f.write(html_content)
660
+
661
+ return html_filename, refs_graph_filename, backrefs_graph_filename
662
+
663
+ def debug_memory(self, prefix: str = "", open_browser: bool = True, output_dir: str = None) -> str:
664
+ """
665
+ Comprehensive memory debugging that writes results to files and opens an HTML report.
666
+
667
+ Args:
668
+ prefix: Optional prefix for output files. If empty, uses target object type.
669
+ open_browser: Whether to automatically open the HTML report in a browser.
670
+ output_dir: Optional directory to write files to. If None, uses EDSL_MEMORY_DEBUG_DIR
671
+ environment variable or current directory.
672
+
673
+ Returns:
674
+ The path to the HTML report file.
675
+ """
676
+ # Generate HTML report with both incoming and outgoing reference visualizations
677
+ html_filename, outgoing_refs_filename, incoming_refs_filename = self._generate_html_report(prefix, output_dir)
678
+
679
+ # Also create the legacy markdown report for backward compatibility
680
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
681
+ if not prefix:
682
+ prefix = type(self.target_obj).__name__.lower()
683
+
684
+ # Determine output directory
685
+ if output_dir is None:
686
+ output_dir = os.environ.get("EDSL_MEMORY_DEBUG_DIR", "")
687
+
688
+ # Write reference analysis to markdown file
689
+ md_filename = os.path.join(output_dir, f"{prefix}_memory_debug_{timestamp}.md") if output_dir else f"{prefix}_memory_debug_{timestamp}.md"
690
+ with open(md_filename, 'w') as f:
691
+ f.write(f"# Memory Debug Report for {type(self.target_obj)}\n\n")
692
+ f.write(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
693
+ f.write(f"HTML Report: [View HTML Report]({html_filename})\n\n")
694
+ f.write(f"Visualizations:\n")
695
+ f.write(f"- [Outgoing References Graph]({outgoing_refs_filename}) - Objects referenced by the target\n")
696
+ f.write(f"- [Incoming References Graph]({incoming_refs_filename}) - Objects that reference the target\n\n")
697
+
698
+ # Capture reference count
699
+ f.write(f"## Reference Count\n")
700
+ f.write(f"Current reference count: {sys.getrefcount(self.target_obj)}\n\n")
701
+
702
+ # Capture referrers
703
+ f.write(f"## Objects Referring to Target\n")
704
+ referrers = gc.get_referrers(self.target_obj)
705
+ for ref in referrers:
706
+ if isinstance(ref, (types.FrameType, types.FunctionType)):
707
+ continue
708
+ f.write(f"- Type: {type(ref)}\n")
709
+
710
+ # Capture reference cycles
711
+ f.write(f"\n## Reference Cycles\n")
712
+
713
+ # Get all reference cycle information (capture stdout for the detailed info)
714
+ import io
715
+ from contextlib import redirect_stdout
716
+
717
+ captured_output = io.StringIO()
718
+ with redirect_stdout(captured_output):
719
+ cycles = self.detect_reference_cycles()
720
+
721
+ # Write the cycle information to file
722
+ cycle_output = captured_output.getvalue()
723
+ if cycles:
724
+ f.write(f"Found {len(cycles)} potential reference cycles:\n")
725
+ for obj in cycles:
726
+ f.write(f"- Type: {type(obj)}, ID: {id(obj)}\n")
727
+ else:
728
+ f.write("No reference cycles detected among hashable objects\n")
729
+
730
+ # Add the detailed unhashable information
731
+ f.write("\n## Unhashable Object Analysis\n")
732
+ f.write(cycle_output)
733
+
734
+ # Open the HTML report in a browser if requested
735
+ if open_browser:
736
+ try:
737
+ webbrowser.open(f"file://{os.path.abspath(html_filename)}")
738
+ print(f"Opened memory report in browser: {html_filename}")
739
+ except Exception as e:
740
+ print(f"Could not open browser: {e}")
741
+ print(f"Report saved to: {html_filename}")
742
+ else:
743
+ print(f"HTML Report saved to: {html_filename}")
744
+
745
+ return html_filename
746
+
747
+ def inspect_references(self, skip_frames: bool = True) -> None:
748
+ """
749
+ Inspect what objects are referring to the target object.
750
+
751
+ Args:
752
+ skip_frames: If True, skip function frames and local namespaces
753
+ """
754
+ print(f"\nReference count for {type(self.target_obj)}: {sys.getrefcount(self.target_obj)}")
755
+ print("\nObjects referring to this object:")
756
+
757
+ referrers = gc.get_referrers(self.target_obj)
758
+ for ref in referrers:
759
+ # Skip frames and function locals if requested
760
+ if skip_frames and (isinstance(ref, (types.FrameType, types.FunctionType)) or
761
+ (isinstance(ref, dict) and ref.get('target_obj') is self.target_obj)):
762
+ continue
763
+
764
+ print(f"\nType: {type(ref)}")
765
+
766
+ if isinstance(ref, dict):
767
+ self._inspect_dict_reference(ref)
768
+ elif isinstance(ref, list):
769
+ self._inspect_list_reference(ref)
770
+ elif isinstance(ref, tuple):
771
+ self._inspect_tuple_reference(ref)
772
+ else:
773
+ print(f" - {ref}")
774
+
775
+ def detect_reference_cycles(self) -> Set[Any]:
776
+ """
777
+ Detect potential reference cycles involving the target object.
778
+
779
+ Returns:
780
+ Set of objects that are part of potential reference cycles
781
+ """
782
+ referrers = gc.get_referrers(self.target_obj)
783
+ referents = gc.get_referents(self.target_obj)
784
+
785
+ # Separate hashable and unhashable objects
786
+ hashable_referrers = []
787
+ unhashable_referrers = []
788
+ hashable_referents = []
789
+ unhashable_referents = []
790
+
791
+ for obj in referrers:
792
+ if self._is_hashable(obj):
793
+ hashable_referrers.append(obj)
794
+ else:
795
+ unhashable_referrers.append(obj)
796
+
797
+ for obj in referents:
798
+ if self._is_hashable(obj):
799
+ hashable_referents.append(obj)
800
+ else:
801
+ unhashable_referents.append(obj)
802
+
803
+ # Find common objects among hashable ones
804
+ common_objects = set(hashable_referrers) & set(hashable_referents)
805
+
806
+ if common_objects:
807
+ print(f"Potential reference cycle detected! Found {len(common_objects)} common objects")
808
+ for shared_obj in common_objects:
809
+ print(f"Type: {type(shared_obj)}, ID: {id(shared_obj)}")
810
+
811
+ # Report on unhashable objects
812
+ if unhashable_referrers or unhashable_referents:
813
+ print(f"Note: {len(unhashable_referrers)} unhashable referrers and {len(unhashable_referents)} unhashable referents were excluded from cycle detection")
814
+
815
+ # Check for unhashable objects that might be in both lists
816
+ potential_unhashable_cycles = []
817
+ for ureferrer in unhashable_referrers:
818
+ for ureferent in unhashable_referents:
819
+ if id(ureferrer) == id(ureferent):
820
+ potential_unhashable_cycles.append(ureferrer)
821
+
822
+ if potential_unhashable_cycles:
823
+ print(f"Warning: Found {len(potential_unhashable_cycles)} unhashable objects that may be part of cycles:")
824
+ for obj in potential_unhashable_cycles:
825
+ print(f" - Type: {type(obj)}, ID: {id(obj)}")
826
+
827
+ # Report details of unhashable objects for further investigation
828
+ print("Unhashable referrers:")
829
+ for i, obj in enumerate(unhashable_referrers[:5]): # Limit to first 5 to avoid flooding output
830
+ print(f" - [{i}] Type: {type(obj)}, ID: {id(obj)}")
831
+ if len(unhashable_referrers) > 5:
832
+ print(f" ... and {len(unhashable_referrers) - 5} more")
833
+
834
+ print("Unhashable referents:")
835
+ for i, obj in enumerate(unhashable_referents[:5]): # Limit to first 5
836
+ print(f" - [{i}] Type: {type(obj)}, ID: {id(obj)}")
837
+ if len(unhashable_referents) > 5:
838
+ print(f" ... and {len(unhashable_referents) - 5} more")
839
+
840
+ # Add unhashable objects that might be in cycles to the full result list
841
+ result_set = common_objects.copy()
842
+ return result_set
843
+
844
+ def _is_hashable(self, obj: Any) -> bool:
845
+ """Helper method to check if an object is hashable."""
846
+ try:
847
+ hash(obj)
848
+ return True
849
+ except TypeError:
850
+ return False
851
+
852
+ def visualize_dependencies(self, max_depth: int = 3, output_dir: str = None, prefix: str = "") -> Tuple[str, str]:
853
+ """
854
+ Visualize object dependencies using objgraph.
855
+
856
+ Args:
857
+ max_depth: Maximum depth of reference tree to display
858
+ output_dir: Directory to save visualization files
859
+ prefix: Prefix for output files
860
+
861
+ Returns:
862
+ Tuple of (outgoing_refs_filename, incoming_refs_filename)
863
+ """
864
+ if not OBJGRAPH_AVAILABLE:
865
+ print("Warning: objgraph package is not available. Install it with 'pip install objgraph' to enable visualizations.")
866
+ return "", ""
867
+
868
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
869
+ if not prefix:
870
+ prefix = type(self.target_obj).__name__.lower()
871
+
872
+ # Determine output directory
873
+ if output_dir is None:
874
+ output_dir = os.environ.get("EDSL_MEMORY_DEBUG_DIR", "")
875
+
876
+ if output_dir:
877
+ # Ensure directory exists
878
+ os.makedirs(output_dir, exist_ok=True)
879
+
880
+ # Prepare filenames
881
+ refs_filename = os.path.join(output_dir, f"{prefix}_outgoing_refs_{timestamp}.png") if output_dir else f"{prefix}_outgoing_refs_{timestamp}.png"
882
+ backrefs_filename = os.path.join(output_dir, f"{prefix}_incoming_refs_{timestamp}.png") if output_dir else f"{prefix}_incoming_refs_{timestamp}.png"
883
+
884
+ try:
885
+ # Filter out frames and functions
886
+ ignore_ids = [id(obj) for obj in gc.get_objects()
887
+ if isinstance(obj, (types.FrameType, types.FunctionType))]
888
+
889
+ # For the regular references (what the object references)
890
+ objgraph.show_refs(
891
+ self.target_obj,
892
+ max_depth=max_depth,
893
+ filename=refs_filename,
894
+ extra_ignore=ignore_ids
895
+ )
896
+
897
+ # For the referrers (what references the object)
898
+ objgraph.show_backrefs(
899
+ self.target_obj,
900
+ max_depth=max_depth,
901
+ filename=backrefs_filename,
902
+ extra_ignore=ignore_ids
903
+ )
904
+
905
+ print(f"Outgoing references saved to: {refs_filename}")
906
+ print(f"Incoming references saved to: {backrefs_filename}")
907
+
908
+ return refs_filename, backrefs_filename
909
+ except Exception as e:
910
+ print(f"Error visualizing dependencies: {e}")
911
+ return "", ""
912
+
913
+ def _inspect_dict_reference(self, ref: Dict) -> None:
914
+ """Helper method to inspect dictionary references."""
915
+ for k, v in ref.items():
916
+ if v is self.target_obj:
917
+ print(f" - Found in dict with key: {k}")
918
+ try:
919
+ owner = [o for o in gc.get_referrers(ref)
920
+ if hasattr(o, '__dict__') and o.__dict__ is ref]
921
+ if owner:
922
+ print(f" (This dict belongs to: {type(owner[0])})")
923
+ except:
924
+ pass
925
+
926
+ def _inspect_list_reference(self, ref: List) -> None:
927
+ """Helper method to inspect list references."""
928
+ try:
929
+ idx = ref.index(self.target_obj)
930
+ print(f" - Found in list at index: {idx}")
931
+ owners = [o for o in gc.get_referrers(ref) if hasattr(o, '__dict__')]
932
+ if owners:
933
+ print(f" (This list belongs to: {type(owners[0])})")
934
+ except ValueError:
935
+ print(" - Found in list (as part of a larger structure)")
936
+
937
+ def _inspect_tuple_reference(self, ref: tuple) -> None:
938
+ """Helper method to inspect tuple references."""
939
+ try:
940
+ idx = ref.index(self.target_obj)
941
+ print(f" - Found in tuple at index: {idx}")
942
+ except ValueError:
943
+ print(" - Found in tuple (as part of a larger structure)")
944
+
945
+ def find_reference_paths(self, max_depth: int = 5) -> None:
946
+ """
947
+ Find and print paths to objects that reference the target object.
948
+ This is a simplified version that directly prints the results.
949
+
950
+ Args:
951
+ max_depth: Maximum depth to search for references
952
+ """
953
+ print(f"\nFinding reference paths to {type(self.target_obj)} (id: {id(self.target_obj)}):")
954
+
955
+ # Create a simplified reference chain explorer
956
+ def find_path_to_referrers(obj, path=None, depth=0, visited=None):
957
+ if path is None:
958
+ path = []
959
+ if visited is None:
960
+ visited = set()
961
+
962
+ if depth > max_depth:
963
+ return
964
+
965
+ # Get referrers excluding frames and functions
966
+ referrers = [ref for ref in gc.get_referrers(obj)
967
+ if not isinstance(ref, (types.FrameType, types.FunctionType))]
968
+
969
+ for ref in referrers:
970
+ ref_id = id(ref)
971
+
972
+ # Skip if we've seen this object already
973
+ if ref_id in visited:
974
+ continue
975
+
976
+ visited.add(ref_id)
977
+
978
+ # Print current path
979
+ ref_type = type(ref).__name__
980
+ current_path = path + [(ref_type, ref_id)]
981
+
982
+ # Print the path
983
+ path_str = " -> ".join([f"{t} (id:{i})" for t, i in current_path])
984
+ print(f"{' ' * depth}• {ref_type} references {type(obj).__name__}")
985
+
986
+ # If it's a container, try to find the specific reference
987
+ if isinstance(ref, dict):
988
+ for k, v in ref.items():
989
+ if v is obj:
990
+ print(f"{' ' * (depth+2)}(via dict key: {k})")
991
+ elif isinstance(ref, (list, tuple)):
992
+ try:
993
+ idx = ref.index(obj)
994
+ print(f"{' ' * (depth+2)}(via {type(ref).__name__} index: {idx})")
995
+ except (ValueError, TypeError):
996
+ print(f"{' ' * (depth+2)}(as part of a larger structure)")
997
+
998
+ # Look for owners of this container
999
+ if isinstance(ref, dict):
1000
+ owners = [o for o in gc.get_referrers(ref)
1001
+ if hasattr(o, '__dict__') and o.__dict__ is ref]
1002
+ if owners:
1003
+ owner = owners[0]
1004
+ print(f"{' ' * (depth+2)}(dict belongs to: {type(owner).__name__} id:{id(owner)})")
1005
+
1006
+ # Continue recursion with increased depth
1007
+ find_path_to_referrers(ref, current_path, depth + 1, visited)
1008
+
1009
+ # Start the recursive search
1010
+ find_path_to_referrers(self.target_obj)