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    biome = Biome()
 29
 30    if force or not biome.is_installed() or biome.needs_update():
 31        version_to_install = get_code_config().get("biome", {}).get("version", "")
 32        if version_to_install:
 33            click.secho(
 34                f"Installing Biome standalone version {version_to_install}...",
 35                bold=True,
 36                nl=False,
 37            )
 38            installed = biome.install(version_to_install)
 39            click.secho(f"Biome {installed} installed", fg="green")
 40        else:
 41            ctx.invoke(update)
 42    else:
 43        click.secho("Biome already installed", fg="green")
 44
 45
 46@cli.command()
 47def update():
 48    """Update the Biome standalone binary to the latest release."""
 49    biome = Biome()
 50    click.secho("Updating Biome standalone...", bold=True)
 51    version = biome.install()
 52    click.secho(f"Biome {version} installed", fg="green")
 53
 54
 55@cli.command()
 56@click.pass_context
 57@click.argument("path", default=".")
 58def check(ctx, path):
 59    """Check the given path for formatting or linting issues."""
 60    ruff_args = ["--config", str(DEFAULT_RUFF_CONFIG)]
 61    config = get_code_config()
 62
 63    for e in config.get("exclude", []):
 64        ruff_args.extend(["--exclude", e])
 65
 66    print_event("Ruff check")
 67    result = subprocess.run(["ruff", "check", path, *ruff_args])
 68    if result.returncode != 0:
 69        sys.exit(result.returncode)
 70
 71    print_event("Ruff format check")
 72    result = subprocess.run(["ruff", "format", path, "--check", *ruff_args])
 73    if result.returncode != 0:
 74        sys.exit(result.returncode)
 75
 76    if config.get("biome", {}).get("enabled", True):
 77        biome = Biome()
 78
 79        if biome.needs_update():
 80            ctx.invoke(install)
 81
 82        print_event("Biome check")
 83        result = biome.invoke("check", path)
 84        if result.returncode != 0:
 85            sys.exit(result.returncode)
 86
 87
 88@register_cli("fix")
 89@cli.command()
 90@click.pass_context
 91@click.argument("path", default=".")
 92@click.option("--unsafe-fixes", is_flag=True, help="Apply ruff unsafe fixes")
 93@click.option("--add-noqa", is_flag=True, help="Add noqa comments to suppress errors")
 94def fix(ctx, path, unsafe_fixes, add_noqa):
 95    """Lint and format the given path."""
 96    ruff_args = ["--config", str(DEFAULT_RUFF_CONFIG)]
 97    config = get_code_config()
 98
 99    for e in config.get("exclude", []):
100        ruff_args.extend(["--exclude", e])
101
102    if unsafe_fixes and add_noqa:
103        print("Cannot use both --unsafe-fixes and --add-noqa")
104        sys.exit(1)
105
106    if unsafe_fixes:
107        print_event("Ruff fix (with unsafe fixes)")
108        result = subprocess.run(
109            ["ruff", "check", path, "--fix", "--unsafe-fixes", *ruff_args]
110        )
111    elif add_noqa:
112        print_event("Ruff fix (add noqa)")
113        result = subprocess.run(["ruff", "check", path, "--add-noqa", *ruff_args])
114    else:
115        print_event("Ruff fix")
116        result = subprocess.run(["ruff", "check", path, "--fix", *ruff_args])
117
118    if result.returncode != 0:
119        sys.exit(result.returncode)
120
121    print_event("Ruff format")
122    result = subprocess.run(["ruff", "format", path, *ruff_args])
123    if result.returncode != 0:
124        sys.exit(result.returncode)
125
126    if config.get("biome", {}).get("enabled", True):
127        biome = Biome()
128
129        if biome.needs_update():
130            ctx.invoke(install)
131
132        print_event("Biome format")
133
134        args = ["check", path, "--write"]
135
136        if unsafe_fixes:
137            args.append("--unsafe")
138
139        result = biome.invoke(*args)
140
141        if result.returncode != 0:
142            sys.exit(result.returncode)
143
144
145def get_code_config():
146    pyproject = Path("pyproject.toml")
147    if not pyproject.exists():
148        return {}
149    with pyproject.open("rb") as f:
150        return tomllib.load(f).get("tool", {}).get("plain", {}).get("code", {})