Plain is headed towards 1.0! Subscribe for development updates →

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