diff --git a/collagraph/sfc/__init__.py b/collagraph/sfc/__init__.py index 4bb28b2..43f89c2 100644 --- a/collagraph/sfc/__init__.py +++ b/collagraph/sfc/__init__.py @@ -1,6 +1,6 @@ from collagraph import Component -from .compiler import construct_ast +from .compiler import DEBUG, construct_ast, format_code def load(path, namespace=None): @@ -43,7 +43,19 @@ def load_from_string(template, path=None, namespace=None): tree, name = construct_ast(path=path, template=template) # Compile the tree into a code object (module) - code = compile(tree, filename=str(path), mode="exec") + # When CGX_DEBUG is set, write the AST to a temporary Python file + # and reparse it so that debuggers can step through the generated + # source with correct line numbers. + filename = str(path) + if DEBUG: # pragma: no cover + debug_file, source = _write_debug_file(tree, path) + if debug_file: + import ast + + filename = str(debug_file) + tree = ast.parse(source, filename=filename) + + code = compile(tree, filename=filename, mode="exec") # Execute the code as module and pass a dictionary that will capture # the global and local scope of the module if namespace is None: @@ -60,3 +72,45 @@ def load_from_string(template, path=None, namespace=None): namespace["__component_class"] = component_class namespace["__component_name"] = name return component_class, namespace + + +def _write_debug_file(tree, path): # pragma: no cover + """Write the compiled AST to a temporary Python file for debugging. + + Returns a tuple of (path, formatted_source), or (None, None) if writing fails. + """ + import ast + import logging + import tempfile + from pathlib import Path + + logger = logging.getLogger(__name__) + + try: + plain_result = ast.unparse(tree) + formatted = format_code(plain_result) + + # Create a meaningful filename based on the source .cgx file + source_name = Path(path).stem if path else "template" + debug_file = Path(tempfile.mktemp(prefix=f"cgx_{source_name}_", suffix=".py")) + debug_file.write_text(formatted, encoding="utf-8") + logger.debug("CGX debug file written to: %s", debug_file) + + try: + from rich.console import Console + from rich.syntax import Syntax + + console = Console() + syntax = Syntax(formatted, "python") + console.print(f"#---{path}---") + console.print(syntax) + console.print(f"[dim]Debug file: {debug_file}[/dim]") + except ImportError: + print(f"#---{path}---") # noqa: T201 + print(formatted) # noqa: T201 + print(f"Debug file: {debug_file}") # noqa: T201 + + return debug_file, formatted + except Exception as e: + logger.warning("Could not write AST debug file", exc_info=e) + return None, None diff --git a/collagraph/sfc/compiler.py b/collagraph/sfc/compiler.py index 1d59409..160534a 100644 --- a/collagraph/sfc/compiler.py +++ b/collagraph/sfc/compiler.py @@ -94,11 +94,6 @@ class definition as `render` function. # method to fix any `lineno` and `col_offset` attributes of the nodes ast.fix_missing_locations(script_tree) - if DEBUG: # pragma: no cover - try: - _print_ast_tree_as_code(script_tree, path) - except Exception as e: - logger.warning("Could not unparse AST", exc_info=e) return script_tree, component_def.name @@ -1008,19 +1003,6 @@ def check_parsed_tree(node: Element): ) -def _print_ast_tree_as_code(tree, path): # pragma: no cover - """Handy function for debugging an ast tree""" - from rich.console import Console - from rich.syntax import Syntax - - plain_result = ast.unparse(tree) - formatted = format_code(plain_result) - console = Console() - syntax = Syntax(formatted, "python") - console.print(f"#---{path}---") - console.print(syntax) - - def format_code(code): # pragma: no cover """ Format the given code string with ruff