Skip to content

Commit 3120021

Browse files
author
codegen-bot
committed
run now works
1 parent e656726 commit 3120021

File tree

5 files changed

+152
-104
lines changed

5 files changed

+152
-104
lines changed
Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22

3-
import rich
43
import rich_click as click
54

65
from codegen.cli.auth.session import CodegenSession
@@ -13,59 +12,35 @@
1312
@requires_init
1413
@click.argument("label", required=True)
1514
@click.option("--web", is_flag=True, help="Run the function on the web service instead of locally")
16-
@click.option("--apply-local", is_flag=True, help="Applies the generated diff to the repository")
1715
@click.option("--diff-preview", type=int, help="Show a preview of the first N lines of the diff")
1816
@click.option("--arguments", type=str, help="Arguments as a json string to pass as the function's 'arguments' parameter")
1917
def run_command(
2018
session: CodegenSession,
2119
label: str,
2220
web: bool = False,
23-
apply_local: bool = False,
2421
diff_preview: int | None = None,
2522
arguments: str | None = None,
2623
):
2724
"""Run a codegen function by its label."""
28-
# First try to find it as a stored codemod
29-
codemod = CodemodManager.get(label)
30-
if codemod:
31-
if codemod.arguments_type_schema and not arguments:
32-
raise click.ClickException(f"This function requires the --arguments parameter. Expected schema: {codemod.arguments_type_schema}")
25+
# Get and validate the codemod
26+
codemod = CodemodManager.get_codemod(label)
3327

34-
if codemod.arguments_type_schema and arguments:
35-
arguments_json = json.loads(arguments)
36-
is_valid = validate_json(codemod.arguments_type_schema, arguments_json)
37-
if not is_valid:
38-
raise click.ClickException(f"Invalid arguments format. Expected schema: {codemod.arguments_type_schema}")
28+
# Handle arguments if needed
29+
if codemod.arguments_type_schema and not arguments:
30+
raise click.ClickException(f"This function requires the --arguments parameter. Expected schema: {codemod.arguments_type_schema}")
3931

40-
if web:
41-
from codegen.cli.commands.run.run_cloud import run_cloud
42-
43-
run_cloud(session, codemod, apply_local=apply_local, diff_preview=diff_preview)
44-
else:
45-
from codegen.cli.commands.run.run_local import run_local
46-
47-
run_local(session, codemod, apply_local=apply_local, diff_preview=diff_preview)
48-
return
49-
50-
# If not found as a stored codemod, look for decorated functions
51-
functions = CodemodManager.get_decorated()
52-
matching = [f for f in functions if f.name == label]
53-
54-
if not matching:
55-
raise click.ClickException(f"No function found with label '{label}'")
56-
57-
if len(matching) > 1:
58-
# If multiple matches, show their locations
59-
rich.print(f"[yellow]Multiple functions found with label '{label}':[/yellow]")
60-
for func in matching:
61-
rich.print(f" • {func.filepath}")
62-
raise click.ClickException("Please specify the exact file with codegen run <path>")
32+
if codemod.arguments_type_schema and arguments:
33+
arguments_json = json.loads(arguments)
34+
is_valid = validate_json(codemod.arguments_type_schema, arguments_json)
35+
if not is_valid:
36+
raise click.ClickException(f"Invalid arguments format. Expected schema: {codemod.arguments_type_schema}")
6337

38+
# Run the codemod
6439
if web:
6540
from codegen.cli.commands.run.run_cloud import run_cloud
6641

67-
run_cloud(session, matching[0], apply_local=apply_local, diff_preview=diff_preview)
42+
run_cloud(session, codemod, diff_preview=diff_preview)
6843
else:
6944
from codegen.cli.commands.run.run_local import run_local
7045

71-
run_local(session, matching[0], apply_local=apply_local, diff_preview=diff_preview)
46+
run_local(session, codemod, diff_preview=diff_preview)

src/codegen/cli/commands/run/run_local.py

Lines changed: 48 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,88 +2,70 @@
22

33
import rich
44
from rich.panel import Panel
5+
from rich.status import Status
56

67
from codegen import Codebase
78
from codegen.cli.auth.session import CodegenSession
8-
from codegen.cli.git.patch import apply_patch
9-
from codegen.cli.rich.codeblocks import format_command
10-
from codegen.cli.rich.spinners import create_spinner
119
from codegen.cli.utils.function_finder import DecoratedFunction
1210

1311

12+
def parse_codebase(repo_root: Path) -> Codebase:
13+
"""Parse the codebase at the given root.
14+
15+
Args:
16+
repo_root: Path to the repository root
17+
18+
Returns:
19+
Parsed Codebase object
20+
"""
21+
codebase = Codebase(repo_root)
22+
return codebase
23+
24+
1425
def run_local(
1526
session: CodegenSession,
1627
function: DecoratedFunction,
17-
apply_local: bool = False,
1828
diff_preview: int | None = None,
1929
) -> None:
2030
"""Run a function locally against the codebase.
2131
2232
Args:
2333
session: The current codegen session
24-
function: The function to run (either a DecoratedFunction or Codemod)
25-
apply_local: Whether to apply changes to the local filesystem
34+
function: The function to run
2635
diff_preview: Number of lines of diff to preview (None for all)
2736
"""
28-
# Initialize codebase from git repo root
37+
# Parse codebase and run
2938
repo_root = Path(session.git_repo.workdir)
3039

31-
with create_spinner("Parsing codebase...") as status:
32-
codebase = Codebase(repo_root)
33-
34-
try:
35-
# Run the function
36-
rich.print(f"Running {function.name} locally...")
37-
result = function.run(codebase)
38-
39-
if not result:
40-
rich.print("\n[yellow]No changes were produced by this codemod[/yellow]")
41-
return
42-
43-
# Show diff preview if requested
44-
if diff_preview:
45-
rich.print("") # Add spacing
46-
diff_lines = result.splitlines()
47-
truncated = len(diff_lines) > diff_preview
48-
limited_diff = "\n".join(diff_lines[:diff_preview])
49-
50-
if truncated:
51-
if apply_local:
52-
limited_diff += f"\n\n...\n\n[yellow]diff truncated to {diff_preview} lines, view the full change set in your local file system[/yellow]"
53-
else:
54-
limited_diff += f"\n\n...\n\n[yellow]diff truncated to {diff_preview} lines, view the full change set by running with --apply-local[/yellow]"
55-
56-
panel = Panel(limited_diff, title="[bold]Diff Preview[/bold]", border_style="blue", padding=(1, 2), expand=False)
57-
rich.print(panel)
58-
59-
# Apply changes if requested
60-
if apply_local:
61-
try:
62-
apply_patch(session.git_repo, f"\n{result}\n")
63-
rich.print("")
64-
rich.print("[green]✓ Changes have been applied to your local filesystem[/green]")
65-
rich.print("[yellow]→ Don't forget to commit your changes:[/yellow]")
66-
rich.print(format_command("git add ."))
67-
rich.print(format_command("git commit -m 'Applied codemod changes'"))
68-
except Exception as e:
69-
rich.print("")
70-
rich.print("[red]✗ Failed to apply changes locally[/red]")
71-
rich.print("\n[yellow]This usually happens when you have uncommitted changes.[/yellow]")
72-
rich.print("\nOption 1 - Save your changes:")
73-
rich.print(" 1. [blue]git status[/blue] (check your working directory)")
74-
rich.print(" 2. [blue]git add .[/blue] (stage your changes)")
75-
rich.print(" 3. [blue]git commit -m 'msg'[/blue] (commit your changes)")
76-
rich.print(" 4. Run this command again")
77-
rich.print("\nOption 2 - Discard your changes:")
78-
rich.print(" 1. [red]git reset --hard HEAD[/red] (⚠️ discards all uncommitted changes)")
79-
rich.print(" 2. [red]git clean -fd[/red] (⚠️ removes all untracked files)")
80-
rich.print(" 3. Run this command again\n")
81-
raise RuntimeError("Failed to apply patch to local filesystem") from e
82-
else:
83-
rich.print("")
84-
rich.print("To apply these changes locally:")
85-
rich.print(format_command(f"codegen run {function.name} --apply-local"))
86-
87-
except Exception as e:
88-
rich.print(f"[red]Error running {function.name}:[/red] {e!s}")
89-
raise
40+
with Status("[bold]Parsing codebase...", spinner="dots") as status:
41+
codebase = parse_codebase(repo_root)
42+
status.update("[bold green]✓ Parsed codebase")
43+
44+
status.update("[bold]Running codemod...")
45+
function.run(codebase) # Run the function
46+
status.update("[bold green]✓ Completed codemod")
47+
48+
# Get the diff from the codebase
49+
result = codebase.get_diff()
50+
51+
# Handle no changes case
52+
if not result:
53+
rich.print("\n[yellow]No changes were produced by this codemod[/yellow]")
54+
return
55+
56+
# Show diff preview if requested
57+
if diff_preview:
58+
rich.print("") # Add spacing
59+
diff_lines = result.splitlines()
60+
truncated = len(diff_lines) > diff_preview
61+
limited_diff = "\n".join(diff_lines[:diff_preview])
62+
63+
if truncated:
64+
limited_diff += f"\n\n...\n\n[yellow]diff truncated to {diff_preview} lines[/yellow]"
65+
66+
panel = Panel(limited_diff, title="[bold]Diff Preview[/bold]", border_style="blue", padding=(1, 2), expand=False)
67+
rich.print(panel)
68+
69+
# Apply changes
70+
rich.print("")
71+
rich.print("[green]✓ Changes have been applied to your local filesystem[/green]")

src/codegen/cli/sdk/decorator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def __call__(self, func: Callable[P, T]) -> Callable[P, T]:
3636
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
3737
return func(*args, **kwargs)
3838

39+
# Set the codegen name on the wrapper function
40+
wrapper.__codegen_name__ = self.name
3941
self.func = wrapper
4042
return wrapper
4143

src/codegen/cli/utils/codemod_manager.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import builtins
22
from pathlib import Path
33

4+
import rich_click as click
5+
46
from codegen.cli.utils.function_finder import DecoratedFunction, find_codegen_functions
57

68

@@ -25,6 +27,39 @@ class CodemodManager:
2527
def get_valid_name(name: str) -> str:
2628
return name.lower().replace(" ", "_").replace("-", "_")
2729

30+
@classmethod
31+
def get_codemod(cls, name: str, start_path: Path | None = None) -> DecoratedFunction:
32+
"""Get and validate a codemod by name.
33+
34+
Args:
35+
name: Name of the codemod to find
36+
start_path: Directory to start searching from (default: current directory)
37+
38+
Returns:
39+
The validated DecoratedFunction
40+
41+
Raises:
42+
click.ClickException: If codemod can't be found or loaded
43+
"""
44+
# First try to find the codemod
45+
codemod = cls.get(name, start_path)
46+
if not codemod:
47+
# If not found, check if any codemods exist
48+
all_codemods = cls.list(start_path)
49+
if not all_codemods:
50+
raise click.ClickException("No codemods found. Create one with:\n" + " codegen create my-codemod")
51+
else:
52+
available = "\n ".join(f"- {c.name}" for c in all_codemods)
53+
raise click.ClickException(f"Codemod '{name}' not found. Available codemods:\n {available}")
54+
55+
# Verify we can import it
56+
try:
57+
# This will raise ValueError if function can't be imported
58+
codemod.validate()
59+
return codemod
60+
except Exception as e:
61+
raise click.ClickException(f"Error loading codemod '{name}': {e!s}")
62+
2863
@classmethod
2964
def list(cls, start_path: Path | None = None) -> builtins.list[DecoratedFunction]:
3065
"""List all codegen decorated functions in Python files under the given path.

src/codegen/cli/utils/function_finder.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,60 @@ class DecoratedFunction:
1818
parameters: list[tuple[str, str | None]] = dataclasses.field(default_factory=list)
1919
arguments_type_schema: dict | None = None
2020

21+
def run(self, codebase) -> str | None:
22+
"""Import and run the actual function from its file.
23+
24+
Args:
25+
codebase: The codebase to run the function on
26+
27+
Returns:
28+
The result of running the function (usually a diff string)
29+
"""
30+
if not self.filepath:
31+
raise ValueError("Cannot run function without filepath")
32+
33+
# Import the module containing the function
34+
spec = importlib.util.spec_from_file_location("module", self.filepath)
35+
if not spec or not spec.loader:
36+
raise ImportError(f"Could not load module from {self.filepath}")
37+
38+
module = importlib.util.module_from_spec(spec)
39+
spec.loader.exec_module(module)
40+
41+
# Find the decorated function
42+
for item_name in dir(module):
43+
item = getattr(module, item_name)
44+
if hasattr(item, "__codegen_name__") and item.__codegen_name__ == self.name:
45+
# Found our function, run it
46+
return item(codebase)
47+
48+
raise ValueError(f"Could not find function '{self.name}' in {self.filepath}")
49+
50+
def validate(self) -> None:
51+
"""Verify that this function can be imported and accessed.
52+
53+
Raises:
54+
ValueError: If the function can't be found or imported
55+
"""
56+
if not self.filepath:
57+
raise ValueError("Cannot validate function without filepath")
58+
59+
# Import the module containing the function
60+
spec = importlib.util.spec_from_file_location("module", self.filepath)
61+
if not spec or not spec.loader:
62+
raise ImportError(f"Could not load module from {self.filepath}")
63+
64+
module = importlib.util.module_from_spec(spec)
65+
spec.loader.exec_module(module)
66+
67+
# Find the decorated function
68+
for item_name in dir(module):
69+
item = getattr(module, item_name)
70+
if hasattr(item, "__codegen_name__") and item.__codegen_name__ == self.name:
71+
return # Found it!
72+
73+
raise ValueError(f"Could not find function '{self.name}' in {self.filepath}")
74+
2175

2276
class CodegenFunctionVisitor(ast.NodeVisitor):
2377
def __init__(self):

0 commit comments

Comments
 (0)