ruff-legibility 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ .coverage*
7
+ htmlcov/
8
+ build/
9
+ dist/
10
+ *.egg-info/
11
+
12
+ # Beads / Dolt files (added by bd init)
13
+ .dolt/
14
+ *.db
15
+ .beads-credential-key
16
+ .claude/
17
+ CLAUDE.md
18
+ .beads/
19
+ venv/
20
+ *.pyc
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeffry Wainwright
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: ruff-legibility
3
+ Version: 0.1.0
4
+ Summary: Ruff-adjacent Python legibility rules for readable, reviewable code.
5
+ Project-URL: Homepage, https://github.com/yowainwright/ruff-legibility
6
+ Project-URL: Issues, https://github.com/yowainwright/ruff-legibility/issues
7
+ Project-URL: Repository, https://github.com/yowainwright/ruff-legibility
8
+ Author: Jeffry Wainwright
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: legibility,lint,python,readability,ruff
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+
25
+ # ruff-legibility
26
+
27
+ [![PyPI version](https://img.shields.io/pypi/v/ruff-legibility.svg)](https://pypi.org/project/ruff-legibility/)
28
+ [![CI](https://github.com/yowainwright/ruff-legibility/actions/workflows/ci.yml/badge.svg)](https://github.com/yowainwright/ruff-legibility/actions/workflows/ci.yml)
29
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/yowainwright/ruff-legibility/badge)](https://scorecard.dev/viewer/?uri=github.com/yowainwright/ruff-legibility)
30
+
31
+ `ruff-legibility` is a Ruff-adjacent Python linter for readability and reviewability rules inspired by `eslint-plugin-legibility`.
32
+
33
+ Ruff does not currently load third-party rule implementations from Python packages. This package therefore runs beside Ruff and emits Ruff-style diagnostics with `LEG###` codes.
34
+
35
+ ```sh
36
+ ruff check .
37
+ ruff-legibility check .
38
+ ```
39
+
40
+ To keep `# noqa: LEG001` comments valid when Ruff checks unused or unknown `noqa` codes, add `LEG` as an external prefix in Ruff:
41
+
42
+ ```toml
43
+ [tool.ruff.lint]
44
+ external = ["LEG"]
45
+ ```
46
+
47
+ ## Install
48
+
49
+ ```sh
50
+ pip install ruff-legibility
51
+ ```
52
+
53
+ For local development:
54
+
55
+ ```sh
56
+ uv sync --all-groups
57
+ make check
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ```sh
63
+ ruff-legibility check .
64
+ ruff-legibility check src tests --output-format json
65
+ ruff-legibility check . --select LEG001,LEG002 --ignore LEG007
66
+ ruff-legibility check . --exit-zero
67
+ ```
68
+
69
+ Default text output is intentionally close to Ruff:
70
+
71
+ ```text
72
+ example.py:4:8: LEG002 If condition has 2 boolean operators (max 0). Hoist it into a named boolean.
73
+ ```
74
+
75
+ ## Configuration
76
+
77
+ Configuration can live in `pyproject.toml` under `[tool.ruff-legibility]`, or in `ruff-legibility.toml` / `.ruff-legibility.toml`.
78
+
79
+ ```toml
80
+ [tool.ruff-legibility]
81
+ select = ["LEG"]
82
+ extend-select = []
83
+ ignore = ["LEG007"]
84
+ extend-ignore = []
85
+ exclude = [".venv", "build", "dist"]
86
+ max-expression-operators = 4
87
+ max-if-operators = 0
88
+ max-ternary-operators = 2
89
+ max-control-flow-depth = 3
90
+
91
+ [tool.ruff-legibility.per-file-ignores]
92
+ "tests/*" = ["LEG003"]
93
+ ```
94
+
95
+ Standalone config files omit the `tool.ruff-legibility` wrapper:
96
+
97
+ ```toml
98
+ select = ["LEG"]
99
+ ignore = ["LEG007"]
100
+ ```
101
+
102
+ This repository includes a `ruff-legibility.toml` for its own source. The default package thresholds stay stricter than the project-local development config.
103
+
104
+ ## Rules
105
+
106
+ | Code | Rule | Default |
107
+ | --- | --- | --- |
108
+ | `LEG001` | Limit readability operators inside a single expression. | on |
109
+ | `LEG002` | Prefer a named boolean before operator-heavy `if` / `while` conditions. | on |
110
+ | `LEG003` | Limit nested control-flow depth. | on |
111
+ | `LEG004` | Avoid complex ternary expressions. | on |
112
+ | `LEG005` | Flag likely quadratic patterns such as nested loops and repeated membership checks in loops. | on |
113
+ | `LEG006` | Avoid redundant boolean comparisons like `flag == True`. | on |
114
+ | `LEG007` | Prefer positive condition names over names like `is_not_ready`. | on |
115
+
116
+ ## Pre-commit
117
+
118
+ ```yaml
119
+ repos:
120
+ - repo: local
121
+ hooks:
122
+ - id: ruff-legibility
123
+ name: ruff-legibility
124
+ entry: ruff-legibility check
125
+ language: system
126
+ types: [python]
127
+ ```
128
+
129
+ ## Development
130
+
131
+ Common commands:
132
+
133
+ ```sh
134
+ uv sync --all-groups
135
+ uv run ruff check .
136
+ uv run ruff-legibility check src tests
137
+ uv run pytest
138
+ uv build
139
+ ```
140
+
141
+ Release builds should use:
142
+
143
+ ```sh
144
+ uv build --no-sources
145
+ ```
146
+
147
+ Publishing is configured for PyPI Trusted Publishing:
148
+
149
+ ```sh
150
+ uv publish
151
+ ```
@@ -0,0 +1,127 @@
1
+ # ruff-legibility
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/ruff-legibility.svg)](https://pypi.org/project/ruff-legibility/)
4
+ [![CI](https://github.com/yowainwright/ruff-legibility/actions/workflows/ci.yml/badge.svg)](https://github.com/yowainwright/ruff-legibility/actions/workflows/ci.yml)
5
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/yowainwright/ruff-legibility/badge)](https://scorecard.dev/viewer/?uri=github.com/yowainwright/ruff-legibility)
6
+
7
+ `ruff-legibility` is a Ruff-adjacent Python linter for readability and reviewability rules inspired by `eslint-plugin-legibility`.
8
+
9
+ Ruff does not currently load third-party rule implementations from Python packages. This package therefore runs beside Ruff and emits Ruff-style diagnostics with `LEG###` codes.
10
+
11
+ ```sh
12
+ ruff check .
13
+ ruff-legibility check .
14
+ ```
15
+
16
+ To keep `# noqa: LEG001` comments valid when Ruff checks unused or unknown `noqa` codes, add `LEG` as an external prefix in Ruff:
17
+
18
+ ```toml
19
+ [tool.ruff.lint]
20
+ external = ["LEG"]
21
+ ```
22
+
23
+ ## Install
24
+
25
+ ```sh
26
+ pip install ruff-legibility
27
+ ```
28
+
29
+ For local development:
30
+
31
+ ```sh
32
+ uv sync --all-groups
33
+ make check
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```sh
39
+ ruff-legibility check .
40
+ ruff-legibility check src tests --output-format json
41
+ ruff-legibility check . --select LEG001,LEG002 --ignore LEG007
42
+ ruff-legibility check . --exit-zero
43
+ ```
44
+
45
+ Default text output is intentionally close to Ruff:
46
+
47
+ ```text
48
+ example.py:4:8: LEG002 If condition has 2 boolean operators (max 0). Hoist it into a named boolean.
49
+ ```
50
+
51
+ ## Configuration
52
+
53
+ Configuration can live in `pyproject.toml` under `[tool.ruff-legibility]`, or in `ruff-legibility.toml` / `.ruff-legibility.toml`.
54
+
55
+ ```toml
56
+ [tool.ruff-legibility]
57
+ select = ["LEG"]
58
+ extend-select = []
59
+ ignore = ["LEG007"]
60
+ extend-ignore = []
61
+ exclude = [".venv", "build", "dist"]
62
+ max-expression-operators = 4
63
+ max-if-operators = 0
64
+ max-ternary-operators = 2
65
+ max-control-flow-depth = 3
66
+
67
+ [tool.ruff-legibility.per-file-ignores]
68
+ "tests/*" = ["LEG003"]
69
+ ```
70
+
71
+ Standalone config files omit the `tool.ruff-legibility` wrapper:
72
+
73
+ ```toml
74
+ select = ["LEG"]
75
+ ignore = ["LEG007"]
76
+ ```
77
+
78
+ This repository includes a `ruff-legibility.toml` for its own source. The default package thresholds stay stricter than the project-local development config.
79
+
80
+ ## Rules
81
+
82
+ | Code | Rule | Default |
83
+ | --- | --- | --- |
84
+ | `LEG001` | Limit readability operators inside a single expression. | on |
85
+ | `LEG002` | Prefer a named boolean before operator-heavy `if` / `while` conditions. | on |
86
+ | `LEG003` | Limit nested control-flow depth. | on |
87
+ | `LEG004` | Avoid complex ternary expressions. | on |
88
+ | `LEG005` | Flag likely quadratic patterns such as nested loops and repeated membership checks in loops. | on |
89
+ | `LEG006` | Avoid redundant boolean comparisons like `flag == True`. | on |
90
+ | `LEG007` | Prefer positive condition names over names like `is_not_ready`. | on |
91
+
92
+ ## Pre-commit
93
+
94
+ ```yaml
95
+ repos:
96
+ - repo: local
97
+ hooks:
98
+ - id: ruff-legibility
99
+ name: ruff-legibility
100
+ entry: ruff-legibility check
101
+ language: system
102
+ types: [python]
103
+ ```
104
+
105
+ ## Development
106
+
107
+ Common commands:
108
+
109
+ ```sh
110
+ uv sync --all-groups
111
+ uv run ruff check .
112
+ uv run ruff-legibility check src tests
113
+ uv run pytest
114
+ uv build
115
+ ```
116
+
117
+ Release builds should use:
118
+
119
+ ```sh
120
+ uv build --no-sources
121
+ ```
122
+
123
+ Publishing is configured for PyPI Trusted Publishing:
124
+
125
+ ```sh
126
+ uv publish
127
+ ```
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ruff-legibility"
7
+ version = "0.1.0"
8
+ description = "Ruff-adjacent Python legibility rules for readable, reviewable code."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Jeffry Wainwright" }]
14
+ keywords = ["ruff", "lint", "legibility", "readability", "python"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Software Development :: Quality Assurance",
26
+ ]
27
+ dependencies = []
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "pytest>=8.0",
32
+ "ruff>=0.15",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/yowainwright/ruff-legibility"
37
+ Issues = "https://github.com/yowainwright/ruff-legibility/issues"
38
+ Repository = "https://github.com/yowainwright/ruff-legibility"
39
+
40
+ [project.scripts]
41
+ ruff-legibility = "ruff_legibility.cli:main"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/ruff_legibility"]
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml", "ruff-legibility.toml"]
48
+
49
+ [tool.ruff]
50
+ line-length = 120
51
+ target-version = "py311"
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "I", "UP", "B", "SIM"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ pythonpath = ["src"]
@@ -0,0 +1,3 @@
1
+ select = ["LEG"]
2
+ max-if-operators = 3
3
+ max-control-flow-depth = 6
@@ -0,0 +1,13 @@
1
+ """Ruff-adjacent legibility checks for Python."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from .core import Diagnostic, check_path, check_source
6
+ from .rules import RULES
7
+
8
+ __all__ = ["Diagnostic", "RULES", "check_path", "check_source"]
9
+
10
+ try:
11
+ __version__ = version("ruff-legibility")
12
+ except PackageNotFoundError:
13
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .config import apply_overrides, load_settings, parse_selectors
11
+ from .core import Diagnostic, check_path, discover_python_files
12
+ from .rules import RULES
13
+
14
+
15
+ def main(argv: Sequence[str] | None = None) -> int:
16
+ args = _parse_args(list(argv) if argv is not None else sys.argv[1:])
17
+
18
+ if args.command == "rules":
19
+ _print_rules()
20
+ return 0
21
+
22
+ try:
23
+ settings = load_settings(Path(args.config) if args.config else None)
24
+ settings = apply_overrides(
25
+ settings,
26
+ select=parse_selectors(args.select),
27
+ ignore=parse_selectors(args.ignore),
28
+ max_expression_operators=args.max_expression_operators,
29
+ max_if_operators=args.max_if_operators,
30
+ max_ternary_operators=args.max_ternary_operators,
31
+ max_control_flow_depth=args.max_control_flow_depth,
32
+ )
33
+ except ValueError as error:
34
+ print(f"ruff-legibility: {error}", file=sys.stderr)
35
+ return 2
36
+
37
+ paths = [Path(path) for path in args.paths]
38
+ files = discover_python_files(paths, settings)
39
+ diagnostics = _check_files(files, settings)
40
+ _print_diagnostics(diagnostics, output_format=args.output_format)
41
+
42
+ if args.exit_zero:
43
+ return 0
44
+ return 1 if diagnostics else 0
45
+
46
+
47
+ def _parse_args(argv: list[str]) -> argparse.Namespace:
48
+ top_level_flags = {"-h", "--help", "--version"}
49
+ if not argv or (argv[0] not in {"check", "rules"} and argv[0] not in top_level_flags):
50
+ argv = ["check", *argv]
51
+
52
+ parser = argparse.ArgumentParser(prog="ruff-legibility")
53
+ parser.add_argument("--version", action="version", version=f"ruff-legibility {__version__}")
54
+ subparsers = parser.add_subparsers(dest="command", required=True)
55
+
56
+ check = subparsers.add_parser("check", help="check Python files")
57
+ check.add_argument("paths", nargs="*", default=["."], help="files or directories to check")
58
+ check.add_argument("--config", help="path to ruff-legibility.toml or pyproject.toml")
59
+ check.add_argument("--select", help="comma-separated rule selectors to enable")
60
+ check.add_argument("--ignore", help="comma-separated rule selectors to ignore")
61
+ check.add_argument(
62
+ "--output-format",
63
+ "--format",
64
+ choices=("text", "json", "github"),
65
+ default="text",
66
+ help="diagnostic output format",
67
+ )
68
+ check.add_argument("--exit-zero", action="store_true", help="always exit successfully")
69
+ check.add_argument("--max-expression-operators", type=int)
70
+ check.add_argument("--max-if-operators", type=int)
71
+ check.add_argument("--max-ternary-operators", type=int)
72
+ check.add_argument("--max-control-flow-depth", type=int)
73
+
74
+ subparsers.add_parser("rules", help="list available rules")
75
+ return parser.parse_args(argv)
76
+
77
+
78
+ def _check_files(files: list[Path], settings) -> list[Diagnostic]:
79
+ diagnostics: list[Diagnostic] = []
80
+ for file in files:
81
+ diagnostics.extend(check_path(file, settings))
82
+ return sorted(diagnostics)
83
+
84
+
85
+ def _print_diagnostics(diagnostics: list[Diagnostic], *, output_format: str) -> None:
86
+ if output_format == "json":
87
+ print(json.dumps([diagnostic.to_json() for diagnostic in diagnostics], indent=2))
88
+ return
89
+
90
+ for diagnostic in diagnostics:
91
+ if output_format == "github":
92
+ file_name = _escape_github_command_property(diagnostic.path.as_posix())
93
+ message = _escape_github_command_data(f"{diagnostic.code} {diagnostic.message}")
94
+ print(
95
+ f"::warning file={file_name},"
96
+ f"line={diagnostic.line},col={diagnostic.column}::"
97
+ f"{message}"
98
+ )
99
+ else:
100
+ print(
101
+ f"{diagnostic.path.as_posix()}:{diagnostic.line}:{diagnostic.column}: "
102
+ f"{diagnostic.code} {diagnostic.message}"
103
+ )
104
+
105
+
106
+ def _escape_github_command_data(value: str) -> str:
107
+ return value.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
108
+
109
+
110
+ def _escape_github_command_property(value: str) -> str:
111
+ return _escape_github_command_data(value).replace(":", "%3A").replace(",", "%2C")
112
+
113
+
114
+ def _print_rules() -> None:
115
+ for code, rule in RULES.items():
116
+ print(f"{code} {rule.name}: {rule.summary}")