Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .cursor/rules/file_length.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
description: File length limits for Python files
globs: *.py
alwaysApply: false
---
## File Length Limit

Python files must not exceed 500 lines. This is enforced by `make file_len_check` (part of `make ci`) and in the linter GitHub Actions workflow.

This limit exists to prevent AI-generated code from becoming monolithic and forces refactoring into smaller, more modular components.

If a file genuinely needs to exceed 500 lines, add it to the `exclude` list in `pyproject.toml` under `[tool.file_length]`:

```toml
[tool.file_length]
max_lines = 500
exclude = [
"onboard.py",
]
```

Do not add files to the exclude list without a clear justification.
2 changes: 2 additions & 0 deletions .github/workflows/linter_require_ruff.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ jobs:
run: uv run python scripts/check_ai_writing.py
- name: Run import-linter
run: make import_lint
- name: Run file length check
run: uv run python scripts/check_file_length.py
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,12 @@ check_deps: install_tools ## Check for unused dependencies
@uv run deptry .
@echo "$(GREEN)✅Dependency check completed.$(RESET)"

ci: ruff vulture import_lint ty docs_lint lint_links check_deps ## Run all CI checks (ruff, vulture, import_lint, ty, docs_lint, lint_links)
file_len_check: check_uv ## Check Python files don't exceed max line count
@echo "$(YELLOW)🔍Checking file lengths...$(RESET)"
@uv run python scripts/check_file_length.py
@echo "$(GREEN)✅File length check completed.$(RESET)"

ci: ruff vulture import_lint ty docs_lint lint_links check_deps file_len_check ## Run all CI checks (ruff, vulture, import_lint, ty, docs_lint, lint_links, file_len_check)
@echo "$(GREEN)✅CI checks completed.$(RESET)"

########################################################
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ exclude = [
"onboard.py"
]

[tool.file_length]
max_lines = 500
exclude = [
"onboard.py",
]

[tool.coverage.run]
branch = true
source = ["src", "common", "utils"]
Expand Down
63 changes: 63 additions & 0 deletions scripts/check_file_length.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import pathlib
import tomllib

REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
ROOT_SKIP_DIRS = {
".git",
".venv",
".uv_cache",
".uv-cache",
".uv_tools",
".uv-tools",
".cache",
"node_modules",
".next",
}
RECURSIVE_SKIP_DIRS = {"__pycache__", ".pytest_cache"}


def load_config() -> tuple[int, set[str]]:
pyproject = REPO_ROOT / "pyproject.toml"
with open(pyproject, "rb") as f:
data = tomllib.load(f)
cfg = data.get("tool", {}).get("file_length", {})
max_lines = cfg.get("max_lines", 500)
exclude = set(cfg.get("exclude", []))
return max_lines, exclude


def main() -> int:
max_lines, exclude = load_config()
violations: list[tuple[pathlib.Path, int]] = []

for path in REPO_ROOT.rglob("*.py"):
rel = path.relative_to(REPO_ROOT)
parts = rel.parts
if parts[0] in ROOT_SKIP_DIRS:
continue
if any(part in RECURSIVE_SKIP_DIRS for part in parts[:-1]):
continue
if rel.as_posix() in exclude:
continue
line_count = len(path.read_text(encoding="utf-8", errors="ignore").splitlines())
if line_count > max_lines:
violations.append((rel, line_count))

if violations:
print(f"File length check failed: {len(violations)} file(s) exceed {max_lines} lines")
for rel_path, count in sorted(violations):
print(f" {rel_path}: {count} lines")
print(
"Refactor large files into smaller modules, "
"or add to [tool.file_length] exclude in pyproject.toml."
)
return 1

print(f"File length check passed (all files <= {max_lines} lines).")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading