Source code for tools.code_dochelper

import importlib
import inspect
import logging
from functools import lru_cache

from cicada.common.utils import colorstring

logger = logging.getLogger(__name__)


[docs] class CodeDocHelper: def __init__(self): """ Initialize the CodeDocHelper """ pass
[docs] def get_function_info(self, function_path, with_docstring=False): """ Retrieve the signature and optionally the docstring of a function or class method. Parameters: function_path (str): The full import path to the function (e.g., "build123d.Box.bounding_box"). with_docstring (bool): If True, include the docstring in the output. Returns: dict: A dictionary containing the function name, signature, and docstring (optional). """ logger.debug( colorstring( f"Getting function info for {function_path}", color="bright_blue" ) ) try: parts = function_path.split(".") module_name = parts[0] module = importlib.import_module(module_name) # Traverse the path to get the function or method obj = module for part in parts[1:]: obj = getattr(obj, part) # Check if the object is a function or method if inspect.isroutine(obj): signature = str(inspect.signature(obj)) if inspect.ismethod(obj) and signature.startswith("(self, "): signature = f"({signature[7:]}" # Remove "(self, " # # `parts[-1]` version: clearer to read, but may be ambiguous for llm # data = {"name": parts[-1], "signature": f"{parts[-1]}{signature}"} # `function_path` version: more verbose, but provide which module it comes from data = { "name": function_path, "signature": f"{function_path}{signature}", } if with_docstring: data["docstring"] = inspect.getdoc(obj) or "No docstring available." return data return {"error": f"Object '{function_path}' is not a function or method."} except Exception as e: return {"error": f"Error getting function info: {e}"}
[docs] @lru_cache(maxsize=128) def get_class_info(self, class_path, with_docstring=False): """ Retrieve the signature, methods, variables, and optionally the docstring of a class. Parameters: class_path (str): The full import path to the class (e.g., "build123d.Box"). with_docstring (bool): If True, include the docstring in the output. Returns: dict: A dictionary containing the class name, signature, flattened methods, variables, and docstring (optional). """ logger.debug(colorstring(f"Getting class info for {class_path}", "cyan")) try: parts = class_path.split(".") module_name = parts[0] module = importlib.import_module(module_name) # Traverse the path to get the class cls = module for part in parts[1:]: cls = getattr(cls, part) # Handle __init__ method init_func = cls.__init__ if "__init__" in cls.__dict__ else None signature = str(inspect.signature(init_func)) if init_func else "()" if signature.startswith("(self, "): signature = signature.replace("(self, ", "(") data = { "name": class_path, "signature": f"{class_path}{signature}", "methods": [], # Flattened methods (as standalone functions) "variables": [], } if with_docstring: data["docstring"] = inspect.getdoc(cls) or "No docstring available." # Collect methods and flatten them using get_function_info for name, member in inspect.getmembers(cls): if inspect.isroutine(member) and not name.startswith("_"): method_path = f"{class_path}.{name}" method_info = self.get_function_info(method_path, with_docstring) if "error" not in method_info: data["methods"].append(method_info) # Collect variables for name, member in inspect.getmembers(cls): if not inspect.isroutine(member) and not name.startswith("_"): data["variables"].append(name) return data except Exception as e: return {"error": f"Error getting class info: {e}"}
[docs] def get_module_info(self, module_name, with_docstring=False): """ Retrieve the classes, functions, variables, and optionally the docstring of a module. Parameters: module_name (str): The name of the module. with_docstring (bool): If True, include the docstring in the output. Returns: dict: A dictionary containing the module name, classes, functions, variables, and docstring (optional). """ logger.debug(colorstring(f"Getting module info for {module_name}", "yellow")) try: module = importlib.import_module(module_name) data = { "name": module_name, "classes": [], "functions": [], "variables": [], } if with_docstring: data["docstring"] = inspect.getdoc(module) or "No docstring available." # Helper function to check if member belongs to the module def belongs_to_module(member): return hasattr(member, "__module__") and member.__module__.startswith( module_name ) # Collect classes for name, member in inspect.getmembers(module, inspect.isclass): if belongs_to_module(member): class_info = self.get_class_info( f"{module_name}.{name}", with_docstring ) if "error" not in class_info: data["classes"].append(class_info) # Collect functions (including built-in functions) for name, member in inspect.getmembers( module, lambda m: inspect.isroutine(m) ): if belongs_to_module(member): func_info = self.get_function_info( f"{module_name}.{name}", with_docstring ) if "error" not in func_info: data["functions"].append(func_info) # Collect variables for name, member in inspect.getmembers(module): if ( not inspect.isclass(member) and not inspect.isroutine(member) and not name.startswith("_") and belongs_to_module(member) ): variable_info = { "name": name, "value": str(member), "type": type(member).__name__, } data["variables"].append(variable_info) return data except ImportError: return {"error": f"Module '{module_name}' not found."} except Exception as e: return {"error": f"An unexpected error occurred: {str(e)}"}
[docs] def get_info(self, path, with_docstring=True): """ Retrieve information about a module, class, function, or variable. """ try: parts = path.split(".") module_name = parts[0] logger.debug(f"Importing module: {module_name}") module = importlib.import_module(module_name) logger.debug(f"Successfully imported module: {module_name}") # Traverse the path to get the member obj = module for part in parts[1:]: logger.debug(f"Traversing path: {part}") obj = getattr(obj, part) logger.debug(f"Retrieved object: {obj}") logger.debug(f"Inspecting object: {obj}") # Determine the type of the member if ( inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj) ): logger.debug(colorstring(f"Detected function: {path}", "magenta")) result = self.get_function_info(path, with_docstring=with_docstring) elif inspect.isclass(obj): logger.debug(colorstring(f"Detected class: {path}", "cyan")) result = self.get_class_info(path, with_docstring=with_docstring) elif inspect.ismodule(obj): logger.debug(colorstring(f"Detected module: {path}", "yellow")) result = self.get_module_info(path, with_docstring=with_docstring) else: logger.debug(f"Detected variable: {path}") # Handle variables or other types result = { "name": path, "type": type(obj).__name__, "value": str(obj), } # Save debug info logger.debug(f"Saving debug info to debug_info.json") if logging.root.level == logging.DEBUG: import json with open("debug_info.json", "w") as f: json.dump(result, f, indent=4) return result except ImportError as e: logger.error(f"ImportError: {e}") return {"error": f"Module '{module_name}' not found."} except AttributeError as e: logger.error(f"AttributeError: {e}") return { "error": f"Member '{parts[-1]}' not found in module '{module_name}'." } except Exception as e: logger.error(f"Unexpected error: {e}") return {"error": f"An unexpected error occurred: {str(e)}"}
[docs] def dict_to_markdown(self, data, show_docstring=True): """ Convert a dictionary containing module, class, function, or variable information into a Markdown-formatted string. Parameters: data (dict): A dictionary containing module, class, function, or variable information. show_docstring (bool): If True, include docstrings in the output. Returns: str: A Markdown-formatted string. """ if "error" in data: return f"# Error\n{data['error']}" if "classes" in data and "functions" in data: # It's module info markdown_output = f"# Module: {data['name']}\n\n" if show_docstring and "docstring" in data: markdown_output += ( f"- **Docstring**:\n```markdown\n{data['docstring']}\n```\n\n" ) if data["classes"]: markdown_output += "## Classes\n" for cls in data["classes"]: markdown_output += f"### {cls['name']}\n" markdown_output += f"- **Signature**: `{cls['signature']}`\n" if show_docstring and "docstring" in cls: markdown_output += f"- **Docstring**:\n```markdown\n{cls['docstring']}\n```\n\n" if data["functions"]: markdown_output += "## Functions\n" for func in data["functions"]: markdown_output += f"### {func['name']}\n" markdown_output += f"- **Signature**: `{func['signature']}`\n" if show_docstring and "docstring" in func: markdown_output += f"- **Docstring**:\n```markdown\n{func['docstring']}\n```\n\n" if data["variables"]: markdown_output += "## Variables\n" for var in data["variables"]: markdown_output += ( f"- **{var['name']}** (`{var['type']}`): {var['value']}\n" ) return markdown_output elif "methods" in data: # It's class info markdown_output = f"# Class: {data['name']}\n\n" markdown_output += f"- **Signature**: `{data['signature']}`\n" if show_docstring and "docstring" in data: markdown_output += ( f"- **Docstring**:\n```markdown\n{data['docstring']}\n```\n\n" ) markdown_output += "## Methods\n" for method in data["methods"]: markdown_output += f"### {method['name']}\n" markdown_output += f"- **Signature**: `{method['signature']}`\n" if show_docstring and "docstring" in method: markdown_output += ( f"- **Docstring**:\n```markdown\n{method['docstring']}\n```\n\n" ) markdown_output += "## Variables\n" for var in data["variables"]: markdown_output += f"- {var}\n" return markdown_output elif "signature" in data: # It's function info markdown_output = f"# Function: {data['name']}\n\n" markdown_output += f"- **Signature**: `{data['signature']}`\n" if show_docstring and "docstring" in data: markdown_output += ( f"- **Docstring**:\n```markdown\n{data['docstring']}\n```\n" ) return markdown_output elif "type" in data: # It's a variable markdown_output = f"# Variable: {data['name']}\n\n" markdown_output += f"- **Type**: `{data['type']}`\n" markdown_output += f"- **Value**: `{data['value']}`\n" return markdown_output else: return "# Unknown\nInvalid data format."
[docs] def doc_helper(import_path, with_docstring: bool = False): """ Query module, class, function, or method information and return it in Markdown format. """ _helper = CodeDocHelper() info = _helper.get_info(import_path, with_docstring=with_docstring) markdown_formatted_str = _helper.dict_to_markdown( info, show_docstring=with_docstring ) return markdown_formatted_str
if __name__ == "__main__": import argparse from cicada.common.utils import colorstring, setup_logging # For fuzzy matching parser = argparse.ArgumentParser( description="Query module, class, function, or method information." ) parser.add_argument( "path", type=str, default="math.sqrt", nargs="?", # Makes the path argument optional (consumes one argument if provided) help="The full import path or keyword to query (e.g., 'Box' or 'build123d.Box').", ) parser.add_argument( "--docstring", action="store_true", help="Include the docstring in the output (only applies to path queries).", ) parser.add_argument( "--no_output", action="store_true", help="Disable output to the console.", ) parser.add_argument("--debug", action="store_true", help="Enable debug logging.") args = parser.parse_args() setup_logging( log_level="DEBUG" if args.debug else "INFO", log_file="/tmp/cicada/code_dochelper.log", ) # Initialize the CodeDocHelper helper = CodeDocHelper() # Get the information based on the provided path info = helper.get_info(args.path, with_docstring=args.docstring) # Convert the information to Markdown and print it markdown_formatted_str = helper.dict_to_markdown( info, show_docstring=args.docstring ) print(markdown_formatted_str) # Save debug info logger.debug(f"Saving debug info to debug_info.json") if logging.root.level == logging.DEBUG: # Save the output to a file with open("debug_info.md", "w") as f: f.write(markdown_formatted_str)