1from __future__ import annotations
2
3import subprocess
4import sys
5import tomllib
6from pathlib import Path
7from typing import Any
8
9import click
10
11from plain.cli import register_cli
12from plain.cli.print import print_event
13from plain.cli.runtime import without_runtime_setup
14
15from .biome import Biome
16
17DEFAULT_RUFF_CONFIG = Path(__file__).parent / "ruff_defaults.toml"
18
19
20@without_runtime_setup
21@register_cli("code")
22@click.group()
23def cli() -> None:
24 """Code formatting and linting"""
25 pass
26
27
28@without_runtime_setup
29@cli.command()
30@click.option("--force", is_flag=True, help="Reinstall even if up to date")
31@click.pass_context
32def install(ctx: click.Context, force: bool) -> None:
33 """Install or update the Biome standalone per configuration."""
34 config = get_code_config()
35
36 if not config.get("biome", {}).get("enabled", True):
37 click.secho("Biome is disabled in configuration", fg="yellow")
38 return
39
40 biome = Biome()
41
42 if force or not biome.is_installed() or biome.needs_update():
43 version_to_install = config.get("biome", {}).get("version", "")
44 if version_to_install:
45 click.secho(
46 f"Installing Biome standalone version {version_to_install}...",
47 bold=True,
48 nl=False,
49 )
50 installed = biome.install(version_to_install)
51 click.secho(f"Biome {installed} installed", fg="green")
52 else:
53 ctx.invoke(update)
54 else:
55 click.secho("Biome already installed", fg="green")
56
57
58@without_runtime_setup
59@cli.command()
60def update() -> None:
61 """Update the Biome standalone binary to the latest release."""
62 config = get_code_config()
63
64 if not config.get("biome", {}).get("enabled", True):
65 click.secho("Biome is disabled in configuration", fg="yellow")
66 return
67
68 biome = Biome()
69 click.secho("Updating Biome standalone...", bold=True)
70 version = biome.install()
71 click.secho(f"Biome {version} installed", fg="green")
72
73
74@without_runtime_setup
75@cli.command()
76@click.pass_context
77@click.argument("path", default=".")
78def check(ctx: click.Context, path: str) -> None:
79 """Check the given path for formatting or linting issues."""
80 ruff_args = ["--config", str(DEFAULT_RUFF_CONFIG)]
81 config = get_code_config()
82
83 for e in config.get("exclude", []):
84 ruff_args.extend(["--exclude", e])
85
86 print_event("Ruff check")
87 result = subprocess.run(["ruff", "check", path, *ruff_args])
88 if result.returncode != 0:
89 sys.exit(result.returncode)
90
91 print_event("Ruff format check")
92 result = subprocess.run(["ruff", "format", path, "--check", *ruff_args])
93 if result.returncode != 0:
94 sys.exit(result.returncode)
95
96 if config.get("biome", {}).get("enabled", True):
97 biome = Biome()
98
99 if biome.needs_update():
100 ctx.invoke(install)
101
102 print_event("Biome check")
103 result = biome.invoke("check", path)
104 if result.returncode != 0:
105 sys.exit(result.returncode)
106
107
108@without_runtime_setup
109@register_cli("fix")
110@cli.command()
111@click.pass_context
112@click.argument("path", default=".")
113@click.option("--unsafe-fixes", is_flag=True, help="Apply ruff unsafe fixes")
114@click.option("--add-noqa", is_flag=True, help="Add noqa comments to suppress errors")
115def fix(ctx: click.Context, path: str, unsafe_fixes: bool, add_noqa: bool) -> None:
116 """Lint and format the given path."""
117 ruff_args = ["--config", str(DEFAULT_RUFF_CONFIG)]
118 config = get_code_config()
119
120 for e in config.get("exclude", []):
121 ruff_args.extend(["--exclude", e])
122
123 if unsafe_fixes and add_noqa:
124 raise click.UsageError("Cannot use both --unsafe-fixes and --add-noqa")
125
126 if unsafe_fixes:
127 print_event("Ruff fix (with unsafe fixes)")
128 result = subprocess.run(
129 ["ruff", "check", path, "--fix", "--unsafe-fixes", *ruff_args]
130 )
131 elif add_noqa:
132 print_event("Ruff fix (add noqa)")
133 result = subprocess.run(["ruff", "check", path, "--add-noqa", *ruff_args])
134 else:
135 print_event("Ruff fix")
136 result = subprocess.run(["ruff", "check", path, "--fix", *ruff_args])
137
138 if result.returncode != 0:
139 sys.exit(result.returncode)
140
141 print_event("Ruff format")
142 result = subprocess.run(["ruff", "format", path, *ruff_args])
143 if result.returncode != 0:
144 sys.exit(result.returncode)
145
146 if config.get("biome", {}).get("enabled", True):
147 biome = Biome()
148
149 if biome.needs_update():
150 ctx.invoke(install)
151
152 print_event("Biome format")
153
154 args = ["check", path, "--write"]
155
156 if unsafe_fixes:
157 args.append("--unsafe")
158
159 result = biome.invoke(*args)
160
161 if result.returncode != 0:
162 sys.exit(result.returncode)
163
164
165def get_code_config() -> dict[str, Any]:
166 pyproject = Path("pyproject.toml")
167 if not pyproject.exists():
168 return {}
169 with pyproject.open("rb") as f:
170 return tomllib.load(f).get("tool", {}).get("plain", {}).get("code", {})