From 30ea3a55efb4bbc2fa5016fd20e977297c7c5eb1 Mon Sep 17 00:00:00 2001 From: Yusuke Watanabe Date: Mon, 27 Apr 2026 04:46:17 +0900 Subject: [PATCH] refactor(introspect): extract scitex.introspect into standalone scitex-introspect package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IPython-style introspection (q/qq/dir/list_api), signature/docstring/source inspection, members enumeration, static-analysis helpers (imports, call_graph, class_hierarchy, examples, resolve), MCP server now live in the standalone scitex-introspect package (https://github.com/ywatanabe1989/scitex-introspect). scitex.introspect/__init__.py becomes a sys.modules alias. The [introspect] extra is updated to depend on scitex-introspect>=0.1.0. Pure stdlib — zero deps. 76/77 tests pass (1 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 3 +- src/scitex/introspect/__init__.py | 86 +---- src/scitex/introspect/_call_graph.py | 303 ------------------ src/scitex/introspect/_class_hierarchy.py | 163 ---------- src/scitex/introspect/_core.py | 41 --- src/scitex/introspect/_docstring.py | 131 -------- src/scitex/introspect/_examples.py | 113 ------- src/scitex/introspect/_imports.py | 271 ---------------- src/scitex/introspect/_list_api.py | 280 ---------------- src/scitex/introspect/_mcp/__init__.py | 38 --- src/scitex/introspect/_mcp/handlers.py | 233 -------------- src/scitex/introspect/_members.py | 155 --------- src/scitex/introspect/_resolve.py | 89 ----- src/scitex/introspect/_signature.py | 131 -------- src/scitex/introspect/_skills/SKILL.md | 87 ----- src/scitex/introspect/_skills/call-graph.md | 176 ---------- .../introspect/_skills/class-hierarchy.md | 129 -------- .../_skills/docstring-and-exports.md | 211 ------------ .../_skills/imports-and-dependencies.md | 168 ---------- .../introspect/_skills/ipython-shortcuts.md | 224 ------------- src/scitex/introspect/_skills/mcp-tools.md | 147 --------- src/scitex/introspect/_skills/resolve.md | 116 ------- src/scitex/introspect/_skills/type-hints.md | 149 --------- src/scitex/introspect/_source.py | 80 ----- src/scitex/introspect/_type_hints.py | 172 ---------- tests/scitex/introspect/test__call_graph.py | 82 ----- .../introspect/test__class_hierarchy.py | 91 ------ tests/scitex/introspect/test__docstring.py | 64 ---- tests/scitex/introspect/test__examples.py | 63 ---- tests/scitex/introspect/test__imports.py | 90 ------ tests/scitex/introspect/test__members.py | 121 ------- tests/scitex/introspect/test__resolve.py | 99 ------ tests/scitex/introspect/test__signature.py | 64 ---- tests/scitex/introspect/test__source.py | 76 ----- tests/scitex/introspect/test__type_hints.py | 82 ----- 35 files changed, 17 insertions(+), 4511 deletions(-) delete mode 100755 src/scitex/introspect/_call_graph.py delete mode 100755 src/scitex/introspect/_class_hierarchy.py delete mode 100755 src/scitex/introspect/_core.py delete mode 100755 src/scitex/introspect/_docstring.py delete mode 100755 src/scitex/introspect/_examples.py delete mode 100755 src/scitex/introspect/_imports.py delete mode 100755 src/scitex/introspect/_list_api.py delete mode 100755 src/scitex/introspect/_mcp/__init__.py delete mode 100755 src/scitex/introspect/_mcp/handlers.py delete mode 100755 src/scitex/introspect/_members.py delete mode 100755 src/scitex/introspect/_resolve.py delete mode 100755 src/scitex/introspect/_signature.py delete mode 100644 src/scitex/introspect/_skills/SKILL.md delete mode 100644 src/scitex/introspect/_skills/call-graph.md delete mode 100644 src/scitex/introspect/_skills/class-hierarchy.md delete mode 100644 src/scitex/introspect/_skills/docstring-and-exports.md delete mode 100644 src/scitex/introspect/_skills/imports-and-dependencies.md delete mode 100644 src/scitex/introspect/_skills/ipython-shortcuts.md delete mode 100644 src/scitex/introspect/_skills/mcp-tools.md delete mode 100644 src/scitex/introspect/_skills/resolve.md delete mode 100644 src/scitex/introspect/_skills/type-hints.md delete mode 100755 src/scitex/introspect/_source.py delete mode 100755 src/scitex/introspect/_type_hints.py delete mode 100755 tests/scitex/introspect/test__call_graph.py delete mode 100755 tests/scitex/introspect/test__class_hierarchy.py delete mode 100755 tests/scitex/introspect/test__docstring.py delete mode 100755 tests/scitex/introspect/test__examples.py delete mode 100755 tests/scitex/introspect/test__imports.py delete mode 100755 tests/scitex/introspect/test__members.py delete mode 100755 tests/scitex/introspect/test__resolve.py delete mode 100755 tests/scitex/introspect/test__signature.py delete mode 100755 tests/scitex/introspect/test__source.py delete mode 100755 tests/scitex/introspect/test__type_hints.py diff --git a/pyproject.toml b/pyproject.toml index bdfecd610..3707ddf00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -412,7 +412,8 @@ io = [ # Introspect Module - Code introspection # Use: pip install scitex[introspect] -introspect = [] +# Real implementation lives in the standalone scitex-introspect package. +introspect = ["scitex-introspect>=0.1.0"] # Linalg Module - Linear algebra # Use: pip install scitex[linalg] diff --git a/src/scitex/introspect/__init__.py b/src/scitex/introspect/__init__.py index 0348f4246..dceb18576 100755 --- a/src/scitex/introspect/__init__.py +++ b/src/scitex/introspect/__init__.py @@ -1,76 +1,20 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/__init__.py +"""SciTeX introspect — thin compatibility shim for scitex-introspect. -""" -Introspection utilities for Python packages. - -Provides IPython-like introspection capabilities for any Python package. - -IPython-style shortcuts: -- q: Function/class signature with type hints (like `func?`) -- qq: Full source code (like `func??`) -- dir: List attributes/methods (like `dir()`) -- list_api: Recursive module API tree +Aliases ``scitex.introspect`` to the standalone ``scitex_introspect`` package +via ``sys.modules``. ``scitex.introspect is scitex_introspect``. -Basic Introspection: -- get_docstring: Docstring extraction with parsing -- get_exports: Module's __all__ contents -- find_examples: Find usage examples in tests/examples - -Advanced Introspection: -- get_class_hierarchy: Inheritance tree (MRO + subclasses) -- get_mro: Method Resolution Order only -- get_type_hints_detailed: Detailed type annotation analysis -- get_class_annotations: Class variable and method annotations -- get_imports: Static import analysis using AST -- get_dependencies: Module dependency analysis -- get_call_graph: Function call graph (with timeout protection) -- get_function_calls: Simple outgoing calls list +Install: ``pip install scitex[introspect]`` (or ``pip install scitex-introspect``). +See: https://github.com/ywatanabe1989/scitex-introspect """ -from ._core import ( # IPython-style names; Basic; Advanced - Call graph; Advanced - Type hints; Advanced - Class hierarchy; Advanced - Imports - dir, - find_examples, - get_call_graph, - get_class_annotations, - get_class_hierarchy, - get_dependencies, - get_docstring, - get_exports, - get_function_calls, - get_imports, - get_mro, - get_type_hints_detailed, - get_type_info, - q, - qq, - resolve_object, -) -from ._list_api import list_api +import sys as _sys + +try: + import scitex_introspect as _real +except ImportError as _e: # pragma: no cover + raise ImportError( + "scitex.introspect requires the 'scitex-introspect' package. " + "Install with: pip install scitex[introspect] (or: pip install scitex-introspect)" + ) from _e -__all__ = [ - # IPython-style names - "q", - "qq", - "dir", - "list_api", - # Basic - "get_docstring", - "get_exports", - "find_examples", - "resolve_object", - "get_type_info", - # Advanced - Class hierarchy - "get_class_hierarchy", - "get_mro", - # Advanced - Type hints - "get_type_hints_detailed", - "get_class_annotations", - # Advanced - Imports - "get_imports", - "get_dependencies", - # Advanced - Call graph - "get_call_graph", - "get_function_calls", -] +_sys.modules[__name__] = _real diff --git a/src/scitex/introspect/_call_graph.py b/src/scitex/introspect/_call_graph.py deleted file mode 100755 index ca411d968..000000000 --- a/src/scitex/introspect/_call_graph.py +++ /dev/null @@ -1,303 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_call_graph.py - -"""Call graph analysis using AST with timeout protection.""" - -from __future__ import annotations - -import ast -import inspect -import signal -from contextlib import contextmanager -from pathlib import Path - -from ._resolve import get_type_info, resolve_object - - -class TimeoutError(Exception): - """Raised when operation times out.""" - - pass - - -@contextmanager -def timeout(seconds: int): - """Context manager for timeout (Unix only).""" - - def handler(signum, frame): - raise TimeoutError(f"Operation timed out after {seconds}s") - - # Only works on Unix - try: - old_handler = signal.signal(signal.SIGALRM, handler) - signal.alarm(seconds) - try: - yield - finally: - signal.alarm(0) - signal.signal(signal.SIGALRM, old_handler) - except (ValueError, AttributeError): - # Windows or signal not available - no timeout - yield - - -def get_call_graph( - dotted_path: str, - max_depth: int = 2, - timeout_seconds: int = 10, - internal_only: bool = True, -) -> dict: - """ - Get the call graph of a function or module using static AST analysis. - - Parameters - ---------- - dotted_path : str - Dotted path to the function or module - max_depth : int - Maximum depth to traverse calls - timeout_seconds : int - Timeout in seconds (0 = no timeout) - internal_only : bool - Only show calls to functions in the same module - - Returns - ------- - dict - calls: list[dict] - Functions this function calls - called_by: list[dict] - Functions that call this (if module) - graph: dict - Full call graph tree - - Examples - -------- - >>> get_call_graph("scitex.audio.speak") - """ - try: - if timeout_seconds > 0: - with timeout(timeout_seconds): - return _analyze_call_graph(dotted_path, max_depth, internal_only) - else: - return _analyze_call_graph(dotted_path, max_depth, internal_only) - except TimeoutError as e: - return { - "success": False, - "error": str(e), - "partial": True, - } - - -def _analyze_call_graph( - dotted_path: str, - max_depth: int, - internal_only: bool, -) -> dict: - """Perform the actual call graph analysis.""" - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - type_info = get_type_info(obj) - - # Get source file - try: - source_file = inspect.getfile(obj) - source = Path(source_file).read_text() - tree = ast.parse(source) - except Exception as e: - return { - "success": False, - "error": f"Cannot parse source: {e}", - "type_info": type_info, - } - - # Build function index for the module - func_index = _build_function_index(tree) - - if inspect.isfunction(obj): - # Analyze single function - func_name = obj.__name__ - if func_name not in func_index: - return { - "success": False, - "error": f"Function '{func_name}' not found in source", - "type_info": type_info, - } - - calls = _get_function_calls(func_index[func_name], internal_only, func_index) - called_by = _find_callers(func_name, func_index) - - return { - "success": True, - "function": func_name, - "calls": calls, - "call_count": len(calls), - "called_by": called_by, - "caller_count": len(called_by), - "type_info": type_info, - } - - elif inspect.ismodule(obj): - # Analyze entire module - graph = {} - for func_name, func_node in func_index.items(): - calls = _get_function_calls(func_node, internal_only, func_index) - graph[func_name] = { - "calls": calls, - "line": func_node.lineno, - } - - return { - "success": True, - "module": dotted_path, - "graph": graph, - "function_count": len(graph), - "type_info": type_info, - } - - else: - return { - "success": False, - "error": "Can only analyze functions or modules", - "type_info": type_info, - } - - -def _build_function_index(tree: ast.AST) -> dict[str, ast.FunctionDef]: - """Build index of all functions in the AST.""" - index = {} - for node in ast.walk(tree): - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - index[node.name] = node - return index - - -def _get_function_calls( - func_node: ast.FunctionDef, - internal_only: bool, - func_index: dict, -) -> list[dict]: - """Extract all function calls from a function.""" - calls = [] - seen = set() - - for node in ast.walk(func_node): - if isinstance(node, ast.Call): - call_info = _extract_call_info(node) - if call_info and call_info["name"] not in seen: - # Filter to internal only if requested - if internal_only and call_info["name"] not in func_index: - continue - seen.add(call_info["name"]) - calls.append(call_info) - - return calls - - -def _extract_call_info(node: ast.Call) -> dict | None: - """Extract information about a function call.""" - func = node.func - - if isinstance(func, ast.Name): - # Simple call: func() - return { - "name": func.id, - "type": "function", - "line": node.lineno, - } - elif isinstance(func, ast.Attribute): - # Method call: obj.method() - if isinstance(func.value, ast.Name): - return { - "name": f"{func.value.id}.{func.attr}", - "type": "method", - "object": func.value.id, - "method": func.attr, - "line": node.lineno, - } - else: - return { - "name": func.attr, - "type": "method", - "method": func.attr, - "line": node.lineno, - } - - return None - - -def _find_callers( - func_name: str, - func_index: dict[str, ast.FunctionDef], -) -> list[dict]: - """Find all functions that call the given function.""" - callers = [] - - for caller_name, caller_node in func_index.items(): - if caller_name == func_name: - continue - - for node in ast.walk(caller_node): - if isinstance(node, ast.Call): - call_info = _extract_call_info(node) - if call_info and call_info["name"] == func_name: - callers.append( - { - "name": caller_name, - "line": caller_node.lineno, - } - ) - break - - return callers - - -def get_function_calls( - dotted_path: str, - include_methods: bool = True, - include_builtins: bool = False, -) -> dict: - """ - Get just the outgoing calls from a function. - - Simpler version of get_call_graph for quick lookup. - - Parameters - ---------- - dotted_path : str - Dotted path to the function - include_methods : bool - Include method calls (obj.method()) - include_builtins : bool - Include builtin function calls - - Returns - ------- - dict - calls: list[str] - Names of called functions - """ - result = get_call_graph(dotted_path, max_depth=1, internal_only=False) - - if not result.get("success"): - return result - - calls = result.get("calls", []) - - # Filter - filtered = [] - builtins = {"print", "len", "range", "str", "int", "float", "list", "dict", "set"} - - for call in calls: - name = call["name"] - if not include_methods and call.get("type") == "method": - continue - if not include_builtins and name in builtins: - continue - filtered.append(name) - - return { - "success": True, - "function": dotted_path, - "calls": filtered, - "call_count": len(filtered), - } diff --git a/src/scitex/introspect/_class_hierarchy.py b/src/scitex/introspect/_class_hierarchy.py deleted file mode 100755 index 66d721880..000000000 --- a/src/scitex/introspect/_class_hierarchy.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_class_hierarchy.py - -"""Class hierarchy analysis utilities.""" - -from __future__ import annotations - -import inspect - -from ._resolve import get_type_info, resolve_object - - -def get_class_hierarchy( - dotted_path: str, - include_builtins: bool = False, - max_depth: int = 10, -) -> dict: - """ - Get the inheritance hierarchy of a class. - - Shows both parent classes (MRO) and known subclasses. - - Parameters - ---------- - dotted_path : str - Dotted path to the class (e.g., 'pandas.DataFrame') - include_builtins : bool - Include builtin classes like object, type in hierarchy - max_depth : int - Maximum depth for subclass traversal - - Returns - ------- - dict - mro: list[str] - Method Resolution Order (parent classes) - subclasses: list[dict] - Known subclasses (recursive) - type_info: dict - - Examples - -------- - >>> get_class_hierarchy("collections.abc.Mapping") - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - type_info = get_type_info(obj) - - if not inspect.isclass(obj): - return { - "success": False, - "error": f"'{dotted_path}' is not a class", - "type_info": type_info, - } - - # Get MRO (parent classes) - mro = [] - for cls in inspect.getmro(obj): - if not include_builtins and cls.__module__ == "builtins": - continue - mro.append( - { - "name": cls.__name__, - "module": cls.__module__, - "qualname": f"{cls.__module__}.{cls.__name__}", - } - ) - - # Get subclasses recursively - subclasses = _get_subclasses_recursive(obj, max_depth, include_builtins) - - return { - "success": True, - "class": dotted_path, - "mro": mro, - "mro_count": len(mro), - "subclasses": subclasses, - "subclass_count": _count_subclasses(subclasses), - "type_info": type_info, - } - - -def _get_subclasses_recursive( - cls: type, - max_depth: int, - include_builtins: bool, - current_depth: int = 0, -) -> list[dict]: - """Recursively get all subclasses.""" - if current_depth >= max_depth: - return [] - - result = [] - try: - for sub in cls.__subclasses__(): - if not include_builtins and sub.__module__ == "builtins": - continue - - sub_info = { - "name": sub.__name__, - "module": sub.__module__, - "qualname": f"{sub.__module__}.{sub.__name__}", - } - - children = _get_subclasses_recursive( - sub, max_depth, include_builtins, current_depth + 1 - ) - if children: - sub_info["subclasses"] = children - - result.append(sub_info) - except Exception: - pass - - return result - - -def _count_subclasses(subclasses: list[dict]) -> int: - """Count total subclasses including nested.""" - count = len(subclasses) - for sub in subclasses: - if "subclasses" in sub: - count += _count_subclasses(sub["subclasses"]) - return count - - -def get_mro(dotted_path: str, include_builtins: bool = False) -> dict: - """ - Get just the Method Resolution Order (parent classes). - - Simpler version of get_class_hierarchy for just parents. - - Parameters - ---------- - dotted_path : str - Dotted path to the class - include_builtins : bool - Include builtin classes - - Returns - ------- - dict - mro: list[str] - Qualified names in MRO order - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - if not inspect.isclass(obj): - return {"success": False, "error": f"'{dotted_path}' is not a class"} - - mro = [] - for cls in inspect.getmro(obj): - if not include_builtins and cls.__module__ == "builtins": - continue - mro.append(f"{cls.__module__}.{cls.__name__}") - - return { - "success": True, - "class": dotted_path, - "mro": mro, - } diff --git a/src/scitex/introspect/_core.py b/src/scitex/introspect/_core.py deleted file mode 100755 index 1fefe9ae0..000000000 --- a/src/scitex/introspect/_core.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_core.py - -"""Core introspection module - re-exports all utilities.""" - -from ._call_graph import get_call_graph, get_function_calls -from ._class_hierarchy import get_class_hierarchy, get_mro -from ._docstring import get_docstring -from ._examples import find_examples -from ._imports import get_dependencies, get_imports -from ._members import dir, get_exports -from ._resolve import get_type_info, resolve_object -from ._signature import q -from ._source import qq -from ._type_hints import get_class_annotations, get_type_hints_detailed - -__all__ = [ - # IPython-style names - "q", # signature (like func?) - "qq", # source (like func??) - "dir", # members (like dir()) - # Basic - "get_docstring", - "get_exports", - "find_examples", - "resolve_object", - "get_type_info", - # Advanced - Class hierarchy - "get_class_hierarchy", - "get_mro", - # Advanced - Type hints - "get_type_hints_detailed", - "get_class_annotations", - # Advanced - Imports - "get_imports", - "get_dependencies", - # Advanced - Call graph - "get_call_graph", - "get_function_calls", -] diff --git a/src/scitex/introspect/_docstring.py b/src/scitex/introspect/_docstring.py deleted file mode 100755 index 5281bc905..000000000 --- a/src/scitex/introspect/_docstring.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_docstring.py - -"""Docstring extraction and parsing utilities.""" - -from __future__ import annotations - -import inspect -import re -from typing import Literal - -from ._resolve import get_type_info, resolve_object - - -def _parse_docstring(docstring: str) -> dict: - """Parse numpy/google style docstring into sections.""" - sections = { - "summary": "", - "description": "", - "parameters": "", - "returns": "", - "examples": "", - "notes": "", - } - - if not docstring: - return sections - - section_patterns = [ - (r"Parameters?\s*\n[-=]+", "parameters"), - (r"Returns?\s*\n[-=]+", "returns"), - (r"Examples?\s*\n[-=]+", "examples"), - (r"Notes?\s*\n[-=]+", "notes"), - (r"Raises?\s*\n[-=]+", "raises"), - (r"See Also\s*\n[-=]+", "see_also"), - ] - - lines = docstring.split("\n") - - i = 0 - summary_lines = [] - while i < len(lines): - line = lines[i].strip() - if not line: - if summary_lines: - break - elif any(re.match(p, line, re.IGNORECASE) for p, _ in section_patterns): - break - else: - summary_lines.append(line) - i += 1 - - sections["summary"] = " ".join(summary_lines) - - current_section = "description" - current_content = [] - - for line in lines[i:]: - matched = False - for pattern, section_name in section_patterns: - if re.match(pattern, line, re.IGNORECASE): - if current_content: - sections[current_section] = "\n".join(current_content).strip() - current_section = section_name - current_content = [] - matched = True - break - - if not matched: - current_content.append(line) - - if current_content: - sections[current_section] = "\n".join(current_content).strip() - - return sections - - -def get_docstring( - dotted_path: str, - format: Literal["raw", "parsed", "summary"] = "raw", -) -> dict: - """ - Get the docstring of a Python object. - - Parameters - ---------- - dotted_path : str - Dotted path to the object - format : str - 'raw' - Return full docstring as-is - 'parsed' - Parse numpy/google style into sections - 'summary' - Return only first line/paragraph - - Returns - ------- - dict - docstring: str - sections: dict (if format='parsed') - type_info: dict - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - type_info = get_type_info(obj) - docstring = inspect.getdoc(obj) or "" - - if format == "summary": - lines = docstring.split("\n\n") - summary = lines[0].strip() if lines else "" - return { - "success": True, - "docstring": summary, - "type_info": type_info, - } - - if format == "parsed": - sections = _parse_docstring(docstring) - return { - "success": True, - "docstring": docstring, - "sections": sections, - "type_info": type_info, - } - - return { - "success": True, - "docstring": docstring, - "type_info": type_info, - } diff --git a/src/scitex/introspect/_examples.py b/src/scitex/introspect/_examples.py deleted file mode 100755 index 194f4eb63..000000000 --- a/src/scitex/introspect/_examples.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_examples.py - -"""Usage example finding utilities.""" - -from __future__ import annotations - -import importlib -from pathlib import Path - -from ._resolve import resolve_object - - -def find_examples( - dotted_path: str, - search_paths: list[str] | None = None, - max_results: int = 10, -) -> dict: - """ - Find usage examples of a function/class in tests and examples. - - Parameters - ---------- - dotted_path : str - Dotted path to search for (e.g., 'scitex.plt.plot') - search_paths : list[str] | None - Paths to search. If None, auto-detect from module location - max_results : int - Maximum number of examples to return - - Returns - ------- - dict - examples: list[dict] - Each with file, line, context - count: int - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - name = getattr(obj, "__name__", dotted_path.split(".")[-1]) - - if search_paths is None: - search_paths = [] - - module = dotted_path.split(".")[0] - try: - mod = importlib.import_module(module) - if hasattr(mod, "__file__") and mod.__file__: - mod_dir = Path(mod.__file__).parent - project_root = mod_dir.parent - for subdir in ["tests", "test", "examples", "example"]: - test_dir = project_root / subdir - if test_dir.exists(): - search_paths.append(str(test_dir)) - except ImportError: - pass - - if not search_paths: - return { - "success": True, - "examples": [], - "count": 0, - "message": "No test/example directories found", - } - - examples = [] - - for search_path in search_paths: - path = Path(search_path) - if not path.exists(): - continue - - for py_file in path.rglob("*.py"): - try: - content = py_file.read_text() - except Exception: - continue - - if name not in content: - continue - - lines = content.split("\n") - for i, line in enumerate(lines): - if name in line: - start = max(0, i - 2) - end = min(len(lines), i + 3) - context = "\n".join(lines[start:end]) - - examples.append( - { - "file": str(py_file), - "line": i + 1, - "context": context, - } - ) - - if len(examples) >= max_results: - break - - if len(examples) >= max_results: - break - - if len(examples) >= max_results: - break - - return { - "success": True, - "examples": examples, - "count": len(examples), - "search_paths": search_paths, - } diff --git a/src/scitex/introspect/_imports.py b/src/scitex/introspect/_imports.py deleted file mode 100755 index a2f652982..000000000 --- a/src/scitex/introspect/_imports.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_imports.py - -"""Import analysis utilities using AST.""" - -from __future__ import annotations - -import ast -import inspect -from pathlib import Path - -from ._resolve import get_type_info, resolve_object - - -def get_imports( - dotted_path: str, - categorize: bool = True, -) -> dict: - """ - Get all imports from a module's source code using AST. - - Parameters - ---------- - dotted_path : str - Dotted path to the module - categorize : bool - Group imports by category (stdlib, third-party, local) - - Returns - ------- - dict - imports: list[dict] - All imports with details - categories: dict - Grouped by category (if categorize=True) - - Examples - -------- - >>> get_imports("scitex.audio") - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - type_info = get_type_info(obj) - - if not inspect.ismodule(obj): - return { - "success": False, - "error": f"'{dotted_path}' is not a module", - "type_info": type_info, - } - - # Get source file - try: - source_file = inspect.getfile(obj) - except TypeError: - return { - "success": False, - "error": "Cannot get source file (builtin module?)", - "type_info": type_info, - } - - # Read and parse source - try: - source = Path(source_file).read_text() - tree = ast.parse(source) - except Exception as e: - return { - "success": False, - "error": f"Cannot parse source: {e}", - "type_info": type_info, - } - - imports = _extract_imports(tree) - - result = { - "success": True, - "module": dotted_path, - "source_file": source_file, - "imports": imports, - "import_count": len(imports), - "type_info": type_info, - } - - if categorize: - result["categories"] = _categorize_imports(imports) - - return result - - -def _extract_imports(tree: ast.AST) -> list[dict]: - """Extract all imports from an AST.""" - imports = [] - - for node in ast.walk(tree): - if isinstance(node, ast.Import): - for alias in node.names: - imports.append( - { - "type": "import", - "module": alias.name, - "alias": alias.asname, - "line": node.lineno, - } - ) - elif isinstance(node, ast.ImportFrom): - module = node.module or "" - level = node.level # Relative import level - - for alias in node.names: - imports.append( - { - "type": "from", - "module": module, - "name": alias.name, - "alias": alias.asname, - "level": level, - "line": node.lineno, - } - ) - - return imports - - -def _categorize_imports(imports: list[dict]) -> dict: - """Categorize imports into stdlib, third-party, local.""" - import sys - - stdlib_modules = ( - set(sys.stdlib_module_names) - if hasattr(sys, "stdlib_module_names") - else _get_stdlib_modules() - ) - - categories = { - "stdlib": [], - "third_party": [], - "local": [], - } - - for imp in imports: - module = imp["module"] - top_level = module.split(".")[0] if module else "" - - # Relative imports are local - if imp.get("level", 0) > 0: - categories["local"].append(imp) - elif top_level in stdlib_modules: - categories["stdlib"].append(imp) - else: - categories["third_party"].append(imp) - - return categories - - -def _get_stdlib_modules() -> set: - """Get stdlib module names for Python < 3.10.""" - import pkgutil - - stdlib = set() - for module in pkgutil.iter_modules(): - if module.name.startswith("_"): - continue - try: - spec = __import__(module.name).__spec__ - if spec and spec.origin: - if "site-packages" not in spec.origin: - stdlib.add(module.name) - except Exception: - pass - - # Add common ones that might be missed - stdlib.update( - [ - "abc", - "ast", - "asyncio", - "collections", - "contextlib", - "dataclasses", - "datetime", - "functools", - "inspect", - "io", - "itertools", - "json", - "logging", - "os", - "pathlib", - "re", - "sys", - "typing", - "unittest", - "warnings", - ] - ) - - return stdlib - - -def get_dependencies( - dotted_path: str, - recursive: bool = False, - max_depth: int = 3, -) -> dict: - """ - Get module dependencies (what it imports). - - Parameters - ---------- - dotted_path : str - Dotted path to the module - recursive : bool - Recursively analyze imported modules - max_depth : int - Maximum recursion depth - - Returns - ------- - dict - dependencies: list[str] - Direct dependencies - tree: dict - Dependency tree (if recursive) - """ - result = get_imports(dotted_path, categorize=True) - if not result.get("success"): - return result - - # Get unique module names - deps = set() - for imp in result["imports"]: - module = imp["module"] - if module: - deps.add(module.split(".")[0]) - - result["dependencies"] = sorted(deps) - result["dependency_count"] = len(deps) - - if recursive: - result["tree"] = _build_dep_tree(dotted_path, max_depth, set()) - - return result - - -def _build_dep_tree( - module_path: str, - max_depth: int, - visited: set, - current_depth: int = 0, -) -> dict: - """Build dependency tree recursively.""" - if current_depth >= max_depth or module_path in visited: - return {"module": module_path, "truncated": True} - - visited.add(module_path) - - result = {"module": module_path, "imports": []} - - imports_result = get_imports(module_path, categorize=False) - if not imports_result.get("success"): - return result - - for imp in imports_result.get("imports", []): - module = imp["module"] - if module and module not in visited: - top_level = module.split(".")[0] - # Only recurse into non-stdlib - if top_level not in _get_stdlib_modules(): - child = _build_dep_tree(module, max_depth, visited, current_depth + 1) - result["imports"].append(child) - - return result diff --git a/src/scitex/introspect/_list_api.py b/src/scitex/introspect/_list_api.py deleted file mode 100755 index 48c82b358..000000000 --- a/src/scitex/introspect/_list_api.py +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-27 -# File: /home/ywatanabe/proj/scitex-python/src/scitex/introspect/_list_api.py - -"""Module API listing utilities.""" - -import importlib -import inspect -import sys -import warnings -from typing import Any, List, Optional, Set, Union - -import pandas as pd - - -def list_api( - module: Union[str, Any], - columns: List[str] = ["Type", "Name", "Docstring", "Depth"], - prefix: str = "", - max_depth: int = 5, - visited: Optional[Set[str]] = None, - docstring: bool = False, - tree: bool = True, - current_depth: int = 0, - print_output: bool = False, - skip_depwarnings: bool = True, - drop_duplicates: bool = True, - root_only: bool = False, -) -> pd.DataFrame: - """ - List the API of a module recursively and return as a DataFrame. - - Like a recursive `dir()` that shows the entire module tree. - - Example - ------- - >>> df = list_api(scitex) - >>> print(df) - Type Name Docstring Depth - 0 M scitex Module description 0 - 1 F scitex.some_function Function description 1 - 2 C scitex.SomeClass Class description 1 - ... - - Parameters - ---------- - module : Union[str, Any] - Module to inspect (string name or actual module) - columns : List[str] - Columns to include in output DataFrame - prefix : str - Prefix for nested modules - max_depth : int - Maximum recursion depth - visited : Optional[Set[str]] - Set of visited modules to prevent cycles - docstring : bool - Whether to include docstrings - tree : bool - Whether to display tree structure - current_depth : int - Current recursion depth - print_output : bool - Whether to print results - skip_depwarnings : bool - Whether to skip DeprecationWarnings - drop_duplicates : bool - Whether to remove duplicate module entries - root_only : bool - Whether to show only root-level modules - - Returns - ------- - pd.DataFrame - Module structure with specified columns - """ - return _list_api_impl( - module=module, - prefix=prefix, - max_depth=max_depth, - visited=visited, - docstring=docstring, - tree=tree, - current_depth=current_depth, - print_output=print_output, - skip_depwarnings=skip_depwarnings, - drop_duplicates=drop_duplicates, - root_only=root_only, - )[columns] - - -def _list_api_impl( - module: Union[str, Any], - columns: List[str] = ["Type", "Name", "Docstring", "Depth"], - prefix: str = "", - max_depth: int = 5, - visited: Optional[Set[str]] = None, - docstring: bool = False, - tree: bool = True, - current_depth: int = 0, - print_output: bool = False, - skip_depwarnings: bool = True, - drop_duplicates: bool = True, - root_only: bool = False, -) -> pd.DataFrame: - """Internal implementation of list_api.""" - if skip_depwarnings: - warnings.filterwarnings("ignore", category=DeprecationWarning) - warnings.filterwarnings("ignore", category=UserWarning) - - if isinstance(module, str): - # Normalize hyphens to underscores for Python module names - module_name = module.replace("-", "_") - try: - module = importlib.import_module(module_name) - except ImportError as err: - print(f"Error importing module {module_name}: {err}") - return pd.DataFrame(columns=columns) - - if visited is None: - visited = set() - - content_list = [] - - try: - module_name = getattr(module, "__name__", "") - if max_depth < 0 or module_name in visited: - return pd.DataFrame(content_list, columns=columns) - - visited.add(module_name) - base_name = module_name.split(".")[-1] - full_path = f"{prefix}.{base_name}" if prefix else base_name - - try: - module_version = ( - f" (v{module.__version__})" if hasattr(module, "__version__") else "" - ) - content_list.append(("M", full_path, module_version, current_depth)) - except Exception: - pass - - for name, obj in inspect.getmembers(module): - if name.startswith("_"): - continue - - obj_name = f"{full_path}.{name}" - - if inspect.ismodule(obj): - # Only recurse into direct submodules (not sibling packages) - obj_mod_name = getattr(obj, "__name__", "") - # Check if this is a direct submodule of current module - if ( - obj_mod_name.startswith(module_name + ".") - and obj_mod_name not in visited - ): - content_list.append( - ( - "M", - obj_name, - obj.__doc__ if docstring and obj.__doc__ else "", - current_depth + 1, # Children are one level deeper - ) - ) - try: - sub_df = _list_api_impl( - obj, - columns=columns, - prefix=full_path, - max_depth=max_depth - 1, - visited=visited, - docstring=docstring, - tree=tree, - current_depth=current_depth + 1, - print_output=print_output, - skip_depwarnings=skip_depwarnings, - drop_duplicates=drop_duplicates, - root_only=root_only, - ) - if sub_df is not None and not sub_df.empty: - content_list.extend(sub_df.values.tolist()) - except Exception as err: - print(f"Error processing module {obj_name}: {err}") - elif inspect.isfunction(obj): - # Only include functions defined in this module (not re-exported from siblings) - obj_module = getattr(obj, "__module__", "") - # Check if function is defined in current module or its submodules - if obj_module == module_name or obj_module.startswith( - module_name + "." - ): - content_list.append( - ( - "F", - obj_name, - obj.__doc__ if docstring and obj.__doc__ else "", - current_depth + 1, # Children are one level deeper - ) - ) - elif inspect.isclass(obj): - # Only include classes defined in this module (not re-exported from siblings) - obj_module = getattr(obj, "__module__", "") - # Check if class is defined in current module or its submodules - if obj_module == module_name or obj_module.startswith( - module_name + "." - ): - content_list.append( - ( - "C", - obj_name, - obj.__doc__ if docstring and obj.__doc__ else "", - current_depth + 1, - ) - ) - # List public methods of the class - for mname, mobj in inspect.getmembers( - obj, predicate=inspect.isfunction - ): - if mname.startswith("_"): - continue - content_list.append( - ( - "F", - f"{obj_name}.{mname}", - mobj.__doc__ if docstring and mobj.__doc__ else "", - current_depth + 2, - ) - ) - - except Exception as err: - print(f"Error processing module structure: {err}") - return pd.DataFrame(columns=columns) - - df = pd.DataFrame(content_list, columns=columns) - - if drop_duplicates: - df = df.drop_duplicates(subset="Name", keep="first") - - if root_only: - mask = df["Name"].str.count(r"\.") <= 1 - df = df[mask] - - if tree and current_depth == 0 and print_output: - _print_module_contents(df) - - return df[columns] - - -def _print_module_contents(df: pd.DataFrame) -> None: - """Prints module contents in tree structure. - - Parameters - ---------- - df : pd.DataFrame - DataFrame containing module structure - """ - df_sorted = df.sort_values(["Depth", "Name"]) - depth_last = {} - - for index, row in df_sorted.iterrows(): - depth = row["Depth"] - is_last = ( - index == len(df_sorted) - 1 or df_sorted.iloc[index + 1]["Depth"] <= depth - ) - - prefix = "" - for d in range(depth): - if d == depth - 1: - prefix += "└── " if is_last else "├── " - else: - prefix += " " if depth_last.get(d, False) else "│ " - - print(f"{prefix}({row['Type']}) {row['Name']}{row['Docstring']}") - depth_last[depth] = is_last - - -if __name__ == "__main__": - import scitex - - sys.setrecursionlimit(10_000) - df = list_api(scitex, docstring=True, print_output=False, columns=["Name"]) - print(scitex.pd.round(df)) diff --git a/src/scitex/introspect/_mcp/__init__.py b/src/scitex/introspect/_mcp/__init__.py deleted file mode 100755 index 55519491c..000000000 --- a/src/scitex/introspect/_mcp/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_mcp/__init__.py - -"""MCP tools for introspection.""" - -from .handlers import ( # Advanced; IPython-style names; Basic - call_graph_handler, - class_hierarchy_handler, - dependencies_handler, - dir_handler, - docstring_handler, - examples_handler, - exports_handler, - imports_handler, - list_api_handler, - q_handler, - qq_handler, - type_hints_handler, -) - -__all__ = [ - # IPython-style names - "q_handler", - "qq_handler", - "dir_handler", - "list_api_handler", - # Basic - "docstring_handler", - "exports_handler", - "examples_handler", - # Advanced - "class_hierarchy_handler", - "type_hints_handler", - "imports_handler", - "dependencies_handler", - "call_graph_handler", -] diff --git a/src/scitex/introspect/_mcp/handlers.py b/src/scitex/introspect/_mcp/handlers.py deleted file mode 100755 index 13e71728e..000000000 --- a/src/scitex/introspect/_mcp/handlers.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_mcp/handlers.py - -"""MCP handlers for introspection tools.""" - -from __future__ import annotations - -from typing import Literal - - -async def q_handler( - dotted_path: str, - include_defaults: bool = True, - include_annotations: bool = True, -) -> dict: - """Get the signature of a function, method, or class (like IPython's func?).""" - try: - from .. import q - - result = q( - dotted_path, - include_defaults=include_defaults, - include_annotations=include_annotations, - ) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def docstring_handler( - dotted_path: str, - format: Literal["raw", "parsed", "summary"] = "raw", -) -> dict: - """Get the docstring of a Python object.""" - try: - from .. import get_docstring - - result = get_docstring(dotted_path, format=format) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def qq_handler( - dotted_path: str, - max_lines: int | None = None, - include_decorators: bool = True, -) -> dict: - """Get the source code of a Python object (like IPython's func??).""" - try: - from .. import qq - - result = qq( - dotted_path, - max_lines=max_lines, - include_decorators=include_decorators, - ) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def dir_handler( - dotted_path: str, - filter: Literal["all", "public", "private", "dunder"] = "public", - kind: Literal["all", "functions", "classes", "data", "modules"] | None = None, - include_inherited: bool = False, -) -> dict: - """List members of a module or class (like dir()).""" - try: - from .. import dir - - result = dir( - dotted_path, - filter=filter, - kind=kind, - include_inherited=include_inherited, - ) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def exports_handler(dotted_path: str) -> dict: - """Get the __all__ exports of a module.""" - try: - from .. import get_exports - - result = get_exports(dotted_path) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def examples_handler( - dotted_path: str, - search_paths: list[str] | None = None, - max_results: int = 10, -) -> dict: - """Find usage examples of a function/class in tests and examples.""" - try: - from .. import find_examples - - result = find_examples( - dotted_path, - search_paths=search_paths, - max_results=max_results, - ) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -# Advanced handlers - - -async def class_hierarchy_handler( - dotted_path: str, - include_builtins: bool = False, - max_depth: int = 10, -) -> dict: - """Get the inheritance hierarchy of a class.""" - try: - from .. import get_class_hierarchy - - result = get_class_hierarchy( - dotted_path, - include_builtins=include_builtins, - max_depth=max_depth, - ) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def type_hints_handler( - dotted_path: str, - include_extras: bool = True, -) -> dict: - """Get detailed type hint information.""" - try: - from .. import get_type_hints_detailed - - result = get_type_hints_detailed( - dotted_path, - include_extras=include_extras, - ) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def imports_handler( - dotted_path: str, - categorize: bool = True, -) -> dict: - """Get all imports from a module.""" - try: - from .. import get_imports - - result = get_imports( - dotted_path, - categorize=categorize, - ) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def dependencies_handler( - dotted_path: str, - recursive: bool = False, - max_depth: int = 3, -) -> dict: - """Get module dependencies.""" - try: - from .. import get_dependencies - - result = get_dependencies( - dotted_path, - recursive=recursive, - max_depth=max_depth, - ) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def call_graph_handler( - dotted_path: str, - max_depth: int = 2, - timeout_seconds: int = 10, - internal_only: bool = True, -) -> dict: - """Get the call graph of a function or module.""" - try: - from .. import get_call_graph - - result = get_call_graph( - dotted_path, - max_depth=max_depth, - timeout_seconds=timeout_seconds, - internal_only=internal_only, - ) - return result - except Exception as e: - return {"success": False, "error": str(e)} - - -async def list_api_handler( - dotted_path: str, - max_depth: int = 5, - docstring: bool = False, - root_only: bool = False, -) -> dict: - """List the API tree of a module recursively.""" - try: - from .. import list_api - - df = list_api( - dotted_path, - max_depth=max_depth, - docstring=docstring, - root_only=root_only, - ) - return { - "success": True, - "api": df.to_dict(orient="records"), - "count": len(df), - } - except Exception as e: - return {"success": False, "error": str(e)} diff --git a/src/scitex/introspect/_members.py b/src/scitex/introspect/_members.py deleted file mode 100755 index 2e1f6bba8..000000000 --- a/src/scitex/introspect/_members.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_members.py - -"""Member listing utilities.""" - -from __future__ import annotations - -import builtins -import inspect -from typing import Literal - -from ._resolve import get_type_info, resolve_object - -# Save reference to built-in dir before shadowing -_builtin_dir = builtins.dir - - -def dir( - dotted_path: str, - filter: Literal["all", "public", "private", "dunder"] = "public", - kind: Literal["all", "functions", "classes", "data", "modules"] | None = None, - include_inherited: bool = False, -) -> dict: - """ - List members of a module or class. - - Like Python's `dir()` but with filtering and metadata. - - Parameters - ---------- - dotted_path : str - Dotted path to the module or class - filter : str - 'all' - All members - 'public' - Only public (no leading _) - 'private' - Only private (single _) - 'dunder' - Only dunder (__name__) - kind : str | None - Filter by type: 'functions', 'classes', 'data', 'modules' - include_inherited : bool - For classes, include inherited members - - Returns - ------- - dict - members: list[dict] - Each with name, kind, summary - count: int - type_info: dict - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - type_info = get_type_info(obj) - - if inspect.isclass(obj) and not include_inherited: - member_names = list(obj.__dict__.keys()) - else: - member_names = _builtin_dir(obj) - - if filter == "public": - member_names = [n for n in member_names if not n.startswith("_")] - elif filter == "private": - member_names = [ - n for n in member_names if n.startswith("_") and not n.startswith("__") - ] - elif filter == "dunder": - member_names = [ - n for n in member_names if n.startswith("__") and n.endswith("__") - ] - - members = [] - for name in sorted(member_names): - try: - member = getattr(obj, name) - except AttributeError: - continue - - member_type_info = get_type_info(member) - member_kind = member_type_info["kind"] - - if kind: - kind_map = { - "functions": ("function", "method", "builtin_function_or_method"), - "classes": ("class",), - "data": ("data",), - "modules": ("module",), - } - if kind in kind_map and member_kind not in kind_map[kind]: - continue - - doc = inspect.getdoc(member) or "" - summary = doc.split("\n")[0] if doc else "" - - members.append( - { - "name": name, - "kind": member_kind, - "summary": summary[:100] + "..." if len(summary) > 100 else summary, - } - ) - - return { - "success": True, - "members": members, - "count": len(members), - "type_info": type_info, - } - - -def get_exports(dotted_path: str) -> dict: - """ - Get the __all__ exports of a module. - - Parameters - ---------- - dotted_path : str - Dotted path to the module - - Returns - ------- - dict - exports: list[str] - Names in __all__ - has_all: bool - Whether __all__ is defined - type_info: dict - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - type_info = get_type_info(obj) - - if not inspect.ismodule(obj): - return { - "success": False, - "error": f"'{dotted_path}' is not a module", - "type_info": type_info, - } - - exports = getattr(obj, "__all__", None) - - if exports is None: - exports = [n for n in _builtin_dir(obj) if not n.startswith("_")] - has_all = False - else: - has_all = True - - return { - "success": True, - "exports": list(exports), - "has_all": has_all, - "count": len(exports), - "type_info": type_info, - } diff --git a/src/scitex/introspect/_resolve.py b/src/scitex/introspect/_resolve.py deleted file mode 100755 index 69a5a038a..000000000 --- a/src/scitex/introspect/_resolve.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_resolve.py - -"""Object resolution and type information utilities.""" - -from __future__ import annotations - -import importlib -import inspect -from typing import Any - - -def resolve_object(dotted_path: str) -> tuple[Any, str | None]: - """ - Resolve a dotted path to a Python object. - - Parameters - ---------- - dotted_path : str - Dotted path like 'scitex.plt.plot' or 'scitex.audio' - - Returns - ------- - tuple[Any, str | None] - (resolved_object, error_message) - If successful, error_message is None - - Examples - -------- - >>> obj, err = resolve_object("scitex.plt") - >>> obj, err = resolve_object("scitex.audio.speak") - """ - parts = dotted_path.split(".") - obj = None - last_error = None - - for i in range(len(parts), 0, -1): - module_path = ".".join(parts[:i]) - try: - obj = importlib.import_module(module_path) - for attr_name in parts[i:]: - obj = getattr(obj, attr_name) - return obj, None - except (ImportError, AttributeError) as e: - last_error = str(e) - continue - - return None, f"Could not resolve '{dotted_path}': {last_error}" - - -def get_type_info(obj: Any) -> dict: - """ - Get type information about an object. - - Returns - ------- - dict - type: str - The type name - kind: str - 'module', 'class', 'function', 'method', 'property', 'data' - module: str - Module where defined - qualname: str - Qualified name - """ - type_name = type(obj).__name__ - - if inspect.ismodule(obj): - kind = "module" - elif inspect.isclass(obj): - kind = "class" - elif inspect.isfunction(obj) or inspect.isbuiltin(obj): - kind = "function" - elif inspect.ismethod(obj): - kind = "method" - elif isinstance(obj, property): - kind = "property" - elif callable(obj): - kind = "callable" - else: - kind = "data" - - module_name = getattr(obj, "__module__", None) - qualname = getattr(obj, "__qualname__", getattr(obj, "__name__", str(obj))) - - return { - "type": type_name, - "kind": kind, - "module": module_name, - "qualname": qualname, - } diff --git a/src/scitex/introspect/_signature.py b/src/scitex/introspect/_signature.py deleted file mode 100755 index 08d546ad0..000000000 --- a/src/scitex/introspect/_signature.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_signature.py - -"""Signature extraction utilities.""" - -from __future__ import annotations - -import inspect -from typing import Any - -from ._resolve import get_type_info, resolve_object - - -def _format_annotation(annotation: Any) -> str: - """Format a type annotation as a string.""" - if annotation is None: - return "None" - if hasattr(annotation, "__name__"): - return annotation.__name__ - return str(annotation).replace("typing.", "") - - -def _build_signature_string( - obj: Any, - parameters: list[dict], - return_annotation: str | None, -) -> str: - """Build a human-readable signature string.""" - name = getattr(obj, "__name__", "?") - - param_strs = [] - for p in parameters: - s = p["name"] - if "annotation" in p: - s += f": {p['annotation']}" - if "default" in p: - s += f" = {p['default']}" - param_strs.append(s) - - sig = f"{name}({', '.join(param_strs)})" - if return_annotation: - sig += f" -> {return_annotation}" - - return sig - - -def q( - dotted_path: str, - include_defaults: bool = True, - include_annotations: bool = True, -) -> dict: - """ - Get the signature of a function, method, or class. - - Like IPython's `func?` (quick info). - - Parameters - ---------- - dotted_path : str - Dotted path to the callable (e.g., 'scitex.plt.plot') - include_defaults : bool - Include default values in signature - include_annotations : bool - Include type annotations - - Returns - ------- - dict - name: str - signature: str - Human-readable signature - parameters: list[dict] - Detailed parameter info - return_annotation: str | None - type_info: dict - - Examples - -------- - >>> q("scitex.plt.plot") - {'name': 'plot', 'signature': 'plot(spec: dict, ...) -> dict', ...} - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - type_info = get_type_info(obj) - - callable_obj = obj - if inspect.isclass(obj): - callable_obj = obj.__init__ - - try: - sig = inspect.signature(callable_obj) - except (ValueError, TypeError) as e: - return { - "success": False, - "error": f"Cannot get signature: {e}", - "type_info": type_info, - } - - parameters = [] - for name, param in sig.parameters.items(): - if name == "self": - continue - - param_info = { - "name": name, - "kind": str(param.kind).split(".")[-1], - } - - if include_annotations and param.annotation != inspect.Parameter.empty: - param_info["annotation"] = _format_annotation(param.annotation) - - if include_defaults and param.default != inspect.Parameter.empty: - param_info["default"] = repr(param.default) - - parameters.append(param_info) - - return_annotation = None - if include_annotations and sig.return_annotation != inspect.Signature.empty: - return_annotation = _format_annotation(sig.return_annotation) - - sig_str = _build_signature_string(obj, parameters, return_annotation) - - return { - "success": True, - "name": getattr(obj, "__name__", dotted_path.split(".")[-1]), - "signature": sig_str, - "parameters": parameters, - "return_annotation": return_annotation, - "type_info": type_info, - } diff --git a/src/scitex/introspect/_skills/SKILL.md b/src/scitex/introspect/_skills/SKILL.md deleted file mode 100644 index 8909857d8..000000000 --- a/src/scitex/introspect/_skills/SKILL.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: stx.introspect -description: IPython-style introspection for Python packages — signatures, source code, API trees, docstrings, type hints, class hierarchies, import analysis, and call graphs. ---- - -# stx.introspect - -The `stx.introspect` module provides runtime and static inspection of any Python object or module. It mirrors the IPython `?` / `??` experience but returns structured dicts suitable for programmatic use, and exposes every function as an MCP tool. - -All functions accept a **dotted path string** (e.g. `"scitex.io.save"`) and internally resolve it to the live Python object. - -## Sub-skills - -### IPython-style shortcuts and API tree -- [ipython-shortcuts.md](ipython-shortcuts.md) — `q` (signature), `qq` (source), `dir` (member listing), `list_api` (recursive module DataFrame) - -### Documentation extraction -- [docstring-and-exports.md](docstring-and-exports.md) — `get_docstring` (raw/parsed/summary), `get_exports` (__all__), `find_examples` (usage search in test files) - -### Class hierarchy -- [class-hierarchy.md](class-hierarchy.md) — `get_class_hierarchy` (MRO + subclass tree), `get_mro` (parent chain only) - -### Type hint analysis -- [type-hints.md](type-hints.md) — `get_type_hints_detailed` (per-parameter breakdown), `get_class_annotations` (class vars + method hints) - -### Import and dependency analysis -- [imports-and-dependencies.md](imports-and-dependencies.md) — `get_imports` (AST-extracted import statements), `get_dependencies` (top-level deps + optional recursive tree) - -### Call graph analysis -- [call-graph.md](call-graph.md) — `get_call_graph` (outgoing calls + callers + module graph, with timeout), `get_function_calls` (simplified outgoing-only) - -### Object resolution -- [resolve.md](resolve.md) — `resolve_object` (dotted path → live object), `get_type_info` (kind classification) - -### MCP interface -- [mcp-tools.md](mcp-tools.md) — async handler signatures, `introspect_*` tool names, serialisation notes - ---- - -## Quick reference - -```python -import scitex as stx - -# --- IPython shortcuts --- -stx.introspect.q("scitex.io.save") # signature dict -stx.introspect.qq("scitex.io.save") # source dict -stx.introspect.dir("scitex.io") # members dict -stx.introspect.list_api(stx.stats) # pd.DataFrame of full API tree - -# --- Documentation --- -stx.introspect.get_docstring("scitex.io.save", format="parsed") -stx.introspect.get_exports("scitex.introspect") -stx.introspect.find_examples("scitex.io.save") - -# --- Class hierarchy --- -stx.introspect.get_class_hierarchy("collections.abc.Mapping") -stx.introspect.get_mro("pandas.Series") - -# --- Type hints --- -stx.introspect.get_type_hints_detailed("scitex.introspect.get_docstring") -stx.introspect.get_class_annotations("pandas.DataFrame") - -# --- Imports & dependencies --- -stx.introspect.get_imports("scitex.introspect._call_graph") -stx.introspect.get_dependencies("scitex.io", recursive=True, max_depth=2) - -# --- Call graph --- -stx.introspect.get_call_graph("scitex.introspect._call_graph.get_call_graph") -stx.introspect.get_function_calls("scitex.introspect._call_graph._analyze_call_graph") - -# --- Resolution --- -obj, err = stx.introspect.resolve_object("scitex.io.save") -info = stx.introspect.get_type_info(obj) -``` - -## All exported names - -``` -q, qq, dir, list_api -get_docstring, get_exports, find_examples -get_class_hierarchy, get_mro -get_type_hints_detailed, get_class_annotations -get_imports, get_dependencies -get_call_graph, get_function_calls -resolve_object, get_type_info -``` diff --git a/src/scitex/introspect/_skills/call-graph.md b/src/scitex/introspect/_skills/call-graph.md deleted file mode 100644 index ca8c0fb50..000000000 --- a/src/scitex/introspect/_skills/call-graph.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -description: Static call graph extraction via AST — which functions a function calls, which functions call it, and the call graph for an entire module. Has timeout protection for large modules. ---- - -# Call Graph Analysis - -Both functions use static AST analysis — they parse the source file without executing any code. Timeout protection (Unix `SIGALRM`) prevents hangs on large codebases. - ---- - -## get_call_graph - -```python -stx.introspect.get_call_graph( - dotted_path: str, - max_depth: int = 2, - timeout_seconds: int = 10, - internal_only: bool = True, -) -> dict -``` - -Builds an outgoing/incoming call graph for a single **function** or an entire **module**. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to a function or module | -| `max_depth` | `int` | `2` | Maximum traversal depth (currently affects module graph; function graph is always 1-level) | -| `timeout_seconds` | `int` | `10` | Abort after this many seconds and return a partial result (`"partial": True`). Set to `0` to disable | -| `internal_only` | `bool` | `True` | For function analysis: include only calls to other functions **defined in the same source file** | - -**Returns — function mode** (when `dotted_path` resolves to a function): - -```python -{ - "success": bool, - "function": str, # Short function name - "calls": [ - { - "name": str, # "other_func" or "obj.method" - "type": str, # "function" or "method" - "line": int, # Source line of the call - # For method calls: - "object": str, # Object variable name (e.g. "self") - "method": str, # Method name - }, - ... - ], - "call_count": int, - "called_by": [ - { - "name": str, # Name of the function that calls this one - "line": int, # Line where the caller is defined - }, - ... - ], - "caller_count": int, - "type_info": dict, -} -``` - -**Returns — module mode** (when `dotted_path` resolves to a module): - -```python -{ - "success": bool, - "module": str, - "graph": { - "function_name": { - "calls": [ { "name": str, "type": str, "line": int }, ... ], - "line": int, # Line where the function is defined - }, - ... - }, - "function_count": int, - "type_info": dict, -} -``` - -**Returns — timeout**: - -```python -{ - "success": False, - "error": "Operation timed out after 10s", - "partial": True, -} -``` - -**Examples** - -```python -import scitex as stx - -# Calls made by a specific function -result = stx.introspect.get_call_graph("scitex.introspect._call_graph.get_call_graph") -print("Calls out:") -for call in result["calls"]: - print(f" {call['name']} (line {call['line']})") - -print("Called by:") -for caller in result["called_by"]: - print(f" {caller['name']}") - -# All calls in a module -result = stx.introspect.get_call_graph( - "scitex.introspect._call_graph", - internal_only=False, - timeout_seconds=30, -) -for func, info in result["graph"].items(): - if info["calls"]: - print(f"{func} → {[c['name'] for c in info['calls']]}") - -# Increase timeout for a big module -result = stx.introspect.get_call_graph("scitex.io", timeout_seconds=30) -``` - ---- - -## get_function_calls - -```python -stx.introspect.get_function_calls( - dotted_path: str, - include_methods: bool = True, - include_builtins: bool = False, -) -> dict -``` - -Simplified wrapper around `get_call_graph` that returns only outgoing calls as a flat list of names, with optional filtering. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to a function | -| `include_methods` | `bool` | `True` | Include method calls (`obj.method()`) | -| `include_builtins` | `bool` | `False` | Include common builtins (`print`, `len`, `range`, `str`, `int`, `float`, `list`, `dict`, `set`) | - -**Returns** - -```python -{ - "success": bool, - "function": str, # dotted_path as given - "calls": list[str], # Names of called functions/methods - "call_count": int, -} -``` - -Internally calls `get_call_graph(..., max_depth=1, internal_only=False)` then applies the filters. - -**Example** - -```python -import scitex as stx - -result = stx.introspect.get_function_calls( - "scitex.introspect._call_graph._analyze_call_graph", - include_builtins=False, -) -print(result["calls"]) -# ["resolve_object", "get_type_info", "_build_function_index", -# "_get_function_calls", "_find_callers", ...] -``` - ---- - -## Implementation notes - -- Analysis is purely static (AST): it detects call nodes in the parse tree but does not resolve dynamic dispatch, `getattr`-based calls, or calls through variables. -- `called_by` for a function is found by scanning all other functions in the **same source file** only — not across modules. -- On Windows, the `SIGALRM`-based timeout is silently disabled; the operation runs without a time limit. -- Method calls are recorded as `"obj.method"` when the object is a simple name (`ast.Name`), or as just `"method"` for more complex expressions. diff --git a/src/scitex/introspect/_skills/class-hierarchy.md b/src/scitex/introspect/_skills/class-hierarchy.md deleted file mode 100644 index f72089e13..000000000 --- a/src/scitex/introspect/_skills/class-hierarchy.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -description: Inspect inheritance trees — MRO (parent classes in resolution order) and all known subclasses. Use when you need to understand a class's ancestry or find classes that extend it. ---- - -# Class Hierarchy Analysis - ---- - -## get_class_hierarchy - -```python -stx.introspect.get_class_hierarchy( - dotted_path: str, - include_builtins: bool = False, - max_depth: int = 10, -) -> dict -``` - -Returns both the upward chain (MRO) and the downward tree (subclasses) for any class. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to a class, e.g. `"pandas.DataFrame"` | -| `include_builtins` | `bool` | `False` | Include builtin classes (`object`, `type`, etc.) from `builtins` module | -| `max_depth` | `int` | `10` | Maximum depth for recursive subclass traversal | - -**Returns** - -```python -{ - "success": bool, - "class": str, # The dotted_path as given - "mro": [ - { - "name": str, # Short class name - "module": str, # Module where the class lives - "qualname": str, # "module.ClassName" - }, - ... - ], - "mro_count": int, - "subclasses": [ - { - "name": str, - "module": str, - "qualname": str, - "subclasses": [...], # Only present if the class has further subclasses - }, - ... - ], - "subclass_count": int, # Total including nested - "type_info": dict, -} -``` - -Returns `{"success": False, "error": "..."}` if the path does not resolve to a class. - -**Example** - -```python -import scitex as stx - -result = stx.introspect.get_class_hierarchy("collections.abc.Mapping") -print("Parents:") -for cls in result["mro"]: - print(f" {cls['qualname']}") - -print(f"Known subclasses: {result['subclass_count']}") -for sub in result["subclasses"]: - print(f" {sub['qualname']}") -``` - ---- - -## get_mro - -```python -stx.introspect.get_mro( - dotted_path: str, - include_builtins: bool = False, -) -> dict -``` - -Simplified version that returns only the Method Resolution Order — parent classes in the order Python uses to resolve attribute lookup. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to a class | -| `include_builtins` | `bool` | `False` | Include `object` and other builtins | - -**Returns** - -```python -{ - "success": bool, - "class": str, - "mro": list[str], # Qualified names: ["module.ClassName", ...] -} -``` - -**Example** - -```python -import scitex as stx - -result = stx.introspect.get_mro("pandas.Series") -for cls in result["mro"]: - print(cls) -# pandas.core.series.Series -# pandas.core.base.IndexOpsMixin -# pandas.core.arraylike.OpsMixin -# ... -``` - ---- - -## Difference between the two functions - -| | `get_mro` | `get_class_hierarchy` | -|---|---|---| -| Parents (MRO) | Yes — flat list of strings | Yes — list of dicts with name/module | -| Subclasses | No | Yes — recursive tree | -| Return size | Small | Potentially large for widely-subclassed base classes | - -Use `get_mro` for a quick ancestry check. Use `get_class_hierarchy` when you also need to find all classes that extend a given base. diff --git a/src/scitex/introspect/_skills/docstring-and-exports.md b/src/scitex/introspect/_skills/docstring-and-exports.md deleted file mode 100644 index a1a12495c..000000000 --- a/src/scitex/introspect/_skills/docstring-and-exports.md +++ /dev/null @@ -1,211 +0,0 @@ ---- -description: Extract docstrings (raw, parsed sections, or summary only) and retrieve a module's __all__ exports list. Use when you need documentation text or the official public API of a module. ---- - -# Docstring Extraction and Module Exports - ---- - -## get_docstring - -```python -stx.introspect.get_docstring( - dotted_path: str, - format: Literal["raw", "parsed", "summary"] = "raw", -) -> dict -``` - -Extracts the docstring from any Python object using `inspect.getdoc` (which strips leading indentation). - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to any Python object | -| `format` | `str` | `"raw"` | `"raw"` — full text as-is; `"parsed"` — split into named sections; `"summary"` — first paragraph only | - -**Returns (format="raw")** - -```python -{ - "success": bool, - "docstring": str, # Full cleaned docstring text - "type_info": dict, -} -``` - -**Returns (format="parsed")** - -```python -{ - "success": bool, - "docstring": str, # Full text (also available) - "sections": { - "summary": str, - "description": str, - "parameters": str, - "returns": str, - "examples": str, - "notes": str, - # Additional keys: "raises", "see_also" if present - }, - "type_info": dict, -} -``` - -**Returns (format="summary")** - -```python -{ - "success": bool, - "docstring": str, # First paragraph only (up to first blank line) - "type_info": dict, -} -``` - -**Docstring parsing rules** - -The parser recognises numpy/google-style section headers of the form: - -``` -Parameters ----------- -``` - -or - -``` -Returns -------- -``` - -Sections detected: `Parameters`, `Returns`, `Examples`, `Notes`, `Raises`, `See Also`. - -**Examples** - -```python -import scitex as stx - -# Quick one-line summary -doc = stx.introspect.get_docstring("scitex.io.save", format="summary") -print(doc["docstring"]) - -# Full structured parse -doc = stx.introspect.get_docstring("scitex.stats.test_ttest_ind", format="parsed") -print(doc["sections"]["parameters"]) -print(doc["sections"]["returns"]) - -# Raw text -doc = stx.introspect.get_docstring("pandas.DataFrame") -print(doc["docstring"]) -``` - ---- - -## get_exports - -```python -stx.introspect.get_exports(dotted_path: str) -> dict -``` - -Returns a module's `__all__` list. If `__all__` is not defined, falls back to all public names (no leading `_`). - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to a module (not a function or class) | - -**Returns** - -```python -{ - "success": bool, - "exports": list[str], # Names in __all__ (or all public names) - "has_all": bool, # True if __all__ was explicitly defined - "count": int, - "type_info": dict, -} -``` - -Returns `{"success": False, "error": "..."}` if the path resolves to something other than a module. - -**Examples** - -```python -import scitex as stx - -result = stx.introspect.get_exports("scitex.introspect") -print(result["has_all"]) # True -print(result["exports"]) -# ['q', 'qq', 'dir', 'list_api', 'get_docstring', ...] - -# Module without __all__ -result = stx.introspect.get_exports("scitex.io") -print(result["has_all"]) # depends on whether __all__ is defined -``` - ---- - -## find_examples - -```python -stx.introspect.find_examples( - dotted_path: str, - search_paths: list[str] | None = None, - max_results: int = 10, -) -> dict -``` - -Searches test and example directories for source files that call the named function or class. Performs a simple string search on the object's `__name__` and returns file + line + surrounding context. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Object whose name to search for | -| `search_paths` | `list[str] \| None` | `None` | Directories to search; if `None`, auto-detects `tests/`, `test/`, `examples/`, `example/` relative to the module's package root | -| `max_results` | `int` | `10` | Stop after this many matches | - -**Returns** - -```python -{ - "success": bool, - "examples": [ - { - "file": str, # Absolute path to the file - "line": int, # 1-based line number of the match - "context": str, # 2 lines before + match line + 2 lines after - }, - ... - ], - "count": int, - "search_paths": list[str], - # "message": str — present when no directories found -} -``` - -**Example** - -```python -import scitex as stx - -result = stx.introspect.find_examples("scitex.io.save") -for ex in result["examples"]: - print(f"{ex['file']}:{ex['line']}") - print(ex["context"]) - print("---") - -# Search a custom directory -result = stx.introspect.find_examples( - "scitex.stats.test_anova", - search_paths=["/my/project/tests"], - max_results=5, -) -``` - -**Notes** - -- Search is purely lexical (no import resolution). A hit means the object's `__name__` string appears on that line — it may include false positives from comments or unrelated identifiers. -- Context window is fixed at 2 lines before and 2 lines after the match line. diff --git a/src/scitex/introspect/_skills/imports-and-dependencies.md b/src/scitex/introspect/_skills/imports-and-dependencies.md deleted file mode 100644 index 034ff60f2..000000000 --- a/src/scitex/introspect/_skills/imports-and-dependencies.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -description: Static import analysis via AST — list all import statements in a module source file, categorised as stdlib/third-party/local, and build a dependency tree. Use when auditing what a module depends on. ---- - -# Import and Dependency Analysis - -Both functions work through static AST parsing of the module's source file — no dynamic import execution. - ---- - -## get_imports - -```python -stx.introspect.get_imports( - dotted_path: str, - categorize: bool = True, -) -> dict -``` - -Reads the source file of a module, parses it with `ast`, and extracts every `import` and `from ... import` statement. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to a module (not a function or class) | -| `categorize` | `bool` | `True` | Group results into `stdlib`, `third_party`, `local` sub-lists | - -**Returns** - -```python -{ - "success": bool, - "module": str, - "source_file": str, - "imports": [ - # For "import foo" or "import foo as bar": - { - "type": "import", - "module": str, # "foo" - "alias": str | None, # "bar" or None - "line": int, - }, - # For "from foo import bar" or "from .foo import bar": - { - "type": "from", - "module": str, # "foo" (empty string for "from . import x") - "name": str, # "bar" - "alias": str | None, - "level": int, # Relative import depth (0 = absolute) - "line": int, - }, - ... - ], - "import_count": int, - # Present when categorize=True: - "categories": { - "stdlib": [ ... ], # Same import dicts - "third_party": [ ... ], - "local": [ ... ], # Relative imports (level > 0) end up here - }, - "type_info": dict, -} -``` - -Returns `{"success": False, "error": "..."}` when: -- The path resolves to something other than a module. -- The source file cannot be found (e.g., compiled builtins). -- The source cannot be parsed. - -**Stdlib detection** - -Uses `sys.stdlib_module_names` (Python 3.10+) or a fallback heuristic for older Python. Common modules (`ast`, `os`, `pathlib`, etc.) are always present. - -**Example** - -```python -import scitex as stx - -result = stx.introspect.get_imports("scitex.introspect._call_graph") -print(f"Total imports: {result['import_count']}") - -cats = result["categories"] -print("stdlib:", [i["module"] for i in cats["stdlib"]]) -print("third-party:", [i["module"] for i in cats["third_party"]]) -print("local:", [i["module"] for i in cats["local"]]) - -# Find all relative imports -relative = [i for i in result["imports"] if i.get("level", 0) > 0] -``` - ---- - -## get_dependencies - -```python -stx.introspect.get_dependencies( - dotted_path: str, - recursive: bool = False, - max_depth: int = 3, -) -> dict -``` - -Builds on `get_imports` to produce a deduplicated list of top-level module names that the target module depends on. Optionally walks the dependency tree recursively. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to a module | -| `recursive` | `bool` | `False` | Recursively analyse each imported non-stdlib module | -| `max_depth` | `int` | `3` | Maximum recursion depth (only relevant when `recursive=True`) | - -**Returns** (extends `get_imports` return with additional keys): - -```python -{ - # All fields from get_imports(..., categorize=True) - "dependencies": list[str], # Sorted top-level module names - "dependency_count": int, - # Only when recursive=True: - "tree": { - "module": str, - "imports": [ - { - "module": str, - "imports": [...], # Nested tree; stops at max_depth or stdlib - # "truncated": True — present when cut off by depth/cycle guard - }, - ... - ], - }, -} -``` - -**Recursion rules:** -- Stdlib modules are not recursed into (only non-stdlib). -- A visited-set prevents infinite cycles. -- Nodes cut off by `max_depth` or already-visited carry `"truncated": True`. - -**Example** - -```python -import scitex as stx - -# Simple: what does scitex.introspect._imports import? -result = stx.introspect.get_dependencies("scitex.introspect._imports") -print(result["dependencies"]) -# ['ast', 'inspect', 'pathlib', ...] - -# Deep tree for a third-party module -result = stx.introspect.get_dependencies( - "scitex.io", - recursive=True, - max_depth=2, -) -import json -print(json.dumps(result["tree"], indent=2)) -``` - ---- - -## Summary - -| Function | Input | Output | -|----------|-------|--------| -| `get_imports` | module path | Every import statement + optional categorisation | -| `get_dependencies` | module path | Deduplicated top-level deps + optional recursive tree | diff --git a/src/scitex/introspect/_skills/ipython-shortcuts.md b/src/scitex/introspect/_skills/ipython-shortcuts.md deleted file mode 100644 index 58f233039..000000000 --- a/src/scitex/introspect/_skills/ipython-shortcuts.md +++ /dev/null @@ -1,224 +0,0 @@ ---- -description: IPython-style quick lookup — q (signature), qq (source), dir (members), list_api (recursive API tree). Use when you need to inspect a function, class, or module interactively. ---- - -# IPython-Style Shortcuts - -Four functions that mirror the interactive IPython `?` / `??` / `dir()` workflow, but work as plain Python calls and return structured dicts instead of printing to stdout. - ---- - -## q — Signature (like `func?`) - -```python -stx.introspect.q( - dotted_path: str, - include_defaults: bool = True, - include_annotations: bool = True, -) -> dict -``` - -Resolves a dotted path to any callable and returns its full signature. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to the callable, e.g. `"scitex.io.save"` | -| `include_defaults` | `bool` | `True` | Include default values in parameter info | -| `include_annotations` | `bool` | `True` | Include type annotations | - -**Returns** - -```python -{ - "success": bool, - "name": str, # Function/class name - "signature": str, # Human-readable: "save(obj, path: str, ...) -> None" - "parameters": [ - { - "name": str, - "kind": str, # POSITIONAL_OR_KEYWORD, VAR_POSITIONAL, etc. - "annotation": str, # omitted if no annotation or include_annotations=False - "default": str, # repr() of default; omitted if no default - }, - ... - ], - "return_annotation": str | None, - "type_info": { - "type": str, "kind": str, "module": str, "qualname": str - }, -} -``` - -**Example** - -```python -import scitex as stx - -result = stx.introspect.q("scitex.io.save") -print(result["signature"]) -# save(obj, path: str, ...) -> None - -# Inspect a class (resolves __init__) -result = stx.introspect.q("pandas.DataFrame") -for p in result["parameters"]: - print(p["name"], p.get("annotation", "")) -``` - ---- - -## qq — Full Source (like `func??`) - -```python -stx.introspect.qq( - dotted_path: str, - max_lines: int | None = None, - include_decorators: bool = True, -) -> dict -``` - -Retrieves the source code of any Python object via `inspect.getsource`. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to the object | -| `max_lines` | `int \| None` | `None` | Truncate to first N lines (appends `... (N more lines)`) | -| `include_decorators` | `bool` | `True` | When `False`, strips leading `@decorator` lines | - -**Returns** - -```python -{ - "success": bool, - "source": str, # Full source text - "file": str, # Absolute path to source file - "line_start": int, # Line number where definition starts - "line_count": int, # Total lines in full source - "type_info": dict, -} -``` - -**Example** - -```python -result = stx.introspect.qq("scitex.io.save", max_lines=20) -print(result["source"]) -print(f"Defined at {result['file']}:{result['line_start']}") -``` - ---- - -## dir — Member Listing (like `dir()`) - -```python -stx.introspect.dir( - dotted_path: str, - filter: Literal["all", "public", "private", "dunder"] = "public", - kind: Literal["all", "functions", "classes", "data", "modules"] | None = None, - include_inherited: bool = False, -) -> dict -``` - -Lists members of a module or class with per-member metadata. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to the module or class | -| `filter` | `str` | `"public"` | `"all"`, `"public"` (no leading `_`), `"private"` (single `_`), `"dunder"` (`__x__`) | -| `kind` | `str \| None` | `None` | Restrict to `"functions"`, `"classes"`, `"data"`, or `"modules"` | -| `include_inherited` | `bool` | `False` | For classes, include members inherited from base classes | - -**Returns** - -```python -{ - "success": bool, - "members": [ - { - "name": str, - "kind": str, # "function", "class", "data", "module", "method", etc. - "summary": str, # First line of docstring, truncated at 100 chars - }, - ... - ], - "count": int, - "type_info": dict, -} -``` - -**Example** - -```python -# List all public functions in stx.io -result = stx.introspect.dir("scitex.io", kind="functions") -for m in result["members"]: - print(f"{m['name']:20s} {m['summary']}") - -# Show only dunder methods on a class -result = stx.introspect.dir("pandas.DataFrame", filter="dunder") -``` - ---- - -## list_api — Recursive Module Tree - -```python -stx.introspect.list_api( - module: str | Any, - columns: list[str] = ["Type", "Name", "Docstring", "Depth"], - max_depth: int = 5, - docstring: bool = False, - tree: bool = True, - print_output: bool = False, - drop_duplicates: bool = True, - root_only: bool = False, - skip_depwarnings: bool = True, -) -> pd.DataFrame -``` - -Recursively walks a module and returns its entire API as a pandas DataFrame. Only public names are included (no leading `_`). Avoids infinite cycles via a visited-set guard. - -**Type column values:** `"M"` = module, `"F"` = function, `"C"` = class. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `module` | `str \| Any` | required | Module name string or already-imported module object | -| `columns` | `list[str]` | `["Type","Name","Docstring","Depth"]` | Columns to keep in the returned DataFrame | -| `max_depth` | `int` | `5` | Maximum recursion depth (0 = root module only) | -| `docstring` | `bool` | `False` | Populate the `Docstring` column | -| `tree` | `bool` | `True` | Enable tree-style console print when `print_output=True` | -| `print_output` | `bool` | `False` | Print tree to stdout | -| `drop_duplicates` | `bool` | `True` | Remove rows with duplicate `Name` values | -| `root_only` | `bool` | `False` | Show only names with at most one `.` in the path | -| `skip_depwarnings` | `bool` | `True` | Suppress DeprecationWarning and UserWarning during traversal | - -**Returns:** `pd.DataFrame` with at least columns `Type`, `Name`, `Docstring`, `Depth`. - -**Example** - -```python -import scitex as stx - -# Full API tree of scitex.stats -df = stx.introspect.list_api(stx.stats, docstring=True) -print(df[df["Type"] == "F"]["Name"].tolist()) - -# Top-level only -df = stx.introspect.list_api("scitex", root_only=True) - -# Print tree to terminal -stx.introspect.list_api(stx.io, print_output=True) -``` - -**Notes** - -- Strings with hyphens are normalized (e.g., `"scitex-stats"` → `"scitex_stats"`) before import. -- Only submodules whose `__name__` starts with the parent `__name__` are traversed (prevents walking sibling packages). -- Functions and classes defined in a different module (re-exports from siblings) are excluded. diff --git a/src/scitex/introspect/_skills/mcp-tools.md b/src/scitex/introspect/_skills/mcp-tools.md deleted file mode 100644 index 9893a3606..000000000 --- a/src/scitex/introspect/_skills/mcp-tools.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -description: MCP tool interface for stx.introspect — async handlers that wrap every Python API function. Use when calling introspect capabilities through the MCP protocol. ---- - -# MCP Tools - -Every Python API function in `stx.introspect` has a corresponding async MCP handler in `scitex.introspect._mcp.handlers`. All handlers accept the same parameters as the Python function and return the same dict structure. - ---- - -## Available MCP Tools - -All tools are prefixed `introspect_` in the MCP namespace. - -| MCP tool | Python function | Description | -|----------|----------------|-------------| -| `introspect_signature` | `q` | Signature + parameters + return annotation | -| `introspect_source` | `qq` | Full source code | -| `introspect_dir` | `dir` | Member listing with filtering | -| `introspect_api` | `list_api` | Recursive module API tree | -| `introspect_docstring` | `get_docstring` | Docstring (raw / parsed / summary) | -| `introspect_exports` | `get_exports` | Module's `__all__` contents | -| `introspect_examples` | `find_examples` | Usage examples found in tests | -| `introspect_class_hierarchy` | `get_class_hierarchy` | MRO + subclass tree | -| `introspect_type_hints` | `get_type_hints_detailed` | Per-parameter type breakdown | -| `introspect_imports` | `get_imports` | All import statements (with categories) | -| `introspect_dependencies` | `get_dependencies` | Top-level deps + optional tree | -| `introspect_call_graph` | `get_call_graph` | Outgoing calls + callers + module graph | - ---- - -## Handler signatures - -```python -# introspect_signature -async def q_handler( - dotted_path: str, - include_defaults: bool = True, - include_annotations: bool = True, -) -> dict - -# introspect_source -async def qq_handler( - dotted_path: str, - max_lines: int | None = None, - include_decorators: bool = True, -) -> dict - -# introspect_dir -async def dir_handler( - dotted_path: str, - filter: Literal["all", "public", "private", "dunder"] = "public", - kind: Literal["all", "functions", "classes", "data", "modules"] | None = None, - include_inherited: bool = False, -) -> dict - -# introspect_api -async def list_api_handler( - dotted_path: str, - max_depth: int = 5, - docstring: bool = False, - root_only: bool = False, -) -> dict -# Returns: {"success": bool, "api": list[dict], "count": int} -# Each dict in "api" has: Type, Name, Docstring, Depth - -# introspect_docstring -async def docstring_handler( - dotted_path: str, - format: Literal["raw", "parsed", "summary"] = "raw", -) -> dict - -# introspect_exports -async def exports_handler(dotted_path: str) -> dict - -# introspect_examples -async def examples_handler( - dotted_path: str, - search_paths: list[str] | None = None, - max_results: int = 10, -) -> dict - -# introspect_class_hierarchy -async def class_hierarchy_handler( - dotted_path: str, - include_builtins: bool = False, - max_depth: int = 10, -) -> dict - -# introspect_type_hints -async def type_hints_handler( - dotted_path: str, - include_extras: bool = True, -) -> dict - -# introspect_imports -async def imports_handler( - dotted_path: str, - categorize: bool = True, -) -> dict - -# introspect_dependencies -async def dependencies_handler( - dotted_path: str, - recursive: bool = False, - max_depth: int = 3, -) -> dict - -# introspect_call_graph -async def call_graph_handler( - dotted_path: str, - max_depth: int = 2, - timeout_seconds: int = 10, - internal_only: bool = True, -) -> dict -``` - ---- - -## list_api_handler vs list_api - -`list_api_handler` serialises the DataFrame returned by `list_api` into a list of dicts using `df.to_dict(orient="records")` before returning, so the MCP result is JSON-serialisable. - -```python -# MCP response shape -{ - "success": True, - "api": [ - {"Type": "M", "Name": "scitex.io", "Docstring": "", "Depth": 0}, - {"Type": "F", "Name": "scitex.io.save", "Docstring": "", "Depth": 1}, - ... - ], - "count": 42, -} -``` - ---- - -## Error handling - -All handlers catch any exception and return: - -```python -{"success": False, "error": ""} -``` - -This means MCP callers always receive a dict with a `"success"` key rather than an unhandled exception propagating through the transport. diff --git a/src/scitex/introspect/_skills/resolve.md b/src/scitex/introspect/_skills/resolve.md deleted file mode 100644 index 7646b56c5..000000000 --- a/src/scitex/introspect/_skills/resolve.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -description: Resolve a dotted path string to a live Python object and classify it by kind (module, class, function, method, data). Foundational utility used internally by all other introspect functions. ---- - -# Object Resolution - ---- - -## resolve_object - -```python -stx.introspect.resolve_object(dotted_path: str) -> tuple[Any, str | None] -``` - -Converts a dotted string path into the corresponding Python object by trying progressively shorter module paths and attribute access. - -**Parameters** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `dotted_path` | `str` | Dotted path such as `"scitex.io.save"`, `"pandas.DataFrame"`, or `"json"` | - -**Returns** - -`(object, error)` tuple: -- On success: `(resolved_object, None)` -- On failure: `(None, "Could not resolve '...': ")` - -**Resolution algorithm** - -For path `a.b.c.d`, it tries: -1. `importlib.import_module("a.b.c.d")` -2. `importlib.import_module("a.b.c")` then `getattr(module, "d")` -3. `importlib.import_module("a.b")` then `getattr(module, "c")`, `getattr(result, "d")` -4. `importlib.import_module("a")` then attribute chain - -The first successful resolution is returned. - -**Example** - -```python -import scitex as stx - -obj, err = stx.introspect.resolve_object("scitex.io.save") -# obj is the save function, err is None - -obj, err = stx.introspect.resolve_object("nonexistent.module") -# obj is None, err is "Could not resolve 'nonexistent.module': ..." - -obj, err = stx.introspect.resolve_object("pandas.DataFrame") -# obj is the DataFrame class - -obj, err = stx.introspect.resolve_object("json") -# obj is the json module -``` - ---- - -## get_type_info - -```python -stx.introspect.get_type_info(obj: Any) -> dict -``` - -Classifies any Python object and returns a metadata dict. Used internally by all introspect functions but also available directly. - -**Parameters** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `obj` | `Any` | Any Python object | - -**Returns** - -```python -{ - "type": str, # type(obj).__name__ - "kind": str, # One of: "module", "class", "function", "method", - # "property", "callable", "data" - "module": str, # obj.__module__ (None if not available) - "qualname": str, # obj.__qualname__ or obj.__name__ or str(obj) -} -``` - -**Kind classification rules** - -| `kind` value | Condition | -|-------------|-----------| -| `"module"` | `inspect.ismodule(obj)` | -| `"class"` | `inspect.isclass(obj)` | -| `"function"` | `inspect.isfunction(obj)` or `inspect.isbuiltin(obj)` | -| `"method"` | `inspect.ismethod(obj)` | -| `"property"` | `isinstance(obj, property)` | -| `"callable"` | `callable(obj)` (fallthrough, e.g. class instances with `__call__`) | -| `"data"` | everything else | - -**Example** - -```python -import scitex as stx -import json - -info = stx.introspect.get_type_info(json.dumps) -# {"type": "builtin_function_or_method", "kind": "function", -# "module": "json", "qualname": "dumps"} - -info = stx.introspect.get_type_info(json) -# {"type": "module", "kind": "module", "module": "json", "qualname": "json"} -``` - ---- - -## Notes - -- `resolve_object` and `get_type_info` are the foundation that every other `stx.introspect` function calls first. You rarely need to call them directly unless building custom introspection tooling. -- `resolve_object` will import the module as a side-effect of resolution. For modules with expensive `__init__` code this may take time. diff --git a/src/scitex/introspect/_skills/type-hints.md b/src/scitex/introspect/_skills/type-hints.md deleted file mode 100644 index f6cc65d7d..000000000 --- a/src/scitex/introspect/_skills/type-hints.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -description: Detailed type annotation analysis — per-parameter hint breakdown (origin, args, Optional/Union flags) and full class-level annotation inventory. Use when you need to understand or validate type signatures programmatically. ---- - -# Type Hint Analysis - ---- - -## get_type_hints_detailed - -```python -stx.introspect.get_type_hints_detailed( - dotted_path: str, - include_extras: bool = True, -) -> dict -``` - -Calls `typing.get_type_hints` on a callable or class and decomposes each annotation into structured metadata: the raw string, the generic origin, type arguments, and flags for `Optional`/`Union`. - -For classes, the analysis targets `__init__` automatically. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to a function, method, or class | -| `include_extras` | `bool` | `True` | Pass `include_extras=True` to `typing.get_type_hints` (preserves `Annotated` metadata) | - -**Returns** - -```python -{ - "success": bool, - "hints": { - "param_name": { - "raw": str, # Type as a readable string, e.g. "list[int]" - "origin": str | None, # Generic origin, e.g. "list", "Union" - "args": list[str], # Type arguments, e.g. ["int"] for list[int] - "is_optional": bool, # True when the type is Union[X, None] - "is_union": bool, # True when the type uses Union - "is_generic": bool, # True when origin is not None - }, - ... - }, - "return_hint": { # Same structure; None if no return annotation - "raw": str, - "origin": str | None, - "args": list[str], - "is_optional": bool, - "is_union": bool, - "is_generic": bool, - } | None, - "hint_count": int, # Number of parameter hints (excludes return) - "type_info": dict, -} -``` - -Falls back to `__annotations__` if `get_type_hints` raises (e.g., forward references that cannot be resolved). Returns `{"success": False, ...}` if no hints are obtainable. - -**Example** - -```python -import scitex as stx - -result = stx.introspect.get_type_hints_detailed("scitex.introspect.get_docstring") -for name, hint in result["hints"].items(): - print(f"{name}: {hint['raw']}", end="") - if hint["is_optional"]: - print(" [optional]", end="") - print() - -# Check the return type -if result["return_hint"]: - print("returns:", result["return_hint"]["raw"]) -``` - ---- - -## get_class_annotations - -```python -stx.introspect.get_class_annotations(dotted_path: str) -> dict -``` - -Inventories all annotations on a class: class-level variable annotations (`__annotations__`) and per-method type hints. - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `dotted_path` | `str` | required | Dotted path to a class | - -**Returns** - -```python -{ - "success": bool, - "class": str, - "class_vars": { - "var_name": { - "raw": str, "origin": str | None, "args": list[str], - "is_optional": bool, "is_union": bool, "is_generic": bool, - }, - ... - }, - "methods": { - "method_name": { - "param_name": { }, - ... - # includes "return" key for return annotation - }, - ... - }, - "class_var_count": int, - "method_count": int, -} -``` - -Returns `{"success": False, "error": "..."}` if path is not a class. - -Methods with no annotations are omitted from `methods`. - -**Example** - -```python -import scitex as stx - -result = stx.introspect.get_class_annotations("pandas.DataFrame") -print(f"Class-level annotations: {result['class_var_count']}") -for var, hint in result["class_vars"].items(): - print(f" {var}: {hint['raw']}") - -print(f"Annotated methods: {result['method_count']}") -``` - ---- - -## Hint structure reference - -Both functions return the same per-hint dict structure: - -| Field | Type | Meaning | -|-------|------|---------| -| `raw` | `str` | Human-readable type string (`"list[int]"`, `"Optional[str]"`) | -| `origin` | `str \| None` | Generic base before specialisation (`"list"`, `"Union"`) | -| `args` | `list[str]` | Type parameters (`["int"]` for `list[int]`) | -| `is_optional` | `bool` | `True` when `None` is one of the Union arms | -| `is_union` | `bool` | `True` when the type is a `Union` | -| `is_generic` | `bool` | `True` when `origin` is not `None` | diff --git a/src/scitex/introspect/_source.py b/src/scitex/introspect/_source.py deleted file mode 100755 index 81da49855..000000000 --- a/src/scitex/introspect/_source.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_source.py - -"""Source code retrieval utilities.""" - -from __future__ import annotations - -import inspect - -from ._resolve import get_type_info, resolve_object - - -def qq( - dotted_path: str, - max_lines: int | None = None, - include_decorators: bool = True, -) -> dict: - """ - Get the source code of a Python object. - - Like IPython's `func??` (full source). - - Parameters - ---------- - dotted_path : str - Dotted path to the object - max_lines : int | None - Limit output to first N lines (None = no limit) - include_decorators : bool - Include decorator lines - - Returns - ------- - dict - source: str - file: str - Source file path - line_start: int - Starting line number - line_count: int - Number of lines - type_info: dict - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - type_info = get_type_info(obj) - - try: - source = inspect.getsource(obj) - source_file = inspect.getfile(obj) - _, line_start = inspect.getsourcelines(obj) - except (TypeError, OSError) as e: - return { - "success": False, - "error": f"Cannot get source: {e}", - "type_info": type_info, - } - - lines = source.split("\n") - line_count = len(lines) - - if not include_decorators and lines: - i = 0 - while i < len(lines) and lines[i].strip().startswith("@"): - i += 1 - lines = lines[i:] - source = "\n".join(lines) - - if max_lines and len(lines) > max_lines: - lines = lines[:max_lines] - source = "\n".join(lines) + f"\n... ({line_count - max_lines} more lines)" - - return { - "success": True, - "source": source, - "file": source_file, - "line_start": line_start, - "line_count": line_count, - "type_info": type_info, - } diff --git a/src/scitex/introspect/_type_hints.py b/src/scitex/introspect/_type_hints.py deleted file mode 100755 index 0e4d5daf3..000000000 --- a/src/scitex/introspect/_type_hints.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/introspect/_type_hints.py - -"""Type hint analysis utilities.""" - -from __future__ import annotations - -import inspect -import typing -from typing import Any, get_type_hints - -from ._resolve import get_type_info, resolve_object - - -def get_type_hints_detailed( - dotted_path: str, - include_extras: bool = True, -) -> dict: - """ - Get detailed type hint information for a callable or class. - - Parameters - ---------- - dotted_path : str - Dotted path to the function, method, or class - include_extras : bool - Include typing extras (Annotated metadata, etc.) - - Returns - ------- - dict - hints: dict[str, dict] - Parameter name to type info - return_hint: dict | None - Return type info - type_info: dict - - Examples - -------- - >>> get_type_hints_detailed("json.dumps") - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - type_info_obj = get_type_info(obj) - - # Get the object to analyze - target = obj - if inspect.isclass(obj): - target = obj.__init__ - - try: - hints = get_type_hints(target, include_extras=include_extras) - except Exception as e: - # Some objects don't support get_type_hints - hints = getattr(target, "__annotations__", {}) - if not hints: - return { - "success": False, - "error": f"Cannot get type hints: {e}", - "type_info": type_info_obj, - } - - # Analyze each hint - analyzed_hints = {} - return_hint = None - - for name, hint in hints.items(): - hint_info = _analyze_type(hint) - if name == "return": - return_hint = hint_info - else: - analyzed_hints[name] = hint_info - - return { - "success": True, - "hints": analyzed_hints, - "return_hint": return_hint, - "hint_count": len(analyzed_hints), - "type_info": type_info_obj, - } - - -def _analyze_type(hint: Any) -> dict: - """Analyze a type hint and return structured info.""" - result = { - "raw": _format_type(hint), - "origin": None, - "args": [], - "is_optional": False, - "is_union": False, - "is_generic": False, - } - - # Get origin (e.g., list from list[int]) - origin = typing.get_origin(hint) - if origin is not None: - result["origin"] = _format_type(origin) - result["is_generic"] = True - - # Get args (e.g., int from list[int]) - args = typing.get_args(hint) - if args: - result["args"] = [_format_type(a) for a in args] - - # Check for Union/Optional - if origin is typing.Union: - result["is_union"] = True - # Optional is Union[X, None] - if type(None) in args: - result["is_optional"] = True - - return result - - -def _format_type(t: Any) -> str: - """Format a type as a readable string.""" - if t is type(None): - return "None" - if hasattr(t, "__name__"): - return t.__name__ - if hasattr(t, "_name"): - return t._name or str(t) - return str(t).replace("typing.", "") - - -def get_class_annotations(dotted_path: str) -> dict: - """ - Get all annotations for a class (class vars and methods). - - Parameters - ---------- - dotted_path : str - Dotted path to the class - - Returns - ------- - dict - class_vars: dict - Class variable annotations - methods: dict - Method annotations (name -> hints) - """ - obj, error = resolve_object(dotted_path) - if error: - return {"success": False, "error": error} - - if not inspect.isclass(obj): - return {"success": False, "error": f"'{dotted_path}' is not a class"} - - # Class-level annotations - class_vars = {} - for name, hint in getattr(obj, "__annotations__", {}).items(): - class_vars[name] = _analyze_type(hint) - - # Method annotations - methods = {} - for name, member in inspect.getmembers(obj): - if inspect.isfunction(member) or inspect.ismethod(member): - try: - hints = get_type_hints(member) - if hints: - methods[name] = {k: _analyze_type(v) for k, v in hints.items()} - except Exception: - pass - - return { - "success": True, - "class": dotted_path, - "class_vars": class_vars, - "methods": methods, - "class_var_count": len(class_vars), - "method_count": len(methods), - } diff --git a/tests/scitex/introspect/test__call_graph.py b/tests/scitex/introspect/test__call_graph.py deleted file mode 100755 index 6f62be829..000000000 --- a/tests/scitex/introspect/test__call_graph.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__call_graph.py - -"""Tests for scitex.introspect._call_graph module.""" - -import pytest - - -class TestGetCallGraph: - """Tests for get_call_graph function.""" - - def test_get_call_graph_function(self): - """Test getting call graph for a function.""" - from scitex.introspect import get_call_graph - - result = get_call_graph("scitex.introspect._resolve.resolve_object") - assert result["success"] is True - assert "calls" in result - assert "call_count" in result - - def test_call_graph_module(self): - """Test getting call graph for a module.""" - from scitex.introspect import get_call_graph - - result = get_call_graph("scitex.introspect._resolve") - assert result["success"] is True - assert "graph" in result or "calls" in result - - def test_call_graph_with_timeout(self): - """Test call graph respects timeout.""" - from scitex.introspect import get_call_graph - - # Short timeout should still work for small modules - result = get_call_graph("scitex.introspect._resolve", timeout_seconds=30) - assert "success" in result - - def test_call_graph_internal_only(self): - """Test internal_only filters external calls.""" - from scitex.introspect import get_call_graph - - result = get_call_graph( - "scitex.introspect._resolve.resolve_object", internal_only=True - ) - assert result["success"] is True - - def test_call_graph_includes_external(self): - """Test including external calls.""" - from scitex.introspect import get_call_graph - - result = get_call_graph( - "scitex.introspect._resolve.resolve_object", internal_only=False - ) - assert result["success"] is True - # Should include calls to importlib, etc. - - def test_call_graph_invalid_path(self): - """Test invalid path returns error.""" - from scitex.introspect import get_call_graph - - result = get_call_graph("nonexistent.module") - assert result["success"] is False - - -class TestGetFunctionCalls: - """Tests for get_function_calls function.""" - - def test_get_function_calls_success(self): - """Test getting function calls.""" - from scitex.introspect import get_function_calls - - result = get_function_calls("scitex.introspect._resolve.resolve_object") - assert result["success"] is True - assert "calls" in result - - def test_function_calls_builtin(self): - """Test function calls for builtin fails gracefully.""" - from scitex.introspect import get_function_calls - - result = get_function_calls("len") - # Builtins don't have source - assert result["success"] is False diff --git a/tests/scitex/introspect/test__class_hierarchy.py b/tests/scitex/introspect/test__class_hierarchy.py deleted file mode 100755 index b5053c9d8..000000000 --- a/tests/scitex/introspect/test__class_hierarchy.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__class_hierarchy.py - -"""Tests for scitex.introspect._class_hierarchy module.""" - -import pytest - - -class TestGetClassHierarchy: - """Tests for get_class_hierarchy function.""" - - def test_get_hierarchy_success(self): - """Test getting class hierarchy successfully.""" - from scitex.introspect import get_class_hierarchy - - result = get_class_hierarchy("collections.abc.Mapping") - assert result["success"] is True - assert "mro" in result - assert "subclasses" in result - assert result["mro_count"] > 0 - - def test_hierarchy_mro_order(self): - """Test MRO is in correct order.""" - from scitex.introspect import get_class_hierarchy - - result = get_class_hierarchy("collections.abc.MutableMapping") - assert result["success"] is True - # MutableMapping should have Mapping in its MRO - mro_names = [c["name"] for c in result["mro"]] - assert "MutableMapping" in mro_names - assert "Mapping" in mro_names - - def test_hierarchy_without_builtins(self): - """Test hierarchy excludes builtins by default.""" - from scitex.introspect import get_class_hierarchy - - result = get_class_hierarchy("pathlib.Path", include_builtins=False) - assert result["success"] is True - mro_names = [c["name"] for c in result["mro"]] - assert "object" not in mro_names - - def test_hierarchy_with_builtins(self): - """Test hierarchy includes builtins when requested.""" - from scitex.introspect import get_class_hierarchy - - result = get_class_hierarchy("pathlib.Path", include_builtins=True) - assert result["success"] is True - mro_names = [c["name"] for c in result["mro"]] - assert "object" in mro_names - - def test_hierarchy_non_class_error(self): - """Test error when path is not a class.""" - from scitex.introspect import get_class_hierarchy - - result = get_class_hierarchy("json.dumps") - assert result["success"] is False - assert "not a class" in result["error"] - - def test_hierarchy_max_depth(self): - """Test max_depth limits subclass traversal.""" - from scitex.introspect import get_class_hierarchy - - result = get_class_hierarchy("collections.abc.Mapping", max_depth=1) - assert result["success"] is True - # With max_depth=1, nested subclasses should not have children - for sub in result.get("subclasses", []): - assert "subclasses" not in sub or len(sub["subclasses"]) == 0 - - -class TestGetMro: - """Tests for get_mro function.""" - - def test_get_mro_success(self): - """Test getting MRO successfully.""" - from scitex.introspect import get_mro - - result = get_mro("collections.OrderedDict") - assert result["success"] is True - assert "mro" in result - assert len(result["mro"]) > 0 - - def test_mro_qualnames(self): - """Test MRO returns qualified names.""" - from scitex.introspect import get_mro - - result = get_mro("pathlib.Path") - assert result["success"] is True - # Each entry should be a qualified name - for name in result["mro"]: - assert "." in name diff --git a/tests/scitex/introspect/test__docstring.py b/tests/scitex/introspect/test__docstring.py deleted file mode 100755 index f836eb665..000000000 --- a/tests/scitex/introspect/test__docstring.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__docstring.py - -"""Tests for scitex.introspect._docstring module.""" - -import pytest - - -class TestGetDocstring: - """Tests for get_docstring function.""" - - def test_get_docstring_success(self): - """Test getting docstring successfully.""" - from scitex.introspect import get_docstring - - result = get_docstring("json.dumps") - assert result["success"] is True - assert "docstring" in result - assert len(result["docstring"]) > 0 - - def test_docstring_raw_format(self): - """Test raw format returns full docstring.""" - from scitex.introspect import get_docstring - - result = get_docstring("json.dumps", format="raw") - assert result["success"] is True - assert "docstring" in result - - def test_docstring_summary_format(self): - """Test summary format returns first line.""" - from scitex.introspect import get_docstring - - result = get_docstring("json.dumps", format="summary") - assert result["success"] is True - assert "docstring" in result - # Summary should be shorter than full docstring - raw_result = get_docstring("json.dumps", format="raw") - assert len(result["docstring"]) <= len(raw_result["docstring"]) - - def test_docstring_parsed_format(self): - """Test parsed format extracts sections.""" - from scitex.introspect import get_docstring - - result = get_docstring("json.dumps", format="parsed") - assert result["success"] is True - assert "sections" in result - - def test_docstring_no_docstring(self): - """Test object without docstring.""" - from scitex.introspect import get_docstring - - # Create a function without docstring dynamically - result = get_docstring("builtins.None") - # Should handle gracefully - assert "success" in result - - def test_docstring_invalid_path(self): - """Test invalid path returns error.""" - from scitex.introspect import get_docstring - - result = get_docstring("nonexistent.module") - assert result["success"] is False - assert "error" in result diff --git a/tests/scitex/introspect/test__examples.py b/tests/scitex/introspect/test__examples.py deleted file mode 100755 index fd6105d74..000000000 --- a/tests/scitex/introspect/test__examples.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__examples.py - -"""Tests for scitex.introspect._examples module.""" - -import pytest - - -class TestFindExamples: - """Tests for find_examples function.""" - - def test_find_examples_success(self): - """Test finding examples successfully.""" - from scitex.introspect import find_examples - - result = find_examples("scitex.introspect.q") - assert result["success"] is True - assert "examples" in result - assert "count" in result - - def test_examples_with_search_paths(self): - """Test finding examples with custom search paths.""" - from scitex.introspect import find_examples - - result = find_examples( - "scitex.introspect.q", - search_paths=["tests/scitex/introspect"], - ) - assert result["success"] is True - - def test_examples_max_results(self): - """Test max_results limits output.""" - from scitex.introspect import find_examples - - result = find_examples("scitex.introspect.q", max_results=2) - assert result["success"] is True - assert len(result["examples"]) <= 2 - - def test_examples_has_context(self): - """Test examples include context.""" - from scitex.introspect import find_examples - - result = find_examples("scitex.introspect.q") - assert result["success"] is True - if result["examples"]: - for ex in result["examples"]: - assert "file" in ex - assert "line" in ex - assert "context" in ex - - def test_examples_no_results(self): - """Test no examples found returns empty list.""" - from scitex.introspect import find_examples - - result = find_examples("nonexistent_function_xyz123") - # May return success=False if function not found, or success=True with empty list - if result["success"]: - assert result["count"] == 0 - assert result["examples"] == [] - else: - # Function not found case - assert "error" in result diff --git a/tests/scitex/introspect/test__imports.py b/tests/scitex/introspect/test__imports.py deleted file mode 100755 index f2edb21cb..000000000 --- a/tests/scitex/introspect/test__imports.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__imports.py - -"""Tests for scitex.introspect._imports module.""" - -import pytest - - -class TestGetImports: - """Tests for get_imports function.""" - - def test_get_imports_success(self): - """Test getting imports successfully.""" - from scitex.introspect import get_imports - - result = get_imports("scitex.introspect._resolve") - assert result["success"] is True - assert "imports" in result - assert result["import_count"] > 0 - - def test_imports_categorized(self): - """Test imports are categorized correctly.""" - from scitex.introspect import get_imports - - result = get_imports("scitex.introspect._resolve", categorize=True) - assert result["success"] is True - assert "categories" in result - assert "stdlib" in result["categories"] - assert "third_party" in result["categories"] - assert "local" in result["categories"] - - def test_imports_not_categorized(self): - """Test imports without categorization.""" - from scitex.introspect import get_imports - - result = get_imports("scitex.introspect._resolve", categorize=False) - assert result["success"] is True - assert "categories" not in result - assert "imports" in result - - def test_imports_include_from_imports(self): - """Test from...import statements are included.""" - from scitex.introspect import get_imports - - result = get_imports("scitex.introspect._resolve") - assert result["success"] is True - # Should have 'from' type imports - from_imports = [i for i in result["imports"] if i["type"] == "from"] - assert len(from_imports) > 0 - - def test_imports_non_module_error(self): - """Test error when path is not a module.""" - from scitex.introspect import get_imports - - result = get_imports("json.dumps") - assert result["success"] is False - assert "not a module" in result["error"] - - -class TestGetDependencies: - """Tests for get_dependencies function.""" - - def test_get_dependencies_success(self): - """Test getting dependencies successfully.""" - from scitex.introspect import get_dependencies - - result = get_dependencies("scitex.introspect._resolve") - assert result["success"] is True - assert "dependencies" in result - assert result["dependency_count"] >= 0 - - def test_dependencies_non_recursive(self): - """Test non-recursive dependencies.""" - from scitex.introspect import get_dependencies - - result = get_dependencies("scitex.introspect._resolve", recursive=False) - assert result["success"] is True - assert "tree" not in result - - @pytest.mark.skip(reason="Recursive deps can timeout due to stdlib scanning") - def test_dependencies_recursive(self): - """Test recursive dependencies.""" - from scitex.introspect import get_dependencies - - # Use json module which has minimal dependencies - result = get_dependencies("json", recursive=True, max_depth=1) - assert result["success"] is True - # May have tree structure for recursive mode - assert "dependencies" in result or "tree" in result diff --git a/tests/scitex/introspect/test__members.py b/tests/scitex/introspect/test__members.py deleted file mode 100755 index 9c1585e95..000000000 --- a/tests/scitex/introspect/test__members.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__members.py - -"""Tests for scitex.introspect._members module.""" - -import pytest - - -class TestDir: - """Tests for dir function.""" - - def test_dir_success(self): - """Test listing members successfully.""" - from scitex.introspect import dir - - result = dir("json") - assert result["success"] is True - assert "members" in result - assert result["count"] > 0 - - def test_dir_public_filter(self): - """Test public filter excludes private members.""" - from scitex.introspect import dir - - result = dir("json", filter="public") - assert result["success"] is True - for m in result["members"]: - assert not m["name"].startswith("_") - - def test_dir_private_filter(self): - """Test private filter returns private members.""" - from scitex.introspect import dir - - result = dir("json", filter="private") - assert result["success"] is True - # All should start with _ but not __ - for m in result["members"]: - assert m["name"].startswith("_") and not m["name"].startswith("__") - - def test_dir_dunder_filter(self): - """Test dunder filter returns dunder members.""" - from scitex.introspect import dir - - result = dir("json", filter="dunder") - assert result["success"] is True - for m in result["members"]: - assert m["name"].startswith("__") - - def test_dir_kind_functions(self): - """Test filtering by function kind.""" - from scitex.introspect import dir - - result = dir("json", kind="functions") - assert result["success"] is True - for m in result["members"]: - assert m["kind"] == "function" - - def test_dir_kind_classes(self): - """Test filtering by class kind.""" - from scitex.introspect import dir - - result = dir("pathlib", kind="classes") - assert result["success"] is True - for m in result["members"]: - assert m["kind"] == "class" - - def test_dir_has_summary(self): - """Test members include summary from docstring.""" - from scitex.introspect import dir - - result = dir("json", filter="public") - assert result["success"] is True - # At least some members should have summaries - summaries = [m["summary"] for m in result["members"] if m["summary"]] - assert len(summaries) > 0 - - def test_dir_class_target(self): - """Test listing members of a class.""" - from scitex.introspect import dir - - result = dir("pathlib.Path") - assert result["success"] is True - assert result["count"] > 0 - - def test_dir_invalid_path(self): - """Test invalid path returns error.""" - from scitex.introspect import dir - - result = dir("nonexistent.module") - assert result["success"] is False - assert "error" in result - - -class TestGetExports: - """Tests for get_exports function.""" - - def test_get_exports_with_all(self): - """Test getting exports from module with __all__.""" - from scitex.introspect import get_exports - - result = get_exports("json") - assert result["success"] is True - assert "exports" in result - assert "has_all" in result - - def test_exports_without_all(self): - """Test getting exports from module without __all__.""" - from scitex.introspect import get_exports - - # Many stdlib modules don't have __all__ - result = get_exports("scitex.introspect._resolve") - assert result["success"] is True - # Should still return public members - - def test_exports_invalid_path(self): - """Test invalid path returns error.""" - from scitex.introspect import get_exports - - result = get_exports("nonexistent.module") - assert result["success"] is False diff --git a/tests/scitex/introspect/test__resolve.py b/tests/scitex/introspect/test__resolve.py deleted file mode 100755 index 2d7451453..000000000 --- a/tests/scitex/introspect/test__resolve.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__resolve.py - -"""Tests for scitex.introspect._resolve module.""" - -import pytest - - -class TestResolveObject: - """Tests for resolve_object function.""" - - def test_resolve_module(self): - """Test resolving a module.""" - from scitex.introspect import resolve_object - - obj, error = resolve_object("json") - assert error is None - assert obj is not None - import json - - assert obj is json - - def test_resolve_function(self): - """Test resolving a function.""" - from scitex.introspect import resolve_object - - obj, error = resolve_object("json.dumps") - assert error is None - import json - - assert obj is json.dumps - - def test_resolve_class(self): - """Test resolving a class.""" - from scitex.introspect import resolve_object - - obj, error = resolve_object("pathlib.Path") - assert error is None - from pathlib import Path - - assert obj is Path - - def test_resolve_nested(self): - """Test resolving nested attribute.""" - from scitex.introspect import resolve_object - - obj, error = resolve_object("collections.abc.Mapping") - assert error is None - from collections.abc import Mapping - - assert obj is Mapping - - def test_resolve_invalid_returns_error(self): - """Test resolving invalid path returns error.""" - from scitex.introspect import resolve_object - - obj, error = resolve_object("nonexistent.module.thing") - assert obj is None - assert error is not None - assert "Could not resolve" in error - - -class TestGetTypeInfo: - """Tests for get_type_info function.""" - - def test_type_info_module(self): - """Test type info for module.""" - import json - - from scitex.introspect import get_type_info - - info = get_type_info(json) - assert info["kind"] == "module" - - def test_type_info_function(self): - """Test type info for function.""" - import json - - from scitex.introspect import get_type_info - - info = get_type_info(json.dumps) - assert info["kind"] == "function" - - def test_type_info_class(self): - """Test type info for class.""" - from pathlib import Path - - from scitex.introspect import get_type_info - - info = get_type_info(Path) - assert info["kind"] == "class" - - def test_type_info_data(self): - """Test type info for data.""" - from scitex.introspect import get_type_info - - info = get_type_info([1, 2, 3]) - assert info["kind"] == "data" diff --git a/tests/scitex/introspect/test__signature.py b/tests/scitex/introspect/test__signature.py deleted file mode 100755 index 48a8a81e6..000000000 --- a/tests/scitex/introspect/test__signature.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__signature.py - -"""Tests for scitex.introspect._signature module.""" - -import pytest - - -class TestQ: - """Tests for q function.""" - - def test_q_success(self): - """Test getting signature successfully.""" - from scitex.introspect import q - - result = q("json.dumps") - assert result["success"] is True - assert result["name"] == "dumps" - assert "signature" in result - assert "parameters" in result - - def test_q_with_annotations(self): - """Test signature includes type annotations.""" - from scitex.introspect import q - - result = q("json.dumps", include_annotations=True) - assert result["success"] is True - # json.dumps has annotations in newer Python - assert "parameters" in result - - def test_q_without_defaults(self): - """Test signature without default values.""" - from scitex.introspect import q - - result = q("json.dumps", include_defaults=False) - assert result["success"] is True - # Check no defaults in parameters - for param in result["parameters"]: - assert "default" not in param - - def test_q_class(self): - """Test getting signature of a class (its __init__).""" - from scitex.introspect import q - - result = q("pathlib.Path") - assert result["success"] is True - assert result["type_info"]["kind"] == "class" - - def test_q_invalid_path(self): - """Test invalid path returns error.""" - from scitex.introspect import q - - result = q("nonexistent.module") - assert result["success"] is False - assert "error" in result - - def test_q_builtin(self): - """Test signature of builtin may fail gracefully.""" - from scitex.introspect import q - - result = q("len") - # Builtins may not have introspectable signatures - assert "success" in result diff --git a/tests/scitex/introspect/test__source.py b/tests/scitex/introspect/test__source.py deleted file mode 100755 index db944ff00..000000000 --- a/tests/scitex/introspect/test__source.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__source.py - -"""Tests for scitex.introspect._source module.""" - -import pytest - - -class TestQQ: - """Tests for qq function.""" - - def test_qq_success(self): - """Test getting source successfully.""" - from scitex.introspect import qq - - result = qq("scitex.introspect._resolve.resolve_object") - assert result["success"] is True - assert "source" in result - assert "file" in result - assert "line_start" in result - assert "line_count" in result - - def test_qq_contains_def(self): - """Test source contains function definition.""" - from scitex.introspect import qq - - result = qq("scitex.introspect._resolve.resolve_object") - assert result["success"] is True - assert "def resolve_object" in result["source"] - - def test_qq_max_lines(self): - """Test max_lines limits output.""" - from scitex.introspect import qq - - result = qq("scitex.introspect._resolve.resolve_object", max_lines=5) - assert result["success"] is True - lines = result["source"].strip().split("\n") - # May include a truncation indicator line, so allow +1 - assert len(lines) <= 6 - - def test_qq_without_decorators(self): - """Test source excludes decorators when requested.""" - from scitex.introspect import qq - - result = qq( - "scitex.introspect._resolve.resolve_object", include_decorators=False - ) - assert result["success"] is True - # First line should be def, not @decorator - first_line = result["source"].strip().split("\n")[0].strip() - assert first_line.startswith("def ") - - def test_qq_builtin_fails(self): - """Test getting source of builtin fails gracefully.""" - from scitex.introspect import qq - - result = qq("len") - assert result["success"] is False - # Builtins don't have Python source - - def test_qq_class(self): - """Test getting source of a class.""" - from scitex.introspect import qq - - result = qq("pathlib.PurePath") - assert result["success"] is True - assert "class PurePath" in result["source"] - - def test_qq_invalid_path(self): - """Test invalid path returns error.""" - from scitex.introspect import qq - - result = qq("nonexistent.module") - assert result["success"] is False - assert "error" in result diff --git a/tests/scitex/introspect/test__type_hints.py b/tests/scitex/introspect/test__type_hints.py deleted file mode 100755 index 3c002c01c..000000000 --- a/tests/scitex/introspect/test__type_hints.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -# Timestamp: 2025-01-20 -# File: tests/scitex/introspect/test__type_hints.py - -"""Tests for scitex.introspect._type_hints module.""" - -import pytest - - -class TestGetTypeHintsDetailed: - """Tests for get_type_hints_detailed function.""" - - def test_get_type_hints_success(self): - """Test getting type hints successfully.""" - from scitex.introspect import get_type_hints_detailed - - result = get_type_hints_detailed("scitex.introspect._resolve.resolve_object") - assert result["success"] is True - assert "hints" in result or "hint_count" in result - - def test_type_hints_with_optional(self): - """Test detecting optional type hints.""" - from scitex.introspect import get_type_hints_detailed - - result = get_type_hints_detailed("scitex.introspect._signature.q") - assert result["success"] is True - # Check for optional detection in hints - if result.get("hints"): - for name, info in result["hints"].items(): - assert "is_optional" in info - - def test_type_hints_return_type(self): - """Test return type is included.""" - from scitex.introspect import get_type_hints_detailed - - result = get_type_hints_detailed("scitex.introspect._resolve.resolve_object") - assert result["success"] is True - # May or may not have return hint depending on function - - def test_type_hints_class(self): - """Test type hints for class methods.""" - from scitex.introspect import get_type_hints_detailed - - result = get_type_hints_detailed("pathlib.Path") - assert result["success"] is True - - def test_type_hints_no_hints(self): - """Test function without type hints.""" - from scitex.introspect import get_type_hints_detailed - - # Some functions may not have hints - result = get_type_hints_detailed("json.loads") - assert result["success"] is True - # Should return empty or minimal hints - - def test_type_hints_invalid_path(self): - """Test invalid path returns error.""" - from scitex.introspect import get_type_hints_detailed - - result = get_type_hints_detailed("nonexistent.module") - assert result["success"] is False - - -class TestGetClassAnnotations: - """Tests for get_class_annotations function.""" - - def test_get_class_annotations_success(self): - """Test getting class annotations.""" - from scitex.introspect import get_class_annotations - - result = get_class_annotations("pathlib.PurePath") - assert result["success"] is True - # Result contains class_vars and methods, not annotations - assert "class_vars" in result or "methods" in result - - def test_class_annotations_non_class(self): - """Test error when path is not a class.""" - from scitex.introspect import get_class_annotations - - result = get_class_annotations("json.dumps") - assert result["success"] is False - assert "not a class" in result["error"] or "error" in result