python-code-quality 0.1.14__tar.gz → 0.1.16__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.
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/PKG-INFO +6 -2
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/README.md +5 -1
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/pyproject.toml +10 -1
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/cli.py +35 -43
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/main.py +1 -1
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/banditparser.py +6 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/common.py +104 -7
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/compileparser.py +20 -6
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/complexityparser.py +8 -1
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/coverageparser.py +3 -3
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/halsteadparser.py +8 -1
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/interrogateparser.py +4 -4
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/maintainabilityparser.py +8 -1
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/pytestparser.py +91 -4
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/ruffparser.py +4 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/typarser.py +23 -1
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/vultureparser.py +4 -0
- python_code_quality-0.1.16/src/py_cq/table_formatter.py +29 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/__init__.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/config/__init__.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/config/config.yaml +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/context_hash.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/execution_engine.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/language_detector.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/llm_formatter.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/localtypes.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/metric_aggregator.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/__init__.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/exitcodeparser.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/linecountparser.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/regexcountparser.py +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/py.typed +0 -0
- {python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/tool_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-code-quality
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.16
|
|
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>
|
|
@@ -37,6 +37,10 @@ Description-Content-Type: text/markdown
|
|
|
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
|
+
This can dramatically reduce the amount of noise for LLMs (and humans) and remove the need for them to know about these tools.
|
|
41
|
+
|
|
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
|
+
|
|
40
44
|
```bash
|
|
41
45
|
cq check . -o llm # top defect as markdown, pipe to an LLM
|
|
42
46
|
cq check . # table overview of all scores
|
|
@@ -175,7 +179,7 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
|
|
|
175
179
|
```bash
|
|
176
180
|
> cq check . -o score
|
|
177
181
|
```
|
|
178
|
-
```
|
|
182
|
+
```
|
|
179
183
|
0.9662730667181059 # this is designed to approach but not reach 1.0
|
|
180
184
|
```
|
|
181
185
|
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
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.
|
|
10
10
|
|
|
11
|
+
This can dramatically reduce the amount of noise for LLMs (and humans) and remove the need for them to know about these tools.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
11
15
|
```bash
|
|
12
16
|
cq check . -o llm # top defect as markdown, pipe to an LLM
|
|
13
17
|
cq check . # table overview of all scores
|
|
@@ -146,7 +150,7 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
|
|
|
146
150
|
```bash
|
|
147
151
|
> cq check . -o score
|
|
148
152
|
```
|
|
149
|
-
```
|
|
153
|
+
```
|
|
150
154
|
0.9662730667181059 # this is designed to approach but not reach 1.0
|
|
151
155
|
```
|
|
152
156
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-code-quality"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.16"
|
|
4
4
|
description = "Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -47,5 +47,14 @@ cq = "py_cq.main:main"
|
|
|
47
47
|
testpaths = ["tests"]
|
|
48
48
|
norecursedirs = [".venv", "dist"]
|
|
49
49
|
|
|
50
|
+
[[tool.ty.overrides]]
|
|
51
|
+
include = ["demo/**"]
|
|
52
|
+
rules = {unresolved-import = "ignore"}
|
|
53
|
+
|
|
50
54
|
[tool.cq]
|
|
51
55
|
exclude = ["demo"]
|
|
56
|
+
|
|
57
|
+
[dependency-groups]
|
|
58
|
+
dev = [
|
|
59
|
+
"hypothesis>=6.151.10",
|
|
60
|
+
]
|
|
@@ -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
|
|
@@ -28,8 +29,9 @@ from py_cq.config import load_user_config
|
|
|
28
29
|
from py_cq.execution_engine import _cache as tool_cache
|
|
29
30
|
from py_cq.execution_engine import run_tools
|
|
30
31
|
from py_cq.language_detector import detect_language
|
|
31
|
-
from py_cq.localtypes import
|
|
32
|
+
from py_cq.localtypes import ToolConfig
|
|
32
33
|
from py_cq.metric_aggregator import aggregate_metrics
|
|
34
|
+
from py_cq.table_formatter import format_as_table
|
|
33
35
|
from py_cq.tool_registry import tool_registry
|
|
34
36
|
|
|
35
37
|
logging.basicConfig(
|
|
@@ -100,8 +102,36 @@ class OutputMode(str, Enum):
|
|
|
100
102
|
RAW = "raw"
|
|
101
103
|
|
|
102
104
|
|
|
105
|
+
def _version_callback(value: bool) -> None:
|
|
106
|
+
if not value:
|
|
107
|
+
return
|
|
108
|
+
import re
|
|
109
|
+
import sys
|
|
110
|
+
if isinstance(sys.stdout, io.TextIOWrapper): # pragma: no branch
|
|
111
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
112
|
+
pkg = "python-code-quality"
|
|
113
|
+
pkg_version = version(pkg)
|
|
114
|
+
dep_versions: list[tuple[str, str]] = []
|
|
115
|
+
for req in (requires(pkg) or []):
|
|
116
|
+
if "; extra ==" in req:
|
|
117
|
+
continue
|
|
118
|
+
dep_name = re.split(r"[>=<!;\s\[]", req)[0]
|
|
119
|
+
try:
|
|
120
|
+
dep_versions.append((dep_name, version(dep_name)))
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
typer.echo(f"{pkg} v{pkg_version}")
|
|
124
|
+
for dep_name, dep_ver in sorted(dep_versions):
|
|
125
|
+
typer.echo(f"\u251c\u2500\u2500 {dep_name} v{dep_ver}")
|
|
126
|
+
raise typer.Exit()
|
|
127
|
+
|
|
128
|
+
|
|
103
129
|
@app.callback()
|
|
104
|
-
def callback(
|
|
130
|
+
def callback(
|
|
131
|
+
_: bool = typer.Option(
|
|
132
|
+
False, "--version", "-V", callback=_version_callback, is_eager=True, help="Show version and dependencies"
|
|
133
|
+
),
|
|
134
|
+
) -> None:
|
|
105
135
|
"""Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm"""
|
|
106
136
|
console = Console()
|
|
107
137
|
|
|
@@ -156,7 +186,7 @@ def check(
|
|
|
156
186
|
if path_obj.is_file():
|
|
157
187
|
if path_obj.suffix != ".py":
|
|
158
188
|
raise typer.BadParameter(f"File must be a Python file (.py): {path}")
|
|
159
|
-
elif path_obj.is_dir():
|
|
189
|
+
elif path_obj.is_dir(): # pragma: no branch
|
|
160
190
|
if not (path_obj / "pyproject.toml").exists():
|
|
161
191
|
raise typer.BadParameter(f"Directory must contain pyproject.toml: {path}")
|
|
162
192
|
log.setLevel(log_level)
|
|
@@ -187,7 +217,7 @@ def check(
|
|
|
187
217
|
elif output == OutputMode.LLM:
|
|
188
218
|
# log.setLevel("CRITICAL")
|
|
189
219
|
from py_cq.llm_formatter import format_for_llm
|
|
190
|
-
|
|
220
|
+
print(format_for_llm(effective_registry, combined_metrics, context_lines=context_lines))
|
|
191
221
|
else:
|
|
192
222
|
console.print(f"[bold green]{path_obj.resolve()}[/]")
|
|
193
223
|
console.print(format_as_table(combined_metrics, effective_registry))
|
|
@@ -255,41 +285,3 @@ def config(
|
|
|
255
285
|
console.print(table)
|
|
256
286
|
|
|
257
287
|
|
|
258
|
-
def format_as_table(data: CombinedToolResults, registry: dict[str, ToolConfig]):
|
|
259
|
-
"""Format combined tool results into a Rich Table.
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
data (CombinedToolResults): Aggregated tool results, including the path,
|
|
263
|
-
individual tool results, and the overall score.
|
|
264
|
-
|
|
265
|
-
Returns:
|
|
266
|
-
rich.table.Table: A Rich table with columns ``Tool``, ``Metric``, ``Score`` and
|
|
267
|
-
``Status``. Each metric row displays a status icon based on thresholds from
|
|
268
|
-
the tool's configuration. The table is titled with the data path and ends
|
|
269
|
-
with a row showing the overall score.
|
|
270
|
-
|
|
271
|
-
Example:
|
|
272
|
-
>>> table = format_as_table(combined_results)
|
|
273
|
-
>>> console.print(table)
|
|
274
|
-
"""
|
|
275
|
-
table = Table(width=80)
|
|
276
|
-
table.add_column("Tool", justify="left", no_wrap=True)
|
|
277
|
-
table.add_column("Time", justify="right", style="dim")
|
|
278
|
-
table.add_column("Metric", justify="right", style="cyan", no_wrap=True)
|
|
279
|
-
table.add_column("Score", style="magenta")
|
|
280
|
-
table.add_column("Status")
|
|
281
|
-
for tr in data.tool_results:
|
|
282
|
-
tool_name = tr.raw.tool_name
|
|
283
|
-
config = next((t for t in registry.values() if t.name == tool_name))
|
|
284
|
-
for i, (name, value) in enumerate(tr.metrics.items()):
|
|
285
|
-
status = ""
|
|
286
|
-
if value < config.error_threshold:
|
|
287
|
-
status = "[bold red]Error[/]"
|
|
288
|
-
elif value < config.warning_threshold:
|
|
289
|
-
status = "[yellow]Warning[/]"
|
|
290
|
-
else:
|
|
291
|
-
status = "[green]OK[/]"
|
|
292
|
-
time_str = f"{tr.duration_s:.2f}s" if i == 0 else ""
|
|
293
|
-
table.add_row(tool_name, time_str, name, f"{value:0.3f}", status)
|
|
294
|
-
table.add_row("", "", "[bold]Score[/]", f"[bold]{data.score:0.3f}[/]", "")
|
|
295
|
-
return table
|
|
@@ -22,6 +22,8 @@ class BanditParser(AbstractParser):
|
|
|
22
22
|
data = json.loads(raw_result.stdout)
|
|
23
23
|
except (json.JSONDecodeError, ValueError):
|
|
24
24
|
return ToolResult(raw=raw_result, metrics={"security": 1.0})
|
|
25
|
+
if not isinstance(data, dict):
|
|
26
|
+
return ToolResult(raw=raw_result, metrics={"security": 1.0})
|
|
25
27
|
|
|
26
28
|
files: dict[str, list] = {}
|
|
27
29
|
weighted = 0
|
|
@@ -46,7 +48,11 @@ class BanditParser(AbstractParser):
|
|
|
46
48
|
if not tr.details:
|
|
47
49
|
return "bandit reported issues (no details available)"
|
|
48
50
|
file, issues = next(iter(tr.details.items()))
|
|
51
|
+
if not isinstance(issues, list) or not issues:
|
|
52
|
+
return "bandit reported issues (no details available)"
|
|
49
53
|
issue = issues[0]
|
|
54
|
+
if not isinstance(issue, dict):
|
|
55
|
+
return "bandit reported issues (no details available)"
|
|
50
56
|
line = issue.get("line", "?")
|
|
51
57
|
code = issue.get("code", "")
|
|
52
58
|
severity = issue.get("severity", "")
|
|
@@ -12,39 +12,134 @@ 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
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def read_source_lines(file_path: str, line: int, count: int = 5) -> str:
|
|
18
20
|
"""Return up to `count` source lines starting at the given 1-based line number."""
|
|
19
|
-
from pathlib import Path
|
|
20
21
|
try:
|
|
21
22
|
all_lines = Path(file_path).read_text(encoding="utf-8").splitlines()
|
|
22
23
|
start = max(0, line - 1)
|
|
23
24
|
return "\n".join(all_lines[start : start + count])
|
|
24
|
-
except OSError:
|
|
25
|
+
except (OSError, ValueError):
|
|
25
26
|
return ""
|
|
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
|
+
stripped = source_line.strip()
|
|
70
|
+
rhs = stripped
|
|
71
|
+
if "=" in stripped and not stripped.startswith(("assert", "return")):
|
|
72
|
+
rhs = stripped.split("=", 1)[1].strip()
|
|
73
|
+
m = re.search(r"\b([a-zA-Z_]\w*)\s*\(", rhs)
|
|
74
|
+
if m and m.group(1) not in _PYTHON_KEYWORDS:
|
|
75
|
+
return m.group(1)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _find_project_root(hint_file: str) -> Path:
|
|
80
|
+
root = Path(hint_file).resolve().parent
|
|
81
|
+
current = root
|
|
82
|
+
for _ in range(8):
|
|
83
|
+
if (current / "pyproject.toml").exists() or (current / "setup.py").exists():
|
|
84
|
+
return current
|
|
85
|
+
parent = current.parent
|
|
86
|
+
if parent == current:
|
|
87
|
+
break
|
|
88
|
+
current = parent
|
|
89
|
+
return root
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def find_in_project(func_name: str, hint_file: str, max_lines: int = 10) -> tuple[str, str]:
|
|
93
|
+
"""Find func_name definition in project files; same file first, then project-wide.
|
|
94
|
+
|
|
95
|
+
Returns ``(file_path, code_block)`` for the first match, or ``("", "")`` if not found.
|
|
96
|
+
"""
|
|
97
|
+
result = find_function_source(hint_file, func_name, max_lines=max_lines)
|
|
98
|
+
if result:
|
|
99
|
+
return hint_file, result
|
|
100
|
+
root = _find_project_root(hint_file)
|
|
101
|
+
for py_file in sorted(root.rglob("*.py")):
|
|
102
|
+
if py_file.resolve() == Path(hint_file).resolve():
|
|
103
|
+
continue
|
|
104
|
+
r = find_function_source(str(py_file), func_name, max_lines=max_lines)
|
|
105
|
+
if r:
|
|
106
|
+
return str(py_file), r
|
|
107
|
+
return "", ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _relative_path(path: str) -> str:
|
|
111
|
+
"""Return path relative to cwd, normalised to forward slashes."""
|
|
112
|
+
try:
|
|
113
|
+
return str(Path(path).relative_to(Path.cwd())).replace("\\", "/")
|
|
114
|
+
except ValueError:
|
|
115
|
+
return path.replace("\\", "/")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def format_callee_context(func_name: str, hint_file: str, max_lines: int = 10) -> str:
|
|
119
|
+
"""Return a labelled callee definition block, or '' if not found in project.
|
|
120
|
+
|
|
121
|
+
Output format::
|
|
122
|
+
|
|
123
|
+
Callee `func_name` — `relative/path/to/file.py`
|
|
124
|
+
```python
|
|
125
|
+
N: def func_name(...):
|
|
126
|
+
...
|
|
127
|
+
```
|
|
128
|
+
"""
|
|
129
|
+
callee_file, code_block = find_in_project(func_name, hint_file, max_lines=max_lines)
|
|
130
|
+
if not code_block:
|
|
131
|
+
return ""
|
|
132
|
+
m = re.search(r"```python\n(\d+):", code_block)
|
|
133
|
+
line_ref = f":{m.group(1)}" if m else ""
|
|
134
|
+
return f"\n`{func_name}` is defined at: `{_relative_path(callee_file)}{line_ref}`{code_block}"
|
|
135
|
+
|
|
136
|
+
|
|
40
137
|
def find_function_source(file: str, func_name: str, max_lines: int = 15) -> str:
|
|
41
138
|
"""Return a fenced python block for the body of func_name, or '' if unavailable."""
|
|
42
|
-
from pathlib import Path
|
|
43
139
|
try:
|
|
44
140
|
all_lines = Path(file).read_text(encoding="utf-8").splitlines()
|
|
45
|
-
except OSError:
|
|
141
|
+
except (OSError, ValueError):
|
|
46
142
|
return ""
|
|
47
|
-
import re
|
|
48
143
|
pattern = re.compile(rf"^(\s*)(?:async\s+)?def\s+{re.escape(func_name)}\s*\(")
|
|
49
144
|
match_result: tuple[int, int] | None = None
|
|
50
145
|
for i, line in enumerate(all_lines):
|
|
@@ -64,6 +159,8 @@ def find_function_source(file: str, func_name: str, max_lines: int = 15) -> str:
|
|
|
64
159
|
collected.append(line)
|
|
65
160
|
if len(collected) >= max_lines:
|
|
66
161
|
break
|
|
162
|
+
while collected and not collected[-1].strip():
|
|
163
|
+
collected.pop()
|
|
67
164
|
numbered = "\n".join(f"{start_idx + 1 + i}: {ln}" for i, ln in enumerate(collected))
|
|
68
165
|
return f"\n```python\n{numbered}\n```"
|
|
69
166
|
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/compileparser.py
RENAMED
|
@@ -64,7 +64,10 @@ class CompileParser(AbstractParser):
|
|
|
64
64
|
compilations += 1
|
|
65
65
|
elif line.startswith("*** File "):
|
|
66
66
|
# This indicates a compilation error
|
|
67
|
-
|
|
67
|
+
parts = line.split('"')
|
|
68
|
+
if len(parts) < 2:
|
|
69
|
+
continue
|
|
70
|
+
file_path = parts[1]
|
|
68
71
|
current_error = {"file": file_path, "error": line}
|
|
69
72
|
elif current_error and line.strip():
|
|
70
73
|
# Append additional error context
|
|
@@ -80,17 +83,21 @@ class CompileParser(AbstractParser):
|
|
|
80
83
|
error_info = {}
|
|
81
84
|
# Extract line number if present
|
|
82
85
|
if "line " in error_lines[0]:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
try:
|
|
87
|
+
error_info["line"] = int(
|
|
88
|
+
error_lines[0].split("line ")[1].split(",")[0]
|
|
89
|
+
)
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass
|
|
86
92
|
# Get source code context if available
|
|
87
93
|
if len(error_lines) > 1:
|
|
88
94
|
error_info["src"] = error_lines[1].strip()
|
|
89
95
|
if len(error_lines) > 3:
|
|
90
96
|
if "Error:" in error_lines[3]:
|
|
91
97
|
error_parts = error_lines[3].split(":")
|
|
98
|
+
type_tokens = error_parts[0].strip().split()
|
|
92
99
|
error_info["type"] = (
|
|
93
|
-
|
|
100
|
+
type_tokens[-1] if type_tokens else "Unknown"
|
|
94
101
|
) # Gets "SyntaxError"
|
|
95
102
|
error_info["help"] = ",".join(
|
|
96
103
|
error_parts[1:]
|
|
@@ -125,4 +132,11 @@ class CompileParser(AbstractParser):
|
|
|
125
132
|
typ = info.get("type", "Error")
|
|
126
133
|
help_msg = info.get("help", "")
|
|
127
134
|
code_block = format_source_context(file, line, count=context_lines) or (f"\n```python\n{info['src']}\n```" if info.get("src") else "")
|
|
128
|
-
|
|
135
|
+
callee = ""
|
|
136
|
+
src_line = info.get("src", "")
|
|
137
|
+
if src_line:
|
|
138
|
+
from py_cq.parsers.common import extract_callee_name, format_callee_context
|
|
139
|
+
func_name = extract_callee_name(src_line)
|
|
140
|
+
if func_name:
|
|
141
|
+
callee = format_callee_context(func_name, file)
|
|
142
|
+
return f"`{file}:{line}` — **{typ}**: {help_msg}{code_block}{callee}"
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/complexityparser.py
RENAMED
|
@@ -64,7 +64,14 @@ class ComplexityParser(AbstractParser):
|
|
|
64
64
|
>>> result.metrics["simplicity"]
|
|
65
65
|
0.4"""
|
|
66
66
|
tr = ToolResult(raw=raw_result)
|
|
67
|
-
|
|
67
|
+
try:
|
|
68
|
+
data = json.loads(raw_result.stdout)
|
|
69
|
+
except (json.JSONDecodeError, ValueError):
|
|
70
|
+
tr.metrics["simplicity"] = 0.0
|
|
71
|
+
return tr
|
|
72
|
+
if not isinstance(data, dict):
|
|
73
|
+
tr.metrics["simplicity"] = 0.0
|
|
74
|
+
return tr
|
|
68
75
|
score = 0
|
|
69
76
|
num_items = 0
|
|
70
77
|
max_complexity = 30
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/coverageparser.py
RENAMED
|
@@ -76,13 +76,13 @@ class CoverageParser(AbstractParser):
|
|
|
76
76
|
score = tr.metrics.get("coverage", 0)
|
|
77
77
|
uncovered = sorted(
|
|
78
78
|
[(f, d) for f, d in tr.details.items() if isinstance(d, dict) and d.get("missing")],
|
|
79
|
-
key=lambda x: x[1]
|
|
79
|
+
key=lambda x: x[1].get("coverage", 0.0),
|
|
80
80
|
)[:5]
|
|
81
81
|
if not uncovered:
|
|
82
82
|
return f"**coverage** score: {score:.3f}"
|
|
83
83
|
lines = [f"**coverage** score: {score:.3f} — files with lowest coverage:"]
|
|
84
84
|
for path, data in uncovered:
|
|
85
|
-
pct = data
|
|
86
|
-
miss = data
|
|
85
|
+
pct = data.get("coverage", 0.0)
|
|
86
|
+
miss = data.get("missing", 0)
|
|
87
87
|
lines.append(f"- `{path}`: {pct:.0%} ({miss} uncovered statements)")
|
|
88
88
|
return "\n".join(lines)
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/halsteadparser.py
RENAMED
|
@@ -59,6 +59,14 @@ class HalsteadParser(AbstractParser):
|
|
|
59
59
|
# {"total": {"h1": 6, "h2": 18, "N1": 13, "N2": 22, "vocabulary": 24, "length": 35, "calculated_length": 90.56842503028855, "volume": 160.4736875252405, "difficulty": 3.6666666666666665, "effort": 588.4035209258818, "time": 32.68908449588233, "bugs": 0.05349122917508017},
|
|
60
60
|
# "functions": {"calc_dist": {"h1": 3, "h2": 9, "N1": 5, "N2": 10, "vocabulary": 12, "length": 15, "calculated_length": 33.28421251514428, "volume": 53.77443751081735, "difficulty": 1.6666666666666667, "effort": 89.62406251802892, "time": 4.9791145843349405, "bugs": 0.017924812503605784}, "find_nearest_city": {"h1": 1, "h2": 2, "N1": 1, "N2": 2, "vocabulary": 3, "length": 3, "calculated_length": 2.0, "volume": 4.754887502163469, "difficulty": 0.5, "effort": 2.3774437510817346, "time": 0.1320802083934297, "bugs": 0.0015849625007211565}, "generate_tour": {"h1": 2, "h2": 5, "N1": 6, "N2": 8, "vocabulary": 7, "length": 14, "calculated_length": 13.60964047443681, "volume": 39.302968908806456, "difficulty": 1.6, "effort": 62.884750254090335, "time": 3.493597236338352, "bugs": 0.01310098963626882}, "main": {"h1": 0, "h2": 0, "N1": 0, "N2": 0, "vocabulary": 0, "length": 0, "calculated_length": 0, "volume": 0, "difficulty": 0, "effort": 0, "time": 0.0, "bugs": 0.0}}}
|
|
61
61
|
tr = ToolResult(raw=raw_result)
|
|
62
|
+
try:
|
|
63
|
+
data = json.loads(raw_result.stdout)
|
|
64
|
+
except (json.JSONDecodeError, ValueError):
|
|
65
|
+
tr.metrics = {"file_bug_free": 1.0, "file_smallness": 1.0, "functions_bug_free": 1.0, "functions_smallness": 1.0}
|
|
66
|
+
return tr
|
|
67
|
+
if not isinstance(data, dict):
|
|
68
|
+
tr.metrics = {"file_bug_free": 1.0, "file_smallness": 1.0, "functions_bug_free": 1.0, "functions_smallness": 1.0}
|
|
69
|
+
return tr
|
|
62
70
|
MAX_FILE_BUGS = 1
|
|
63
71
|
MAX_FILE_VOLUME = 2000
|
|
64
72
|
MAX_FUNCTION_BUGS = 0.2
|
|
@@ -67,7 +75,6 @@ class HalsteadParser(AbstractParser):
|
|
|
67
75
|
min_file_sm = 1.0
|
|
68
76
|
min_function_nb = 1.0
|
|
69
77
|
min_function_sm = 1.0
|
|
70
|
-
data = json.loads(raw_result.stdout)
|
|
71
78
|
for file, values in data.items():
|
|
72
79
|
file_name = file.replace("\\", "/")
|
|
73
80
|
if file_name not in tr.details:
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/interrogateparser.py
RENAMED
|
@@ -45,14 +45,14 @@ class InterrogateParser(AbstractParser):
|
|
|
45
45
|
def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
|
|
46
46
|
score = tr.metrics.get("doc_coverage", 0)
|
|
47
47
|
uncovered = sorted(
|
|
48
|
-
[(f, d) for f, d in tr.details.items() if d.get("missing", 0) > 0],
|
|
49
|
-
key=lambda x: x[1]
|
|
48
|
+
[(f, d) for f, d in tr.details.items() if isinstance(d, dict) and d.get("missing", 0) > 0],
|
|
49
|
+
key=lambda x: x[1].get("coverage", 0.0),
|
|
50
50
|
)[:5]
|
|
51
51
|
if not uncovered:
|
|
52
52
|
return f"**doc_coverage** score: {score:.3f}"
|
|
53
53
|
lines = [f"**doc coverage** {score:.1%} — files with most missing docstrings:"]
|
|
54
54
|
for path, data in uncovered:
|
|
55
|
-
miss = data
|
|
56
|
-
pct = data
|
|
55
|
+
miss = data.get("missing", 0)
|
|
56
|
+
pct = data.get("coverage", 0.0)
|
|
57
57
|
lines.append(f"- `{path}`: {pct:.0%} ({miss} undocumented)")
|
|
58
58
|
return "\n".join(lines)
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/maintainabilityparser.py
RENAMED
|
@@ -41,7 +41,14 @@ class MaintainabilityParser(AbstractParser):
|
|
|
41
41
|
* ``details`` - a mapping from each file name (converted to use forward slashes) to a dictionary with keys ``mi``, ``rank``, and optionally ``error``.
|
|
42
42
|
* ``details['return_code']`` - the tool's exit code."""
|
|
43
43
|
tr = ToolResult(raw=raw_result)
|
|
44
|
-
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(raw_result.stdout)
|
|
46
|
+
except (json.JSONDecodeError, ValueError):
|
|
47
|
+
tr.metrics["maintainability"] = 0.0
|
|
48
|
+
return tr
|
|
49
|
+
if not isinstance(data, dict):
|
|
50
|
+
tr.metrics["maintainability"] = 0.0
|
|
51
|
+
return tr
|
|
45
52
|
num_items = 0
|
|
46
53
|
score = 0
|
|
47
54
|
for file, values in data.items():
|
|
@@ -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```"
|
|
@@ -50,7 +50,11 @@ class RuffParser(AbstractParser):
|
|
|
50
50
|
if not tr.details:
|
|
51
51
|
return "ruff reported issues (no details available)"
|
|
52
52
|
file, issues = next(iter(tr.details.items()))
|
|
53
|
+
if not isinstance(issues, list) or not issues:
|
|
54
|
+
return "ruff reported issues (no details available)"
|
|
53
55
|
issue = issues[0]
|
|
56
|
+
if not isinstance(issue, dict):
|
|
57
|
+
return "ruff reported issues (no details available)"
|
|
54
58
|
line = issue.get("line", "?")
|
|
55
59
|
code = issue.get("code", "")
|
|
56
60
|
message = issue.get("message", "")
|
|
@@ -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."""
|
|
@@ -54,8 +64,20 @@ class TyParser(AbstractParser):
|
|
|
54
64
|
if not tr.details:
|
|
55
65
|
return "ty reported issues (no details available)"
|
|
56
66
|
file, issues = next(iter(tr.details.items()))
|
|
67
|
+
if not isinstance(issues, list) or not issues:
|
|
68
|
+
return "ty reported issues (no details available)"
|
|
57
69
|
issue = issues[0]
|
|
70
|
+
if not isinstance(issue, dict):
|
|
71
|
+
return "ty reported issues (no details available)"
|
|
58
72
|
line = issue.get("line", "?")
|
|
59
73
|
code = issue.get("code", "")
|
|
60
74
|
message = issue.get("message", "")
|
|
61
|
-
|
|
75
|
+
src_ctx = format_source_context(file, line, count=context_lines)
|
|
76
|
+
callee = ""
|
|
77
|
+
if code in _CALL_CODES and isinstance(line, int):
|
|
78
|
+
from py_cq.parsers.common import extract_callee_name, format_callee_context, read_source_lines
|
|
79
|
+
src_line = read_source_lines(file, line, count=1)
|
|
80
|
+
func_name = extract_callee_name(src_line)
|
|
81
|
+
if func_name:
|
|
82
|
+
callee = format_callee_context(func_name, file)
|
|
83
|
+
return f"`{file}:{line}` — **{code}**: {message}{src_ctx}{callee}"
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/vultureparser.py
RENAMED
|
@@ -40,7 +40,11 @@ class VultureParser(AbstractParser):
|
|
|
40
40
|
if not tr.details:
|
|
41
41
|
return "vulture reported issues (no details available)"
|
|
42
42
|
file, issues = next(iter(tr.details.items()))
|
|
43
|
+
if not isinstance(issues, list) or not issues:
|
|
44
|
+
return "vulture reported issues (no details available)"
|
|
43
45
|
issue = issues[0]
|
|
46
|
+
if not isinstance(issue, dict):
|
|
47
|
+
return "vulture reported issues (no details available)"
|
|
44
48
|
line = issue.get("line", "?")
|
|
45
49
|
kind = issue.get("type", "unused")
|
|
46
50
|
name = issue.get("name", "")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Rich table formatter for combined tool results."""
|
|
2
|
+
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
|
|
5
|
+
from py_cq.localtypes import CombinedToolResults, ToolConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_as_table(data: CombinedToolResults, registry: dict[str, ToolConfig]) -> Table:
|
|
9
|
+
"""Format combined tool results into a Rich Table."""
|
|
10
|
+
table = Table(width=80)
|
|
11
|
+
table.add_column("Tool", justify="left", no_wrap=True)
|
|
12
|
+
table.add_column("Time", justify="right", style="dim")
|
|
13
|
+
table.add_column("Metric", justify="right", style="cyan", no_wrap=True)
|
|
14
|
+
table.add_column("Score", style="magenta")
|
|
15
|
+
table.add_column("Status")
|
|
16
|
+
for tr in data.tool_results:
|
|
17
|
+
tool_name = tr.raw.tool_name
|
|
18
|
+
config = next((t for t in registry.values() if t.name == tool_name))
|
|
19
|
+
for i, (name, value) in enumerate(tr.metrics.items()):
|
|
20
|
+
if value < config.error_threshold:
|
|
21
|
+
status = "[bold red]Error[/]"
|
|
22
|
+
elif value < config.warning_threshold:
|
|
23
|
+
status = "[yellow]Warning[/]"
|
|
24
|
+
else:
|
|
25
|
+
status = "[green]OK[/]"
|
|
26
|
+
time_str = f"{tr.duration_s:.2f}s" if i == 0 else ""
|
|
27
|
+
table.add_row(tool_name, time_str, name, f"{value:0.3f}", status)
|
|
28
|
+
table.add_row("", "", "[bold]Score[/]", f"[bold]{data.score:0.3f}[/]", "")
|
|
29
|
+
return table
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/exitcodeparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/linecountparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.14 → python_code_quality-0.1.16}/src/py_cq/parsers/regexcountparser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|