From 60ccfcc07eb8dc816da81428f5bf32879ecbe094 Mon Sep 17 00:00:00 2001 From: Berend Klein Haneveld Date: Fri, 5 Jun 2026 10:00:56 +0200 Subject: [PATCH] Write compiled AST to temp file when CGX_DEBUG is set When CGX_DEBUG is enabled, write the formatted generated Python source to a temporary file and use it as the compile filename. The formatted source is reparsed so line numbers in the code object match the file on disk, allowing debuggers (pdb, PyCharm, VS Code) to step through the generated render methods with correct source display. --- collagraph/sfc/__init__.py | 58 ++++++++++++++++++++++++++++++++++++-- collagraph/sfc/compiler.py | 18 ------------ 2 files changed, 56 insertions(+), 20 deletions(-) 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