A superset of ruff: runs ruff internally, adds built-in rules, and supports user-defined plugin executables
Project description
ruffian
A ruffian breaks rules. This tool adds the ones ruff refused.
ruffian is a drop-in superset of ruff. It runs ruff internally, adds its own built-in lint rules, and supports user-defined plugin executables — all producing output that is indistinguishable from ruff's own.
Replace ruff with ruffian in your CI scripts, pre-commit hooks, and editor config. Everything ruff does still works. ruffian adds on top.
Installation
pip install ruffian
# or
uv add --dev ruffian
ruff is a declared dependency and will be installed automatically.
Usage
# Check files (ruff rules + ruffian built-in rules + your plugins)
ruffian check src/
# Format files — pure passthrough to ruff format
ruffian format src/
# Show documentation for a built-in rule
ruffian rule PLC0302
# JSON output (same format as ruff --output-format json)
ruffian check src/ --output-format json
ruffian accepts the same flags as ruff check for the options it passes through. Run ruffian --help for the full list.
Output format note: ruffian's text output matches
ruff check --output-format concise --quiet— onepath:row:col: CODE [*] messageline per violation, no source context or summary footer. Use--output-format jsonfor full detail.
Inline suppression
Add a # ruffian: noqa comment to suppress ruffian violations on a specific line:
some_huge_module_header = True # ruffian: noqa # suppress all ruffian rules on this line
some_huge_module_header = True # ruffian: noqa PLC0302 # suppress a specific rule
some_huge_module_header = True # ruffian: noqa PLC0302, RFN001 # suppress multiple rules
Note: ruff's own
# noqasuppression is handled by ruff before violations reach ruffian. Use# noqa: CODEto suppress ruff violations and# ruffian: noqa CODEto suppress ruffian violations.
Configuration
All ruffian config lives in pyproject.toml under [tool.ruffian]. Ruff's own [tool.ruff] section is untouched and passed directly to ruff.
[tool.ruffian]
select = ["PLC0302"] # built-in rules to enable (empty = all enabled)
ignore = []
[tool.ruffian.rules.PLC0302]
max-module-lines = 800 # override the default (1000); `max-lines` also accepted for compatibility
# User plugins
[[tool.ruffian.plugins]]
name = "no-todo"
executable = "./scripts/no_todo.py" # any executable
config = {} # passed to the plugin as JSON on stdin
Built-in rules
| Code | Name | Pylint source | Default |
|---|---|---|---|
| PLC0302 | too-many-module-lines | C0302 | 1000 |
Rule code prefixes
ruffian follows ruff's own prefix conventions. Since ruffian only implements rules that ruff has not, there is no overlap.
| Prefix | Meaning |
|---|---|
PLC, PLE, PLR, PLW |
Pylint rules not implemented by ruff — same PL prefix ruff uses, no conflict since we only ship what ruff skipped |
RFN |
Novel rules with no equivalent in any existing linter |
RFC |
Reserved for user plugin rules — plugin authors must use this prefix to avoid collisions with ruffian built-ins |
PLC0302 — too-many-module-lines
Reports Python modules that exceed a configurable line count. Encourages splitting large files before they become hard to navigate.
[tool.ruffian.rules.PLC0302]
max-module-lines = 800 # default: 1000; `max-lines` also accepted for v0.1 compatibility
To raise the limit or disable the rule globally:
[tool.ruffian]
ignore = ["PLC0302"] # disable entirely
To suppress it for a single file, add # ruffian: noqa PLC0302 to line 1 of that file (the rule always fires on line 1):
# ruffian: noqa PLC0302 — this file is intentionally large (generated code)
...
Plugin system
Any executable — Python script, shell script, compiled binary — can act as a ruffian plugin. This is how you add project-specific rules without writing any Rust.
How ruffian calls your plugin
./my_plugin.py file1.py file2.py ...
Files to check are passed as positional arguments. A JSON config blob is written to stdin:
{
"ruffian_version": "0.1.0",
"config": { "threshold": 5 }
}
config is whatever you put in [[tool.ruffian.plugins]] → config.
What your plugin must write to stdout
A JSON array of violations. Empty array means no violations.
[
{
"code": "MY001",
"message": "Human-readable description",
"filename": "/abs/path/to/file.py",
"location": { "row": 42, "column": 0 },
"end_location": { "row": 42, "column": 10 },
"url": "https://my-docs.example.com/rules/MY001",
"fix": null
}
]
code, message, filename, and location are required. end_location, url, and fix are optional.
Exit codes: exit 0 regardless of violations found — violations are communicated via JSON, not the exit code. Exit non-zero to signal that the plugin itself failed (ruffian will report an error, separate from any lint violations).
Stderr: any output to stderr is forwarded to ruffian's stderr with a [plugin: name] prefix.
Minimal Python plugin
#!/usr/bin/env python3
import json, sys
config_blob = json.loads(sys.stdin.read())
files = sys.argv[1:]
violations = []
for path in files:
source = open(path).read()
# ... your logic ...
print(json.dumps(violations))
A working example is in python/example_plugins/example_plugin.py.
Security note
Plugins run as arbitrary executables with the same permissions as your shell. Only register plugins you trust.
Editor integration
ruffian's output format is identical to ruff's, so any editor integration that already works with ruff will work with ruffian without changes. Replace ruff with ruffian in your editor's lint command setting.
For VS Code with the Ruff extension, point it at the ruffian binary:
{
"ruff.path": ["/path/to/ruffian"]
}
GitHub Actions
- name: Lint with ruffian
run: |
pip install ruffian
ruffian check src/
Or pin the version for reproducible CI:
- name: Lint with ruffian
run: |
pip install ruffian==0.1.0
ruffian check src/
Pre-commit
# .pre-commit-config.yaml
- repo: local
hooks:
- id: ruffian
name: ruffian
entry: ruffian check
language: system
types: [python]
Contributing
Proposing a new built-in rule
ruffian only ships rules that ruff has explicitly declined to implement. Before opening a PR here, please follow this process:
-
Propose the rule to ruff first. Open a feature request in the ruff issue tracker. Many rules belong there, not here — ruff has a much larger audience and faster release cadence.
-
Use the plugin system in the meantime. While ruff considers your proposal, implement the rule as a ruffian plugin. This lets you and your team use it immediately with zero Rust code and no waiting on anyone.
-
If ruff declines, open an issue here first. If ruff closes or explicitly refuses your issue, open a ruffian issue with a link to that discussion. This lets us align on the rule design before any code is written.
-
Then submit a PR. Built-in ruffian rules are written in Rust — see Adding a built-in rule below. Your PR must reference the ruffian issue and the upstream ruff discussion that refused the rule.
This keeps ruffian's built-in rule set small and intentional: every rule here has a paper trail explaining why ruff said no.
Prerequisites
# 1. Install the task runner (provides the `task` command)
brew install go-task
# 2. Install everything else
task setup
task setup installs the Rust toolchain via rustup, dev tools (cargo-llvm-cov, llvm-tools), and maturin for packaging. After it completes, restart your terminal or run source ~/.cargo/env to pick up the Rust toolchain.
Adding a built-in rule
- Create
src/rules/<rule_name>.rsand implement theRuletrait:
pub trait Rule: Send + Sync {
fn code(&self) -> &'static str;
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn check(&self, file: &ParsedFile) -> Vec<Violation>;
}
ParsedFile exposes three fields:
| Field | Type | Notes |
|---|---|---|
path |
String |
Path to the file as provided to ruffian on the CLI |
source |
String |
Raw source text |
ast |
Option<Parsed<ModModule>> |
Parsed AST from ruff_python_parser; None if the file has syntax errors |
For source-level checks (line count, regex, etc.) use file.source. For structural checks use file.ast.as_ref().map(|p| p.syntax()) to get the ModModule and walk the statement list.
- Register it in
src/rules/mod.rs— one line inall_rules(). No other changes required.
See PLC0302 for a complete example.
Development commands
task build # debug build
task test # run all tests
task test:coverage # run tests with coverage report, fail below 85%
task lint # cargo clippy -D warnings
task lint:fix # clippy --fix (--allow-dirty) + cargo fmt
task fmt:check # check formatting without writing changes (what CI runs)
task install:local # build and install into the active Python env for manual testing
task version:bump # bump minor version and push (optionally: task version:bump -- 1.0.0)
task release # interactive release: checks CI, generates notes, creates GitHub release
task ruff:update # update ruff dependency pins to latest release
Coding style
- Functional-style Rust where it doesn't fight the type system
- No traits or structs for things with only one implementation
- Files stay under 500 lines — split by responsibility when they grow
thiserrorfor error types;anyhowonly at the binary boundary- All PRs must pass
cargo clippy -- -D warningsandcargo fmt -- --check
License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ruffian-0.9.0-py3-none-win_amd64.whl.
File metadata
- Download URL: ruffian-0.9.0-py3-none-win_amd64.whl
- Upload date:
- Size: 2.0 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c3851dd11cb0516894c5f9cc148a07d2fc2827c8227004292810bf6d6a26736
|
|
| MD5 |
88270be80a65bb9333b943c51394f671
|
|
| BLAKE2b-256 |
54899af61aba88010ded8fc69986626e7285912733aaa2ecab88eb51181d87b1
|
Provenance
The following attestation bundles were made for ruffian-0.9.0-py3-none-win_amd64.whl:
Publisher:
publish.yml on tluolamo/ruffian
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ruffian-0.9.0-py3-none-win_amd64.whl -
Subject digest:
8c3851dd11cb0516894c5f9cc148a07d2fc2827c8227004292810bf6d6a26736 - Sigstore transparency entry: 1376419730
- Sigstore integration time:
-
Permalink:
tluolamo/ruffian@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/tluolamo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Trigger Event:
release
-
Statement type:
File details
Details for the file ruffian-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: ruffian-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.3 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1eb0bb8b31f9bf183cbdcb8083cf3d49bf763a09a1d07a86a1ea80c49e65cfaa
|
|
| MD5 |
3a9e68ea67961424175311fa42f404f6
|
|
| BLAKE2b-256 |
bedcf16d8ca4e4a86deacf0d28cadb16f334252551d18a65533482c60cbfe8f3
|
Provenance
The following attestation bundles were made for ruffian-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
publish.yml on tluolamo/ruffian
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ruffian-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
1eb0bb8b31f9bf183cbdcb8083cf3d49bf763a09a1d07a86a1ea80c49e65cfaa - Sigstore transparency entry: 1376419687
- Sigstore integration time:
-
Permalink:
tluolamo/ruffian@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/tluolamo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Trigger Event:
release
-
Statement type:
File details
Details for the file ruffian-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: ruffian-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 2.2 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
208762e2c46f4c3b81a8eec69eca74f909ca2b9f922bd48a26e7b3a2422d4a4b
|
|
| MD5 |
a0f4d74561f08306e5aa07f002d8aea1
|
|
| BLAKE2b-256 |
8d1a653e4bee7aa31c24ae8b492bdfe43f3e7b6909ca0c352f9c8aec74d4773b
|
Provenance
The following attestation bundles were made for ruffian-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
publish.yml on tluolamo/ruffian
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ruffian-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
208762e2c46f4c3b81a8eec69eca74f909ca2b9f922bd48a26e7b3a2422d4a4b - Sigstore transparency entry: 1376419794
- Sigstore integration time:
-
Permalink:
tluolamo/ruffian@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/tluolamo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Trigger Event:
release
-
Statement type:
File details
Details for the file ruffian-0.9.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: ruffian-0.9.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.1 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
367e457345b36d78fdc809061f371aa5bb70e00214f6ef1f1cc6de310a052198
|
|
| MD5 |
489f8d2a2ac92a032a20d91728489ccf
|
|
| BLAKE2b-256 |
87b266afda3bb76a969c2d64399beb18a768b3504ca8b8b1029c8d5898db1d48
|
Provenance
The following attestation bundles were made for ruffian-0.9.0-py3-none-macosx_11_0_arm64.whl:
Publisher:
publish.yml on tluolamo/ruffian
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ruffian-0.9.0-py3-none-macosx_11_0_arm64.whl -
Subject digest:
367e457345b36d78fdc809061f371aa5bb70e00214f6ef1f1cc6de310a052198 - Sigstore transparency entry: 1376419773
- Sigstore integration time:
-
Permalink:
tluolamo/ruffian@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/tluolamo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Trigger Event:
release
-
Statement type:
File details
Details for the file ruffian-0.9.0-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: ruffian-0.9.0-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 2.2 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eca7087bfecc8e97f36fbfd69a14abb1d5621269a682b51023792946f303899c
|
|
| MD5 |
5f529b411cfd6cc0548236da7c4a1e2d
|
|
| BLAKE2b-256 |
cb6a8d24be3288417337ced61f534dc20dfa2db10d9b8eb517eb23803b14746c
|
Provenance
The following attestation bundles were made for ruffian-0.9.0-py3-none-macosx_10_12_x86_64.whl:
Publisher:
publish.yml on tluolamo/ruffian
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ruffian-0.9.0-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
eca7087bfecc8e97f36fbfd69a14abb1d5621269a682b51023792946f303899c - Sigstore transparency entry: 1376419679
- Sigstore integration time:
-
Permalink:
tluolamo/ruffian@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Branch / Tag:
refs/tags/v0.9.0 - Owner: https://github.com/tluolamo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f8cb035060be453bf6e532170fa4f0d4a72da00a -
Trigger Event:
release
-
Statement type: