py-code-quality-guard 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.
@@ -0,0 +1,3 @@
1
+ """Automated quality gates for Python projects."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from code_quality_guard.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1 @@
1
+ """Application orchestration for quality checks."""
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from code_quality_guard.config.settings import Settings
4
+ from code_quality_guard.runner import CommandResult
5
+ from code_quality_guard.tools.registry import build_tools
6
+
7
+
8
+ def run_quality_checks(settings: Settings) -> dict[str, CommandResult]:
9
+ return {tool.name: tool.run(settings) for tool in build_tools(settings.tools)}
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from code_quality_guard.application.service import run_quality_checks
8
+ from code_quality_guard.config.settings import Settings
9
+ from code_quality_guard.reporter.console import render_console
10
+ from code_quality_guard.reporter.json import render_json
11
+
12
+ TOOL_NAMES = ["ruff", "ruff-format", "mypy"]
13
+
14
+
15
+ @click.command()
16
+ @click.argument("target", type=click.Path(path_type=Path, exists=True), default=Path.cwd())
17
+ @click.option("--tool", "tools", multiple=True, type=click.Choice(TOOL_NAMES))
18
+ @click.option("--fix", is_flag=True, help="Apply supported fixes.")
19
+ @click.option("--check-only", is_flag=True, help="Check formatting without changing files.")
20
+ @click.option("--output", type=click.Choice(["text", "json"]), default="text", show_default=True)
21
+ @click.option(
22
+ "--tool-arg",
23
+ "tool_args",
24
+ nargs=2,
25
+ multiple=True,
26
+ metavar="TOOL ARG",
27
+ help="Pass an additional argument to a tool; repeat as needed.",
28
+ )
29
+ def main(
30
+ target: Path,
31
+ tools: tuple[str, ...],
32
+ fix: bool,
33
+ check_only: bool,
34
+ output: str,
35
+ tool_args: tuple[tuple[str, str], ...],
36
+ ) -> None:
37
+ """Run selected code quality tools against TARGET."""
38
+ selected_tools = tools or tuple(TOOL_NAMES)
39
+ args_by_tool: dict[str, tuple[str, ...]] = {}
40
+ for tool_name, argument in tool_args:
41
+ if tool_name not in selected_tools:
42
+ raise click.BadParameter(f"{tool_name!r} is not selected with --tool")
43
+ args_by_tool[tool_name] = (*args_by_tool.get(tool_name, ()), argument)
44
+ settings = Settings(
45
+ target=target.resolve(),
46
+ tools=selected_tools,
47
+ fix=fix,
48
+ check_only=check_only,
49
+ output=output,
50
+ tool_args=args_by_tool,
51
+ )
52
+ results = run_quality_checks(settings)
53
+ if settings.output == "json":
54
+ click.echo(render_json(results))
55
+ else:
56
+ render_console(results)
57
+ if not all(result.passed for result in results.values()):
58
+ raise click.exceptions.Exit(1)
@@ -0,0 +1,3 @@
1
+ from .settings import Settings
2
+
3
+ __all__ = ["Settings"]
@@ -0,0 +1,15 @@
1
+ from pathlib import Path
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field
5
+
6
+
7
+ class Settings(BaseModel):
8
+ model_config = ConfigDict(arbitrary_types_allowed=True)
9
+
10
+ target: Path = Field(default_factory=Path.cwd)
11
+ tools: tuple[str, ...] = ("ruff", "mypy")
12
+ fix: bool = False
13
+ check_only: bool = False
14
+ output: Literal["text", "json"] = "text"
15
+ tool_args: dict[str, tuple[str, ...]] = Field(default_factory=dict)
@@ -0,0 +1 @@
1
+ """Human and machine-readable reporting."""
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from rich.console import Console
4
+
5
+ from code_quality_guard.runner import CommandResult
6
+
7
+
8
+ def render_console(results: dict[str, CommandResult]) -> None:
9
+ console = Console()
10
+ for name, result in results.items():
11
+ status = "PASS" if result.passed else "FAIL"
12
+ color = "green" if result.passed else "red"
13
+ console.print(
14
+ f"[{color}][{status}][/{color}] {name} ({result.duration_seconds:.2f}s)"
15
+ )
16
+ diagnostics = "\n".join(
17
+ output.strip() for output in (result.stdout, result.stderr) if output.strip()
18
+ )
19
+ if diagnostics:
20
+ console.print(diagnostics, markup=False)
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict
5
+ from typing import Any
6
+
7
+ from code_quality_guard.runner import CommandResult
8
+
9
+
10
+ def render_json(results: dict[str, CommandResult]) -> str:
11
+ payload: dict[str, Any] = {
12
+ "checks": {name: asdict(result) for name, result in results.items()},
13
+ "passed": all(result.passed for result in results.values()),
14
+ }
15
+ return json.dumps(payload, indent=2, sort_keys=True)
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import time
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CommandResult:
11
+ command: list[str]
12
+ passed: bool
13
+ exit_code: int
14
+ stdout: str
15
+ stderr: str
16
+ duration_seconds: float
17
+
18
+
19
+ class SubprocessRunner:
20
+ def run(self, command: list[str], cwd: Path | None = None) -> CommandResult:
21
+ started = time.perf_counter()
22
+ try:
23
+ completed = subprocess.run(
24
+ command, cwd=cwd, capture_output=True, text=True, check=False
25
+ )
26
+ stdout, stderr, exit_code = completed.stdout, completed.stderr, completed.returncode
27
+ except FileNotFoundError as error:
28
+ stdout, stderr, exit_code = (
29
+ "",
30
+ f"Command not found: {error.filename or command[0]}",
31
+ 127,
32
+ )
33
+ return CommandResult(
34
+ command=command,
35
+ passed=exit_code == 0,
36
+ exit_code=exit_code,
37
+ stdout=stdout,
38
+ stderr=stderr,
39
+ duration_seconds=time.perf_counter() - started,
40
+ )
@@ -0,0 +1 @@
1
+ """Adapters for external quality tools."""
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from code_quality_guard.config.settings import Settings
6
+ from code_quality_guard.runner import CommandResult
7
+
8
+
9
+ class Tool(Protocol):
10
+ name: str
11
+
12
+ def run(self, settings: Settings) -> CommandResult:
13
+ """Run the tool for the configured target."""
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ def resolve_executable(name: str) -> str:
9
+ environment_executable = Path(sys.executable).with_name(name)
10
+ if environment_executable.exists():
11
+ return str(environment_executable)
12
+ return shutil.which(name) or name
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from code_quality_guard.config.settings import Settings
4
+ from code_quality_guard.runner import CommandResult, SubprocessRunner
5
+ from code_quality_guard.tools.command import resolve_executable
6
+
7
+
8
+ class MypyTool:
9
+ name = "mypy"
10
+
11
+ def __init__(self, runner: SubprocessRunner | None = None) -> None:
12
+ self.runner = runner or SubprocessRunner()
13
+
14
+ def run(self, settings: Settings) -> CommandResult:
15
+ command = [resolve_executable("mypy"), str(settings.target)]
16
+ command.extend(settings.tool_args.get(self.name, ()))
17
+ cwd = settings.target if settings.target.is_dir() else settings.target.parent
18
+ return self.runner.run(command, cwd=cwd)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from code_quality_guard.runner import SubprocessRunner
6
+ from code_quality_guard.tools.base import Tool
7
+ from code_quality_guard.tools.mypy import MypyTool
8
+ from code_quality_guard.tools.ruff import RuffTool
9
+ from code_quality_guard.tools.ruff_format import RuffFormatTool
10
+
11
+ TOOL_TYPES: dict[str, type[Tool]] = {
12
+ "ruff": RuffTool,
13
+ "ruff-format": RuffFormatTool,
14
+ "mypy": MypyTool,
15
+ }
16
+
17
+
18
+ def build_tools(tool_names: Iterable[str], runner: SubprocessRunner | None = None) -> list[Tool]:
19
+ shared_runner = runner or SubprocessRunner()
20
+ return [TOOL_TYPES[name](shared_runner) for name in tool_names]
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from code_quality_guard.config.settings import Settings
4
+ from code_quality_guard.runner import CommandResult, SubprocessRunner
5
+ from code_quality_guard.tools.command import resolve_executable
6
+
7
+
8
+ class RuffTool:
9
+ name = "ruff"
10
+
11
+ def __init__(self, runner: SubprocessRunner | None = None) -> None:
12
+ self.runner = runner or SubprocessRunner()
13
+
14
+ def run(self, settings: Settings) -> CommandResult:
15
+ command = [resolve_executable("ruff"), "check", str(settings.target)]
16
+ if settings.fix:
17
+ command.append("--fix")
18
+ command.extend(settings.tool_args.get(self.name, ()))
19
+ cwd = settings.target if settings.target.is_dir() else settings.target.parent
20
+ return self.runner.run(command, cwd=cwd)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from code_quality_guard.config.settings import Settings
4
+ from code_quality_guard.runner import CommandResult, SubprocessRunner
5
+ from code_quality_guard.tools.command import resolve_executable
6
+
7
+
8
+ class RuffFormatTool:
9
+ name = "ruff-format"
10
+
11
+ def __init__(self, runner: SubprocessRunner | None = None) -> None:
12
+ self.runner = runner or SubprocessRunner()
13
+
14
+ def run(self, settings: Settings) -> CommandResult:
15
+ command = [resolve_executable("ruff"), "format", str(settings.target)]
16
+ if not settings.fix or settings.check_only:
17
+ command.append("--check")
18
+ command.extend(settings.tool_args.get(self.name, ()))
19
+ cwd = settings.target if settings.target.is_dir() else settings.target.parent
20
+ return self.runner.run(command, cwd=cwd)
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.5
2
+ Name: py-code-quality-guard
3
+ Version: 0.1.0
4
+ Summary: Automated Code Quality Guard for Python
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: click>=8.1
7
+ Requires-Dist: mypy>=1.11
8
+ Requires-Dist: pydantic>=2.8
9
+ Requires-Dist: rich>=13.7
10
+ Requires-Dist: ruff>=0.6
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Code Quality Guard
14
+
15
+ [![CI](https://github.com/karma369-labs/code-quality-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/karma369-labs/code-quality-guard/actions/workflows/ci.yml)
16
+ [![PyPI](https://img.shields.io/pypi/v/py-code-quality-guard)](https://pypi.org/project/py-code-quality-guard/)
17
+ [![Python](https://img.shields.io/badge/python-3.12%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/)
18
+ [![Ruff](https://img.shields.io/badge/linter-Ruff-D7FF64?logo=ruff&logoColor=black)](https://docs.astral.sh/ruff/)
19
+ [![Mypy](https://img.shields.io/badge/type%20checker-Mypy-4B8BBE?logo=mypy&logoColor=white)](https://mypy-lang.org/)
20
+
21
+ `py-code-quality-guard` is a CI-friendly command-line quality gate for Python
22
+ projects. It runs Ruff linting, Ruff formatting, and Mypy through one
23
+ consistent interface, with human-readable or JSON output for local workflows
24
+ and automation.
25
+
26
+ ## Features
27
+
28
+ - Run Ruff, Ruff format, and Mypy independently or together.
29
+ - Check a project directory or a single file.
30
+ - Apply Ruff fixes when supported.
31
+ - Forward additional arguments to each selected tool.
32
+ - Emit text for developers or JSON for scripts and CI integrations.
33
+ - Return a non-zero exit code when any selected check fails.
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.12 or newer
38
+ - Ruff and Mypy are installed automatically with the package
39
+
40
+ ## Installation
41
+
42
+ Install the latest release from PyPI in an active virtual environment:
43
+
44
+ ```bash
45
+ python -m pip install py-code-quality-guard
46
+ ```
47
+
48
+ For local development with `uv`:
49
+
50
+ ```bash
51
+ uv sync --dev
52
+ ```
53
+
54
+ ## Pre-commit Hook
55
+
56
+ Add `py-code-quality-guard` to a project-level `.pre-commit-config.yaml`:
57
+
58
+ ```yaml
59
+ repos:
60
+ - repo: https://github.com/karma369-labs/code-quality-guard
61
+ rev: v0.1.0
62
+ hooks:
63
+ - id: cqg
64
+ ```
65
+
66
+ Install the hook and run it against the repository:
67
+
68
+ ```bash
69
+ python -m pip install pre-commit
70
+ pre-commit install
71
+ pre-commit run --all-files
72
+ ```
73
+
74
+ The `cqg` hook runs Ruff linting, Ruff formatting checks, and Mypy over the
75
+ project. To use a newer release, update `rev` to the corresponding package
76
+ release tag.
77
+
78
+ ## Usage
79
+
80
+ Run all checks against the current directory:
81
+
82
+ ```bash
83
+ pyqguard .
84
+ ```
85
+
86
+ Run selected tools:
87
+
88
+ ```bash
89
+ pyqguard --tool ruff --tool mypy src/
90
+ ```
91
+
92
+ Checks report the tools' diagnostics without changing files by default:
93
+
94
+ ```bash
95
+ pyqguard --tool ruff-format .
96
+ ```
97
+
98
+ Apply supported fixes:
99
+
100
+ ```bash
101
+ pyqguard --tool ruff --tool ruff-format --fix .
102
+ ```
103
+
104
+ Produce machine-readable output:
105
+
106
+ ```bash
107
+ pyqguard --output json .
108
+ ```
109
+
110
+ Pass an additional argument to a selected tool. Repeat `--tool-arg` as
111
+ needed:
112
+
113
+ ```bash
114
+ pyqguard \
115
+ --tool ruff \
116
+ --tool-arg ruff --select \
117
+ --tool-arg ruff E,F \
118
+ .
119
+ ```
120
+
121
+ Run `pyqguard --help` for the complete option reference.
122
+
123
+ ## Development
124
+
125
+ Install the development dependencies and run the test suite:
126
+
127
+ ```bash
128
+ uv sync --dev
129
+ uv run pytest
130
+ ```
131
+
132
+ The repository uses pre-commit for linting, formatting, type checking, and
133
+ other repository checks:
134
+
135
+ ```bash
136
+ uv run pre-commit install
137
+ uv run pre-commit run --all-files
138
+ ```
139
+
140
+ Pull requests targeting `main` run the same pre-commit checks in GitHub Actions.
141
+
142
+
143
+ ## License
144
+
145
+ License information has not yet been published for this repository.
@@ -0,0 +1,22 @@
1
+ code_quality_guard/__init__.py,sha256=FHnejSyA5evlvUzI5BZ_nGclmTRD0TYGE_LLQon0i0o,74
2
+ code_quality_guard/__main__.py,sha256=z6avlHkTjs-a1WryEOKKIm89j5UpRLr1-vladd4NVng,79
3
+ code_quality_guard/cli.py,sha256=RP-WzxVW0rcvEdN95FTn8JlMI9MiaCE9prk_DcoMYmk,2021
4
+ code_quality_guard/runner.py,sha256=asrXo8C3fxqJI51IjcTK9AUjoaAWwUdef5R-Yuho5eQ,1139
5
+ code_quality_guard/application/__init__.py,sha256=MOpct-O3F3gJEWUV338kWgT_xk3TXaMFqRuJqil7lLM,52
6
+ code_quality_guard/application/service.py,sha256=Ovw5hRoQCEtic-WascA_6npqW9o6mdTNETgWp6ijQ9k,359
7
+ code_quality_guard/config/__init__.py,sha256=hTPvRTo00qLky4IQytXV_BdOEiPmWdFiDecCKr4tf-o,55
8
+ code_quality_guard/config/settings.py,sha256=qfizoCxRK9pYgX8szbQi3BFz_6ca2fB9DP8U6bsUhIg,458
9
+ code_quality_guard/reporter/__init__.py,sha256=XuylpgedmzS1SWzq2wT5eEqCZJ4sgOmCrSg7XoQTLAw,44
10
+ code_quality_guard/reporter/console.py,sha256=JTfQ5c4JifgclsFDs3923JDd4kgorUxkKNk_PU-yolU,685
11
+ code_quality_guard/reporter/json.py,sha256=xjwNbhaQFTClud7eBEoYaU4Yzk-86CP9HBaZtR5rOX8,458
12
+ code_quality_guard/tools/__init__.py,sha256=oRrDTXhlOPv6UcBxlwRCMx-DXWUTrpq4kuSNxqWGH4s,43
13
+ code_quality_guard/tools/base.py,sha256=0ULTPaIhlLlWCzLiCC0Sl0Huh3y5xBeC75J2sTF9Bgo,322
14
+ code_quality_guard/tools/command.py,sha256=m2CUZQhtmjZv9YzPhcAH2jvocZVgm5bWumzcIWdhvFA,317
15
+ code_quality_guard/tools/mypy.py,sha256=2q5IKHlc1HKWDTZwzwh6MzmxDlioyQ9GnHGDxn61ps4,709
16
+ code_quality_guard/tools/registry.py,sha256=efx5TRweS3nVeAHF7x1w_fYrpe1QBSBogbsHDonOT18,679
17
+ code_quality_guard/tools/ruff.py,sha256=DcXm3cgnX9lHKzz4kQfPxFJPvh-RePBRa6Tk-IpTHM4,779
18
+ code_quality_guard/tools/ruff_format.py,sha256=iRJEAK8TgosFIC40J-YdHoASZ0-EX7V1ZJ8WWkf_XBw,822
19
+ py_code_quality_guard-0.1.0.dist-info/METADATA,sha256=4kI6u9JL4D35UJGbIAXfoykB6Ljf9x_df84wwOeDby8,3494
20
+ py_code_quality_guard-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
21
+ py_code_quality_guard-0.1.0.dist-info/entry_points.txt,sha256=6gEyXFusP1d8acz_PL4MAHrE3EMF3vnWFnVzycVBOUE,57
22
+ py_code_quality_guard-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyqguard = code_quality_guard.cli:main