pycodegate 0.2.0__tar.gz → 0.5.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.
Files changed (107) hide show
  1. {pycodegate-0.2.0 → pycodegate-0.5.0}/.gitignore +1 -0
  2. {pycodegate-0.2.0 → pycodegate-0.5.0}/AGENTS.md +23 -6
  3. pycodegate-0.5.0/CHANGELOG.md +20 -0
  4. {pycodegate-0.2.0 → pycodegate-0.5.0}/PKG-INFO +77 -25
  5. {pycodegate-0.2.0 → pycodegate-0.5.0}/README.md +75 -23
  6. pycodegate-0.5.0/benchmarks/README.md +31 -0
  7. pycodegate-0.5.0/benchmarks/results/0.5.0.json +227 -0
  8. pycodegate-0.5.0/benchmarks/results/0.5.0.md +84 -0
  9. pycodegate-0.5.0/benchmarks/run_corpus.py +140 -0
  10. {pycodegate-0.2.0 → pycodegate-0.5.0}/pyproject.toml +11 -1
  11. {pycodegate-0.2.0 → pycodegate-0.5.0}/skills/pycodegate/SKILL.md +3 -1
  12. pycodegate-0.5.0/src/pycodegate/__init__.py +10 -0
  13. pycodegate-0.5.0/src/pycodegate/__main__.py +6 -0
  14. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/api.py +2 -2
  15. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/cli.py +8 -4
  16. pycodegate-0.5.0/src/pycodegate/config.py +130 -0
  17. pycodegate-0.5.0/src/pycodegate/constants.py +61 -0
  18. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/discover.py +27 -0
  19. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/output.py +20 -52
  20. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/profile.py +1 -1
  21. pycodegate-0.5.0/src/pycodegate/rules/__init__.py +58 -0
  22. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/architecture.py +30 -3
  23. pycodegate-0.5.0/src/pycodegate/rules/celery.py +165 -0
  24. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/complexity.py +2 -1
  25. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/correctness.py +36 -44
  26. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/dead_code.py +5 -2
  27. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/django.py +33 -1
  28. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/fastapi.py +39 -8
  29. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/flask.py +22 -15
  30. pycodegate-0.5.0/src/pycodegate/rules/import_boundaries.py +70 -0
  31. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/imports.py +13 -1
  32. pycodegate-0.5.0/src/pycodegate/rules/logging_rules.py +165 -0
  33. pycodegate-0.5.0/src/pycodegate/rules/numpy_rules.py +164 -0
  34. pycodegate-0.5.0/src/pycodegate/rules/pandas_rules.py +165 -0
  35. pycodegate-0.5.0/src/pycodegate/rules/performance.py +117 -0
  36. pycodegate-0.5.0/src/pycodegate/rules/pydantic.py +271 -0
  37. pycodegate-0.5.0/src/pycodegate/rules/pytest_rules.py +142 -0
  38. pycodegate-0.5.0/src/pycodegate/rules/requests_rules.py +84 -0
  39. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/security.py +39 -0
  40. pycodegate-0.5.0/src/pycodegate/rules/sqlalchemy.py +145 -0
  41. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/structure.py +18 -3
  42. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/scan.py +51 -10
  43. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/score.py +32 -16
  44. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/types.py +18 -1
  45. pycodegate-0.5.0/src/pycodegate/utils/ast_helpers.py +34 -0
  46. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/utils/badge.py +2 -2
  47. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/utils/file_discovery.py +22 -10
  48. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/utils/fixer.py +4 -4
  49. pycodegate-0.5.0/src/pycodegate/utils/is_test_file.py +22 -0
  50. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/utils/precommit.py +10 -13
  51. pycodegate-0.5.0/tests/rules/__init__.py +0 -0
  52. pycodegate-0.5.0/tests/rules/test_celery.py +99 -0
  53. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_correctness.py +25 -10
  54. pycodegate-0.5.0/tests/rules/test_import_boundaries.py +135 -0
  55. pycodegate-0.5.0/tests/rules/test_imports.py +91 -0
  56. pycodegate-0.5.0/tests/rules/test_logging_rules.py +49 -0
  57. pycodegate-0.5.0/tests/rules/test_numpy_rules.py +71 -0
  58. pycodegate-0.5.0/tests/rules/test_pandas_rules.py +76 -0
  59. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_performance.py +0 -15
  60. pycodegate-0.5.0/tests/rules/test_pydantic.py +206 -0
  61. pycodegate-0.5.0/tests/rules/test_pytest_rules.py +59 -0
  62. pycodegate-0.5.0/tests/rules/test_requests_rules.py +80 -0
  63. pycodegate-0.5.0/tests/rules/test_sqlalchemy.py +73 -0
  64. pycodegate-0.5.0/tests/test_audit_fixes.py +180 -0
  65. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_cli.py +3 -2
  66. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_config.py +20 -0
  67. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_fixer.py +0 -5
  68. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_output.py +0 -32
  69. pycodegate-0.5.0/tests/test_precision.py +75 -0
  70. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_profile.py +3 -2
  71. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_score.py +2 -30
  72. {pycodegate-0.2.0 → pycodegate-0.5.0}/uv.lock +1 -1
  73. pycodegate-0.2.0/src/pycodegate/__init__.py +0 -3
  74. pycodegate-0.2.0/src/pycodegate/config.py +0 -81
  75. pycodegate-0.2.0/src/pycodegate/constants.py +0 -32
  76. pycodegate-0.2.0/src/pycodegate/rules/__init__.py +0 -38
  77. pycodegate-0.2.0/src/pycodegate/rules/performance.py +0 -104
  78. pycodegate-0.2.0/src/pycodegate/utils/ast_helpers.py +0 -19
  79. pycodegate-0.2.0/tests/rules/test_imports.py +0 -49
  80. {pycodegate-0.2.0 → pycodegate-0.5.0}/.github/workflows/ci.yml +0 -0
  81. {pycodegate-0.2.0 → pycodegate-0.5.0}/.github/workflows/publish.yml +0 -0
  82. {pycodegate-0.2.0 → pycodegate-0.5.0}/LICENSE +0 -0
  83. {pycodegate-0.2.0 → pycodegate-0.5.0}/docs/superpowers/plans/2026-03-22-python-doctor.md +0 -0
  84. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/_compat.py +0 -0
  85. /pycodegate-0.2.0/tests/rules/__init__.py → /pycodegate-0.5.0/src/pycodegate/py.typed +0 -0
  86. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/base.py +0 -0
  87. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/rules/dependencies.py +0 -0
  88. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/utils/__init__.py +0 -0
  89. {pycodegate-0.2.0 → pycodegate-0.5.0}/src/pycodegate/utils/diff.py +0 -0
  90. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/conftest.py +0 -0
  91. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/fixtures/basic_python/app.py +0 -0
  92. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/fixtures/clean_project/app.py +0 -0
  93. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_architecture.py +0 -0
  94. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_complexity.py +0 -0
  95. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_dead_code.py +0 -0
  96. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_dependencies.py +0 -0
  97. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_django.py +0 -0
  98. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_fastapi.py +0 -0
  99. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_flask.py +0 -0
  100. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_security.py +0 -0
  101. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_security_new.py +0 -0
  102. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/rules/test_structure.py +0 -0
  103. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_discover.py +0 -0
  104. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_integration.py +0 -0
  105. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_scan.py +0 -0
  106. {pycodegate-0.2.0 → pycodegate-0.5.0}/tests/test_types.py +0 -0
  107. {pycodegate-0.2.0 → pycodegate-0.5.0}/whitelist.py +0 -0
@@ -15,3 +15,4 @@ htmlcov/
15
15
  *.so
16
16
  .env
17
17
  .DS_Store
18
+ benchmarks/.corpus/
@@ -63,16 +63,33 @@ src/pycodegate/
63
63
  constants.py — Magic numbers and thresholds
64
64
  rules/
65
65
  base.py — Abstract BaseRules with AST parsing
66
- security.py — eval, exec, pickle, yaml, secrets, hashes
67
- performance.py — string concat, imports in functions, star imports
66
+ security.py — eval, exec, pickle, yaml, secrets, hashes, os.system, shell
67
+ performance.py — string concat in loops, star imports
68
68
  architecture.py — giant modules, nesting, god functions, too many args
69
+ complexity.py — cyclomatic complexity
69
70
  correctness.py — mutable defaults, bare except, assert, return in init
70
- django.py — raw SQL, DEBUG, SECRET_KEY, N+1
71
- fastapi.py — sync endpoints, missing response_model
72
- flask.py — secret key, debug mode, SQL f-strings
71
+ imports.py — circular imports (project-level)
72
+ structure.py — tests/README/LICENSE/linter/type-checker, type coverage
73
+ dependencies.py — pip-audit vulnerability scan
73
74
  dead_code.py — Vulture integration
75
+ django.py / fastapi.py / flask.py — web framework rules
76
+ pydantic.py / sqlalchemy.py / celery.py / requests_rules.py /
77
+ logging_rules.py / pandas_rules.py / pytest_rules.py / numpy_rules.py — library rules
74
78
  utils/
75
- file_discovery.py — Python file discovery (git + fallback)
79
+ file_discovery.py — Python file discovery (git + fallback), excludes non-source dirs
80
+ is_test_file.py — test-file detection (drives test-only rule suppression)
76
81
  ast_helpers.py — Common AST utilities
77
82
  diff.py — Git diff file resolution
78
83
  ```
84
+
85
+ ## Precision is the product
86
+
87
+ PyCodeGate is a quality **gate** — false positives destroy its value. Be precision-first:
88
+
89
+ - A new rule MUST ship with a negative fixture/test proving it does NOT fire on the
90
+ idiomatic, correct form of the pattern, not just a positive test.
91
+ - Rules that are idiomatic in tests (asserts, long fixtures) belong in
92
+ `RULES_SUPPRESSED_IN_TESTS` in `constants.py`; suppression happens centrally in
93
+ `scan._suppress_test_noise`, keyed on the project-relative path.
94
+ - Validate rule changes against the corpus benchmark: `python benchmarks/run_corpus.py`.
95
+ A rule lighting up respected codebases needs to be narrowed, suppressed, or removed.
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0
4
+
5
+ - Add opt-in, path-scoped import boundaries to turn architectural constraints into
6
+ deterministic `imports/boundary` errors with project-specific remediation guidance.
7
+ - Support absolute and relative static imports, aliases, nested imports, and
8
+ type-checking blocks in conventional flat and `src/` layouts.
9
+ - Integrate boundaries with JSON, SARIF, CI failure thresholds, rule ignores,
10
+ per-file suppression, and the lint toggle.
11
+ - Reject malformed boundary configuration with a readable CLI error.
12
+ - Enforce separation between PyCodeGate's analysis rules, shared data types, and
13
+ presentation using the same boundary configuration available to users.
14
+
15
+ No import restrictions are enabled for projects without boundary configuration.
16
+ Dynamic imports, re-export tracing, and custom import roots are outside the scope
17
+ of this check. See the README for configuration and matching semantics.
18
+
19
+ This release also includes the precision, scoring, and output fixes already on
20
+ main after the published 0.3.0 release.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pycodegate
3
- Version: 0.2.0
3
+ Version: 0.5.0
4
4
  Summary: Trust, but verify. The quality gate for AI-generated Python code.
5
5
  Project-URL: Homepage, https://github.com/themohitkhare/pycodegate
6
6
  Project-URL: Repository, https://github.com/themohitkhare/pycodegate
@@ -53,7 +53,9 @@ AI coding agents ship code fast — but fast doesn't mean safe. Every `eval()` a
53
53
 
54
54
  PyCodeGate detects your project's context: framework (Django, FastAPI, Flask), Python version, package manager (uv, poetry, pip), and test framework. That context drives which rules are active — Django projects get SQL injection checks, FastAPI projects get async-correctness checks, and so on.
55
55
 
56
- It then runs two analysis passes **in parallel**: a lint pass that evaluates 40+ rules across 8 categories (Security, Correctness, Complexity, Architecture, Performance, Structure, Imports, Dead Code), and a dead-code pass powered by [Vulture](https://github.com/jendrikseipp/vulture) that finds unused functions, classes, imports, and variables.
56
+ It then runs analysis passes **in parallel**: a lint pass across 9 categories (Security, Correctness, Complexity, Architecture, Performance, Structure, Imports, Dependencies, Dead Code), a dead-code pass powered by [Vulture](https://github.com/jendrikseipp/vulture) that finds unused functions, classes, imports, and variables, and an optional dependency-vulnerability pass via [pip-audit](https://github.com/pypa/pip-audit).
57
+
58
+ PyCodeGate is **precision-first**: it is meant to stay quiet on idiomatic, correct code so that a finding always means something. Test files only receive rules that make sense for tests (asserts, long fixtures, and long test functions are not flagged), and generated or non-source trees — `migrations`, `examples`, `docs`, and vendored directories — are skipped. The rule set is benchmarked against a corpus of respected PyPI packages (see [`benchmarks/`](benchmarks/)) to keep false positives down.
57
59
 
58
60
  Findings are filtered through your configuration and scored using a **weighted category-budget system**. Each category has a maximum deduction budget proportional to its weight. Within a category the top 3 findings apply at full cost; additional findings apply diminishing returns (10% each), so fixing the worst issues always moves the needle. The final result is a **0–100 health score** with a label: Excellent (90+), Great (75–89), Needs work (50–74), or Critical (<50).
59
61
 
@@ -107,7 +109,7 @@ pycodegate . --json
107
109
 
108
110
  ```json
109
111
  {
110
- "version": "0.1.0",
112
+ "version": "0.3.0",
111
113
  "path": ".",
112
114
  "score": 87,
113
115
  "label": "Great",
@@ -225,26 +227,33 @@ jobs:
225
227
  | `--verbose` | off | Show file path and line number per finding |
226
228
  | `--score` | off | Print only the numeric score and exit |
227
229
  | `--json` | off | Emit structured JSON (for agents and CI) |
230
+ | `--sarif` | off | Emit SARIF 2.1.0 (for GitHub Code Scanning) |
228
231
  | `--fix` | off | Run `ruff --fix` before scanning |
229
232
  | `--diff TEXT` | — | Scan only files changed vs this base branch |
230
233
  | `--fail-on [error\|warning\|none]` | `none` | Exit code 1 when findings at this level exist |
234
+ | `--min-score INT` | — | Exit code 1 when the score is below this threshold |
235
+ | `--profile [cli\|web\|library\|script]` | auto | Override the auto-detected project profile |
236
+ | `--badge` | off | Print a shields.io badge markdown snippet |
237
+ | `--ci` | off | Print a ready-to-use GitHub Actions workflow |
238
+ | `--pre-commit` | off | Install a git pre-commit hook |
231
239
  | `-v, --version` | — | Show version and exit |
232
240
  | `-h, --help` | — | Show help and exit |
233
241
 
234
242
  ## What It Checks
235
243
 
236
- | Category | Weight | Max Deduction | What it catches |
237
- |----------|--------|---------------|-----------------|
238
- | Security | 5 | ~24 pts | `eval`, `exec`, `pickle.load`, unsafe YAML, hardcoded secrets, weak hashes |
239
- | Correctness | 4 | ~19 pts | Mutable defaults, bare/broad except, assert in production, bad `__init__` return |
240
- | Complexity | 3 | ~14 pts | Cyclomatic complexity > 15 (warning) or > 25 (error) |
241
- | Architecture | 3 | ~14 pts | Giant modules (>500 lines), deep nesting (>5), god functions (>50 lines), too many args (>7) |
242
- | Performance | 2 | ~10 pts | String concat in loops, imports inside functions, star imports |
243
- | Structure | 2 | ~10 pts | Missing `__init__.py`, missing tests directory, no type hints |
244
- | Imports | 1 | ~5 pts | Circular imports, wildcard imports, import order issues |
245
- | Dead Code | 1 | ~5 pts | Unused functions, classes, variables, and imports via Vulture |
244
+ | Category | Weight | What it catches |
245
+ |----------|--------|-----------------|
246
+ | Security | 5 | `eval`, `exec`, `pickle.load`, unsafe YAML, hardcoded secrets, weak hashes (excluding `usedforsecurity=False`), `os.system`, `shell=True`, `tempfile.mktemp` |
247
+ | Correctness | 4 | Mutable defaults, bare except, assert in production, bad `__init__` return |
248
+ | Complexity | 3 | Cyclomatic complexity > 15 (warning) or > 25 (error) |
249
+ | Architecture | 3 | Giant modules (>500 lines), deep nesting (>5), god functions (>50 lines), too many args (>7) |
250
+ | Performance | 2 | String concatenation in loops, star imports |
251
+ | Structure | 2 | Missing tests, README, LICENSE, linter/type-checker config, low type coverage |
252
+ | Imports | 1 | Circular imports between sibling modules |
253
+ | Dependencies | 1 | Known-vulnerable dependencies (via `pip-audit`, when installed) |
254
+ | Dead Code | 1 | Unused functions, classes, variables, and imports via Vulture |
246
255
 
247
- Framework-specific rules (Django, FastAPI, Flask) are mapped into the Security or Correctness budget.
256
+ Weights are normalized to a 100-point deduction budget. Framework- and library-specific rules (Django, FastAPI, Flask, Pydantic, SQLAlchemy, Celery, requests, logging, pandas, pytest, NumPy) are mapped into the Security or Correctness budget.
248
257
 
249
258
  ## Scoring
250
259
 
@@ -281,8 +290,7 @@ files = ["tests/fixtures/**", "migrations/**", "scripts/**"]
281
290
  "src/legacy/*.py" = ["high-complexity", "no-god-function"]
282
291
 
283
292
  [scoring]
284
- max-deduction.Security = 20
285
- max-deduction.Dead Code = 0 # disable dead-code penalty entirely
293
+ max-deduction = { Security = 20, "Dead Code" = 0 } # 0 disables a category's penalty
286
294
  ```
287
295
 
288
296
  Or use `pyproject.toml`:
@@ -306,26 +314,70 @@ files = ["tests/fixtures/**"]
306
314
 
307
315
  If both files exist, `pycodegate.toml` takes precedence. CLI flags always override config values.
308
316
 
317
+ ## Import boundaries
318
+
319
+ Turn architectural review feedback into a deterministic check. Import boundaries
320
+ are opt-in: forbid a module (and its submodules) in selected files and provide
321
+ actionable guidance for the developer or agent.
322
+
323
+ In `pycodegate.toml`:
324
+
325
+ ```toml
326
+ [[import-boundaries]]
327
+ files = ["src/app/domain/*.py"]
328
+ forbidden = ["app.infrastructure", "requests", "fastapi"]
329
+ message = "Keep the domain independent of infrastructure; use a domain port."
330
+
331
+ [[import-boundaries]]
332
+ files = ["src/app/*.py"]
333
+ forbidden = ["app.legacy_client"]
334
+ message = "Use app.clients instead of adding new legacy dependencies."
335
+ ```
336
+
337
+ For `pyproject.toml`, use `[[tool.pycodegate.import-boundaries]]` with the same
338
+ fields. `files` uses case-sensitive project-relative POSIX path globs; `*` can
339
+ span directories. `forbidden` contains literal dotted module prefixes, so
340
+ `requests` matches `requests.sessions` but not `requests_extra`. Both arrays
341
+ must contain at least one nonempty string. The optional `message` supplies the
342
+ diagnostic's remediation guidance.
343
+
344
+ Checks cover static `import` and `from ... import ...` statements, including
345
+ aliases, nested functions, and type-checking blocks. Relative imports are resolved
346
+ using the path under the project root, stripping a leading `src/` for conventional
347
+ src layouts. Custom import roots, runtime imports via `importlib` or `__import__`,
348
+ and re-export tracing are not supported. Importing a name from a package is
349
+ treated as importing that dotted name, so avoid forbidden prefixes that collide
350
+ with allowed attributes on the same package.
351
+
352
+ Violations emit `imports/boundary` errors with the source location and your
353
+ message in terminal, JSON, and SARIF output. Use
354
+ `pycodegate . --fail-on error` to enforce the boundary in CI. Existing rule and
355
+ per-file ignores apply; `--no-lint` disables boundary checks. Without configured
356
+ boundaries, scan behavior is unchanged. Static checks supplement tests and
357
+ runtime verification; a high score alone does not prove behavior is correct.
358
+
309
359
  ## Profiles
310
360
 
311
361
  PyCodeGate auto-detects a project profile and adjusts rule weights accordingly. You can also set a profile explicitly in config (`profile = "library"`).
312
362
 
313
363
  | Profile | Auto-detected when | Adjustments |
314
364
  |---------|--------------------|-------------|
315
- | `cli` | `[project.scripts]` in pyproject.toml | Architecture rules weighted up; dead-code weighted down |
316
- | `web` | Django / FastAPI / Flask detected | Security and correctness weighted up; framework rules active |
317
- | `library` | No scripts, no framework, has `py.typed` | Public API checks active; dead-code weighted up |
318
- | `script` | Single-file project or `scripts/` directory | Architecture rules relaxed; complexity thresholds raised |
365
+ | `cli` | `[project.scripts]` or a CLI dependency (click, typer, …) | Security budget relaxed; `subprocess`/`shell` rules suppressed |
366
+ | `web` | Django / FastAPI / Flask / Starlette / aiohttp / … detected | Framework rules active |
367
+ | `library` | Has a `[build-system]` and no scripts | Default budgets |
368
+ | `script` | No package `__init__.py` and ≤ 5 top-level `.py` files | Structure budget relaxed; `no-tests`/`no-license` suppressed |
319
369
 
320
370
  ## Pre-commit Hook
321
371
 
322
- Use `--pre-commit` to run PyCodeGate as a pre-commit hook. It automatically scans only the staged files:
372
+ `--pre-commit` installs a git `pre-commit` hook (one-time) that scores the project on
373
+ every commit and blocks the commit when the score falls below the threshold:
323
374
 
324
375
  ```bash
325
- pycodegate . --pre-commit --fail-on error
376
+ pycodegate . --pre-commit --min-score 80
326
377
  ```
327
378
 
328
- Add to `.pre-commit-config.yaml`:
379
+ If you use the [pre-commit framework](https://pre-commit.com), add it to
380
+ `.pre-commit-config.yaml` instead:
329
381
 
330
382
  ```yaml
331
383
  repos:
@@ -333,7 +385,7 @@ repos:
333
385
  hooks:
334
386
  - id: pycodegate
335
387
  name: PyCodeGate quality check
336
- entry: pycodegate . --pre-commit --fail-on error
388
+ entry: pycodegate . --fail-on error
337
389
  language: system
338
390
  types: [python]
339
391
  pass_filenames: false
@@ -19,7 +19,9 @@ AI coding agents ship code fast — but fast doesn't mean safe. Every `eval()` a
19
19
 
20
20
  PyCodeGate detects your project's context: framework (Django, FastAPI, Flask), Python version, package manager (uv, poetry, pip), and test framework. That context drives which rules are active — Django projects get SQL injection checks, FastAPI projects get async-correctness checks, and so on.
21
21
 
22
- It then runs two analysis passes **in parallel**: a lint pass that evaluates 40+ rules across 8 categories (Security, Correctness, Complexity, Architecture, Performance, Structure, Imports, Dead Code), and a dead-code pass powered by [Vulture](https://github.com/jendrikseipp/vulture) that finds unused functions, classes, imports, and variables.
22
+ It then runs analysis passes **in parallel**: a lint pass across 9 categories (Security, Correctness, Complexity, Architecture, Performance, Structure, Imports, Dependencies, Dead Code), a dead-code pass powered by [Vulture](https://github.com/jendrikseipp/vulture) that finds unused functions, classes, imports, and variables, and an optional dependency-vulnerability pass via [pip-audit](https://github.com/pypa/pip-audit).
23
+
24
+ PyCodeGate is **precision-first**: it is meant to stay quiet on idiomatic, correct code so that a finding always means something. Test files only receive rules that make sense for tests (asserts, long fixtures, and long test functions are not flagged), and generated or non-source trees — `migrations`, `examples`, `docs`, and vendored directories — are skipped. The rule set is benchmarked against a corpus of respected PyPI packages (see [`benchmarks/`](benchmarks/)) to keep false positives down.
23
25
 
24
26
  Findings are filtered through your configuration and scored using a **weighted category-budget system**. Each category has a maximum deduction budget proportional to its weight. Within a category the top 3 findings apply at full cost; additional findings apply diminishing returns (10% each), so fixing the worst issues always moves the needle. The final result is a **0–100 health score** with a label: Excellent (90+), Great (75–89), Needs work (50–74), or Critical (<50).
25
27
 
@@ -73,7 +75,7 @@ pycodegate . --json
73
75
 
74
76
  ```json
75
77
  {
76
- "version": "0.1.0",
78
+ "version": "0.3.0",
77
79
  "path": ".",
78
80
  "score": 87,
79
81
  "label": "Great",
@@ -191,26 +193,33 @@ jobs:
191
193
  | `--verbose` | off | Show file path and line number per finding |
192
194
  | `--score` | off | Print only the numeric score and exit |
193
195
  | `--json` | off | Emit structured JSON (for agents and CI) |
196
+ | `--sarif` | off | Emit SARIF 2.1.0 (for GitHub Code Scanning) |
194
197
  | `--fix` | off | Run `ruff --fix` before scanning |
195
198
  | `--diff TEXT` | — | Scan only files changed vs this base branch |
196
199
  | `--fail-on [error\|warning\|none]` | `none` | Exit code 1 when findings at this level exist |
200
+ | `--min-score INT` | — | Exit code 1 when the score is below this threshold |
201
+ | `--profile [cli\|web\|library\|script]` | auto | Override the auto-detected project profile |
202
+ | `--badge` | off | Print a shields.io badge markdown snippet |
203
+ | `--ci` | off | Print a ready-to-use GitHub Actions workflow |
204
+ | `--pre-commit` | off | Install a git pre-commit hook |
197
205
  | `-v, --version` | — | Show version and exit |
198
206
  | `-h, --help` | — | Show help and exit |
199
207
 
200
208
  ## What It Checks
201
209
 
202
- | Category | Weight | Max Deduction | What it catches |
203
- |----------|--------|---------------|-----------------|
204
- | Security | 5 | ~24 pts | `eval`, `exec`, `pickle.load`, unsafe YAML, hardcoded secrets, weak hashes |
205
- | Correctness | 4 | ~19 pts | Mutable defaults, bare/broad except, assert in production, bad `__init__` return |
206
- | Complexity | 3 | ~14 pts | Cyclomatic complexity > 15 (warning) or > 25 (error) |
207
- | Architecture | 3 | ~14 pts | Giant modules (>500 lines), deep nesting (>5), god functions (>50 lines), too many args (>7) |
208
- | Performance | 2 | ~10 pts | String concat in loops, imports inside functions, star imports |
209
- | Structure | 2 | ~10 pts | Missing `__init__.py`, missing tests directory, no type hints |
210
- | Imports | 1 | ~5 pts | Circular imports, wildcard imports, import order issues |
211
- | Dead Code | 1 | ~5 pts | Unused functions, classes, variables, and imports via Vulture |
210
+ | Category | Weight | What it catches |
211
+ |----------|--------|-----------------|
212
+ | Security | 5 | `eval`, `exec`, `pickle.load`, unsafe YAML, hardcoded secrets, weak hashes (excluding `usedforsecurity=False`), `os.system`, `shell=True`, `tempfile.mktemp` |
213
+ | Correctness | 4 | Mutable defaults, bare except, assert in production, bad `__init__` return |
214
+ | Complexity | 3 | Cyclomatic complexity > 15 (warning) or > 25 (error) |
215
+ | Architecture | 3 | Giant modules (>500 lines), deep nesting (>5), god functions (>50 lines), too many args (>7) |
216
+ | Performance | 2 | String concatenation in loops, star imports |
217
+ | Structure | 2 | Missing tests, README, LICENSE, linter/type-checker config, low type coverage |
218
+ | Imports | 1 | Circular imports between sibling modules |
219
+ | Dependencies | 1 | Known-vulnerable dependencies (via `pip-audit`, when installed) |
220
+ | Dead Code | 1 | Unused functions, classes, variables, and imports via Vulture |
212
221
 
213
- Framework-specific rules (Django, FastAPI, Flask) are mapped into the Security or Correctness budget.
222
+ Weights are normalized to a 100-point deduction budget. Framework- and library-specific rules (Django, FastAPI, Flask, Pydantic, SQLAlchemy, Celery, requests, logging, pandas, pytest, NumPy) are mapped into the Security or Correctness budget.
214
223
 
215
224
  ## Scoring
216
225
 
@@ -247,8 +256,7 @@ files = ["tests/fixtures/**", "migrations/**", "scripts/**"]
247
256
  "src/legacy/*.py" = ["high-complexity", "no-god-function"]
248
257
 
249
258
  [scoring]
250
- max-deduction.Security = 20
251
- max-deduction.Dead Code = 0 # disable dead-code penalty entirely
259
+ max-deduction = { Security = 20, "Dead Code" = 0 } # 0 disables a category's penalty
252
260
  ```
253
261
 
254
262
  Or use `pyproject.toml`:
@@ -272,26 +280,70 @@ files = ["tests/fixtures/**"]
272
280
 
273
281
  If both files exist, `pycodegate.toml` takes precedence. CLI flags always override config values.
274
282
 
283
+ ## Import boundaries
284
+
285
+ Turn architectural review feedback into a deterministic check. Import boundaries
286
+ are opt-in: forbid a module (and its submodules) in selected files and provide
287
+ actionable guidance for the developer or agent.
288
+
289
+ In `pycodegate.toml`:
290
+
291
+ ```toml
292
+ [[import-boundaries]]
293
+ files = ["src/app/domain/*.py"]
294
+ forbidden = ["app.infrastructure", "requests", "fastapi"]
295
+ message = "Keep the domain independent of infrastructure; use a domain port."
296
+
297
+ [[import-boundaries]]
298
+ files = ["src/app/*.py"]
299
+ forbidden = ["app.legacy_client"]
300
+ message = "Use app.clients instead of adding new legacy dependencies."
301
+ ```
302
+
303
+ For `pyproject.toml`, use `[[tool.pycodegate.import-boundaries]]` with the same
304
+ fields. `files` uses case-sensitive project-relative POSIX path globs; `*` can
305
+ span directories. `forbidden` contains literal dotted module prefixes, so
306
+ `requests` matches `requests.sessions` but not `requests_extra`. Both arrays
307
+ must contain at least one nonempty string. The optional `message` supplies the
308
+ diagnostic's remediation guidance.
309
+
310
+ Checks cover static `import` and `from ... import ...` statements, including
311
+ aliases, nested functions, and type-checking blocks. Relative imports are resolved
312
+ using the path under the project root, stripping a leading `src/` for conventional
313
+ src layouts. Custom import roots, runtime imports via `importlib` or `__import__`,
314
+ and re-export tracing are not supported. Importing a name from a package is
315
+ treated as importing that dotted name, so avoid forbidden prefixes that collide
316
+ with allowed attributes on the same package.
317
+
318
+ Violations emit `imports/boundary` errors with the source location and your
319
+ message in terminal, JSON, and SARIF output. Use
320
+ `pycodegate . --fail-on error` to enforce the boundary in CI. Existing rule and
321
+ per-file ignores apply; `--no-lint` disables boundary checks. Without configured
322
+ boundaries, scan behavior is unchanged. Static checks supplement tests and
323
+ runtime verification; a high score alone does not prove behavior is correct.
324
+
275
325
  ## Profiles
276
326
 
277
327
  PyCodeGate auto-detects a project profile and adjusts rule weights accordingly. You can also set a profile explicitly in config (`profile = "library"`).
278
328
 
279
329
  | Profile | Auto-detected when | Adjustments |
280
330
  |---------|--------------------|-------------|
281
- | `cli` | `[project.scripts]` in pyproject.toml | Architecture rules weighted up; dead-code weighted down |
282
- | `web` | Django / FastAPI / Flask detected | Security and correctness weighted up; framework rules active |
283
- | `library` | No scripts, no framework, has `py.typed` | Public API checks active; dead-code weighted up |
284
- | `script` | Single-file project or `scripts/` directory | Architecture rules relaxed; complexity thresholds raised |
331
+ | `cli` | `[project.scripts]` or a CLI dependency (click, typer, …) | Security budget relaxed; `subprocess`/`shell` rules suppressed |
332
+ | `web` | Django / FastAPI / Flask / Starlette / aiohttp / … detected | Framework rules active |
333
+ | `library` | Has a `[build-system]` and no scripts | Default budgets |
334
+ | `script` | No package `__init__.py` and ≤ 5 top-level `.py` files | Structure budget relaxed; `no-tests`/`no-license` suppressed |
285
335
 
286
336
  ## Pre-commit Hook
287
337
 
288
- Use `--pre-commit` to run PyCodeGate as a pre-commit hook. It automatically scans only the staged files:
338
+ `--pre-commit` installs a git `pre-commit` hook (one-time) that scores the project on
339
+ every commit and blocks the commit when the score falls below the threshold:
289
340
 
290
341
  ```bash
291
- pycodegate . --pre-commit --fail-on error
342
+ pycodegate . --pre-commit --min-score 80
292
343
  ```
293
344
 
294
- Add to `.pre-commit-config.yaml`:
345
+ If you use the [pre-commit framework](https://pre-commit.com), add it to
346
+ `.pre-commit-config.yaml` instead:
295
347
 
296
348
  ```yaml
297
349
  repos:
@@ -299,7 +351,7 @@ repos:
299
351
  hooks:
300
352
  - id: pycodegate
301
353
  name: PyCodeGate quality check
302
- entry: pycodegate . --pre-commit --fail-on error
354
+ entry: pycodegate . --fail-on error
303
355
  language: system
304
356
  types: [python]
305
357
  pass_filenames: false
@@ -0,0 +1,31 @@
1
+ # Precision benchmark
2
+
3
+ A quality gate is only useful if it stays quiet on good code. This benchmark runs
4
+ PyCodeGate against the source of widely used, idiomatic PyPI packages and reports
5
+ the findings per rule. A rule that fires heavily here is a false-positive suspect.
6
+
7
+ ```bash
8
+ python benchmarks/run_corpus.py # default corpus, lint only
9
+ python benchmarks/run_corpus.py --with-dead-code
10
+ python benchmarks/run_corpus.py flask django # a custom package list
11
+ ```
12
+
13
+ Source distributions are fetched from PyPI into `benchmarks/.corpus/` (git-ignored)
14
+ and cached between runs. No GitHub access is required.
15
+
16
+ ## How to read the output
17
+
18
+ The per-package table shows the score and finding count; the tail aggregates
19
+ findings by rule across the whole corpus. When triaging a rule with a high count,
20
+ open a few of the flagged files — if the pattern is idiomatic and correct, the rule
21
+ needs to be narrowed, suppressed in the relevant context (e.g. test files), or
22
+ removed. Genuine issues (a 100-branch function, `verify=False`) should remain.
23
+
24
+ The precision fixes derived from this benchmark are locked in by
25
+ `tests/test_precision.py` and the per-rule unit tests under `tests/rules/`.
26
+
27
+ ## Release validation
28
+
29
+ [0.5.0 validation](results/0.5.0.md) records the Python test matrix, packaging
30
+ checks, configured boundary checks, and an exact baseline comparison across the
31
+ 29-package corpus. Machine-readable results are saved alongside the report.
@@ -0,0 +1,227 @@
1
+ {
2
+ "release": "0.5.0",
3
+ "date": "2026-11-15",
4
+ "baseline_commit": "2a294b180daefe0511e253905b685de68d761175",
5
+ "candidate_code_commit": "8960979",
6
+ "packages": {
7
+ "requests": {
8
+ "files": 35,
9
+ "score": 68,
10
+ "findings": 169,
11
+ "matches_baseline": true,
12
+ "source_distribution": "requests-2.34.2"
13
+ },
14
+ "flask": {
15
+ "files": 65,
16
+ "score": 74,
17
+ "findings": 61,
18
+ "matches_baseline": true,
19
+ "source_distribution": "flask-3.1.3"
20
+ },
21
+ "click": {
22
+ "files": 76,
23
+ "score": 72,
24
+ "findings": 102,
25
+ "matches_baseline": true,
26
+ "source_distribution": "click-8.5.1"
27
+ },
28
+ "rich": {
29
+ "files": 100,
30
+ "score": 58,
31
+ "findings": 273,
32
+ "matches_baseline": true,
33
+ "source_distribution": "rich-15.0.0"
34
+ },
35
+ "httpx": {
36
+ "files": 60,
37
+ "score": 74,
38
+ "findings": 110,
39
+ "matches_baseline": true,
40
+ "source_distribution": "httpx-0.28.1"
41
+ },
42
+ "sqlalchemy": {
43
+ "files": 603,
44
+ "score": 22,
45
+ "findings": 2256,
46
+ "matches_baseline": true,
47
+ "source_distribution": "sqlalchemy-2.1.8"
48
+ },
49
+ "fastapi": {
50
+ "files": 738,
51
+ "score": 53,
52
+ "findings": 2383,
53
+ "matches_baseline": true,
54
+ "source_distribution": "fastapi-0.163.1"
55
+ },
56
+ "starlette": {
57
+ "files": 84,
58
+ "score": 71,
59
+ "findings": 162,
60
+ "matches_baseline": true,
61
+ "source_distribution": "starlette-1.10.0"
62
+ },
63
+ "pydantic": {
64
+ "files": 324,
65
+ "score": 35,
66
+ "findings": 720,
67
+ "matches_baseline": true,
68
+ "source_distribution": "pydantic-2.14.3"
69
+ },
70
+ "jinja2": {
71
+ "files": 49,
72
+ "score": 64,
73
+ "findings": 104,
74
+ "matches_baseline": true,
75
+ "source_distribution": "jinja2-3.1.6"
76
+ },
77
+ "werkzeug": {
78
+ "files": 84,
79
+ "score": 59,
80
+ "findings": 156,
81
+ "matches_baseline": true,
82
+ "source_distribution": "werkzeug-3.1.9"
83
+ },
84
+ "urllib3": {
85
+ "files": 80,
86
+ "score": 71,
87
+ "findings": 144,
88
+ "matches_baseline": true,
89
+ "source_distribution": "urllib3-2.8.0"
90
+ },
91
+ "django": {
92
+ "files": 2592,
93
+ "score": 33,
94
+ "findings": 969,
95
+ "matches_baseline": true,
96
+ "source_distribution": "django-6.1.3"
97
+ },
98
+ "celery": {
99
+ "files": 406,
100
+ "score": 37,
101
+ "findings": 472,
102
+ "matches_baseline": true,
103
+ "source_distribution": "celery-5.7.0"
104
+ },
105
+ "black": {
106
+ "files": 366,
107
+ "score": 35,
108
+ "findings": 392,
109
+ "matches_baseline": true,
110
+ "source_distribution": "black-26.10.1"
111
+ },
112
+ "isort": {
113
+ "files": 110,
114
+ "score": 70,
115
+ "findings": 144,
116
+ "matches_baseline": true,
117
+ "source_distribution": "isort-9.0.2"
118
+ },
119
+ "pytest": {
120
+ "files": 253,
121
+ "score": 35,
122
+ "findings": 1168,
123
+ "matches_baseline": true,
124
+ "source_distribution": "pytest-9.2.0"
125
+ },
126
+ "attrs": {
127
+ "files": 53,
128
+ "score": 77,
129
+ "findings": 54,
130
+ "matches_baseline": true,
131
+ "source_distribution": "attrs-26.1.0"
132
+ },
133
+ "typer": {
134
+ "files": 339,
135
+ "score": 66,
136
+ "findings": 155,
137
+ "matches_baseline": true,
138
+ "source_distribution": "typer-0.29.1"
139
+ },
140
+ "pyyaml": {
141
+ "files": 44,
142
+ "score": 58,
143
+ "findings": 127,
144
+ "matches_baseline": true,
145
+ "source_distribution": "pyyaml-6.0.3"
146
+ },
147
+ "redis": {
148
+ "files": 267,
149
+ "score": 43,
150
+ "findings": 1143,
151
+ "matches_baseline": true,
152
+ "source_distribution": "redis-8.1.0"
153
+ },
154
+ "scrapy": {
155
+ "files": 538,
156
+ "score": 45,
157
+ "findings": 417,
158
+ "matches_baseline": true,
159
+ "source_distribution": "scrapy-2.20.0"
160
+ },
161
+ "beautifulsoup4": {
162
+ "files": 38,
163
+ "score": 72,
164
+ "findings": 90,
165
+ "matches_baseline": true,
166
+ "source_distribution": "beautifulsoup4-4.15.0"
167
+ },
168
+ "loguru": {
169
+ "files": 19,
170
+ "score": 67,
171
+ "findings": 60,
172
+ "matches_baseline": true,
173
+ "source_distribution": "loguru-0.8.0"
174
+ },
175
+ "pandas": {
176
+ "files": 1448,
177
+ "score": 20,
178
+ "findings": 3210,
179
+ "matches_baseline": true,
180
+ "source_distribution": "pandas-3.1.1"
181
+ },
182
+ "numpy": {
183
+ "files": 1046,
184
+ "score": 22,
185
+ "findings": 2930,
186
+ "matches_baseline": true,
187
+ "source_distribution": "numpy-2.5.4"
188
+ },
189
+ "pillow": {
190
+ "files": 292,
191
+ "score": 45,
192
+ "findings": 666,
193
+ "matches_baseline": true,
194
+ "source_distribution": "pillow-13.0.0"
195
+ },
196
+ "tornado": {
197
+ "files": 77,
198
+ "score": 41,
199
+ "findings": 309,
200
+ "matches_baseline": true,
201
+ "source_distribution": "tornado-6.6.1"
202
+ },
203
+ "aiohttp": {
204
+ "files": 144,
205
+ "score": 44,
206
+ "findings": 430,
207
+ "matches_baseline": true,
208
+ "source_distribution": "aiohttp-3.14.5"
209
+ }
210
+ },
211
+ "files": 10330,
212
+ "diagnostic_regressions": 0,
213
+ "configured_boundary_checks": {
214
+ "requests": {
215
+ "forbidden_dependency": "urllib3",
216
+ "expected_violations": 28
217
+ },
218
+ "flask": {
219
+ "forbidden_dependency": "werkzeug",
220
+ "expected_violations": 79
221
+ },
222
+ "fastapi": {
223
+ "forbidden_dependency": "starlette",
224
+ "expected_violations": 115
225
+ }
226
+ }
227
+ }