Skip to content
Merged
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
9 changes: 8 additions & 1 deletion {{cookiecutter.project_slug}}/.github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ jobs:
enable-cache: true
python-version: '3.14'
- name: Run Architecture Evaluation
run: uv run scripts/evaluate_architecture.py
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
gh pr diff ${{ github.event.pull_request.number }} | uv run scripts/evaluate_architecture.py
else
echo "Skipping PR diff for non-pull_request event"
fi
shell: bash

test:
Expand Down
2 changes: 1 addition & 1 deletion {{cookiecutter.project_slug}}/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ repos:
rev: v1.18.2
hooks:
- id: mypy
additional_dependencies: ["pydantic>=2.0"]
additional_dependencies: ["pydantic>=2.0", "pytest", "types-PyYAML"]
- repo: https://github.com/AleksaC/hadolint-py
rev: v2.14.0
hooks:
Expand Down
10 changes: 8 additions & 2 deletions {{cookiecutter.project_slug}}/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ dev = [
"deptry",
"deepdiff",
"hypothesis",
"syrupy"
"syrupy",
"mkdocstrings[python]>=0.24.0",
"types-pyyaml>=6.0.12.20250915",
"tzdata>=2025.3",
"pytest-xdist>=3.6.1",
"mike>=2.1.3",
"mkdocs-minify-plugin>=0.8.0",
"mkdocs-git-revision-date-localized-plugin>=1.3.0"
]

[tool.hatch.build.targets.wheel]
Expand All @@ -57,7 +64,6 @@ ignore = []
[tool.mypy]
python_version = "3.14"
strict = true
ignore_missing_imports = true
disallow_untyped_defs = true
warn_unused_ignores = true
plugins = ["pydantic.mypy"]
Expand Down
68 changes: 66 additions & 2 deletions {{cookiecutter.project_slug}}/scripts/evaluate_architecture.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,44 @@
for hidden kinetic execution side-effects outside designated structural components.
"""
import ast
import re
import sys
from collections import defaultdict
from pathlib import Path


def evaluate_file(filepath: Path) -> list[str]:
def parse_diff(diff_text: str) -> dict[Path, set[int]]:
"""
Parses a unified diff and returns a mapping of changed files
to a set of newly added or modified line numbers.
"""
changes: dict[Path, set[int]] = defaultdict(set)
current_file: Path | None = None
current_line = 0

for line in diff_text.splitlines():
if line.startswith("+++ b/"):
filepath = line[6:]
current_file = Path(filepath)
elif line.startswith("@@"):
# e.g., @@ -1,4 +1,5 @@
match = re.search(r"@@ -[0-9]+(?:,[0-9]+)? \+([0-9]+)(?:,[0-9]+)? @@", line)
if match:
current_line = int(match.group(1))
elif current_file is not None:
if line.startswith("+") and not line.startswith("+++"):
changes[current_file].add(current_line)
current_line += 1
elif line.startswith(" ") or line.startswith("-"):
if not line.startswith("-"):
current_line += 1
# Note: We skip decrementing or incrementing for lines starting with '-'
# as they are not in the new file version.

return dict(changes)


def evaluate_file(filepath: Path, changed_lines: set[int] | None = None) -> list[str]:
violations = []
try:
content = filepath.read_text(encoding="utf-8")
Expand All @@ -21,6 +54,13 @@ def evaluate_file(filepath: Path) -> list[str]:
return [f"Could not read {filepath}: {e}"]

for node in ast.walk(tree):
if not hasattr(node, "lineno"):
continue

# If we are given changed_lines, only check nodes on those lines.
if changed_lines is not None and node.lineno not in changed_lines:
continue

# Example naive check: block generic print() in production code outside cli/main.
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name) and node.func.id == "print":
Expand All @@ -32,11 +72,35 @@ def evaluate_file(filepath: Path) -> list[str]:

def main() -> None:
src_dir = Path("src")
all_violations = []

# Check if data is piped into stdin
if not sys.stdin.isatty():
diff_text = sys.stdin.read()
if diff_text.strip():
changed_files_map = parse_diff(diff_text)
for filepath, changed_lines in changed_files_map.items():
if filepath.suffix == ".py" and src_dir in filepath.parents and filepath.exists():
violations = evaluate_file(filepath, changed_lines)
if violations:
all_violations.append((filepath, violations))

if all_violations:
print("Architecture evaluation failed on PR diff:")
for filepath, violations in all_violations:
print(f" {filepath}:")
for v in violations:
print(f" - {v}")
sys.exit(1)
else:
print("Architecture evaluation passed on PR diff.")
sys.exit(0)

# Fallback: analyze all files in src/ if no diff is provided
if not src_dir.exists():
print("No src directory found to evaluate.")
sys.exit(0)

all_violations = []
for filepath in src_dir.rglob("*.py"):
violations = evaluate_file(filepath)
if violations:
Expand Down