Source code for firecrown.fctools.print_code

"""Module for displaying class definitions with attributes and decorators.

This module provides utilities to inspect and display Python class
definitions in a formatted way suitable for syntax highlighting.
"""

import ast
import inspect
from typing import TYPE_CHECKING, Any, List

import typer
from rich import print as richprint
from rich.console import Console

if TYPE_CHECKING:
    from .ast_utils import format_class_docstring, get_class_definition
    from .common import import_class_from_path
else:
    try:
        from .ast_utils import format_class_docstring, get_class_definition
        from .common import import_class_from_path
    except ImportError:  # pragma: no cover
        from ast_utils import format_class_docstring, get_class_definition
        from common import import_class_from_path


def _render_attributes(class_def: ast.ClassDef) -> list[str]:
    """Render class attributes as formatted code lines."""
    lines: list[str] = []
    for item in class_def.body:
        if isinstance(item, (ast.AnnAssign, ast.Assign)):
            lines.append(f"    {ast.unparse(item)}")
    return lines


def _build_class_code(cls: type[Any]) -> str:
    source = inspect.getsource(cls)
    class_def = get_class_definition(source, cls.__name__)
    if class_def is None:
        raise ValueError(f"Could not find class definition for {cls.__name__}")

    code_lines: list[str] = []
    for decorator in class_def.decorator_list:
        code_lines.append(f"@{ast.unparse(decorator)}")

    bases_str = ", ".join(ast.unparse(base) for base in class_def.bases)
    code_lines.append(f"class {class_def.name}({bases_str}):")

    # Add formatted docstring
    docstring_lines = format_class_docstring(class_def)
    code_lines.extend(docstring_lines)

    code_lines.extend(_render_attributes(class_def))

    return "\n".join(code_lines)


[docs] def display_class_attributes(cls: type[Any]) -> None: """Display class definition with attributes and decorators. Formatted for syntax highlighting in Quarto/Jupyter. Args: cls: The class to display """ try: code_str = _build_class_code(cls) except OSError: # pragma: no cover # Defensive: inspect.getsource raises TypeError for built-ins, not OSError # OSError would require file read failure for an importable class (very rare) richprint( f"Source code not available for {cls.__name__} " f"(likely a built-in or C extension class)" ) return richprint("```python") richprint(code_str) richprint("```")
[docs] def display_class_without_markdown(cls: type[Any]) -> None: """Display class definition without markdown code blocks. Same as display_class_attributes but outputs plain code without markdown wrapper for syntax highlighting. Args: cls: The class to display """ try: code_str = _build_class_code(cls) except OSError: # pragma: no cover # Defensive: inspect.getsource raises TypeError for built-ins, not OSError # OSError would require file read failure for an importable class (very rare) richprint( f"Source code not available for {cls.__name__} " f"(likely a built-in or C extension class)" ) return richprint(code_str)
app = typer.Typer()
[docs] @app.command() def main( class_names: List[str] = typer.Argument( ..., help="One or more fully qualified class names" ), no_markdown: bool = typer.Option( False, "--no-markdown", help="Output plain code without markdown code blocks" ), ): """Display class definitions with attributes and decorators. This tool inspects Python classes and displays their definitions in a formatted way, showing decorators, inheritance, docstrings, and class attributes (but not methods). """ console = Console() # Select the appropriate display function based on the no_markdown flag if no_markdown: display_class_attributes_func = display_class_without_markdown else: display_class_attributes_func = display_class_attributes for class_name in class_names: try: cls = import_class_from_path(console, class_name) if len(class_names) > 1: print(f"\n{'=' * 60}") richprint(f"Class: {class_name}") richprint("=" * 60) display_class_attributes_func(cls) if len(class_names) > 1: richprint() except (ImportError, ValueError, AttributeError) as e: # pragma: no cover # Defensive: import_class_from_path calls cli_error -> sys.exit(1) # So SystemExit is raised instead of ImportError/ValueError/AttributeError richprint(f"Could not import or display class {class_name}") richprint(f"Error message: {e}") if len(class_names) > 1: richprint()
if __name__ == "__main__": # pragma: no cover app()