python-code-quality 0.1.16__py3-none-any.whl → 0.2.2__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.
@@ -11,11 +11,149 @@ The concise output format is one violation per line::
11
11
  followed by a summary line ``Found N error.`` or ``All checks passed!``."""
12
12
 
13
13
  import re
14
+ from collections.abc import Callable
15
+ from pathlib import Path
14
16
 
15
17
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
16
- from py_cq.parsers.common import format_source_context, score_logistic_variant
18
+ from py_cq.parsers.common import (
19
+ enclosing_function_range,
20
+ extract_first_issue,
21
+ find_enclosing_function,
22
+ format_issue_header,
23
+ format_source_context,
24
+ score_logistic_variant,
25
+ )
17
26
 
18
- _DIAG_RE = re.compile(r"^(.+):(\d+):(\d+): ([A-Z]\d+) (.+)$")
27
+ _DIAG_RE = re.compile(r"^(.+):(\d+):(\d+): ([A-Z]{1,5}\d+) (.+)$")
28
+ _VARNAME_RE = re.compile(r"[`'](\w+)[`']")
29
+
30
+
31
+ def _format_F841(file: str, line: int, message: str) -> str:
32
+ """Unused variable: show source context, then report same-function references or advise deletion."""
33
+ base = format_issue_header(file, line, "F841", message) + format_source_context(
34
+ file, line
35
+ )
36
+ m = _VARNAME_RE.search(message)
37
+ if not m:
38
+ return base
39
+ var = m.group(1)
40
+ try:
41
+ lines = Path(file).read_text(encoding="utf-8", errors="replace").splitlines()
42
+ except OSError:
43
+ return base
44
+ func_range = enclosing_function_range(file, line)
45
+ var_re = re.compile(rf"\b{re.escape(var)}\b")
46
+ refs = [
47
+ i + 1
48
+ for i, ln in enumerate(lines)
49
+ if i + 1 != line
50
+ and var_re.search(ln)
51
+ and (func_range is None or func_range[0] <= i + 1 <= func_range[1])
52
+ ]
53
+ if refs and len(var) > 1:
54
+ ref_str = ", ".join(str(r) for r in refs[:8])
55
+ return (
56
+ base
57
+ + f"\n\n`{var}` is also referenced at line(s): {ref_str}. Determine whether this assignment should feed into those uses or is redundant."
58
+ )
59
+ return (
60
+ base
61
+ + f"\n\n`{var}` is not referenced anywhere else in this function. Delete line {line}."
62
+ )
63
+
64
+
65
+ def _format_F541(file: str, line: int, message: str) -> str:
66
+ """Format F541 error message."""
67
+ base = format_issue_header(file, line, "F541", message) + format_source_context(
68
+ file, line
69
+ )
70
+ return base + "\n\nFix: remove the `f` prefix from this string literal."
71
+
72
+
73
+ def _file_refs(file: str, name: str, exclude_line: int) -> list[tuple[int, str]]:
74
+ """Return (line_no, stripped_text) for every line in file containing `name` as a word."""
75
+ try:
76
+ lines = Path(file).read_text(encoding="utf-8", errors="replace").splitlines()
77
+ except OSError:
78
+ return []
79
+ name_re = re.compile(rf"\b{re.escape(name)}\b")
80
+ return [
81
+ (i + 1, ln.strip())
82
+ for i, ln in enumerate(lines)
83
+ if i + 1 != exclude_line and name_re.search(ln)
84
+ ]
85
+
86
+
87
+ def _format_F401(file: str, line: int, message: str) -> str:
88
+ """Unused import: warn if name appears elsewhere (string annotations, __all__), else confirm safe delete."""
89
+ autofixable = "[*]" in message
90
+ clean_message = re.sub(r"^\[\*\]\s*", "", message)
91
+ base = format_issue_header(
92
+ file, line, "F401", clean_message
93
+ ) + format_source_context(file, line)
94
+ m = re.search(r"`([^`]+)`", clean_message)
95
+ if not m:
96
+ return base
97
+ # "typing.Optional" -> bound name is "Optional"; "os" -> "os"
98
+ name = m.group(1).split(".")[-1]
99
+ refs = _file_refs(file, name, exclude_line=line)
100
+ if refs:
101
+ ref_lines = "\n".join(f" line {line_no}: {txt}" for line_no, txt in refs[:6])
102
+ return (
103
+ base
104
+ + f"\n\n`{name}` appears elsewhere in this file - verify these are not active uses before deleting:\n{ref_lines}"
105
+ )
106
+ suffix = " Auto-fixable: `ruff check --fix`." if autofixable else ""
107
+ return base + f"\n\nNo other uses found. Delete this import.{suffix}"
108
+
109
+
110
+ def _format_F821(file: str, line: int, message: str) -> str:
111
+ """Undefined name: show enclosing function and all in-file references to help diagnose the gap."""
112
+ base = format_issue_header(file, line, "F821", message) + format_source_context(
113
+ file, line
114
+ )
115
+ m = re.search(r"`(\w+)`", message)
116
+ if not m:
117
+ return base
118
+ name = m.group(1)
119
+ refs = _file_refs(file, name, exclude_line=line)
120
+ enclosing = find_enclosing_function(file, line)
121
+ if refs:
122
+ ref_lines = "\n".join(f" line {line_no}: {txt}" for line_no, txt in refs[:8])
123
+ base += f"\n\nOther references to `{name}` in this file:\n{ref_lines}"
124
+ else:
125
+ base += f"\n\n`{name}` is not imported or defined anywhere else in this file."
126
+ if enclosing:
127
+ base += f"\n\nEnclosing function:{enclosing}"
128
+ return base
129
+
130
+
131
+ def _format_E701(file: str, line: int, message: str) -> str:
132
+ base = format_issue_header(file, line, "E701", message) + format_source_context(
133
+ file, line, context=2, count=4
134
+ )
135
+ return base + "\n\nFix: split the second statement onto its own line."
136
+
137
+
138
+ def _format_E721(file: str, line: int, message: str) -> str:
139
+ base = format_issue_header(file, line, "E721", message) + format_source_context(
140
+ file, line, context=2, count=4
141
+ )
142
+ return base + (
143
+ "\n\nFix: if comparing a type-holding variable against a type literal, use `is` "
144
+ "(e.g. `output_type is str`). If checking the type of a value, use `isinstance()` "
145
+ "(e.g. `isinstance(x, str)`)."
146
+ )
147
+
148
+
149
+ _CUSTOM_FORMAT: dict[str, Callable[[str, int, str], str]] = {
150
+ "E701": _format_E701,
151
+ "E721": _format_E721,
152
+ "F401": _format_F401,
153
+ "F541": _format_F541,
154
+ "F821": _format_F821,
155
+ "F841": _format_F841,
156
+ }
19
157
 
20
158
 
21
159
  class RuffParser(AbstractParser):
@@ -35,27 +173,33 @@ class RuffParser(AbstractParser):
35
173
  m = _DIAG_RE.match(line)
36
174
  if m:
37
175
  path = m.group(1).replace("\\", "/")
38
- files.setdefault(path, []).append({
39
- "line": int(m.group(2)),
40
- "code": m.group(4),
41
- "message": m.group(5),
42
- })
176
+ files.setdefault(path, []).append(
177
+ {
178
+ "line": int(m.group(2)),
179
+ "col": int(m.group(3)),
180
+ "code": m.group(4),
181
+ "message": m.group(5),
182
+ }
183
+ )
43
184
  score = score_logistic_variant(
44
185
  sum(len(v) for v in files.values()), scale_factor=20
45
186
  )
46
187
  return ToolResult(raw=raw_result, metrics={"lint": score}, details=files)
47
188
 
48
- def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
189
+ def format_llm_message(
190
+ self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
191
+ ) -> str:
49
192
  """Return the first lint violation as a defect description."""
50
- if not tr.details:
51
- return "ruff reported issues (no details available)"
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)"
55
- issue = issues[0]
56
- if not isinstance(issue, dict):
193
+ result = extract_first_issue(tr.details)
194
+ if result is None:
57
195
  return "ruff reported issues (no details available)"
196
+ file, issue = result
58
197
  line = issue.get("line", "?")
59
198
  code = issue.get("code", "")
60
199
  message = issue.get("message", "")
61
- return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line, count=context_lines)}"
200
+ fmt_fn = _CUSTOM_FORMAT.get(code)
201
+ if fmt_fn and isinstance(line, int):
202
+ return fmt_fn(file, line, message)
203
+ return format_issue_header(file, line, code, message) + format_source_context(
204
+ file, line, count=context_lines
205
+ )
py_cq/parsers/typarser.py CHANGED
@@ -12,72 +12,204 @@ followed by a summary line ``Found N diagnostic`` or ``All checks passed!``.
12
12
  Errors count more heavily than warnings toward the score."""
13
13
 
14
14
  import re
15
+ from collections.abc import Callable
15
16
 
16
17
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
17
- from py_cq.parsers.common import format_source_context, score_logistic_variant
18
+ from py_cq.parsers.common import (
19
+ extract_first_issue,
20
+ find_enclosing_function,
21
+ format_issue_header,
22
+ format_source_context,
23
+ resolve_path,
24
+ score_logistic_variant,
25
+ )
18
26
 
19
27
  _DIAG_RE = re.compile(r"^(.+):(\d+):\d+:\s+(error|warning)\[([^\]]+)\] (.+)$")
28
+ _EXPECTED_FOUND_RE = re.compile(r"Expected `([^`]+)`, found `([^`]+)`")
20
29
 
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
+
31
+ def _format_invalid_argument_type(file: str, line: int, message: str) -> str:
32
+ """Format a ty 'invalid-argument-type' diagnostic for LLM consumption.
33
+
34
+ Includes hint text when ty reports ``Unknown`` as the found type.
35
+
36
+ Args:
37
+ file: Resolved file path.
38
+ line: Line number of the diagnostic.
39
+ message: The diagnostic message from ty.
40
+
41
+ Returns:
42
+ Markdown-formatted issue report with fix hints.
43
+ """
44
+ display_msg = re.sub(r": (Expected `)", r":\n\1", message)
45
+ base = format_issue_header(
46
+ file, line, "invalid-argument-type", display_msg
47
+ ) + format_source_context(file, line)
48
+ m = _EXPECTED_FOUND_RE.search(message)
49
+ if not m or "Unknown" not in m.group(2):
50
+ return base
51
+ enclosing = find_enclosing_function(file, line)
52
+ note = "\n\n`Unknown` means ty cannot infer this argument's type - the variable has no annotation."
53
+ if enclosing:
54
+ note += (
55
+ f" Trace the argument back to its source and annotate its type:{enclosing}"
56
+ )
57
+ else:
58
+ note += " Annotate the variable's type or cast the argument explicitly."
59
+ return base + note
60
+
61
+
62
+ _TYPE_NAME_RE = re.compile(r"Object of type `([^`]+)` is not callable")
63
+ _MODULE_RE = re.compile(r"Submodule `([^`]+)`")
64
+ _IMPORT_MODULE_RE = re.compile(r"Cannot resolve imported module `([^`]+)`")
65
+
66
+
67
+ def _format_call_non_callable(file: str, line: int, message: str) -> str:
68
+ """Format a ty 'call-non-callable' diagnostic for LLM consumption.
69
+
70
+ Provides fix advice about incomplete type stubs and Callable annotations.
71
+
72
+ Args:
73
+ file: Resolved file path.
74
+ line: Line number of the diagnostic.
75
+ message: The diagnostic message from ty.
76
+
77
+ Returns:
78
+ Markdown-formatted issue report with fix hints.
79
+ """
80
+ base = format_issue_header(
81
+ file, line, "call-non-callable", message
82
+ ) + format_source_context(file, line, context=3, count=8)
83
+ m = _TYPE_NAME_RE.search(message)
84
+ type_name = f"`{m.group(1)}`" if m else "this type"
85
+ return base + (
86
+ f"\n\nty cannot see a `__call__` declaration on {type_name} - common when a library's "
87
+ f"type stubs are incomplete. Fix: annotate the variable as `Callable[..., <ReturnType>]`, "
88
+ f"or suppress with `# type: ignore[call-non-callable]` if the call is correct at runtime."
89
+ )
90
+
91
+
92
+ def _format_possibly_missing_submodule(file: str, line: int, message: str) -> str:
93
+ """Format a ty 'possibly-missing-submodule' diagnostic for LLM consumption.
94
+
95
+ Advises adding an explicit import for the submodule before attribute access.
96
+
97
+ Args:
98
+ file: Resolved file path.
99
+ line: Line number of the diagnostic.
100
+ message: The diagnostic message from ty.
101
+
102
+ Returns:
103
+ Markdown-formatted issue report with fix hints.
104
+ """
105
+ base = format_issue_header(
106
+ file, line, "possibly-missing-submodule", message
107
+ ) + format_source_context(file, line, context=1, count=3)
108
+ m = _MODULE_RE.search(message)
109
+ submodule = m.group(1) if m else "the submodule"
110
+ return (
111
+ base
112
+ + f"\n\nFix: add `import <package>.{submodule}` before accessing it as an attribute."
113
+ )
114
+
115
+
116
+ def _format_unresolved_import(file: str, line: int, message: str) -> str:
117
+ """Format a ty 'unresolved-import' diagnostic for LLM consumption.
118
+
119
+ Advises checking whether the module was renamed, deleted, or needs installation.
120
+
121
+ Args:
122
+ file: Resolved file path.
123
+ line: Line number of the diagnostic.
124
+ message: The diagnostic message from ty.
125
+
126
+ Returns:
127
+ Markdown-formatted issue report with fix hints.
128
+ """
129
+ base = format_issue_header(
130
+ file, line, "unresolved-import", message
131
+ ) + format_source_context(file, line, context=1, count=3)
132
+ m = _IMPORT_MODULE_RE.search(message)
133
+ module = f"`{m.group(1)}`" if m else "the module"
134
+ return base + (
135
+ f"\n\n{module} cannot be found. Check whether it has been renamed or deleted. "
136
+ f"If it is no longer needed, remove the import. "
137
+ f"If the module exists but is not installed, add it to your dependencies."
138
+ )
139
+
140
+
141
+ _CUSTOM_FORMAT: dict[str, Callable[[str, int, str], str]] = {
142
+ "call-non-callable": _format_call_non_callable,
143
+ "invalid-argument-type": _format_invalid_argument_type,
144
+ "possibly-missing-submodule": _format_possibly_missing_submodule,
145
+ "unresolved-import": _format_unresolved_import,
146
+ }
30
147
 
31
148
 
32
149
  class TyParser(AbstractParser):
33
150
  """Parses raw output from ``ty check`` into a structured ToolResult."""
34
151
 
35
152
  def parse(self, raw_result: RawResult) -> ToolResult:
36
- """Parse concise ty output and return a ToolResult.
37
-
38
- Args:
39
- raw_result: Raw output from ``ty check --output-format concise``.
40
-
41
- Returns:
42
- ToolResult with a ``type_check`` metric in [0, 1] and per-file diagnostics in details.
43
- """
44
153
  files: dict[str, list] = {}
154
+ seen: set[tuple[str, int, str]] = set()
45
155
  weighted = 0
46
156
  for line in (raw_result.stdout or "").splitlines():
47
157
  m = _DIAG_RE.match(line)
48
158
  if m:
49
159
  path = m.group(1).replace("\\", "/")
160
+ lineno = int(m.group(2))
161
+ code = m.group(4)
50
162
  severity = m.group(3)
51
- files.setdefault(path, []).append({
52
- "line": int(m.group(2)),
53
- "code": m.group(4),
54
- "severity": severity,
55
- "message": m.group(5),
56
- })
163
+ key = (path, lineno, code)
164
+ if key in seen:
165
+ continue
166
+ seen.add(key)
167
+ files.setdefault(path, []).append(
168
+ {
169
+ "line": lineno,
170
+ "code": code,
171
+ "severity": severity,
172
+ "message": m.group(5),
173
+ }
174
+ )
57
175
  weighted += 3 if severity == "error" else 1
58
176
 
59
177
  score = score_logistic_variant(weighted, scale_factor=10)
60
- return ToolResult(raw=raw_result, metrics={"type_check": score}, details=files)
178
+ return ToolResult(
179
+ raw=raw_result,
180
+ metrics={"type_check": score},
181
+ details=files,
182
+ project_path=raw_result.project_path,
183
+ )
61
184
 
62
- def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
63
- """Return the first type-check diagnostic as a defect description."""
64
- if not tr.details:
65
- return "ty reported issues (no details available)"
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)"
69
- issue = issues[0]
70
- if not isinstance(issue, dict):
185
+ def format_llm_message(
186
+ self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
187
+ ) -> str:
188
+ """Return a markdown description of the most important ty defect.
189
+
190
+ Delegates to a custom formatter when the code has a registered handler
191
+ in ``_CUSTOM_FORMAT`` (e.g. ``call-non-callable``, ``invalid-argument-type``),
192
+ otherwise falls back to the standard header + source context format.
193
+
194
+ Args:
195
+ tr: The parsed tool result containing details and raw output.
196
+ context_lines: Number of source context lines to show.
197
+ limit: Maximum number of issues to display (unused - always 1).
198
+
199
+ Returns:
200
+ Markdown-formatted issue description.
201
+ """
202
+ result = extract_first_issue(tr.details)
203
+ if result is None:
71
204
  return "ty reported issues (no details available)"
205
+ file, issue = result
72
206
  line = issue.get("line", "?")
73
207
  code = issue.get("code", "")
74
208
  message = issue.get("message", "")
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}"
209
+ resolved_file = resolve_path(tr.project_path, file)
210
+ fmt_fn = _CUSTOM_FORMAT.get(code)
211
+ if fmt_fn and isinstance(line, int):
212
+ return fmt_fn(resolved_file, line, message)
213
+ return format_issue_header(
214
+ resolved_file, line, code, message
215
+ ) + format_source_context(resolved_file, line, count=context_lines)
@@ -12,7 +12,11 @@ score stored under the ``dead_code`` metric key.
12
12
  import re
13
13
 
14
14
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
15
- from py_cq.parsers.common import format_source_context, score_logistic_variant
15
+ from py_cq.parsers.common import (
16
+ extract_first_issue,
17
+ format_source_context,
18
+ score_logistic_variant,
19
+ )
16
20
 
17
21
  _LINE_RE = re.compile(r"^(.+):(\d+): (unused \S+) '(.+)' \((\d+)% confidence\)$")
18
22
 
@@ -21,32 +25,34 @@ class VultureParser(AbstractParser):
21
25
  """Parses raw text output from ``vulture`` into a ToolResult."""
22
26
 
23
27
  def parse(self, raw_result: RawResult) -> ToolResult:
28
+ """Parses the raw text output from vulture."""
24
29
  files: dict[str, list] = {}
25
30
  for line in (raw_result.stdout or "").splitlines():
26
31
  m = _LINE_RE.match(line)
27
32
  if m:
28
33
  path = m.group(1).replace("\\", "/")
29
- files.setdefault(path, []).append({
30
- "line": int(m.group(2)),
31
- "type": m.group(3),
32
- "name": m.group(4),
33
- "confidence": int(m.group(5)),
34
- })
34
+ files.setdefault(path, []).append(
35
+ {
36
+ "line": int(m.group(2)),
37
+ "type": m.group(3),
38
+ "name": m.group(4),
39
+ "confidence": int(m.group(5)),
40
+ }
41
+ )
35
42
  count = sum(len(v) for v in files.values())
36
43
  score = score_logistic_variant(count, scale_factor=15)
37
44
  return ToolResult(raw=raw_result, metrics={"dead_code": score}, details=files)
38
45
 
39
- def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
40
- if not tr.details:
41
- return "vulture reported issues (no details available)"
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)"
45
- issue = issues[0]
46
- if not isinstance(issue, dict):
46
+ def format_llm_message(
47
+ self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
48
+ ) -> str:
49
+ """Formats the LLM message from the ToolResult."""
50
+ result = extract_first_issue(tr.details)
51
+ if result is None:
47
52
  return "vulture reported issues (no details available)"
53
+ file, issue = result
48
54
  line = issue.get("line", "?")
49
55
  kind = issue.get("type", "unused")
50
56
  name = issue.get("name", "")
51
57
  confidence = issue.get("confidence", "?")
52
- return f"`{file}:{line}` **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line, count=context_lines)}"
58
+ return f"{file}:{line} - **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line, count=context_lines)}"
py_cq/table_formatter.py CHANGED
@@ -1,11 +1,17 @@
1
1
  """Rich table formatter for combined tool results."""
2
2
 
3
3
  from rich.table import Table
4
+ from rich.text import Text
4
5
 
5
6
  from py_cq.localtypes import CombinedToolResults, ToolConfig
6
7
 
7
8
 
8
- def format_as_table(data: CombinedToolResults, registry: dict[str, ToolConfig]) -> Table:
9
+ def format_as_table(
10
+ data: CombinedToolResults,
11
+ registry: dict[str, ToolConfig],
12
+ *,
13
+ total_s: float | None = None,
14
+ ) -> Table:
9
15
  """Format combined tool results into a Rich Table."""
10
16
  table = Table(width=80)
11
17
  table.add_column("Tool", justify="left", no_wrap=True)
@@ -25,5 +31,13 @@ def format_as_table(data: CombinedToolResults, registry: dict[str, ToolConfig])
25
31
  status = "[green]OK[/]"
26
32
  time_str = f"{tr.duration_s:.2f}s" if i == 0 else ""
27
33
  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}[/]", "")
34
+ total_label = (
35
+ Text("Total", style="bold", justify="right")
36
+ if total_s is not None
37
+ else Text("")
38
+ )
39
+ total_time = f"[bold]{total_s:.2f}s[/]" if total_s is not None else ""
40
+ table.add_row(
41
+ total_label, total_time, "[bold]Score[/]", f"[bold]{data.score:0.3f}[/]", ""
42
+ )
29
43
  return table
py_cq/tool_registry.py CHANGED
@@ -1,20 +1,19 @@
1
- """Loads tool configurations from a YAML file and builds a registry that maps tool names to their configuration objects, enabling efficient lookup and instantiation of tools throughout the application. The module relies on PyYAML for parsing the configuration file."""
1
+ """Loads tool configurations from a TOML file and builds a registry that maps tool names to their configuration objects, enabling efficient lookup and instantiation of tools throughout the application."""
2
2
 
3
+ import tomllib
3
4
  from importlib import import_module
4
5
  from importlib.resources import files
5
6
 
6
- import yaml
7
-
8
7
  from py_cq.localtypes import ToolConfig
9
8
 
10
9
 
11
10
  def load_tool_configs() -> dict[str, ToolConfig]:
12
- """Load tool configurations from the bundled config.yaml and return a registry.
11
+ """Load tool configurations from the bundled config.toml and return a registry.
13
12
 
14
13
  Returns:
15
14
  dict[str, ToolConfig]: A mapping from tool ID to its configuration instance."""
16
- yaml_text = files("py_cq.config").joinpath("config.yaml").read_text(encoding="utf-8")
17
- config = yaml.safe_load(yaml_text)
15
+ toml_bytes = files("py_cq.config").joinpath("config.toml").read_bytes()
16
+ config = tomllib.loads(toml_bytes.decode())
18
17
  registry = {}
19
18
  for tool_id, tool_data in config["python"].items():
20
19
  # Dynamically import parser class
@@ -31,6 +30,8 @@ def load_tool_configs() -> dict[str, ToolConfig]:
31
30
  extra_deps=tool_data.get("extra_deps", []),
32
31
  parser_config=tool_data.get("parser_config", {}),
33
32
  exclude_format=tool_data.get("exclude_format", ""),
33
+ scan_exclude_names=tool_data.get("scan_exclude_names", []),
34
+ skip_for_file=tool_data.get("skip_for_file", False),
34
35
  )
35
36
  return registry
36
37