Skip to content

Commit e6c84a2

Browse files
Add --show-code flag to collagraph CLI
Pretty print the Python code that is compiled for a .cgx component (with rich when available, plain print otherwise) and exit, instead of rendering the component. This provides a direct way to inspect the generated code, complementing the CGX_DEBUG temp-file flow. The printing logic is extracted from _write_debug_file into a shared print_source helper, and a generate_source function is added to the compiler. format_code now also falls back to unformatted code when ruff is not installed, since the CLI can run without dev dependencies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 239d843 commit e6c84a2

5 files changed

Lines changed: 94 additions & 22 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ Instead of using a python file as an entry point to run components, you can run
7575
uv run collagraph examples/pyside/counter.cgx
7676
```
7777

78+
To inspect the Python code that is compiled for a component, use the `--show-code` flag:
79+
80+
```sh
81+
uv run collagraph --show-code examples/pyside/counter.cgx
82+
```
83+
7884
For more examples, please take a look at the [examples folder](examples).
7985

8086
Currently there are two renderers:

collagraph/__main__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ def animate():
8181
breakpoint() # noqa: T100
8282

8383

84+
def show_code(component_path: Path):
85+
"""Pretty print the Python code that is compiled for a component."""
86+
from collagraph.sfc import print_source
87+
from collagraph.sfc.compiler import generate_source
88+
89+
print_source(generate_source(component_path), component_path)
90+
91+
8492
def existing_component_file(value):
8593
path = Path(value)
8694
if not path.exists():
@@ -134,8 +142,17 @@ def run():
134142
action="store_true",
135143
help="Enable hot reloading (reload on file changes)",
136144
)
145+
parser.add_argument(
146+
"--show-code",
147+
action="store_true",
148+
help="Pretty print the compiled Python code for the component and exit",
149+
)
137150
args = parser.parse_args()
138151

152+
if args.show_code:
153+
show_code(args.component)
154+
return
155+
139156
init_collagraph(args.renderer, args.component, args.state, args.hot_reload)
140157

141158

collagraph/sfc/__init__.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,27 @@ def load_from_string(template, path=None, namespace=None):
8383
return component_class, namespace
8484

8585

86-
def _write_debug_file(tree, path): # pragma: no cover
86+
def print_source(source, path, footer=None):
87+
"""Pretty print (Python) source with rich, when available,
88+
falling back to plain print otherwise.
89+
"""
90+
try:
91+
from rich.console import Console
92+
from rich.syntax import Syntax
93+
94+
console = Console()
95+
console.print(f"#---{path}---")
96+
console.print(Syntax(source, "python", line_numbers=True))
97+
if footer:
98+
console.print(f"[dim]{footer}[/dim]")
99+
except ImportError:
100+
print(f"#---{path}---") # noqa: T201
101+
print(source) # noqa: T201
102+
if footer:
103+
print(footer) # noqa: T201
104+
105+
106+
def _write_debug_file(tree, path):
87107
"""Write the compiled AST to a temporary Python file for debugging.
88108
89109
Returns a tuple of (path, formatted_source), or (None, None) if writing fails.
@@ -96,8 +116,7 @@ def _write_debug_file(tree, path): # pragma: no cover
96116
logger = logging.getLogger(__name__)
97117

98118
try:
99-
plain_result = ast.unparse(tree)
100-
formatted = format_code(plain_result)
119+
formatted = format_code(ast.unparse(tree))
101120

102121
# Create a meaningful filename based on the source .cgx file
103122
source_name = Path(path).stem if path else "template"
@@ -112,19 +131,7 @@ def _write_debug_file(tree, path): # pragma: no cover
112131
debug_file = Path(fh.name)
113132
logger.debug("CGX debug file written to: %s", debug_file)
114133

115-
try:
116-
from rich.console import Console
117-
from rich.syntax import Syntax
118-
119-
console = Console()
120-
syntax = Syntax(formatted, "python")
121-
console.print(f"#---{path}---")
122-
console.print(syntax)
123-
console.print(f"[dim]Debug file: {debug_file}[/dim]")
124-
except ImportError:
125-
print(f"#---{path}---") # noqa: T201
126-
print(formatted) # noqa: T201
127-
print(f"Debug file: {debug_file}") # noqa: T201
134+
print_source(formatted, path, footer=f"Debug file: {debug_file}")
128135

129136
return debug_file, formatted
130137
except Exception as e:

collagraph/sfc/compiler.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,13 +1011,26 @@ def format_code(code):
10111011
"""
10121012
from subprocess import run
10131013

1014-
result = run(
1015-
["ruff", "format", "-"],
1016-
input=code,
1017-
encoding="utf-8",
1018-
capture_output=True,
1019-
)
1014+
try:
1015+
result = run(
1016+
["ruff", "format", "-"],
1017+
input=code,
1018+
encoding="utf-8",
1019+
capture_output=True,
1020+
)
1021+
except FileNotFoundError:
1022+
logger.warning("Could not format code: ruff not found")
1023+
return code
10201024
if result.returncode != 0 or not result.stdout:
10211025
logger.warning("Could not format code with ruff: %s", result.stderr)
10221026
return code
10231027
return result.stdout
1028+
1029+
1030+
def generate_source(path, template=None):
1031+
"""
1032+
Return the formatted Python source that is generated
1033+
for the .cgx file at the given path.
1034+
"""
1035+
tree, _ = construct_ast(path=path, template=template)
1036+
return format_code(ast.unparse(tree))

tests/test_show_code.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Tests for the --show-code CLI feature."""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
from collagraph.__main__ import run
7+
from collagraph.sfc.compiler import generate_source
8+
9+
EXAMPLE_CGX = Path(__file__).parent / "data" / "example.cgx"
10+
11+
12+
def test_generate_source():
13+
source = generate_source(EXAMPLE_CGX)
14+
15+
assert "class Example(cg.Component):" in source
16+
assert "def render(self, renderer):" in source
17+
# The generated source should be valid Python
18+
compile(source, "example.py", mode="exec")
19+
20+
21+
def test_cli_show_code(monkeypatch, capsys):
22+
monkeypatch.setattr(sys, "argv", ["collagraph", "--show-code", str(EXAMPLE_CGX)])
23+
24+
run()
25+
26+
captured = capsys.readouterr()
27+
assert "example.cgx" in captured.out
28+
assert "class Example" in captured.out
29+
assert "def render" in captured.out

0 commit comments

Comments
 (0)