scitex 2.14.0__py3-none-any.whl → 2.15.1__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 (218) hide show
  1. scitex/__init__.py +47 -0
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +191 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/writer.py +21 -204
  17. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  18. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  19. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  20. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  21. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  22. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  23. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  24. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  25. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  26. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  27. scitex/audio/README.md +40 -36
  28. scitex/audio/__init__.py +127 -59
  29. scitex/audio/_branding.py +185 -0
  30. scitex/audio/_mcp/__init__.py +32 -0
  31. scitex/audio/_mcp/handlers.py +59 -6
  32. scitex/audio/_mcp/speak_handlers.py +238 -0
  33. scitex/audio/_relay.py +225 -0
  34. scitex/audio/engines/elevenlabs_engine.py +6 -1
  35. scitex/audio/mcp_server.py +228 -75
  36. scitex/canvas/README.md +1 -1
  37. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  38. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  39. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  40. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  41. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  42. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  43. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  44. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  45. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  46. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  47. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  48. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  49. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  50. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  51. scitex/cli/__init__.py +38 -43
  52. scitex/cli/audio.py +76 -27
  53. scitex/cli/capture.py +13 -20
  54. scitex/cli/introspect.py +443 -0
  55. scitex/cli/main.py +198 -109
  56. scitex/cli/mcp.py +60 -34
  57. scitex/cli/scholar/__init__.py +8 -0
  58. scitex/cli/scholar/_crossref_scitex.py +296 -0
  59. scitex/cli/scholar/_fetch.py +25 -3
  60. scitex/cli/social.py +314 -0
  61. scitex/cli/writer.py +117 -0
  62. scitex/config/README.md +1 -1
  63. scitex/config/__init__.py +16 -2
  64. scitex/config/_env_registry.py +191 -0
  65. scitex/diagram/__init__.py +42 -19
  66. scitex/diagram/mcp_server.py +13 -125
  67. scitex/introspect/__init__.py +75 -0
  68. scitex/introspect/_call_graph.py +303 -0
  69. scitex/introspect/_class_hierarchy.py +163 -0
  70. scitex/introspect/_core.py +42 -0
  71. scitex/introspect/_docstring.py +131 -0
  72. scitex/introspect/_examples.py +113 -0
  73. scitex/introspect/_imports.py +271 -0
  74. scitex/introspect/_mcp/__init__.py +37 -0
  75. scitex/introspect/_mcp/handlers.py +208 -0
  76. scitex/introspect/_members.py +151 -0
  77. scitex/introspect/_resolve.py +89 -0
  78. scitex/introspect/_signature.py +131 -0
  79. scitex/introspect/_source.py +80 -0
  80. scitex/introspect/_type_hints.py +172 -0
  81. scitex/io/bundle/README.md +1 -1
  82. scitex/mcp_server.py +98 -5
  83. scitex/plt/__init__.py +248 -550
  84. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  85. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  86. scitex/plt/gallery/README.md +1 -1
  87. scitex/plt/utils/_hitmap/__init__.py +82 -0
  88. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  89. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  90. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  91. scitex/plt/utils/_hitmap/_constants.py +40 -0
  92. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  93. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  94. scitex/plt/utils/_hitmap/_query.py +113 -0
  95. scitex/plt/utils/_hitmap.py +46 -1616
  96. scitex/plt/utils/_metadata/__init__.py +80 -0
  97. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  98. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  99. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  100. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  101. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  102. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  103. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  104. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  105. scitex/plt/utils/_metadata/_csv.py +416 -0
  106. scitex/plt/utils/_metadata/_detect.py +225 -0
  107. scitex/plt/utils/_metadata/_legend.py +127 -0
  108. scitex/plt/utils/_metadata/_rounding.py +117 -0
  109. scitex/plt/utils/_metadata/_verification.py +202 -0
  110. scitex/schema/README.md +1 -1
  111. scitex/scholar/__init__.py +8 -0
  112. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  113. scitex/scholar/core/Scholar.py +63 -1700
  114. scitex/scholar/core/_mixins/__init__.py +36 -0
  115. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  116. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  117. scitex/scholar/core/_mixins/_loaders.py +103 -0
  118. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  119. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  120. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  121. scitex/scholar/core/_mixins/_savers.py +69 -0
  122. scitex/scholar/core/_mixins/_search.py +103 -0
  123. scitex/scholar/core/_mixins/_services.py +88 -0
  124. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  125. scitex/scholar/crossref_scitex.py +367 -0
  126. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  127. scitex/scholar/examples/00_run_all.sh +120 -0
  128. scitex/scholar/jobs/_executors.py +27 -3
  129. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  130. scitex/scholar/pdf_download/_cli.py +154 -0
  131. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  132. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  133. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  134. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  135. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  136. scitex/scholar/pipelines/_single_steps.py +71 -36
  137. scitex/scholar/storage/_LibraryManager.py +97 -1695
  138. scitex/scholar/storage/_mixins/__init__.py +30 -0
  139. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  140. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  141. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  142. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  143. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  144. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  145. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  146. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  147. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  148. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  149. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  150. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  151. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  152. scitex/security/README.md +3 -3
  153. scitex/session/README.md +1 -1
  154. scitex/sh/README.md +1 -1
  155. scitex/social/__init__.py +153 -0
  156. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  157. scitex/template/README.md +1 -1
  158. scitex/template/clone_writer_directory.py +5 -5
  159. scitex/writer/README.md +1 -1
  160. scitex/writer/_mcp/handlers.py +11 -744
  161. scitex/writer/_mcp/tool_schemas.py +5 -335
  162. scitex-2.15.1.dist-info/METADATA +648 -0
  163. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
  164. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  165. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  166. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  167. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  168. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  169. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  170. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  171. scitex/diagram/_compile.py +0 -312
  172. scitex/diagram/_diagram.py +0 -355
  173. scitex/diagram/_mcp/__init__.py +0 -4
  174. scitex/diagram/_mcp/handlers.py +0 -400
  175. scitex/diagram/_mcp/tool_schemas.py +0 -157
  176. scitex/diagram/_presets.py +0 -173
  177. scitex/diagram/_schema.py +0 -182
  178. scitex/diagram/_split.py +0 -278
  179. scitex/plt/_mcp/__init__.py +0 -4
  180. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  181. scitex/plt/_mcp/_handlers_figure.py +0 -195
  182. scitex/plt/_mcp/_handlers_plot.py +0 -252
  183. scitex/plt/_mcp/_handlers_style.py +0 -219
  184. scitex/plt/_mcp/handlers.py +0 -74
  185. scitex/plt/_mcp/tool_schemas.py +0 -497
  186. scitex/plt/mcp_server.py +0 -231
  187. scitex/scholar/data/.gitkeep +0 -0
  188. scitex/scholar/data/README.md +0 -44
  189. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  190. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  191. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  192. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  193. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  194. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  195. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  196. scitex/scholar/data/bib_files/pac.bib +0 -698
  197. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  198. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  199. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  200. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  201. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  202. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  203. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  204. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  205. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  206. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  207. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  208. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  209. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  210. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  211. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  212. scitex/scholar/data/impact_factor.db +0 -0
  213. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  214. scitex/scholar/examples/dev.py +0 -38
  215. scitex-2.14.0.dist-info/METADATA +0 -1238
  216. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
  217. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
  218. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-01-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_call_graph.py
4
+
5
+ """Call graph analysis using AST with timeout protection."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ import inspect
11
+ import signal
12
+ from contextlib import contextmanager
13
+ from pathlib import Path
14
+
15
+ from ._resolve import get_type_info, resolve_object
16
+
17
+
18
+ class TimeoutError(Exception):
19
+ """Raised when operation times out."""
20
+
21
+ pass
22
+
23
+
24
+ @contextmanager
25
+ def timeout(seconds: int):
26
+ """Context manager for timeout (Unix only)."""
27
+
28
+ def handler(signum, frame):
29
+ raise TimeoutError(f"Operation timed out after {seconds}s")
30
+
31
+ # Only works on Unix
32
+ try:
33
+ old_handler = signal.signal(signal.SIGALRM, handler)
34
+ signal.alarm(seconds)
35
+ try:
36
+ yield
37
+ finally:
38
+ signal.alarm(0)
39
+ signal.signal(signal.SIGALRM, old_handler)
40
+ except (ValueError, AttributeError):
41
+ # Windows or signal not available - no timeout
42
+ yield
43
+
44
+
45
+ def get_call_graph(
46
+ dotted_path: str,
47
+ max_depth: int = 2,
48
+ timeout_seconds: int = 10,
49
+ internal_only: bool = True,
50
+ ) -> dict:
51
+ """
52
+ Get the call graph of a function or module using static AST analysis.
53
+
54
+ Parameters
55
+ ----------
56
+ dotted_path : str
57
+ Dotted path to the function or module
58
+ max_depth : int
59
+ Maximum depth to traverse calls
60
+ timeout_seconds : int
61
+ Timeout in seconds (0 = no timeout)
62
+ internal_only : bool
63
+ Only show calls to functions in the same module
64
+
65
+ Returns
66
+ -------
67
+ dict
68
+ calls: list[dict] - Functions this function calls
69
+ called_by: list[dict] - Functions that call this (if module)
70
+ graph: dict - Full call graph tree
71
+
72
+ Examples
73
+ --------
74
+ >>> get_call_graph("scitex.audio.speak")
75
+ """
76
+ try:
77
+ if timeout_seconds > 0:
78
+ with timeout(timeout_seconds):
79
+ return _analyze_call_graph(dotted_path, max_depth, internal_only)
80
+ else:
81
+ return _analyze_call_graph(dotted_path, max_depth, internal_only)
82
+ except TimeoutError as e:
83
+ return {
84
+ "success": False,
85
+ "error": str(e),
86
+ "partial": True,
87
+ }
88
+
89
+
90
+ def _analyze_call_graph(
91
+ dotted_path: str,
92
+ max_depth: int,
93
+ internal_only: bool,
94
+ ) -> dict:
95
+ """Perform the actual call graph analysis."""
96
+ obj, error = resolve_object(dotted_path)
97
+ if error:
98
+ return {"success": False, "error": error}
99
+
100
+ type_info = get_type_info(obj)
101
+
102
+ # Get source file
103
+ try:
104
+ source_file = inspect.getfile(obj)
105
+ source = Path(source_file).read_text()
106
+ tree = ast.parse(source)
107
+ except Exception as e:
108
+ return {
109
+ "success": False,
110
+ "error": f"Cannot parse source: {e}",
111
+ "type_info": type_info,
112
+ }
113
+
114
+ # Build function index for the module
115
+ func_index = _build_function_index(tree)
116
+
117
+ if inspect.isfunction(obj):
118
+ # Analyze single function
119
+ func_name = obj.__name__
120
+ if func_name not in func_index:
121
+ return {
122
+ "success": False,
123
+ "error": f"Function '{func_name}' not found in source",
124
+ "type_info": type_info,
125
+ }
126
+
127
+ calls = _get_function_calls(func_index[func_name], internal_only, func_index)
128
+ called_by = _find_callers(func_name, func_index)
129
+
130
+ return {
131
+ "success": True,
132
+ "function": func_name,
133
+ "calls": calls,
134
+ "call_count": len(calls),
135
+ "called_by": called_by,
136
+ "caller_count": len(called_by),
137
+ "type_info": type_info,
138
+ }
139
+
140
+ elif inspect.ismodule(obj):
141
+ # Analyze entire module
142
+ graph = {}
143
+ for func_name, func_node in func_index.items():
144
+ calls = _get_function_calls(func_node, internal_only, func_index)
145
+ graph[func_name] = {
146
+ "calls": calls,
147
+ "line": func_node.lineno,
148
+ }
149
+
150
+ return {
151
+ "success": True,
152
+ "module": dotted_path,
153
+ "graph": graph,
154
+ "function_count": len(graph),
155
+ "type_info": type_info,
156
+ }
157
+
158
+ else:
159
+ return {
160
+ "success": False,
161
+ "error": "Can only analyze functions or modules",
162
+ "type_info": type_info,
163
+ }
164
+
165
+
166
+ def _build_function_index(tree: ast.AST) -> dict[str, ast.FunctionDef]:
167
+ """Build index of all functions in the AST."""
168
+ index = {}
169
+ for node in ast.walk(tree):
170
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
171
+ index[node.name] = node
172
+ return index
173
+
174
+
175
+ def _get_function_calls(
176
+ func_node: ast.FunctionDef,
177
+ internal_only: bool,
178
+ func_index: dict,
179
+ ) -> list[dict]:
180
+ """Extract all function calls from a function."""
181
+ calls = []
182
+ seen = set()
183
+
184
+ for node in ast.walk(func_node):
185
+ if isinstance(node, ast.Call):
186
+ call_info = _extract_call_info(node)
187
+ if call_info and call_info["name"] not in seen:
188
+ # Filter to internal only if requested
189
+ if internal_only and call_info["name"] not in func_index:
190
+ continue
191
+ seen.add(call_info["name"])
192
+ calls.append(call_info)
193
+
194
+ return calls
195
+
196
+
197
+ def _extract_call_info(node: ast.Call) -> dict | None:
198
+ """Extract information about a function call."""
199
+ func = node.func
200
+
201
+ if isinstance(func, ast.Name):
202
+ # Simple call: func()
203
+ return {
204
+ "name": func.id,
205
+ "type": "function",
206
+ "line": node.lineno,
207
+ }
208
+ elif isinstance(func, ast.Attribute):
209
+ # Method call: obj.method()
210
+ if isinstance(func.value, ast.Name):
211
+ return {
212
+ "name": f"{func.value.id}.{func.attr}",
213
+ "type": "method",
214
+ "object": func.value.id,
215
+ "method": func.attr,
216
+ "line": node.lineno,
217
+ }
218
+ else:
219
+ return {
220
+ "name": func.attr,
221
+ "type": "method",
222
+ "method": func.attr,
223
+ "line": node.lineno,
224
+ }
225
+
226
+ return None
227
+
228
+
229
+ def _find_callers(
230
+ func_name: str,
231
+ func_index: dict[str, ast.FunctionDef],
232
+ ) -> list[dict]:
233
+ """Find all functions that call the given function."""
234
+ callers = []
235
+
236
+ for caller_name, caller_node in func_index.items():
237
+ if caller_name == func_name:
238
+ continue
239
+
240
+ for node in ast.walk(caller_node):
241
+ if isinstance(node, ast.Call):
242
+ call_info = _extract_call_info(node)
243
+ if call_info and call_info["name"] == func_name:
244
+ callers.append(
245
+ {
246
+ "name": caller_name,
247
+ "line": caller_node.lineno,
248
+ }
249
+ )
250
+ break
251
+
252
+ return callers
253
+
254
+
255
+ def get_function_calls(
256
+ dotted_path: str,
257
+ include_methods: bool = True,
258
+ include_builtins: bool = False,
259
+ ) -> dict:
260
+ """
261
+ Get just the outgoing calls from a function.
262
+
263
+ Simpler version of get_call_graph for quick lookup.
264
+
265
+ Parameters
266
+ ----------
267
+ dotted_path : str
268
+ Dotted path to the function
269
+ include_methods : bool
270
+ Include method calls (obj.method())
271
+ include_builtins : bool
272
+ Include builtin function calls
273
+
274
+ Returns
275
+ -------
276
+ dict
277
+ calls: list[str] - Names of called functions
278
+ """
279
+ result = get_call_graph(dotted_path, max_depth=1, internal_only=False)
280
+
281
+ if not result.get("success"):
282
+ return result
283
+
284
+ calls = result.get("calls", [])
285
+
286
+ # Filter
287
+ filtered = []
288
+ builtins = {"print", "len", "range", "str", "int", "float", "list", "dict", "set"}
289
+
290
+ for call in calls:
291
+ name = call["name"]
292
+ if not include_methods and call.get("type") == "method":
293
+ continue
294
+ if not include_builtins and name in builtins:
295
+ continue
296
+ filtered.append(name)
297
+
298
+ return {
299
+ "success": True,
300
+ "function": dotted_path,
301
+ "calls": filtered,
302
+ "call_count": len(filtered),
303
+ }
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-01-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_class_hierarchy.py
4
+
5
+ """Class hierarchy analysis utilities."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+
11
+ from ._resolve import get_type_info, resolve_object
12
+
13
+
14
+ def get_class_hierarchy(
15
+ dotted_path: str,
16
+ include_builtins: bool = False,
17
+ max_depth: int = 10,
18
+ ) -> dict:
19
+ """
20
+ Get the inheritance hierarchy of a class.
21
+
22
+ Shows both parent classes (MRO) and known subclasses.
23
+
24
+ Parameters
25
+ ----------
26
+ dotted_path : str
27
+ Dotted path to the class (e.g., 'pandas.DataFrame')
28
+ include_builtins : bool
29
+ Include builtin classes like object, type in hierarchy
30
+ max_depth : int
31
+ Maximum depth for subclass traversal
32
+
33
+ Returns
34
+ -------
35
+ dict
36
+ mro: list[str] - Method Resolution Order (parent classes)
37
+ subclasses: list[dict] - Known subclasses (recursive)
38
+ type_info: dict
39
+
40
+ Examples
41
+ --------
42
+ >>> get_class_hierarchy("collections.abc.Mapping")
43
+ """
44
+ obj, error = resolve_object(dotted_path)
45
+ if error:
46
+ return {"success": False, "error": error}
47
+
48
+ type_info = get_type_info(obj)
49
+
50
+ if not inspect.isclass(obj):
51
+ return {
52
+ "success": False,
53
+ "error": f"'{dotted_path}' is not a class",
54
+ "type_info": type_info,
55
+ }
56
+
57
+ # Get MRO (parent classes)
58
+ mro = []
59
+ for cls in inspect.getmro(obj):
60
+ if not include_builtins and cls.__module__ == "builtins":
61
+ continue
62
+ mro.append(
63
+ {
64
+ "name": cls.__name__,
65
+ "module": cls.__module__,
66
+ "qualname": f"{cls.__module__}.{cls.__name__}",
67
+ }
68
+ )
69
+
70
+ # Get subclasses recursively
71
+ subclasses = _get_subclasses_recursive(obj, max_depth, include_builtins)
72
+
73
+ return {
74
+ "success": True,
75
+ "class": dotted_path,
76
+ "mro": mro,
77
+ "mro_count": len(mro),
78
+ "subclasses": subclasses,
79
+ "subclass_count": _count_subclasses(subclasses),
80
+ "type_info": type_info,
81
+ }
82
+
83
+
84
+ def _get_subclasses_recursive(
85
+ cls: type,
86
+ max_depth: int,
87
+ include_builtins: bool,
88
+ current_depth: int = 0,
89
+ ) -> list[dict]:
90
+ """Recursively get all subclasses."""
91
+ if current_depth >= max_depth:
92
+ return []
93
+
94
+ result = []
95
+ try:
96
+ for sub in cls.__subclasses__():
97
+ if not include_builtins and sub.__module__ == "builtins":
98
+ continue
99
+
100
+ sub_info = {
101
+ "name": sub.__name__,
102
+ "module": sub.__module__,
103
+ "qualname": f"{sub.__module__}.{sub.__name__}",
104
+ }
105
+
106
+ children = _get_subclasses_recursive(
107
+ sub, max_depth, include_builtins, current_depth + 1
108
+ )
109
+ if children:
110
+ sub_info["subclasses"] = children
111
+
112
+ result.append(sub_info)
113
+ except Exception:
114
+ pass
115
+
116
+ return result
117
+
118
+
119
+ def _count_subclasses(subclasses: list[dict]) -> int:
120
+ """Count total subclasses including nested."""
121
+ count = len(subclasses)
122
+ for sub in subclasses:
123
+ if "subclasses" in sub:
124
+ count += _count_subclasses(sub["subclasses"])
125
+ return count
126
+
127
+
128
+ def get_mro(dotted_path: str, include_builtins: bool = False) -> dict:
129
+ """
130
+ Get just the Method Resolution Order (parent classes).
131
+
132
+ Simpler version of get_class_hierarchy for just parents.
133
+
134
+ Parameters
135
+ ----------
136
+ dotted_path : str
137
+ Dotted path to the class
138
+ include_builtins : bool
139
+ Include builtin classes
140
+
141
+ Returns
142
+ -------
143
+ dict
144
+ mro: list[str] - Qualified names in MRO order
145
+ """
146
+ obj, error = resolve_object(dotted_path)
147
+ if error:
148
+ return {"success": False, "error": error}
149
+
150
+ if not inspect.isclass(obj):
151
+ return {"success": False, "error": f"'{dotted_path}' is not a class"}
152
+
153
+ mro = []
154
+ for cls in inspect.getmro(obj):
155
+ if not include_builtins and cls.__module__ == "builtins":
156
+ continue
157
+ mro.append(f"{cls.__module__}.{cls.__name__}")
158
+
159
+ return {
160
+ "success": True,
161
+ "class": dotted_path,
162
+ "mro": mro,
163
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-01-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_core.py
4
+
5
+ """Core introspection module - re-exports all utilities."""
6
+
7
+ # Basic introspection
8
+ # Advanced introspection
9
+ from ._call_graph import get_call_graph, get_function_calls
10
+ from ._class_hierarchy import get_class_hierarchy, get_mro
11
+ from ._docstring import get_docstring
12
+ from ._examples import find_examples
13
+ from ._imports import get_dependencies, get_imports
14
+ from ._members import get_exports, list_members
15
+ from ._resolve import get_type_info, resolve_object
16
+ from ._signature import get_signature
17
+ from ._source import get_source
18
+ from ._type_hints import get_class_annotations, get_type_hints_detailed
19
+
20
+ __all__ = [
21
+ # Basic
22
+ "get_signature",
23
+ "get_docstring",
24
+ "get_source",
25
+ "list_members",
26
+ "get_exports",
27
+ "find_examples",
28
+ "resolve_object",
29
+ "get_type_info",
30
+ # Advanced - Class hierarchy
31
+ "get_class_hierarchy",
32
+ "get_mro",
33
+ # Advanced - Type hints
34
+ "get_type_hints_detailed",
35
+ "get_class_annotations",
36
+ # Advanced - Imports
37
+ "get_imports",
38
+ "get_dependencies",
39
+ # Advanced - Call graph
40
+ "get_call_graph",
41
+ "get_function_calls",
42
+ ]
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-01-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_docstring.py
4
+
5
+ """Docstring extraction and parsing utilities."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+ import re
11
+ from typing import Literal
12
+
13
+ from ._resolve import get_type_info, resolve_object
14
+
15
+
16
+ def _parse_docstring(docstring: str) -> dict:
17
+ """Parse numpy/google style docstring into sections."""
18
+ sections = {
19
+ "summary": "",
20
+ "description": "",
21
+ "parameters": "",
22
+ "returns": "",
23
+ "examples": "",
24
+ "notes": "",
25
+ }
26
+
27
+ if not docstring:
28
+ return sections
29
+
30
+ section_patterns = [
31
+ (r"Parameters?\s*\n[-=]+", "parameters"),
32
+ (r"Returns?\s*\n[-=]+", "returns"),
33
+ (r"Examples?\s*\n[-=]+", "examples"),
34
+ (r"Notes?\s*\n[-=]+", "notes"),
35
+ (r"Raises?\s*\n[-=]+", "raises"),
36
+ (r"See Also\s*\n[-=]+", "see_also"),
37
+ ]
38
+
39
+ lines = docstring.split("\n")
40
+
41
+ i = 0
42
+ summary_lines = []
43
+ while i < len(lines):
44
+ line = lines[i].strip()
45
+ if not line:
46
+ if summary_lines:
47
+ break
48
+ elif any(re.match(p, line, re.IGNORECASE) for p, _ in section_patterns):
49
+ break
50
+ else:
51
+ summary_lines.append(line)
52
+ i += 1
53
+
54
+ sections["summary"] = " ".join(summary_lines)
55
+
56
+ current_section = "description"
57
+ current_content = []
58
+
59
+ for line in lines[i:]:
60
+ matched = False
61
+ for pattern, section_name in section_patterns:
62
+ if re.match(pattern, line, re.IGNORECASE):
63
+ if current_content:
64
+ sections[current_section] = "\n".join(current_content).strip()
65
+ current_section = section_name
66
+ current_content = []
67
+ matched = True
68
+ break
69
+
70
+ if not matched:
71
+ current_content.append(line)
72
+
73
+ if current_content:
74
+ sections[current_section] = "\n".join(current_content).strip()
75
+
76
+ return sections
77
+
78
+
79
+ def get_docstring(
80
+ dotted_path: str,
81
+ format: Literal["raw", "parsed", "summary"] = "raw",
82
+ ) -> dict:
83
+ """
84
+ Get the docstring of a Python object.
85
+
86
+ Parameters
87
+ ----------
88
+ dotted_path : str
89
+ Dotted path to the object
90
+ format : str
91
+ 'raw' - Return full docstring as-is
92
+ 'parsed' - Parse numpy/google style into sections
93
+ 'summary' - Return only first line/paragraph
94
+
95
+ Returns
96
+ -------
97
+ dict
98
+ docstring: str
99
+ sections: dict (if format='parsed')
100
+ type_info: dict
101
+ """
102
+ obj, error = resolve_object(dotted_path)
103
+ if error:
104
+ return {"success": False, "error": error}
105
+
106
+ type_info = get_type_info(obj)
107
+ docstring = inspect.getdoc(obj) or ""
108
+
109
+ if format == "summary":
110
+ lines = docstring.split("\n\n")
111
+ summary = lines[0].strip() if lines else ""
112
+ return {
113
+ "success": True,
114
+ "docstring": summary,
115
+ "type_info": type_info,
116
+ }
117
+
118
+ if format == "parsed":
119
+ sections = _parse_docstring(docstring)
120
+ return {
121
+ "success": True,
122
+ "docstring": docstring,
123
+ "sections": sections,
124
+ "type_info": type_info,
125
+ }
126
+
127
+ return {
128
+ "success": True,
129
+ "docstring": docstring,
130
+ "type_info": type_info,
131
+ }