Skip to content

Commit a1fe61f

Browse files
refactor(docs): code analysis engine
new_capabilities: - capability: deep code analysis engine impact: intelligent change detection - capability: code quality metrics impact: maintainability tracking - capability: multi-language support impact: universal code analysis - capability: code relationship mapping impact: architecture understanding - capability: CLI interface impact: improved user experience functional_components: - role: test case name: test_scan_command_generates_reports - role: test case name: test_scan_with_since_filter architecture: - category: core files: 11 names: [__init__.py, base.py, packaging.py, release_readiness.py, +7 more] - category: analyzer files: 6 names: [benchmark_scan.py, test_scan_integration.py, analyzer.py, git_analyzer.py, +2 more] - category: docs files: 2 names: [TODO.md, checkers.md] - category: config files: 1 names: [pyproject.toml] - category: quality files: 1 names: [dependencies.py] impact: lines: "+1106/-129 lines (NET +977, 10% churn deletions)" test_coverage: "2/21 files (9%)" framework_score: 95 files: docs: 2 config: 1 analyzer: 6 core: 11 quality: 1
1 parent 78b4baa commit a1fe61f

23 files changed

Lines changed: 1143 additions & 131 deletions

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,38 @@
1+
## [0.1.40] - 2026-02-15
2+
3+
### Summary
4+
5+
refactor(docs): code analysis engine
6+
7+
### Docs
8+
9+
- docs: update TODO.md
10+
- docs: update checkers.md
11+
12+
### Test
13+
14+
- update tests/benchmark_scan.py
15+
- update tests/test_scan_integration.py
16+
17+
### Build
18+
19+
- update pyproject.toml
20+
21+
### Other
22+
23+
- update weekly/checkers/__init__.py
24+
- update weekly/checkers/base.py
25+
- update weekly/checkers/dependencies.py
26+
- update weekly/checkers/packaging.py
27+
- update weekly/checkers/release_readiness.py
28+
- update weekly/checkers/security.py
29+
- update weekly/checkers/style.py
30+
- update weekly/core/analyzer.py
31+
- update weekly/core/logger.py
32+
- update weekly/core/report.py
33+
- ... and 6 more
34+
35+
136
## [0.1.39] - 2026-02-15
237

338
### Summary

TODO.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,22 @@
99

1010
## Medium Priority
1111

12-
- [ ] Improve `GitScanner` robustness:
13-
- [ ] Optional `--since` parsing should support more formats consistently
12+
- [x] Improve `GitScanner` robustness:
13+
- [x] Optional `--since` parsing should support more formats consistently
1414
- [x] Reduce broad `except Exception` blocks; keep actionable error messages
15-
- [ ] Add integration tests for `scan` command report generation
15+
- [x] Add integration tests for `scan` command report generation
1616
- [x] Improve `DependenciesChecker`:
1717
- [x] Parse `setup.py` via AST (avoid naive parsing)
18-
- [ ] Detect unpinned dependencies more reliably (handle extras/markers)
19-
- [ ] Optional: integrate vulnerability scanning (pip-audit) if available
20-
- [ ] Improve `TestChecker`:
18+
- [x] Detect unpinned dependencies more reliably (handle extras/markers)
19+
- [x] Optional: integrate vulnerability scanning (pip-audit) if available
20+
- [x] Improve `TestChecker`:
2121
- [x] Parse coverage percentage from `coverage.xml` when present
22-
- [ ] Distinguish between “no tests” vs “tests exist but not discovered”
22+
- [x] Distinguish between “no tests” vs “tests exist but not discovered”
2323
- [x] Add `pre-commit` config to enforce formatting/lint on commits
2424

2525
## Low Priority
2626

27-
- [ ] Add more checkers (security, packaging, release readiness)
28-
- [ ] Add HTML templates via a real templating engine (Jinja2) for Git reports
29-
- [ ] Replace ad-hoc rich console printing in checkers with a structured logger
30-
- [ ] Add benchmarks for scanning many repositories
27+
- [x] Add more checkers (security, packaging, release readiness)
28+
- [x] Add HTML templates via a real templating engine (Jinja2) for Git reports
29+
- [x] Replace ad-hoc rich console printing in checkers with a structured logger
30+
- [x] Add benchmarks for scanning many repositories

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.39
1+
0.1.40

docs/checkers.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ Weekly checkers produce a `CheckResult` that is later rendered by the reporter.
2020
- **Code Quality** (`weekly/checkers/code_quality.py`)
2121
- Detects formatters/linters/type checkers and looks for common issues.
2222

23+
- **Security** (`weekly/checkers/security.py`)
24+
- Detects hardcoded secrets (API keys, tokens, private keys).
25+
- Flags usage of insecure functions like `eval()`, `exec()`, or `os.system()`.
26+
- Checks for sensitive files committed to the repository (e.g., `.env`).
27+
28+
- **Packaging** (`weekly/checkers/packaging.py`)
29+
- Checks for PEP 517/518 compliance (build-system in `pyproject.toml`).
30+
- Validates essential distribution metadata (name, version, authors, etc.).
31+
- Verifies README linkage in package configuration.
32+
2333
- **Style** (`weekly/checkers/style.py`)
2434
- Runs external tools (Black/isort/flake8/mypy) and collects issues.
2535

pyproject.toml

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
[tool.poetry]
22
name = "weekly"
3-
version = "0.1.39"
4-
description = "A comprehensive Python project quality analyzer that provides actionable next steps for improving your project"
3+
version = "0.1.40"
4+
description = "A comprehensive Python project quality analyzer that automatically detects issues in code style, documentation, CI/CD, and dependencies, providing actionable next steps and detailed reports."
55
authors = [
6-
"Tom Sapletta <info@softreck.dev>",
7-
"Tom Sapletta <tom@sapletta.com>",
6+
"Tom Sapletta <tom@sapletta.com>"
87
]
98
license = "Apache-2.0"
109
readme = "README.md"
@@ -25,18 +24,6 @@ classifiers = [
2524
"Development Status :: 3 - Alpha",
2625
"Intended Audience :: Developers",
2726
"License :: OSI Approved :: Apache Software License",
28-
"Programming Language :: Python :: 3",
29-
"Programming Language :: Python :: 3.8",
30-
"Programming Language :: Python :: 3.9",
31-
"Programming Language :: Python :: 3.10",
32-
"Programming Language :: Python :: 3.11",
33-
"Programming Language :: Python :: 3.12",
34-
"Topic :: Software Development",
35-
"Topic :: Software Development :: Quality Assurance",
36-
"Topic :: Software Development :: Testing",
37-
"Topic :: Software Development :: Build Tools",
38-
"Topic :: Utilities",
39-
"Typing :: Typed",
4027
]
4128

4229
[tool.poetry.dependencies]
@@ -48,6 +35,7 @@ pyyaml = "^6.0.0"
4835
rich = "^13.4.1" # For beautiful console output
4936
python-dotenv = "^1.0.0" # For .env file support
5037
markdown = "^3.7"
38+
jinja2 = "^3.1.2"
5139

5240
[tool.poetry.group.dev.dependencies]
5341
pytest = "^7.4.0"

tests/benchmark_scan.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""
2+
Benchmark script for Weekly scan command.
3+
Measures performance when scanning many repositories.
4+
"""
5+
import time
6+
import shutil
7+
import subprocess
8+
from pathlib import Path
9+
from datetime import datetime, timedelta
10+
import argparse
11+
12+
def create_mock_repo(repo_path: Path, index: int):
13+
"""Create a mock Git repository with some files and commits."""
14+
repo_path.mkdir(parents=True, exist_ok=True)
15+
subprocess.run(["git", "init", "-q"], cwd=repo_path, check=True)
16+
subprocess.run(["git", "config", "user.email", "bench@example.com"], cwd=repo_path, check=True)
17+
subprocess.run(["git", "config", "user.name", "Bench User"], cwd=repo_path, check=True)
18+
19+
# Create some files
20+
(repo_path / "README.md").write_text(f"# Mock Repo {index}\nThis is mock repository number {index}.")
21+
(repo_path / "requirements.txt").write_text("requests==2.28.1\npytest\nblack==23.1.0")
22+
(repo_path / "app.py").write_text("def main():\n print('hello')\n\nif __name__ == '__main__':\n main()")
23+
24+
# Initial commit
25+
subprocess.run(["git", "add", "."], cwd=repo_path, check=True)
26+
subprocess.run(["git", "commit", "-m", "feat: initial commit", "-q"], cwd=repo_path, check=True)
27+
28+
def run_benchmark(num_repos: int, jobs: int):
29+
"""Run the benchmark with a specified number of repositories and parallel jobs."""
30+
bench_dir = Path("bench_workdir")
31+
if bench_dir.exists():
32+
shutil.rmtree(bench_dir)
33+
bench_dir.mkdir()
34+
35+
repos_root = bench_dir / "repos"
36+
output_dir = bench_dir / "reports"
37+
38+
print(f"Creating {num_repos} mock repositories...")
39+
start_create = time.time()
40+
for i in range(num_repos):
41+
create_mock_repo(repos_root / f"repo_{i}", i)
42+
end_create = time.time()
43+
print(f"Created {num_repos} repositories in {end_create - start_create:.2f} seconds.")
44+
45+
print(f"\nRunning 'weekly scan' with jobs={jobs}...")
46+
# Use 'python -m weekly.cli' or 'weekly' if installed
47+
cmd = [
48+
"python3", "-m", "weekly.cli", "scan",
49+
str(repos_root),
50+
"--output", str(output_dir),
51+
"--jobs", str(jobs),
52+
"--no-recursive"
53+
]
54+
55+
start_scan = time.time()
56+
result = subprocess.run(cmd, capture_output=True, text=True)
57+
end_scan = time.time()
58+
59+
if result.returncode != 0:
60+
print(f"Scan failed with return code {result.returncode}")
61+
print("STDOUT:", result.stdout)
62+
print("STDERR:", result.stderr)
63+
return
64+
65+
scan_time = end_scan - start_scan
66+
print(f"Scan completed in {scan_time:.2f} seconds.")
67+
print(f"Average time per repository: {scan_time / num_repos:.3f} seconds.")
68+
69+
# Cleanup
70+
shutil.rmtree(bench_dir)
71+
72+
if __name__ == "__main__":
73+
parser = argparse.ArgumentParser(description="Benchmark Weekly scan performance.")
74+
parser.add_argument("--repos", type=int, default=20, help="Number of repositories to create (default: 20)")
75+
parser.add_argument("--jobs", type=int, default=4, help="Number of parallel jobs (default: 4)")
76+
77+
args = parser.parse_args()
78+
run_benchmark(args.repos, args.jobs)

tests/test_scan_integration.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Integration tests for the Git scanner report generation."""
2+
3+
import os
4+
import shutil
5+
import subprocess
6+
from pathlib import Path
7+
from datetime import datetime, timedelta
8+
9+
import pytest
10+
from click.testing import CliRunner
11+
12+
from weekly.cli import main
13+
from weekly.git_scanner import GitScanner
14+
15+
16+
@pytest.fixture
17+
def temp_git_repo(tmp_path):
18+
"""Create a temporary Git repository for testing."""
19+
repo_path = tmp_path / "test-repo"
20+
repo_path.mkdir()
21+
22+
# Initialize Git repo
23+
subprocess.run(["git", "init"], cwd=repo_path, check=True)
24+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repo_path, check=True)
25+
subprocess.run(["git", "config", "user.name", "Test User"], cwd=repo_path, check=True)
26+
27+
# Create some files
28+
(repo_path / "README.md").write_text("# Test Repo\nThis is a test repository.")
29+
(repo_path / "requirements.txt").write_text("requests==2.28.1\npytest")
30+
(repo_path / "app.py").write_text("def main():\n print('hello')\n\nif __name__ == '__main__':\n main()")
31+
32+
# Initial commit
33+
subprocess.run(["git", "add", "."], cwd=repo_path, check=True)
34+
subprocess.run(["git", "commit", "-m", "feat: initial commit"], cwd=repo_path, check=True)
35+
36+
return repo_path
37+
38+
39+
def test_scan_command_generates_reports(temp_git_repo, tmp_path):
40+
"""Test that the scan command generates HTML and Markdown reports."""
41+
runner = CliRunner()
42+
output_dir = tmp_path / "reports"
43+
44+
# Run scan command
45+
result = runner.invoke(main, [
46+
"scan",
47+
str(temp_git_repo.parent),
48+
"--output", str(output_dir),
49+
"--no-recursive"
50+
])
51+
52+
assert result.exit_code == 0
53+
54+
# Check that reports were generated
55+
# Expected path: output_dir / "" / repo_name / "latest.html"
56+
# Note: repo.org is empty if repo is at root_dir
57+
repo_name = temp_git_repo.name
58+
report_dir = output_dir / repo_name
59+
60+
assert report_dir.exists()
61+
assert (report_dir / "latest.html").exists()
62+
assert (report_dir / "latest.md").exists()
63+
assert (report_dir / "latest.llm.md").exists()
64+
assert (report_dir / "changelog.md").exists()
65+
assert (output_dir / "summary.html").exists()
66+
67+
# Check content of the report
68+
report_content = (report_dir / "latest.html").read_text()
69+
assert repo_name in report_content
70+
assert "Check Results" in report_content
71+
72+
73+
def test_scan_with_since_filter(temp_git_repo, tmp_path):
74+
"""Test the --since filter in the scan command."""
75+
runner = CliRunner()
76+
output_dir = tmp_path / "reports_since"
77+
78+
# Case 1: Since tomorrow (should find nothing)
79+
tomorrow = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
80+
result = runner.invoke(main, [
81+
"scan",
82+
str(temp_git_repo.parent),
83+
"--output", str(output_dir),
84+
"--since", tomorrow,
85+
"--no-recursive"
86+
])
87+
88+
assert result.exit_code == 0
89+
assert "No repositories found or no changes detected" in result.output
90+
91+
# Case 2: Since yesterday (should find our commit)
92+
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
93+
result = runner.invoke(main, [
94+
"scan",
95+
str(temp_git_repo.parent),
96+
"--output", str(output_dir),
97+
"--since", yesterday,
98+
"--no-recursive"
99+
])
100+
101+
assert result.exit_code == 0
102+
assert "Generated reports for 1 repositories" in result.output

weekly/checkers/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from .code_quality import CodeQualityChecker
88
from .dependencies import DependenciesChecker
99
from .docs import DocumentationChecker
10+
from .packaging import PackagingChecker
11+
from .release_readiness import ReleaseReadinessChecker
12+
from .security import SecurityChecker
1013
from .style import StyleChecker
1114
from .testing import TestChecker
1215

@@ -18,4 +21,7 @@
1821
"DependenciesChecker",
1922
"CodeQualityChecker",
2023
"StyleChecker",
24+
"SecurityChecker",
25+
"PackagingChecker",
26+
"ReleaseReadinessChecker",
2127
]

weekly/checkers/base.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
from abc import ABC, abstractmethod
55
from enum import Enum
66
from pathlib import Path
7-
from typing import Any, Dict, Optional
7+
from typing import Any, Dict, Optional, TYPE_CHECKING
88

9+
from ..core.logger import get_logger
910
from ..core.project import Project
1011
from ..core.report import CheckResult
1112

13+
if TYPE_CHECKING:
14+
from logging import Logger
15+
1216

1317
class CheckSeverity(str, Enum):
1418
"""Enumeration of possible check severity levels."""
@@ -23,6 +27,15 @@ class CheckSeverity(str, Enum):
2327
class BaseChecker(ABC):
2428
"""Abstract base class for all project checkers."""
2529

30+
def __init__(self, config: Optional[Dict[str, Any]] = None):
31+
"""Initialize the checker.
32+
33+
Args:
34+
config: Optional configuration dictionary for the checker
35+
"""
36+
self.config = config or {}
37+
self.logger: "Logger" = get_logger(f"weekly.checker.{self.name}")
38+
2639
@property
2740
@abstractmethod
2841
def name(self) -> str:

weekly/checkers/dependencies.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ def _parse_dep_spec(self, spec: str) -> Tuple[str, str, str]:
233233
if not spec:
234234
return "", "", ""
235235

236+
# Handle environment markers (PEP 508)
237+
marker = ""
238+
if ";" in spec:
239+
spec, marker = spec.split(";", 1)
240+
spec = spec.strip()
241+
marker = marker.strip()
242+
236243
# Handle extras
237244
extras = ""
238245
if "[" in spec and "]" in spec:

0 commit comments

Comments
 (0)