Skip to content

k0pernikus/actually

Repository files navigation

actually

Well, actually, your code should read like this.

actually is a highly opinionated Python linter and formatter built on ast-grep. It enforces a guard-clause style through rules with ruff-style stable codes, grouped by language construct (ADR 1). Every rule is checkable; the auto-fix column marks what format can rewrite. Each rule links to its documentation page with rationale and a banned/wanted example pair — generated from rules.toml and validated by the linter itself (ADR 2). actually rules --list prints the same catalog in the terminal, docs links included:

actually-conditionals

Code Rule Status Auto-fix What it enforces
ACTC001 no-else stable partial else on if, and the completion clauses on for, while, try
ACTC002 no-elif stable no elif — use guard clauses or a dispatch table
ACTC003 ternary-not-nested stable no a ternary inside another ternary's arm (elif in expression form)
ACTC004 ternary-not-empty unstable no a degenerate ternary arm (None, "", empty container) — conditional inclusion in disguise

actually-literals

Code Rule Status Auto-fix What it enforces
ACTL001 trailing-comma unstable yes a dict/list/set literal whose last element lacks a trailing comma
ACTL002 one-element-per-line unstable partial a dict/list/set literal with elements sharing a line with a bracket or each other

actually-returns

Code Rule Status Auto-fix What it enforces
ACTR001 blank-before-return stable yes a return stacked directly under other statements in its block
ACTR002 blank-after-return stable yes code directly under a return line

Standing on Ruff and Ty

actually deliberately covers only what ruff and ty cannot express — they do most of the lifting; adopt them first. Our recommended configurations are this repo's own ruff.toml and ty.toml. actually's opinionation starts where those stop, and it MANDATES compatibility of its own output: no actually rule demands, and no actually format fix produces, code those rule sets reject — after actually format, a ruff format under the recommended configuration is a no-op. well-actually never runs ruff or ty itself — it is its own tool with neither as a dependency; pair them in your own pipeline. This repo gates itself on both toolchains plus its own linter on every commit, which is the guarantee exercised live.

Usage

uvx well-actually@latest check .
uvx well-actually@latest format .

The @latest matters: a bare uvx well-actually reuses a cached tool environment and can silently run an outdated version; @latest re-resolves against the index every time.

Installed (uv tool install well-actually), the short command is available too:

actually check .
actually format .

check reports violations and exits non-zero when it finds any. Both commands lint .py files only; directory scans skip environment, cache, and VCS directories (.venv, venv, .git, __pycache__, node_modules, and friends) and respect .gitignore files — nested ones and negations included, matched via pathspec (black's approach), so no git installation is required. When a .git directory is found above the scanned path, .gitignore files up to that repo root apply as well. Global excludes (core.excludesFile, .git/info/exclude) are not consulted. A .py file passed explicitly is always linted.

format rewrites files in place, then reports what it could not fix:

  • inserts the missing blank lines around return
  • dedents a try/except/else completion clause into straight-line code when every except body already exits (return, raise, continue, break) — when one falls through, the rewrite would change behaviour, so it is reported for human refactoring instead
  • rewrites dict/list/set literals to one element per line with a trailing comma — literals carrying comments or multiline elements are reported for human formatting instead
  • --only-autofixable makes it best effort: every available fix is applied, the remaining violations are still reported, and the exit code stays 0

Configuration

Select the rule subset in a well-actually.toml (sourced from the current working directory only — never a parent) or with repeatable --include / --exclude options, which override the file's corresponding list (ADR 5). Every invocation declares its selection on stderr — Found well-actually.toml. Running with: … or No well-actually.toml found, running with default '__ALL__' — so the active subset is never a matter of guessing. Entries are rule codes (ACTC004), group prefixes (ACTC), or __ALL__ — the special all-encompassing group (ADR 6). The longest match per rule wins; ties go to exclude. include defaults to __ALL__, so exclude-only configs just work:

exclude = ["ACTL"]

Any subset is expressible — one rule only:

exclude = ["__ALL__"]
include = ["ACTC004"]

or a group off with one member kept:

exclude = ["ACTL"]
include = ["__ALL__", "ACTL001"]

Hard errors instead of silent tolerance: an unknown selector, __ALL__ appearing more than once across both lists, exclude = ["__ALL__"] without any include entry, and a selection that enables no rules. format obeys the selection — a disabled rule neither reports nor fixes.

CI Reports

check and format emit machine-readable reports via --output-format (text/gitlab/github/sarif) and --output-file (ADR 7). GitLab code quality:

actually:
  script:
    - uvx well-actually@latest check --output-format=gitlab --output-file=gl-code-quality-report.json .
  artifacts:
    when: always
    reports:
      codequality: gl-code-quality-report.json

GitHub inline annotations need no upload — --output-format=github prints workflow commands; --output-format=sarif produces SARIF 2.1.0 for GitHub code scanning or any SARIF consumer.

Example

def describe_config(path):
    try:
        config = parse_json_file(path)
    except ParseError:
        return "invalid config"
    else:
        return describe(config)

actually format rewrites this to:

def describe_config(path):
    try:
        config = parse_json_file(path)
    except ParseError:
        return "invalid config"

    return describe(config)

Development

uv sync
mise install
hk install
uv run pytest

README.md and rules/*.md are generated from README.template.md and src/actually/rules.toml by scripts/generate_docs.py; an hk pre-commit hook regenerates and stages them. Edit the sources, never the outputs.

About

Well, actually - an opinionated Python linter and formatter: bans else and elif, allows only flat ternaries, enforces blank lines around return

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors