python-code-quality 0.1.13__py3-none-any.whl → 0.1.15__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.
- py_cq/__init__.py +0 -4
- py_cq/cli.py +39 -3
- py_cq/config/config.yaml +12 -6
- py_cq/execution_engine.py +18 -6
- py_cq/localtypes.py +1 -0
- py_cq/parsers/__init__.py +1 -0
- py_cq/parsers/common.py +106 -2
- py_cq/parsers/compileparser.py +8 -1
- py_cq/parsers/interrogateparser.py +4 -4
- py_cq/parsers/pytestparser.py +91 -4
- py_cq/parsers/typarser.py +19 -1
- py_cq/tool_registry.py +1 -0
- {python_code_quality-0.1.13.dist-info → python_code_quality-0.1.15.dist-info}/METADATA +43 -45
- {python_code_quality-0.1.13.dist-info → python_code_quality-0.1.15.dist-info}/RECORD +16 -16
- {python_code_quality-0.1.13.dist-info → python_code_quality-0.1.15.dist-info}/WHEEL +0 -0
- {python_code_quality-0.1.13.dist-info → python_code_quality-0.1.15.dist-info}/entry_points.txt +0 -0
py_cq/__init__.py
CHANGED
|
@@ -4,7 +4,3 @@ The module defines a single function, `hello`, which returns the string
|
|
|
4
4
|
`'Hello from py_cq!'`. It can serve as a minimal example, placeholder, or
|
|
5
5
|
testing stub in larger applications."""
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
def hello() -> str:
|
|
9
|
-
"""Returns the greeting string `'Hello from py_cq!'`."""
|
|
10
|
-
return "Hello from py_cq!"
|
py_cq/cli.py
CHANGED
|
@@ -10,13 +10,14 @@ analysis.
|
|
|
10
10
|
Helper functions such as `format_as_table` convert the aggregated tool
|
|
11
11
|
results into a Rich Table for convenient console display.
|
|
12
12
|
"""
|
|
13
|
-
|
|
14
13
|
import copy
|
|
14
|
+
import io
|
|
15
15
|
import json
|
|
16
16
|
import logging
|
|
17
17
|
import tomllib
|
|
18
18
|
from enum import Enum
|
|
19
19
|
from importlib import import_module
|
|
20
|
+
from importlib.metadata import requires, version
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
|
|
22
23
|
import typer
|
|
@@ -84,6 +85,7 @@ def _apply_user_config(base: dict[str, ToolConfig], user_cfg: dict) -> dict[str,
|
|
|
84
85
|
run_in_target_env=tool_data.get("run_in_target_env", False),
|
|
85
86
|
extra_deps=tool_data.get("extra_deps", []),
|
|
86
87
|
parser_config=tool_data.get("parser_config", {}),
|
|
88
|
+
exclude_format=tool_data.get("exclude_format", ""),
|
|
87
89
|
)
|
|
88
90
|
except KeyError as e:
|
|
89
91
|
raise typer.BadParameter(f"[tool.cq.tools.{tool_id}] missing required field {e}")
|
|
@@ -99,8 +101,36 @@ class OutputMode(str, Enum):
|
|
|
99
101
|
RAW = "raw"
|
|
100
102
|
|
|
101
103
|
|
|
104
|
+
def _version_callback(value: bool) -> None:
|
|
105
|
+
if not value:
|
|
106
|
+
return
|
|
107
|
+
import re
|
|
108
|
+
import sys
|
|
109
|
+
if isinstance(sys.stdout, io.TextIOWrapper):
|
|
110
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
111
|
+
pkg = "python-code-quality"
|
|
112
|
+
pkg_version = version(pkg)
|
|
113
|
+
dep_versions: list[tuple[str, str]] = []
|
|
114
|
+
for req in (requires(pkg) or []):
|
|
115
|
+
if "; extra ==" in req:
|
|
116
|
+
continue
|
|
117
|
+
dep_name = re.split(r"[>=<!;\s\[]", req)[0]
|
|
118
|
+
try:
|
|
119
|
+
dep_versions.append((dep_name, version(dep_name)))
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
typer.echo(f"{pkg} v{pkg_version}")
|
|
123
|
+
for dep_name, dep_ver in sorted(dep_versions):
|
|
124
|
+
typer.echo(f"\u251c\u2500\u2500 {dep_name} v{dep_ver}")
|
|
125
|
+
raise typer.Exit()
|
|
126
|
+
|
|
127
|
+
|
|
102
128
|
@app.callback()
|
|
103
|
-
def callback(
|
|
129
|
+
def callback(
|
|
130
|
+
_: bool = typer.Option(
|
|
131
|
+
False, "--version", "-V", callback=_version_callback, is_eager=True, help="Show version and dependencies"
|
|
132
|
+
),
|
|
133
|
+
) -> None:
|
|
104
134
|
"""Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm"""
|
|
105
135
|
console = Console()
|
|
106
136
|
|
|
@@ -131,6 +161,9 @@ def check(
|
|
|
131
161
|
skip: str | None = typer.Option(
|
|
132
162
|
None, "--skip", help="Comma-separated tool IDs to skip (e.g. bandit,vulture)"
|
|
133
163
|
),
|
|
164
|
+
exclude: str | None = typer.Option(
|
|
165
|
+
None, "--exclude", help="Comma-separated paths to exclude (e.g. demo,docs)"
|
|
166
|
+
),
|
|
134
167
|
):
|
|
135
168
|
"""Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm""" # --help
|
|
136
169
|
path_obj = Path(path)
|
|
@@ -165,9 +198,12 @@ def check(
|
|
|
165
198
|
if skip:
|
|
166
199
|
drop = set(skip.split(","))
|
|
167
200
|
effective_registry = {k: v for k, v in effective_registry.items() if k not in drop}
|
|
201
|
+
config_excludes: list[str] = user_cfg.get("exclude", [])
|
|
202
|
+
cli_excludes: list[str] = [e.strip() for e in exclude.split(",")] if exclude else []
|
|
203
|
+
excludes = list(dict.fromkeys(config_excludes + cli_excludes))
|
|
168
204
|
if clear_cache:
|
|
169
205
|
tool_cache.clear()
|
|
170
|
-
tool_results = run_tools(effective_registry.values(), path, workers, early_exit=(output == OutputMode.LLM))
|
|
206
|
+
tool_results = run_tools(effective_registry.values(), path, workers, early_exit=(output == OutputMode.LLM), excludes=excludes)
|
|
171
207
|
# for tr in tool_results:
|
|
172
208
|
# log.debug(json.dumps(tr.to_dict(), indent=2))
|
|
173
209
|
combined_metrics = aggregate_metrics(path=path, metrics=tool_results)
|
py_cq/config/config.yaml
CHANGED
|
@@ -8,14 +8,16 @@ python:
|
|
|
8
8
|
error_threshold: 0.9999
|
|
9
9
|
|
|
10
10
|
ruff:
|
|
11
|
-
command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\""
|
|
11
|
+
command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\"{exclude}"
|
|
12
|
+
exclude_format: " --exclude {path}"
|
|
12
13
|
parser: "RuffParser"
|
|
13
14
|
order: 2
|
|
14
15
|
warning_threshold: 0.9999
|
|
15
16
|
error_threshold: 0.9
|
|
16
17
|
|
|
17
18
|
ty:
|
|
18
|
-
command: "{python} -m ty check --output-format concise --color never \"{context_path}\""
|
|
19
|
+
command: "{python} -m ty check --output-format concise --color never \"{context_path}\"{exclude}"
|
|
20
|
+
exclude_format: " --exclude {path}"
|
|
19
21
|
parser: "TyParser"
|
|
20
22
|
order: 3
|
|
21
23
|
warning_threshold: 0.9999
|
|
@@ -25,14 +27,16 @@ python:
|
|
|
25
27
|
- ty
|
|
26
28
|
|
|
27
29
|
bandit:
|
|
28
|
-
command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests\""
|
|
30
|
+
command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests{exclude}\""
|
|
31
|
+
exclude_format: ",{input_path_posix}/{path}"
|
|
29
32
|
parser: "BanditParser"
|
|
30
33
|
order: 4
|
|
31
34
|
warning_threshold: 0.9999
|
|
32
35
|
error_threshold: 0.8
|
|
33
36
|
|
|
34
37
|
pytest:
|
|
35
|
-
command: "{python} -m pytest -v \"{context_path}\""
|
|
38
|
+
command: "{python} -m pytest -v \"{context_path}\"{exclude}"
|
|
39
|
+
exclude_format: " --ignore {path}"
|
|
36
40
|
parser: "PytestParser"
|
|
37
41
|
order: 5
|
|
38
42
|
warning_threshold: 1.0
|
|
@@ -74,14 +78,16 @@ python:
|
|
|
74
78
|
error_threshold: 0.3
|
|
75
79
|
|
|
76
80
|
vulture:
|
|
77
|
-
command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
81
|
+
command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git{exclude}"
|
|
82
|
+
exclude_format: ",{path}"
|
|
78
83
|
parser: "VultureParser"
|
|
79
84
|
order: 10
|
|
80
85
|
warning_threshold: 0.9999
|
|
81
86
|
error_threshold: 0.8
|
|
82
87
|
|
|
83
88
|
interrogate:
|
|
84
|
-
command: "{python} -m interrogate \"{context_path}\" -v --fail-under 0"
|
|
89
|
+
command: "{python} -m interrogate \"{context_path}\" -e tests{exclude} -v --fail-under 0"
|
|
90
|
+
exclude_format: " -e {path}"
|
|
85
91
|
parser: "InterrogateParser"
|
|
86
92
|
order: 11
|
|
87
93
|
warning_threshold: 0.8
|
py_cq/execution_engine.py
CHANGED
|
@@ -53,7 +53,17 @@ def _dep_in_venv(dep: str, project_root: Path) -> bool:
|
|
|
53
53
|
return False
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def
|
|
56
|
+
def _build_exclude_str(exclude_format: str, excludes: list[str], **extra_vars: str) -> str:
|
|
57
|
+
if not exclude_format or not excludes:
|
|
58
|
+
return ""
|
|
59
|
+
parts = []
|
|
60
|
+
for exc in excludes:
|
|
61
|
+
abs_posix_path = Path(exc).resolve().as_posix()
|
|
62
|
+
parts.append(exclude_format.format(path=exc, abs_posix_path=abs_posix_path, **extra_vars))
|
|
63
|
+
return "".join(parts)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_tool(tool_config: ToolConfig, context_path: str, excludes: list[str] | None = None) -> RawResult:
|
|
57
67
|
"""Runs a tool defined by its configuration and returns the execution result.
|
|
58
68
|
|
|
59
69
|
Args:
|
|
@@ -72,7 +82,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
72
82
|
>>> result.return_code
|
|
73
83
|
0"""
|
|
74
84
|
python = sys.executable
|
|
75
|
-
path = context_path
|
|
85
|
+
path = str(Path(context_path))
|
|
76
86
|
if tool_config.run_in_target_env:
|
|
77
87
|
uv = shutil.which("uv")
|
|
78
88
|
if uv:
|
|
@@ -87,10 +97,12 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
87
97
|
project_root_path = Path(abs_dir)
|
|
88
98
|
missing_deps = [d for d in tool_config.extra_deps if not _dep_in_venv(d, project_root_path)]
|
|
89
99
|
with_flags = " ".join(f"--with {dep}" for dep in missing_deps)
|
|
90
|
-
|
|
100
|
+
no_sync = "--no-sync" if sys.executable.startswith(abs_dir) else ""
|
|
101
|
+
python = f'"{uv}" run {no_sync} --directory "{abs_dir}" {with_flags}'.strip()
|
|
91
102
|
abs_context_path = str(Path(context_path).resolve())
|
|
92
103
|
input_path_posix = Path(context_path).as_posix().rstrip("/")
|
|
93
|
-
|
|
104
|
+
exclude = _build_exclude_str(tool_config.exclude_format, excludes or [], input_path_posix=input_path_posix)
|
|
105
|
+
command = tool_config.command.format(context_path=path, abs_context_path=abs_context_path, input_path_posix=input_path_posix, python=python, exclude=exclude)
|
|
94
106
|
cache_key = f"{command}:{get_context_hash(context_path)}"
|
|
95
107
|
if cache_key in _cache:
|
|
96
108
|
log.info(f"Cache hit: {command}")
|
|
@@ -110,7 +122,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
110
122
|
return raw_result
|
|
111
123
|
|
|
112
124
|
|
|
113
|
-
def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int = 0, early_exit: bool = False) -> list[ToolResult]:
|
|
125
|
+
def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int = 0, early_exit: bool = False, excludes: list[str] | None = None) -> list[ToolResult]:
|
|
114
126
|
"""Run multiple tools and return their parsed results.
|
|
115
127
|
|
|
116
128
|
Runs each tool specified in *tool_configs* on the file or directory at
|
|
@@ -152,7 +164,7 @@ def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int
|
|
|
152
164
|
>>> results = run_tools(configs, '/path/to/project', parallel=True)"""
|
|
153
165
|
def _run_and_parse(tool_config: ToolConfig) -> tuple[int, ToolResult]:
|
|
154
166
|
t0 = time.perf_counter()
|
|
155
|
-
raw_result = run_tool(tool_config, path)
|
|
167
|
+
raw_result = run_tool(tool_config, path, excludes)
|
|
156
168
|
tr = tool_config.parser_class(tool_config.parser_config).parse(raw_result)
|
|
157
169
|
tr.duration_s = time.perf_counter() - t0
|
|
158
170
|
return tool_config.order, tr
|
py_cq/localtypes.py
CHANGED
|
@@ -22,6 +22,7 @@ class ToolConfig:
|
|
|
22
22
|
run_in_target_env: bool = False # If True, run in target project's env via uv
|
|
23
23
|
extra_deps: list[str] = field(default_factory=list) # Extra deps to inject via uv --with
|
|
24
24
|
parser_config: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
exclude_format: str = "" # Per-path template for --exclude injection, e.g. " --exclude {path}"
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
@dataclass
|
py_cq/parsers/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tool Response parsers"""
|
py_cq/parsers/common.py
CHANGED
|
@@ -12,6 +12,7 @@ performance metrics or error scores:
|
|
|
12
12
|
Both functions return a float and can be used directly in downstream analytics,
|
|
13
13
|
visualisation or decision-making pipelines."""
|
|
14
14
|
|
|
15
|
+
from pathlib import Path
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def read_source_lines(file_path: str, line: int, count: int = 5) -> str:
|
|
@@ -26,17 +27,118 @@ def read_source_lines(file_path: str, line: int, count: int = 5) -> str:
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def format_source_context(file: str, line: int | str, context: int = 3, count: int = 8) -> str:
|
|
29
|
-
"""Return a fenced python code block for the source around `line`, or '' if unavailable.
|
|
30
|
+
"""Return a fenced python code block for the source around `line`, or '' if unavailable.
|
|
31
|
+
|
|
32
|
+
Stops before spilling into the next top-level ``def`` or ``class`` definition.
|
|
33
|
+
"""
|
|
30
34
|
if not isinstance(line, int):
|
|
31
35
|
return ""
|
|
32
36
|
context_start = max(1, line - context)
|
|
33
37
|
raw_lines = read_source_lines(file, context_start, count=count).splitlines()
|
|
34
38
|
if not raw_lines:
|
|
35
39
|
return ""
|
|
36
|
-
|
|
40
|
+
error_offset = line - context_start # 0-based index of the error line in raw_lines
|
|
41
|
+
collected = []
|
|
42
|
+
for i, rline in enumerate(raw_lines):
|
|
43
|
+
if i > error_offset and (
|
|
44
|
+
rline.startswith("def ")
|
|
45
|
+
or rline.startswith("async def ")
|
|
46
|
+
or rline.startswith("class ")
|
|
47
|
+
):
|
|
48
|
+
break
|
|
49
|
+
collected.append(f"{context_start + i}: {rline}")
|
|
50
|
+
src = "\n".join(collected)
|
|
37
51
|
return f"\n```python\n{src}\n```"
|
|
38
52
|
|
|
39
53
|
|
|
54
|
+
_PYTHON_KEYWORDS = frozenset([
|
|
55
|
+
"if", "elif", "else", "for", "while", "with", "assert", "return",
|
|
56
|
+
"raise", "import", "from", "class", "def", "lambda", "yield",
|
|
57
|
+
"del", "pass", "break", "continue", "not", "and", "or", "in", "is",
|
|
58
|
+
"print", "super", "type", "len", "range",
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def extract_callee_name(source_line: str) -> str | None:
|
|
63
|
+
"""Extract the primary callee function name from a source line, or None.
|
|
64
|
+
|
|
65
|
+
Prefers the RHS of an assignment so that ``result = func(...)`` returns
|
|
66
|
+
``func`` rather than the variable on the left. Python keywords and
|
|
67
|
+
built-ins listed in ``_PYTHON_KEYWORDS`` are excluded.
|
|
68
|
+
"""
|
|
69
|
+
import re
|
|
70
|
+
stripped = source_line.strip()
|
|
71
|
+
rhs = stripped
|
|
72
|
+
if "=" in stripped and not stripped.startswith(("assert", "return")):
|
|
73
|
+
rhs = stripped.split("=", 1)[1].strip()
|
|
74
|
+
m = re.search(r"\b([a-zA-Z_]\w*)\s*\(", rhs)
|
|
75
|
+
if m and m.group(1) not in _PYTHON_KEYWORDS:
|
|
76
|
+
return m.group(1)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _find_project_root(hint_file: str) -> Path:
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
root = Path(hint_file).resolve().parent
|
|
83
|
+
current = root
|
|
84
|
+
for _ in range(8):
|
|
85
|
+
if (current / "pyproject.toml").exists() or (current / "setup.py").exists():
|
|
86
|
+
return current
|
|
87
|
+
parent = current.parent
|
|
88
|
+
if parent == current:
|
|
89
|
+
break
|
|
90
|
+
current = parent
|
|
91
|
+
return root
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def find_in_project(func_name: str, hint_file: str, max_lines: int = 10) -> tuple[str, str]:
|
|
95
|
+
"""Find func_name definition in project files; same file first, then project-wide.
|
|
96
|
+
|
|
97
|
+
Returns ``(file_path, code_block)`` for the first match, or ``("", "")`` if not found.
|
|
98
|
+
"""
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
result = find_function_source(hint_file, func_name, max_lines=max_lines)
|
|
101
|
+
if result:
|
|
102
|
+
return hint_file, result
|
|
103
|
+
root = _find_project_root(hint_file)
|
|
104
|
+
for py_file in sorted(root.rglob("*.py")):
|
|
105
|
+
if py_file.resolve() == Path(hint_file).resolve():
|
|
106
|
+
continue
|
|
107
|
+
r = find_function_source(str(py_file), func_name, max_lines=max_lines)
|
|
108
|
+
if r:
|
|
109
|
+
return str(py_file), r
|
|
110
|
+
return "", ""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _relative_path(path: str) -> str:
|
|
114
|
+
"""Return path relative to cwd, normalised to forward slashes."""
|
|
115
|
+
from pathlib import Path
|
|
116
|
+
try:
|
|
117
|
+
return str(Path(path).relative_to(Path.cwd())).replace("\\", "/")
|
|
118
|
+
except ValueError:
|
|
119
|
+
return path.replace("\\", "/")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def format_callee_context(func_name: str, hint_file: str, max_lines: int = 10) -> str:
|
|
123
|
+
"""Return a labelled callee definition block, or '' if not found in project.
|
|
124
|
+
|
|
125
|
+
Output format::
|
|
126
|
+
|
|
127
|
+
Callee `func_name` — `relative/path/to/file.py`
|
|
128
|
+
```python
|
|
129
|
+
N: def func_name(...):
|
|
130
|
+
...
|
|
131
|
+
```
|
|
132
|
+
"""
|
|
133
|
+
import re
|
|
134
|
+
callee_file, code_block = find_in_project(func_name, hint_file, max_lines=max_lines)
|
|
135
|
+
if not code_block:
|
|
136
|
+
return ""
|
|
137
|
+
m = re.search(r"```python\n(\d+):", code_block)
|
|
138
|
+
line_ref = f":{m.group(1)}" if m else ""
|
|
139
|
+
return f"\n`{func_name}` is defined at: `{_relative_path(callee_file)}{line_ref}`{code_block}"
|
|
140
|
+
|
|
141
|
+
|
|
40
142
|
def find_function_source(file: str, func_name: str, max_lines: int = 15) -> str:
|
|
41
143
|
"""Return a fenced python block for the body of func_name, or '' if unavailable."""
|
|
42
144
|
from pathlib import Path
|
|
@@ -64,6 +166,8 @@ def find_function_source(file: str, func_name: str, max_lines: int = 15) -> str:
|
|
|
64
166
|
collected.append(line)
|
|
65
167
|
if len(collected) >= max_lines:
|
|
66
168
|
break
|
|
169
|
+
while collected and not collected[-1].strip():
|
|
170
|
+
collected.pop()
|
|
67
171
|
numbered = "\n".join(f"{start_idx + 1 + i}: {ln}" for i, ln in enumerate(collected))
|
|
68
172
|
return f"\n```python\n{numbered}\n```"
|
|
69
173
|
|
py_cq/parsers/compileparser.py
CHANGED
|
@@ -125,4 +125,11 @@ class CompileParser(AbstractParser):
|
|
|
125
125
|
typ = info.get("type", "Error")
|
|
126
126
|
help_msg = info.get("help", "")
|
|
127
127
|
code_block = format_source_context(file, line, count=context_lines) or (f"\n```python\n{info['src']}\n```" if info.get("src") else "")
|
|
128
|
-
|
|
128
|
+
callee = ""
|
|
129
|
+
src_line = info.get("src", "")
|
|
130
|
+
if src_line:
|
|
131
|
+
from py_cq.parsers.common import extract_callee_name, format_callee_context
|
|
132
|
+
func_name = extract_callee_name(src_line)
|
|
133
|
+
if func_name:
|
|
134
|
+
callee = format_callee_context(func_name, file)
|
|
135
|
+
return f"`{file}:{line}` — **{typ}**: {help_msg}{code_block}{callee}"
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Interrogate is invoked with ``-v --fail-under 0``, producing a table of
|
|
4
4
|
per-file docstring coverage on stdout::
|
|
5
5
|
|
|
6
|
-
| src/foo.py | 5 | 2 | 60% |
|
|
7
|
-
| TOTAL | 5 | 2 | 60% |
|
|
6
|
+
| src/foo.py | 5 | 2 | 3 | 60% |
|
|
7
|
+
| TOTAL | 5 | 2 | 3 | 60.0% |
|
|
8
8
|
|
|
9
9
|
The parser extracts per-file coverage and the TOTAL row, storing the TOTAL
|
|
10
10
|
as the ``doc_coverage`` metric (0.0–1.0).
|
|
@@ -14,7 +14,7 @@ import re
|
|
|
14
14
|
|
|
15
15
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
16
16
|
|
|
17
|
-
_ROW_RE = re.compile(r"^\|\s+(.+?)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)%\s
|
|
17
|
+
_ROW_RE = re.compile(r"^\|\s+(.+?)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+\d+\s+\|\s+(\d+(?:\.\d+)?)%\s*\|")
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class InterrogateParser(AbstractParser):
|
|
@@ -30,7 +30,7 @@ class InterrogateParser(AbstractParser):
|
|
|
30
30
|
name = m.group(1).strip()
|
|
31
31
|
total = int(m.group(2))
|
|
32
32
|
miss = int(m.group(3))
|
|
33
|
-
cover =
|
|
33
|
+
cover = float(m.group(4))
|
|
34
34
|
if name == "TOTAL":
|
|
35
35
|
total_coverage = cover / 100.0
|
|
36
36
|
elif total > 0:
|
py_cq/parsers/pytestparser.py
CHANGED
|
@@ -13,6 +13,58 @@ import re as _re
|
|
|
13
13
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _last_call_line_for_test(stdout: str, test_name: str) -> str:
|
|
17
|
+
"""Return the last source line before E-lines in a test's failure section.
|
|
18
|
+
|
|
19
|
+
Captures both indented context lines and pytest's ``>``-prefixed
|
|
20
|
+
current-executing-line marker.
|
|
21
|
+
"""
|
|
22
|
+
lines = stdout.splitlines()
|
|
23
|
+
pattern = _re.compile(rf"_{{4,}}\s+{_re.escape(test_name)}\s+_{{4,}}")
|
|
24
|
+
in_section = False
|
|
25
|
+
last_src = ""
|
|
26
|
+
for line in lines:
|
|
27
|
+
if not in_section:
|
|
28
|
+
if pattern.search(line):
|
|
29
|
+
in_section = True
|
|
30
|
+
else:
|
|
31
|
+
stripped = line.strip()
|
|
32
|
+
if stripped.startswith(("_", "=")):
|
|
33
|
+
break
|
|
34
|
+
if stripped.startswith("E ") or stripped == "E":
|
|
35
|
+
break
|
|
36
|
+
if line.startswith((" ", "\t", ">")):
|
|
37
|
+
src = line.lstrip("> \t")
|
|
38
|
+
if src:
|
|
39
|
+
last_src = src
|
|
40
|
+
return last_src
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_COLLECTION_FILE_RE = _re.compile(r'E\s+File "([^"]+)", line (\d+)')
|
|
44
|
+
_COLLECTION_ERROR_RE = _re.compile(r"E\s+(\w+(?:Error|Warning|Exception)):\s*(.*)")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _extract_collection_error(stdout: str) -> dict | None:
|
|
48
|
+
"""Return {file, line, type, help} if pytest stdout contains a collection error."""
|
|
49
|
+
file_match = None
|
|
50
|
+
error_match = None
|
|
51
|
+
for line in stdout.splitlines():
|
|
52
|
+
m = _COLLECTION_FILE_RE.search(line)
|
|
53
|
+
if m:
|
|
54
|
+
file_match = m
|
|
55
|
+
m = _COLLECTION_ERROR_RE.search(line)
|
|
56
|
+
if m:
|
|
57
|
+
error_match = m
|
|
58
|
+
if file_match and error_match:
|
|
59
|
+
return {
|
|
60
|
+
"file": file_match.group(1).replace("\\", "/"),
|
|
61
|
+
"line": int(file_match.group(2)),
|
|
62
|
+
"type": error_match.group(1),
|
|
63
|
+
"help": error_match.group(2).strip(),
|
|
64
|
+
}
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
16
68
|
def _extract_failure(stdout: str, test_name: str, max_lines: int) -> str:
|
|
17
69
|
"""Extract the failure section for test_name from pytest stdout."""
|
|
18
70
|
lines = stdout.splitlines()
|
|
@@ -102,8 +154,12 @@ class PytestParser(AbstractParser):
|
|
|
102
154
|
return tr
|
|
103
155
|
|
|
104
156
|
def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
|
|
105
|
-
"""Return the first failing test with function body and
|
|
106
|
-
from py_cq.parsers.common import
|
|
157
|
+
"""Return the first failing test with function body, failure output, and callee signature."""
|
|
158
|
+
from py_cq.parsers.common import (
|
|
159
|
+
extract_callee_name,
|
|
160
|
+
find_function_source,
|
|
161
|
+
format_callee_context,
|
|
162
|
+
)
|
|
107
163
|
for file, tests in tr.details.items():
|
|
108
164
|
if not isinstance(tests, dict):
|
|
109
165
|
continue
|
|
@@ -111,13 +167,22 @@ class PytestParser(AbstractParser):
|
|
|
111
167
|
if status != "FAILED":
|
|
112
168
|
continue
|
|
113
169
|
header = f"`{file}::{test_name}` — test **FAILED**"
|
|
114
|
-
|
|
170
|
+
bare_name = test_name.split("[")[0]
|
|
171
|
+
body = find_function_source(file, bare_name, max_lines=context_lines)
|
|
115
172
|
failure = _extract_failure(tr.raw.stdout, test_name, max_lines=context_lines)
|
|
173
|
+
callee = ""
|
|
174
|
+
call_line = _last_call_line_for_test(tr.raw.stdout, test_name)
|
|
175
|
+
if call_line:
|
|
176
|
+
func_name = extract_callee_name(call_line)
|
|
177
|
+
if func_name and func_name != bare_name:
|
|
178
|
+
callee = format_callee_context(func_name, file)
|
|
116
179
|
parts = [header]
|
|
117
180
|
if body:
|
|
118
181
|
parts.append(body)
|
|
119
182
|
if failure:
|
|
120
183
|
parts.append(failure)
|
|
184
|
+
if callee:
|
|
185
|
+
parts.append(callee)
|
|
121
186
|
return "\n".join(parts)
|
|
122
187
|
if "no tests ran" in tr.raw.stdout:
|
|
123
188
|
return (
|
|
@@ -125,7 +190,29 @@ class PytestParser(AbstractParser):
|
|
|
125
190
|
"Add a `tests/` directory with at least one test file (e.g. `tests/test_basic.py`) "
|
|
126
191
|
"and write a first test covering a core function."
|
|
127
192
|
)
|
|
128
|
-
|
|
193
|
+
from py_cq.parsers.common import (
|
|
194
|
+
extract_callee_name,
|
|
195
|
+
format_callee_context,
|
|
196
|
+
format_source_context,
|
|
197
|
+
)
|
|
198
|
+
combined = tr.raw.stdout + tr.raw.stderr
|
|
199
|
+
err = _extract_collection_error(combined)
|
|
200
|
+
if err:
|
|
201
|
+
file, line, typ, help_msg = err["file"], err["line"], err["type"], err["help"]
|
|
202
|
+
code_block = format_source_context(file, line, count=context_lines) or ""
|
|
203
|
+
callee = ""
|
|
204
|
+
# try to find callee from the offending source line via format_source_context result
|
|
205
|
+
src_line = ""
|
|
206
|
+
for ln in (tr.raw.stdout + tr.raw.stderr).splitlines():
|
|
207
|
+
m = _re.match(r"E\s{6,}(\S.*)", ln)
|
|
208
|
+
if m:
|
|
209
|
+
src_line = m.group(1)
|
|
210
|
+
if src_line:
|
|
211
|
+
func_name = extract_callee_name(src_line)
|
|
212
|
+
if func_name:
|
|
213
|
+
callee = format_callee_context(func_name, file)
|
|
214
|
+
return f"`{file}:{line}` — **{typ}**: {help_msg}{code_block}{callee}"
|
|
215
|
+
output = combined.strip()
|
|
129
216
|
if output:
|
|
130
217
|
tail = "\n".join(output.splitlines()[-30:])
|
|
131
218
|
return f"pytest reported failures:\n\n```\n{tail}\n```"
|
py_cq/parsers/typarser.py
CHANGED
|
@@ -18,6 +18,16 @@ from py_cq.parsers.common import format_source_context, score_logistic_variant
|
|
|
18
18
|
|
|
19
19
|
_DIAG_RE = re.compile(r"^(.+):(\d+):\d+:\s+(error|warning)\[([^\]]+)\] (.+)$")
|
|
20
20
|
|
|
21
|
+
_CALL_CODES = frozenset([
|
|
22
|
+
"call-non-callable",
|
|
23
|
+
"missing-argument",
|
|
24
|
+
"unexpected-keyword",
|
|
25
|
+
"argument-type",
|
|
26
|
+
"too-many-positional-arguments",
|
|
27
|
+
"invalid-argument-type",
|
|
28
|
+
"no-matching-overload",
|
|
29
|
+
])
|
|
30
|
+
|
|
21
31
|
|
|
22
32
|
class TyParser(AbstractParser):
|
|
23
33
|
"""Parses raw output from ``ty check`` into a structured ToolResult."""
|
|
@@ -58,4 +68,12 @@ class TyParser(AbstractParser):
|
|
|
58
68
|
line = issue.get("line", "?")
|
|
59
69
|
code = issue.get("code", "")
|
|
60
70
|
message = issue.get("message", "")
|
|
61
|
-
|
|
71
|
+
src_ctx = format_source_context(file, line, count=context_lines)
|
|
72
|
+
callee = ""
|
|
73
|
+
if code in _CALL_CODES and isinstance(line, int):
|
|
74
|
+
from py_cq.parsers.common import extract_callee_name, format_callee_context, read_source_lines
|
|
75
|
+
src_line = read_source_lines(file, line, count=1)
|
|
76
|
+
func_name = extract_callee_name(src_line)
|
|
77
|
+
if func_name:
|
|
78
|
+
callee = format_callee_context(func_name, file)
|
|
79
|
+
return f"`{file}:{line}` — **{code}**: {message}{src_ctx}{callee}"
|
py_cq/tool_registry.py
CHANGED
|
@@ -30,6 +30,7 @@ def load_tool_configs() -> dict[str, ToolConfig]:
|
|
|
30
30
|
run_in_target_env=tool_data.get("run_in_target_env", False),
|
|
31
31
|
extra_deps=tool_data.get("extra_deps", []),
|
|
32
32
|
parser_config=tool_data.get("parser_config", {}),
|
|
33
|
+
exclude_format=tool_data.get("exclude_format", ""),
|
|
33
34
|
)
|
|
34
35
|
return registry
|
|
35
36
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-code-quality
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.15
|
|
4
4
|
Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens.
|
|
5
5
|
Author: Chris Kilner
|
|
6
6
|
Author-email: Chris Kilner <chris@rhiza.fr>
|
|
@@ -35,39 +35,19 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
[](https://pypi.org/project/python-code-quality/)
|
|
36
36
|
[](LICENSE)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Run 11+ code quality tools, aggregate results into one score, and surface the single most critical defect as a focused markdown prompt — ready to pipe to any LLM.
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
This can dramatically reduce the amount of noise for LLMs (and humans) and remove the need for them to know about these tools.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Note: It never edits your files. This is a job for you or an LLM. You may wish to run `ruff check --fix` and `ruff format` first.
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
|
|
46
|
-
cq check .
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```python
|
|
50
|
-
`data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
|
|
51
|
-
|
|
52
|
-
18: min_dist = float("inf")
|
|
53
|
-
19: nearest_city = None
|
|
54
|
-
20: for city in cities:
|
|
55
|
-
21: unused_variable = 67
|
|
56
|
-
22: dist = calc_dist(current_city, city)
|
|
57
|
-
23: if dist < min_dist:
|
|
58
|
-
24: min_dist = dist
|
|
59
|
-
25: nearest_city = city
|
|
60
|
-
|
|
61
|
-
Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
|
|
62
|
-
```
|
|
63
|
-
Feed to an LLM with edit tools and repeat until there are no issues, e.g.
|
|
64
|
-
|
|
65
|
-
```python
|
|
66
|
-
cq check . -o llm | claude -p "fix this"
|
|
67
|
-
# or
|
|
68
|
-
cq check . -o llm | ollama run gpt-oss:20b "Explain how to fix this"
|
|
45
|
+
cq check . -o llm # top defect as markdown, pipe to an LLM
|
|
46
|
+
cq check . # table overview of all scores
|
|
47
|
+
cq check . -o score # numeric score only, exits 1 on errors (CI gate)
|
|
69
48
|
```
|
|
70
49
|
|
|
50
|
+

|
|
71
51
|
|
|
72
52
|
## Install
|
|
73
53
|
|
|
@@ -114,6 +94,7 @@ cq check . -o raw # Raw tool output for debug
|
|
|
114
94
|
cq check path/to/file.py # Just one file (skips pytest and coverage)
|
|
115
95
|
cq check . --only ruff,ty # Run only specific tools
|
|
116
96
|
cq check . --skip bandit # Skip specific tools
|
|
97
|
+
cq check . --exclude demo # Exclude paths from all tools
|
|
117
98
|
cq check . --workers 1 # Run sequentially if you like things slow
|
|
118
99
|
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
119
100
|
cq config path/to/project/ # Show effective tool configuration
|
|
@@ -133,10 +114,17 @@ Add a stop hook to your project's `.claude/settings.json` so Claude automaticall
|
|
|
133
114
|
```json
|
|
134
115
|
{
|
|
135
116
|
"hooks": {
|
|
136
|
-
"Stop": [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
117
|
+
"Stop": [
|
|
118
|
+
{
|
|
119
|
+
"matcher": "",
|
|
120
|
+
"hooks": [
|
|
121
|
+
{
|
|
122
|
+
"type": "command",
|
|
123
|
+
"command": "cq check . -o score && echo 'CQ: all clear' || cq check . -o llm; true"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
140
128
|
}
|
|
141
129
|
}
|
|
142
130
|
```
|
|
@@ -191,7 +179,7 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
|
|
|
191
179
|
```bash
|
|
192
180
|
> cq check . -o score
|
|
193
181
|
```
|
|
194
|
-
```
|
|
182
|
+
```
|
|
195
183
|
0.9662730667181059 # this is designed to approach but not reach 1.0
|
|
196
184
|
```
|
|
197
185
|
|
|
@@ -252,6 +240,9 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
|
252
240
|
# Skip tools that are slow or not relevant to your project
|
|
253
241
|
disable = ["coverage", "interrogate"]
|
|
254
242
|
|
|
243
|
+
# Exclude paths from all tools (merged with --exclude CLI flag)
|
|
244
|
+
exclude = ["demo", "docs"]
|
|
245
|
+
|
|
255
246
|
# Lines of source context shown around each defect in LLM output (default: 15)
|
|
256
247
|
context_lines = 15
|
|
257
248
|
|
|
@@ -270,21 +261,23 @@ Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandi
|
|
|
270
261
|
python:
|
|
271
262
|
|
|
272
263
|
compile:
|
|
273
|
-
command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
|
|
264
|
+
command: "{python} -m compileall -r 10 -j 8 \"{context_path}\" -x .*venv"
|
|
274
265
|
parser: "CompileParser"
|
|
275
266
|
order: 1
|
|
276
267
|
warning_threshold: 0.9999
|
|
277
268
|
error_threshold: 0.9999
|
|
278
269
|
|
|
279
270
|
ruff:
|
|
280
|
-
command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
|
|
271
|
+
command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\"{exclude}"
|
|
272
|
+
exclude_format: " --exclude {path}"
|
|
281
273
|
parser: "RuffParser"
|
|
282
274
|
order: 2
|
|
283
275
|
warning_threshold: 0.9999
|
|
284
276
|
error_threshold: 0.9
|
|
285
277
|
|
|
286
278
|
ty:
|
|
287
|
-
command: "{python} -m ty check --output-format concise --color never {context_path}"
|
|
279
|
+
command: "{python} -m ty check --output-format concise --color never \"{context_path}\"{exclude}"
|
|
280
|
+
exclude_format: " --exclude {path}"
|
|
288
281
|
parser: "TyParser"
|
|
289
282
|
order: 3
|
|
290
283
|
warning_threshold: 0.9999
|
|
@@ -294,22 +287,26 @@ python:
|
|
|
294
287
|
- ty
|
|
295
288
|
|
|
296
289
|
bandit:
|
|
297
|
-
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
290
|
+
command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests{exclude}\""
|
|
291
|
+
exclude_format: ",{input_path_posix}/{path}"
|
|
298
292
|
parser: "BanditParser"
|
|
299
293
|
order: 4
|
|
300
294
|
warning_threshold: 0.9999
|
|
301
295
|
error_threshold: 0.8
|
|
302
296
|
|
|
303
297
|
pytest:
|
|
304
|
-
command: "{python} -m pytest -v {context_path}"
|
|
298
|
+
command: "{python} -m pytest -v \"{context_path}\"{exclude}"
|
|
299
|
+
exclude_format: " --ignore {path}"
|
|
305
300
|
parser: "PytestParser"
|
|
306
301
|
order: 5
|
|
307
302
|
warning_threshold: 1.0
|
|
308
303
|
error_threshold: 1.0
|
|
309
304
|
run_in_target_env: true
|
|
305
|
+
extra_deps:
|
|
306
|
+
- pytest
|
|
310
307
|
|
|
311
308
|
coverage:
|
|
312
|
-
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
309
|
+
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest \"{context_path}\" && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
313
310
|
parser: "CoverageParser"
|
|
314
311
|
order: 6
|
|
315
312
|
warning_threshold: 0.9
|
|
@@ -320,40 +317,41 @@ python:
|
|
|
320
317
|
- pytest
|
|
321
318
|
|
|
322
319
|
radon-cc:
|
|
323
|
-
command: "{python} -m radon cc --json {context_path}"
|
|
320
|
+
command: "{python} -m radon cc --json \"{context_path}\""
|
|
324
321
|
parser: "ComplexityParser"
|
|
325
322
|
order: 7
|
|
326
323
|
warning_threshold: 0.6
|
|
327
324
|
error_threshold: 0.4
|
|
328
325
|
|
|
329
326
|
radon-mi:
|
|
330
|
-
command: "{python} -m radon mi -s --json {context_path}"
|
|
327
|
+
command: "{python} -m radon mi -s --json \"{context_path}\""
|
|
331
328
|
parser: "MaintainabilityParser"
|
|
332
329
|
order: 8
|
|
333
330
|
warning_threshold: 0.6
|
|
334
331
|
error_threshold: 0.4
|
|
335
332
|
|
|
336
333
|
radon-hal:
|
|
337
|
-
command: "{python} -m radon hal -f --json {context_path}"
|
|
334
|
+
command: "{python} -m radon hal -f --json \"{context_path}\""
|
|
338
335
|
parser: "HalsteadParser"
|
|
339
336
|
order: 9
|
|
340
337
|
warning_threshold: 0.5
|
|
341
338
|
error_threshold: 0.3
|
|
342
339
|
|
|
343
340
|
vulture:
|
|
344
|
-
command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
341
|
+
command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git{exclude}"
|
|
342
|
+
exclude_format: ",{path}"
|
|
345
343
|
parser: "VultureParser"
|
|
346
344
|
order: 10
|
|
347
345
|
warning_threshold: 0.9999
|
|
348
346
|
error_threshold: 0.8
|
|
349
347
|
|
|
350
348
|
interrogate:
|
|
351
|
-
command: "{python} -m interrogate {context_path} -v --fail-under 0"
|
|
349
|
+
command: "{python} -m interrogate \"{context_path}\" -e tests{exclude} -v --fail-under 0"
|
|
350
|
+
exclude_format: " -e {path}"
|
|
352
351
|
parser: "InterrogateParser"
|
|
353
352
|
order: 11
|
|
354
353
|
warning_threshold: 0.8
|
|
355
354
|
error_threshold: 0.3
|
|
356
|
-
|
|
357
355
|
```
|
|
358
356
|
|
|
359
357
|
## Respect
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
py_cq/__init__.py,sha256=
|
|
2
|
-
py_cq/cli.py,sha256=
|
|
1
|
+
py_cq/__init__.py,sha256=U2ysDtSFdv2mlXZz4w1Q42pfgfi6YY_3Ln24bkZq14I,260
|
|
2
|
+
py_cq/cli.py,sha256=HWTMtA9Gfn3YeO7B94kter6HNNSzPjyX1HLJQ1eNBvU,12936
|
|
3
3
|
py_cq/config/__init__.py,sha256=f0wc51O_3kGDTZUnCbGv8_zWnC5yYGl4NWcf2buSImQ,670
|
|
4
|
-
py_cq/config/config.yaml,sha256=
|
|
4
|
+
py_cq/config/config.yaml,sha256=TPZJogpWbyf0Ml2mHrHzTNyTik3k07KPVGsA1wT9GEc,2696
|
|
5
5
|
py_cq/context_hash.py,sha256=h-i7Rhd7AUfLv9SkQvE79bjJvTsm_ZwoVwSmUKXWmfM,2977
|
|
6
|
-
py_cq/execution_engine.py,sha256=
|
|
6
|
+
py_cq/execution_engine.py,sha256=nAsCvldUfDagZnfOwXf4A37VQLxCGiv12T8ymutflC4,8697
|
|
7
7
|
py_cq/language_detector.py,sha256=6av5HaimcZ54RkN69xQmGgC0mxtTvGzPV3SL8NGG8Uc,1116
|
|
8
8
|
py_cq/llm_formatter.py,sha256=l74O5iqNWMR16789Xncoi93xOqgQJhW5IQK9_OyA9mE,1632
|
|
9
|
-
py_cq/localtypes.py,sha256=
|
|
9
|
+
py_cq/localtypes.py,sha256=UGI2kl1xB2TedKGByth3URiqJKY56YZMNds1hnLEzvU,6228
|
|
10
10
|
py_cq/main.py,sha256=VKoXI8R8rB2fEROBYoTVURfinLqyh8XTNIIWAOtH7dw,380
|
|
11
11
|
py_cq/metric_aggregator.py,sha256=M2ymo62S7p7qPUqjjoiPg4IVyXQhLMuTr9-jxLiFjCY,853
|
|
12
|
-
py_cq/parsers/__init__.py,sha256=
|
|
12
|
+
py_cq/parsers/__init__.py,sha256=YS3wPS0cMNU80zkdSZBEZOkqDKE6Jk--0Xd_bX7VMcA,27
|
|
13
13
|
py_cq/parsers/banditparser.py,sha256=Ju_CkuXtkVn1Th9aQ6mv6fTUTpb9pc1YD8Nzt_nMgFQ,2326
|
|
14
|
-
py_cq/parsers/common.py,sha256=
|
|
15
|
-
py_cq/parsers/compileparser.py,sha256=
|
|
14
|
+
py_cq/parsers/common.py,sha256=h3-eLyi0YNkk-2ZiNUKxZhIFZNHUf01kyAqCtFbxNIY,8803
|
|
15
|
+
py_cq/parsers/compileparser.py,sha256=EBoqZyPDkkfc9FssGkKrIATU6TDHSSB7xWeqrnOiMEc,6576
|
|
16
16
|
py_cq/parsers/complexityparser.py,sha256=2t1-wmNjUu65fULcIm5jcgv7ZLWwjakyiY_r-Fx1QQg,3983
|
|
17
17
|
py_cq/parsers/coverageparser.py,sha256=n4h_RCxKvJoYgp356PA4laAmFcngjQ41b_GXZvKkVgc,4041
|
|
18
18
|
py_cq/parsers/exitcodeparser.py,sha256=ZFL3EbPhGvFjm2qZhfLD1_5dNjR9ULYNQpKgy0Z_dGo,729
|
|
19
19
|
py_cq/parsers/halsteadparser.py,sha256=ZCN7LP1iUZ91tf3tlspKAPQrTEa78XVdE9Fjn9yPvYk,9008
|
|
20
|
-
py_cq/parsers/interrogateparser.py,sha256=
|
|
20
|
+
py_cq/parsers/interrogateparser.py,sha256=gk9pJ7yFXMzLjk6PP0X8fxW_gAE2DTIoWMsPqF2xIXs,2260
|
|
21
21
|
py_cq/parsers/linecountparser.py,sha256=nxWvFntuR8T6FDGpafrlevvt-jR3rwkbVo_t2JX4TF0,1067
|
|
22
22
|
py_cq/parsers/maintainabilityparser.py,sha256=Ax0ZFA6zzqYIWZH1hP1_GUtdVn2LIJ8SKWtqVNdszYs,3411
|
|
23
|
-
py_cq/parsers/pytestparser.py,sha256=
|
|
23
|
+
py_cq/parsers/pytestparser.py,sha256=_seSfvAD_88A-yDyWYpLuIrLL2Wjp3rbcqG3fNCNEnA,9705
|
|
24
24
|
py_cq/parsers/regexcountparser.py,sha256=KSoNh2spucXU06pxxr2QW0LrPLfJkFMAsmSgjooaFv0,1316
|
|
25
25
|
py_cq/parsers/ruffparser.py,sha256=ZdIya4sct2PrsOyfKfMGmXHLh3Qu7HtFqXNY9IuNFog,2275
|
|
26
|
-
py_cq/parsers/typarser.py,sha256=
|
|
26
|
+
py_cq/parsers/typarser.py,sha256=oJWudNyRZw-r_-gr2nIzDfS7YJGpt5K3otk9E4G5Ldo,3140
|
|
27
27
|
py_cq/parsers/vultureparser.py,sha256=U6zC7P0ATA_N4SB90BahKF5QHMITf_z-NsOgrh_Q5rA,1995
|
|
28
28
|
py_cq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
-
py_cq/tool_registry.py,sha256=
|
|
30
|
-
python_code_quality-0.1.
|
|
31
|
-
python_code_quality-0.1.
|
|
32
|
-
python_code_quality-0.1.
|
|
33
|
-
python_code_quality-0.1.
|
|
29
|
+
py_cq/tool_registry.py,sha256=UfmwJH8rtmTY9kX02QaE4OJIWyboTMcXUavgb9R4fxc,1648
|
|
30
|
+
python_code_quality-0.1.15.dist-info/WHEEL,sha256=M4DeIjVCA49okfALADZoWX5JOGwnmHb-JOpQHtI-1c0,80
|
|
31
|
+
python_code_quality-0.1.15.dist-info/entry_points.txt,sha256=cfWbTw7eYO6Trv1-Z_odL6Zta9CqYU6Vk9lAHdfB60Q,40
|
|
32
|
+
python_code_quality-0.1.15.dist-info/METADATA,sha256=bh2Ex3HJZiNMiJ_LaVtOE_MHf497VN3s0xH8ISWHmvs,12749
|
|
33
|
+
python_code_quality-0.1.15.dist-info/RECORD,,
|
|
File without changes
|
{python_code_quality-0.1.13.dist-info → python_code_quality-0.1.15.dist-info}/entry_points.txt
RENAMED
|
File without changes
|