pycodegate 0.1.0__py3-none-any.whl

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.
pycodegate/types.py ADDED
@@ -0,0 +1,65 @@
1
+ """Core data types for python-doctor diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+
8
+
9
+ class Severity(str, Enum):
10
+ ERROR = "error"
11
+ WARNING = "warning"
12
+
13
+
14
+ class Category(str, Enum):
15
+ SECURITY = "Security"
16
+ PERFORMANCE = "Performance"
17
+ ARCHITECTURE = "Architecture"
18
+ CORRECTNESS = "Correctness"
19
+ COMPLEXITY = "Complexity"
20
+ DEAD_CODE = "Dead Code"
21
+ DJANGO = "Django"
22
+ FASTAPI = "FastAPI"
23
+ FLASK = "Flask"
24
+ STRUCTURE = "Structure"
25
+ IMPORTS = "Imports"
26
+ DEPENDENCIES = "Dependencies"
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class Diagnostic:
31
+ file_path: str
32
+ rule: str
33
+ severity: Severity
34
+ category: Category
35
+ message: str
36
+ help: str
37
+ line: int
38
+ column: int = 0
39
+ cost: float = field(default=1.0)
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class ProjectInfo:
44
+ path: str
45
+ python_version: str | None
46
+ framework: str | None
47
+ package_manager: str | None
48
+ test_framework: str | None
49
+ has_type_hints: bool
50
+ source_file_count: int
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class Score:
55
+ value: int
56
+ label: str
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class ScanResult:
61
+ score: Score
62
+ diagnostics: list[Diagnostic]
63
+ project: ProjectInfo
64
+ elapsed_ms: int
65
+ profile: str | None = None
@@ -0,0 +1 @@
1
+ """Utility modules for python-doctor."""
@@ -0,0 +1,19 @@
1
+ """Common AST traversal utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+
12
+ def parse_file(file_path: Path) -> tuple[str, ast.Module | None]:
13
+ """Read and parse a Python file. Returns (source, tree) or (source, None) on error."""
14
+ try:
15
+ source = file_path.read_text(encoding="utf-8", errors="ignore")
16
+ tree = ast.parse(source)
17
+ return source, tree
18
+ except (SyntaxError, UnicodeDecodeError):
19
+ return "", None
@@ -0,0 +1,51 @@
1
+ """Badge and CI workflow generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import urllib.parse
6
+
7
+
8
+ def generate_badge(score: int, label: str) -> str:
9
+ """Generate shields.io badge markdown."""
10
+ if score >= 90:
11
+ color = "brightgreen"
12
+ elif score >= 75:
13
+ color = "green"
14
+ elif score >= 50:
15
+ color = "yellow"
16
+ else:
17
+ color = "red"
18
+
19
+ score_text = urllib.parse.quote(f"{score}/100")
20
+ return f"![py-gate score](https://img.shields.io/badge/py--gate-{score_text}-{color})"
21
+
22
+
23
+ def generate_ci_workflow() -> str:
24
+ """Generate GitHub Actions workflow YAML for auto-updating badge."""
25
+ return """name: Py Gate Score
26
+ on:
27
+ push:
28
+ branches: [main]
29
+
30
+ permissions:
31
+ contents: write
32
+
33
+ jobs:
34
+ score:
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: astral-sh/setup-uv@v5
39
+ - run: uv tool install py-gate
40
+ - name: Run py-gate
41
+ run: |
42
+ SCORE=$(py-gate . --score)
43
+ BADGE=$(py-gate . --badge)
44
+ # Update badge in README
45
+ sed -i "s|!\\[py-gate score\\](.*)|${BADGE}|" README.md
46
+ - name: Commit badge update
47
+ run: |
48
+ git config user.name "github-actions[bot]"
49
+ git config user.email "github-actions[bot]@users.noreply.github.com"
50
+ git diff --quiet README.md || (git add README.md && git commit -m "chore: update py-gate score badge" && git push)
51
+ """
@@ -0,0 +1,24 @@
1
+ """Git diff utilities for scanning only changed files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+
9
+ def get_changed_files(project_path: str, base: str = "main") -> list[Path] | None:
10
+ """Get Python files changed compared to a base branch. Returns None if git fails."""
11
+ try:
12
+ result = subprocess.run(
13
+ ["git", "diff", "--name-only", "--diff-filter=ACMR", base, "--", "*.py"],
14
+ cwd=project_path,
15
+ capture_output=True,
16
+ text=True,
17
+ timeout=10,
18
+ )
19
+ if result.returncode != 0:
20
+ return None
21
+ root = Path(project_path)
22
+ return [root / f.strip() for f in result.stdout.strip().splitlines() if f.strip()]
23
+ except (subprocess.SubprocessError, FileNotFoundError):
24
+ return None
@@ -0,0 +1,62 @@
1
+ """File discovery utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ IGNORE_DIRS = {
9
+ ".venv",
10
+ "venv",
11
+ "env",
12
+ ".env",
13
+ "node_modules",
14
+ "__pycache__",
15
+ ".git",
16
+ ".hg",
17
+ ".svn",
18
+ "dist",
19
+ "build",
20
+ ".eggs",
21
+ "*.egg-info",
22
+ ".tox",
23
+ ".nox",
24
+ ".mypy_cache",
25
+ ".pytest_cache",
26
+ ".ruff_cache",
27
+ "htmlcov",
28
+ "site-packages",
29
+ }
30
+
31
+
32
+ def find_python_files(project_path: str) -> list[Path]:
33
+ """Find all Python files in the project, respecting gitignore."""
34
+ root = Path(project_path)
35
+
36
+ # Try git ls-files first
37
+ try:
38
+ result = subprocess.run(
39
+ ["git", "ls-files", "--cached", "--others", "--exclude-standard", "*.py"],
40
+ cwd=root,
41
+ capture_output=True,
42
+ text=True,
43
+ timeout=10,
44
+ )
45
+ if result.returncode == 0 and result.stdout.strip():
46
+ return [root / f for f in result.stdout.strip().splitlines() if f.endswith(".py")]
47
+ except (subprocess.SubprocessError, FileNotFoundError):
48
+ pass
49
+
50
+ # Fallback: walk filesystem
51
+ return _walk_for_python_files(root)
52
+
53
+
54
+ def _walk_for_python_files(root: Path) -> list[Path]:
55
+ files: list[Path] = []
56
+ for path in root.rglob("*.py"):
57
+ if not any(
58
+ part in IGNORE_DIRS or part.endswith(".egg-info")
59
+ for part in path.relative_to(root).parts
60
+ ):
61
+ files.append(path)
62
+ return files
@@ -0,0 +1,32 @@
1
+ """Auto-fix utilities using ruff."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import subprocess
7
+
8
+
9
+ def run_ruff_fix(project_path: str) -> int:
10
+ """Run ``ruff check --fix`` on *project_path* and return the number of fixes applied.
11
+
12
+ Returns 0 if ruff is not installed or if no fixes were made.
13
+ Raises no exceptions — all subprocess errors are caught gracefully.
14
+ """
15
+ try:
16
+ result = subprocess.run(
17
+ ["ruff", "check", "--fix", project_path],
18
+ capture_output=True,
19
+ text=True,
20
+ )
21
+ except FileNotFoundError:
22
+ # ruff is not installed
23
+ return -1
24
+ except Exception:
25
+ return 0
26
+
27
+ # ruff prints something like "Fixed 3 errors." to stdout or stderr
28
+ output = result.stdout + result.stderr
29
+ match = re.search(r"Fixed (\d+)", output)
30
+ if match:
31
+ return int(match.group(1))
32
+ return 0
@@ -0,0 +1,48 @@
1
+ """Pre-commit hook installer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def install_precommit_hook(project_path: str, min_score: int = 50) -> str:
9
+ """Install a git pre-commit hook that runs py-gate."""
10
+ hooks_dir = Path(project_path) / ".git" / "hooks"
11
+ if not hooks_dir.exists():
12
+ return "Error: not a git repository"
13
+
14
+ hook_path = hooks_dir / "pre-commit"
15
+ hook_content = f"""#!/bin/sh
16
+ # py-gate pre-commit hook
17
+ SCORE=$(py-gate . --score 2>/dev/null)
18
+ if [ $? -ne 0 ]; then
19
+ echo "py-gate not found. Install with: uvx py-gate"
20
+ exit 0
21
+ fi
22
+ if [ "$SCORE" -lt {min_score} ]; then
23
+ echo "py-gate: score $SCORE is below minimum {min_score}. Fix issues before committing."
24
+ py-gate . 2>/dev/null
25
+ exit 1
26
+ fi
27
+ echo "py-gate: score $SCORE/{min_score} — passed"
28
+ """
29
+
30
+ # Don't overwrite existing hook — append or warn
31
+ if hook_path.exists():
32
+ existing = hook_path.read_text()
33
+ if "py-gate" in existing:
34
+ hook_path.write_text(hook_content)
35
+ hook_path.chmod(0o755)
36
+ return f"Updated pre-commit hook at {hook_path} (min-score: {min_score})"
37
+ else:
38
+ # Append to existing hook
39
+ # Strip the shebang line before appending
40
+ lines = hook_content.splitlines(keepends=True)
41
+ body = "".join(lines[1:]) if lines[0].startswith("#!/") else hook_content
42
+ with open(hook_path, "a") as f:
43
+ f.write("\n" + body)
44
+ return f"Appended py-gate to existing pre-commit hook at {hook_path}"
45
+
46
+ hook_path.write_text(hook_content)
47
+ hook_path.chmod(0o755)
48
+ return f"Installed pre-commit hook at {hook_path} (min-score: {min_score})"
@@ -0,0 +1,357 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycodegate
3
+ Version: 0.1.0
4
+ Summary: One command. One score. Your Python quality gate.
5
+ Project-URL: Homepage, https://github.com/themohitkhare/pycodegate
6
+ Project-URL: Repository, https://github.com/themohitkhare/pycodegate
7
+ Project-URL: Issues, https://github.com/themohitkhare/pycodegate/issues
8
+ Author: Mohit Khare
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agents,code-quality,developer-tools,linting,python,quality-gate,static-analysis
12
+ Classifier: Development Status :: 4 - Beta
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.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: click>=8.1
26
+ Requires-Dist: rich>=13.0
27
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
28
+ Requires-Dist: vulture>=2.11
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.4; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ <h1 align="center">Py Gate</h1>
36
+ <p align="center">
37
+ <strong>One command. One score. Your Python quality gate.</strong>
38
+ </p>
39
+ <p align="center">
40
+ <a href="https://github.com/themohitkhare/pycodegate/actions/workflows/ci.yml"><img src="https://github.com/themohitkhare/pycodegate/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
41
+ <a href="https://pypi.org/project/py-gate/"><img src="https://img.shields.io/pypi/v/py-gate" alt="PyPI"></a>
42
+ <a href="https://pypi.org/project/py-gate/"><img src="https://img.shields.io/pypi/pyversions/py-gate" alt="Python"></a>
43
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
44
+ </p>
45
+
46
+ ---
47
+
48
+ ## How it works
49
+
50
+ Py Gate starts by detecting 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.
51
+
52
+ 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.
53
+
54
+ 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).
55
+
56
+ ## Install
57
+
58
+ Run instantly with `uvx` — no install needed:
59
+
60
+ ```bash
61
+ uvx py-gate .
62
+ ```
63
+
64
+ Install globally with pipx or uv:
65
+
66
+ ```bash
67
+ pipx install py-gate
68
+ # or
69
+ uv tool install py-gate
70
+ # or
71
+ pip install py-gate
72
+ ```
73
+
74
+ ## Quick Start
75
+
76
+ ```bash
77
+ # Basic scan — score + summary
78
+ py-gate .
79
+
80
+ # Show file paths and line numbers for every finding
81
+ py-gate . --verbose
82
+
83
+ # Machine-readable output for AI agents and CI
84
+ py-gate . --json
85
+
86
+ # Auto-fix ruff-fixable issues, then scan
87
+ py-gate . --fix
88
+
89
+ # Output only the numeric score (useful in scripts)
90
+ py-gate . --score
91
+
92
+ # Scan only files changed vs a base branch
93
+ py-gate . --diff main
94
+ ```
95
+
96
+ ## JSON Output
97
+
98
+ Pass `--json` to get structured output that AI agents and CI pipelines can parse:
99
+
100
+ ```bash
101
+ py-gate . --json
102
+ ```
103
+
104
+ ```json
105
+ {
106
+ "version": "0.1.0",
107
+ "path": ".",
108
+ "score": 87,
109
+ "label": "Great",
110
+ "errors": 1,
111
+ "warnings": 4,
112
+ "elapsed_ms": 212,
113
+ "project": {
114
+ "framework": "fastapi",
115
+ "python_version": "3.12",
116
+ "package_manager": "uv",
117
+ "test_framework": "pytest"
118
+ },
119
+ "diagnostics": [
120
+ {
121
+ "rule": "no-mutable-default",
122
+ "severity": "error",
123
+ "category": "Correctness",
124
+ "message": "Mutable default argument `[]` is shared across all calls",
125
+ "file_path": "src/api/routes.py",
126
+ "line": 34
127
+ },
128
+ {
129
+ "rule": "high-complexity",
130
+ "severity": "warning",
131
+ "category": "Complexity",
132
+ "message": "Function 'process_order' has cyclomatic complexity 17 (max 15)",
133
+ "file_path": "src/api/orders.py",
134
+ "line": 88
135
+ }
136
+ ]
137
+ }
138
+ ```
139
+
140
+ ## Agent Integration
141
+
142
+ Py Gate is designed to be used by AI coding agents. Add it to your agent's context so it runs after every Python change.
143
+
144
+ ### Claude Code
145
+
146
+ Add the skill to your project:
147
+
148
+ ```bash
149
+ mkdir -p .claude/skills
150
+ curl -fsSL https://raw.githubusercontent.com/themohitkhare/pycodegate/main/skills/py-gate/SKILL.md \
151
+ -o .claude/skills/py-gate.md
152
+ ```
153
+
154
+ Or copy `AGENTS.md` to your project root — Claude Code picks it up automatically.
155
+
156
+ ### Cursor
157
+
158
+ Add to `.cursor/rules/py-gate.mdc`:
159
+
160
+ ```markdown
161
+ After modifying Python files, run `uvx py-gate . --json` and fix all findings
162
+ with severity "error" before marking the task complete. Target score: 80+.
163
+ ```
164
+
165
+ ### Windsurf
166
+
167
+ Add to `.windsurfrules`:
168
+
169
+ ```
170
+ After modifying Python files, run: uvx py-gate . --json
171
+ Fix all "error" severity findings. Re-run to verify the score improved.
172
+ ```
173
+
174
+ ### Codex
175
+
176
+ Add to your system prompt:
177
+
178
+ ```
179
+ After modifying Python files, run `uvx py-gate . --json` to check code quality.
180
+ Fix errors first. Target score: 80+.
181
+ ```
182
+
183
+ ### Aider
184
+
185
+ ```bash
186
+ aider --read AGENTS.md
187
+ ```
188
+
189
+ ## GitHub Actions
190
+
191
+ ```yaml
192
+ name: Quality Gate
193
+
194
+ on: [push, pull_request]
195
+
196
+ jobs:
197
+ py-gate:
198
+ runs-on: ubuntu-latest
199
+ steps:
200
+ - uses: actions/checkout@v4
201
+ with:
202
+ fetch-depth: 0 # required for --diff
203
+
204
+ - uses: actions/setup-python@v5
205
+ with:
206
+ python-version: "3.12"
207
+
208
+ - name: Run Py Gate
209
+ run: |
210
+ pip install py-gate
211
+ py-gate . --verbose --diff main --fail-on error
212
+ ```
213
+
214
+ ## CLI Reference
215
+
216
+ | Flag | Default | Description |
217
+ |------|---------|-------------|
218
+ | `[DIRECTORY]` | `.` | Path to the Python project to scan |
219
+ | `--lint / --no-lint` | on | Enable or disable lint checks |
220
+ | `--dead-code / --no-dead-code` | on | Enable or disable dead code detection |
221
+ | `--verbose` | off | Show file path and line number per finding |
222
+ | `--score` | off | Print only the numeric score and exit |
223
+ | `--json` | off | Emit structured JSON (for agents and CI) |
224
+ | `--fix` | off | Run `ruff --fix` before scanning |
225
+ | `--diff TEXT` | — | Scan only files changed vs this base branch |
226
+ | `--fail-on [error\|warning\|none]` | `none` | Exit code 1 when findings at this level exist |
227
+ | `-v, --version` | — | Show version and exit |
228
+ | `-h, --help` | — | Show help and exit |
229
+
230
+ ## What It Checks
231
+
232
+ | Category | Weight | Max Deduction | What it catches |
233
+ |----------|--------|---------------|-----------------|
234
+ | Security | 5 | ~24 pts | `eval`, `exec`, `pickle.load`, unsafe YAML, hardcoded secrets, weak hashes |
235
+ | Correctness | 4 | ~19 pts | Mutable defaults, bare/broad except, assert in production, bad `__init__` return |
236
+ | Complexity | 3 | ~14 pts | Cyclomatic complexity > 15 (warning) or > 25 (error) |
237
+ | Architecture | 3 | ~14 pts | Giant modules (>500 lines), deep nesting (>5), god functions (>50 lines), too many args (>7) |
238
+ | Performance | 2 | ~10 pts | String concat in loops, imports inside functions, star imports |
239
+ | Structure | 2 | ~10 pts | Missing `__init__.py`, missing tests directory, no type hints |
240
+ | Imports | 1 | ~5 pts | Circular imports, wildcard imports, import order issues |
241
+ | Dead Code | 1 | ~5 pts | Unused functions, classes, variables, and imports via Vulture |
242
+
243
+ Framework-specific rules (Django, FastAPI, Flask) are mapped into the Security or Correctness budget.
244
+
245
+ ## Scoring
246
+
247
+ Py Gate uses a **weighted category-budget system**:
248
+
249
+ 1. Each category has a weight (see table above). Weights are normalized to sum to 100 points of total deduction budget.
250
+ 2. Within a category, findings are sorted by cost (errors cost more than warnings). The top 3 findings apply at full cost; every additional finding applies **diminishing returns** (10% of its cost).
251
+ 3. A category's deduction is capped at its budget, so a single broken category can never zero out an otherwise healthy project.
252
+ 4. Final score = `100 - sum(capped category deductions)`, floored at 0.
253
+
254
+ | Score | Label | Meaning |
255
+ |-------|-------|---------|
256
+ | 90–100 | Excellent | Production-ready |
257
+ | 75–89 | Great | Minor issues to address |
258
+ | 50–74 | Needs work | Significant issues present |
259
+ | 0–49 | Critical | Blocking issues, do not ship |
260
+
261
+ ## Configuration
262
+
263
+ Create `py-gate.toml` in your project root:
264
+
265
+ ```toml
266
+ [options]
267
+ lint = true
268
+ dead_code = true
269
+ verbose = false
270
+ fail_on = "none"
271
+
272
+ [ignore]
273
+ rules = ["dead-code", "no-import-in-function"]
274
+ files = ["tests/fixtures/**", "migrations/**", "scripts/**"]
275
+
276
+ [per-file-ignores]
277
+ "src/legacy/*.py" = ["high-complexity", "no-god-function"]
278
+
279
+ [scoring]
280
+ max-deduction.Security = 20
281
+ max-deduction.Dead Code = 0 # disable dead-code penalty entirely
282
+ ```
283
+
284
+ Or use `pyproject.toml`:
285
+
286
+ ```toml
287
+ [tool.py-gate]
288
+ lint = true
289
+ dead_code = true
290
+ fail_on = "error"
291
+
292
+ [tool.py-gate.ignore]
293
+ rules = ["no-import-in-function"]
294
+ files = ["tests/fixtures/**"]
295
+
296
+ [tool.py-gate.per-file-ignores]
297
+ "src/legacy/*.py" = ["high-complexity"]
298
+
299
+ [tool.py-gate.scoring]
300
+ "max-deduction" = { Security = 20, "Dead Code" = 0 }
301
+ ```
302
+
303
+ If both files exist, `py-gate.toml` takes precedence. CLI flags always override config values.
304
+
305
+ ## Profiles
306
+
307
+ Py Gate auto-detects a project profile and adjusts rule weights accordingly. You can also set a profile explicitly in config (`profile = "library"`).
308
+
309
+ | Profile | Auto-detected when | Adjustments |
310
+ |---------|--------------------|-------------|
311
+ | `cli` | `[project.scripts]` in pyproject.toml | Architecture rules weighted up; dead-code weighted down |
312
+ | `web` | Django / FastAPI / Flask detected | Security and correctness weighted up; framework rules active |
313
+ | `library` | No scripts, no framework, has `py.typed` | Public API checks active; dead-code weighted up |
314
+ | `script` | Single-file project or `scripts/` directory | Architecture rules relaxed; complexity thresholds raised |
315
+
316
+ ## Pre-commit Hook
317
+
318
+ Use `--pre-commit` to run Py Gate as a pre-commit hook. It automatically scans only the staged files:
319
+
320
+ ```bash
321
+ py-gate . --pre-commit --fail-on error
322
+ ```
323
+
324
+ Add to `.pre-commit-config.yaml`:
325
+
326
+ ```yaml
327
+ repos:
328
+ - repo: local
329
+ hooks:
330
+ - id: py-gate
331
+ name: Py Gate quality check
332
+ entry: py-gate . --pre-commit --fail-on error
333
+ language: system
334
+ types: [python]
335
+ pass_filenames: false
336
+ ```
337
+
338
+ ## Contributing
339
+
340
+ ```bash
341
+ git clone https://github.com/themohitkhare/pycodegate
342
+ cd pycodegate
343
+ uv sync --all-extras
344
+ uv run pytest -q
345
+ uv run py-gate . --verbose # dogfood it
346
+ ```
347
+
348
+ To add a new rule:
349
+
350
+ 1. Create a file in `src/pycodegate/rules/` extending `BaseRules`
351
+ 2. Implement `check(self, source: str, filename: str) -> list[Diagnostic]`
352
+ 3. Register it in `src/pycodegate/rules/__init__.py`
353
+ 4. Add tests in `tests/rules/`
354
+
355
+ ## License
356
+
357
+ MIT