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.
- scitex/__init__.py +47 -0
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +191 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +244 -0
- scitex/_mcp_tools/writer.py +21 -204
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +127 -59
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/engines/elevenlabs_engine.py +6 -1
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +443 -0
- scitex/cli/main.py +198 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/scholar/__init__.py +8 -0
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/writer.py +117 -0
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +191 -0
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/introspect/__init__.py +75 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +42 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/introspect/_mcp/__init__.py +37 -0
- scitex/introspect/_mcp/handlers.py +208 -0
- scitex/introspect/_members.py +151 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/bundle/README.md +1 -1
- scitex/mcp_server.py +98 -5
- scitex/plt/__init__.py +248 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/sh/README.md +1 -1
- scitex/social/__init__.py +153 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/template/README.md +1 -1
- scitex/template/clone_writer_directory.py +5 -5
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.1.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
- {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
|
+
}
|