Plain is headed towards 1.0! Subscribe for development updates →

  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", {})