lintro 0.3.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.
Potentially problematic release.
This version of lintro might be problematic. Click here for more details.
- lintro/__init__.py +3 -0
- lintro/__main__.py +6 -0
- lintro/ascii-art/fail.txt +404 -0
- lintro/ascii-art/success.txt +484 -0
- lintro/cli.py +70 -0
- lintro/cli_utils/__init__.py +7 -0
- lintro/cli_utils/commands/__init__.py +7 -0
- lintro/cli_utils/commands/check.py +210 -0
- lintro/cli_utils/commands/format.py +167 -0
- lintro/cli_utils/commands/list_tools.py +114 -0
- lintro/enums/__init__.py +0 -0
- lintro/enums/action.py +29 -0
- lintro/enums/darglint_strictness.py +22 -0
- lintro/enums/group_by.py +31 -0
- lintro/enums/hadolint_enums.py +46 -0
- lintro/enums/output_format.py +40 -0
- lintro/enums/tool_name.py +36 -0
- lintro/enums/tool_type.py +27 -0
- lintro/enums/yamllint_format.py +22 -0
- lintro/exceptions/__init__.py +0 -0
- lintro/exceptions/errors.py +15 -0
- lintro/formatters/__init__.py +0 -0
- lintro/formatters/core/__init__.py +0 -0
- lintro/formatters/core/output_style.py +21 -0
- lintro/formatters/core/table_descriptor.py +24 -0
- lintro/formatters/styles/__init__.py +17 -0
- lintro/formatters/styles/csv.py +41 -0
- lintro/formatters/styles/grid.py +91 -0
- lintro/formatters/styles/html.py +48 -0
- lintro/formatters/styles/json.py +61 -0
- lintro/formatters/styles/markdown.py +41 -0
- lintro/formatters/styles/plain.py +39 -0
- lintro/formatters/tools/__init__.py +35 -0
- lintro/formatters/tools/darglint_formatter.py +72 -0
- lintro/formatters/tools/hadolint_formatter.py +84 -0
- lintro/formatters/tools/prettier_formatter.py +76 -0
- lintro/formatters/tools/ruff_formatter.py +116 -0
- lintro/formatters/tools/yamllint_formatter.py +87 -0
- lintro/models/__init__.py +0 -0
- lintro/models/core/__init__.py +0 -0
- lintro/models/core/tool.py +104 -0
- lintro/models/core/tool_config.py +23 -0
- lintro/models/core/tool_result.py +39 -0
- lintro/parsers/__init__.py +0 -0
- lintro/parsers/darglint/__init__.py +0 -0
- lintro/parsers/darglint/darglint_issue.py +9 -0
- lintro/parsers/darglint/darglint_parser.py +62 -0
- lintro/parsers/hadolint/__init__.py +1 -0
- lintro/parsers/hadolint/hadolint_issue.py +24 -0
- lintro/parsers/hadolint/hadolint_parser.py +65 -0
- lintro/parsers/prettier/__init__.py +0 -0
- lintro/parsers/prettier/prettier_issue.py +10 -0
- lintro/parsers/prettier/prettier_parser.py +60 -0
- lintro/parsers/ruff/__init__.py +1 -0
- lintro/parsers/ruff/ruff_issue.py +43 -0
- lintro/parsers/ruff/ruff_parser.py +89 -0
- lintro/parsers/yamllint/__init__.py +0 -0
- lintro/parsers/yamllint/yamllint_issue.py +24 -0
- lintro/parsers/yamllint/yamllint_parser.py +68 -0
- lintro/tools/__init__.py +40 -0
- lintro/tools/core/__init__.py +0 -0
- lintro/tools/core/tool_base.py +320 -0
- lintro/tools/core/tool_manager.py +167 -0
- lintro/tools/implementations/__init__.py +0 -0
- lintro/tools/implementations/tool_darglint.py +245 -0
- lintro/tools/implementations/tool_hadolint.py +302 -0
- lintro/tools/implementations/tool_prettier.py +270 -0
- lintro/tools/implementations/tool_ruff.py +618 -0
- lintro/tools/implementations/tool_yamllint.py +240 -0
- lintro/tools/tool_enum.py +17 -0
- lintro/utils/__init__.py +0 -0
- lintro/utils/ascii_normalize_cli.py +84 -0
- lintro/utils/config.py +39 -0
- lintro/utils/console_logger.py +783 -0
- lintro/utils/formatting.py +173 -0
- lintro/utils/output_manager.py +301 -0
- lintro/utils/path_utils.py +41 -0
- lintro/utils/tool_executor.py +443 -0
- lintro/utils/tool_utils.py +431 -0
- lintro-0.3.2.dist-info/METADATA +338 -0
- lintro-0.3.2.dist-info/RECORD +85 -0
- lintro-0.3.2.dist-info/WHEEL +5 -0
- lintro-0.3.2.dist-info/entry_points.txt +2 -0
- lintro-0.3.2.dist-info/licenses/LICENSE +21 -0
- lintro-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Yamllint YAML linter integration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from lintro.enums.tool_type import ToolType
|
|
10
|
+
from lintro.enums.yamllint_format import (
|
|
11
|
+
YamllintFormat,
|
|
12
|
+
normalize_yamllint_format,
|
|
13
|
+
)
|
|
14
|
+
from lintro.models.core.tool import ToolConfig, ToolResult
|
|
15
|
+
from lintro.parsers.yamllint.yamllint_parser import parse_yamllint_output
|
|
16
|
+
from lintro.tools.core.tool_base import BaseTool
|
|
17
|
+
from lintro.utils.tool_utils import walk_files_with_excludes
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
YAMLLINT_DEFAULT_TIMEOUT: int = 15
|
|
21
|
+
YAMLLINT_DEFAULT_PRIORITY: int = 40
|
|
22
|
+
YAMLLINT_FILE_PATTERNS: list[str] = [
|
|
23
|
+
"*.yml",
|
|
24
|
+
"*.yaml",
|
|
25
|
+
".yamllint",
|
|
26
|
+
".yamllint.yml",
|
|
27
|
+
".yamllint.yaml",
|
|
28
|
+
]
|
|
29
|
+
YAMLLINT_FORMATS: tuple[str, ...] = tuple(m.name.lower() for m in YamllintFormat)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class YamllintTool(BaseTool):
|
|
34
|
+
"""Yamllint YAML linter integration.
|
|
35
|
+
|
|
36
|
+
Yamllint is a linter for YAML files that checks for syntax errors,
|
|
37
|
+
formatting issues, and other YAML best practices.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
name: Tool name
|
|
41
|
+
description: Tool description
|
|
42
|
+
can_fix: Whether the tool can fix issues
|
|
43
|
+
config: Tool configuration
|
|
44
|
+
exclude_patterns: List of patterns to exclude
|
|
45
|
+
include_venv: Whether to include virtual environment files
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
name: str = "yamllint"
|
|
49
|
+
description: str = "YAML linter for syntax and style checking"
|
|
50
|
+
can_fix: bool = False
|
|
51
|
+
config: ToolConfig = field(
|
|
52
|
+
default_factory=lambda: ToolConfig(
|
|
53
|
+
priority=YAMLLINT_DEFAULT_PRIORITY,
|
|
54
|
+
conflicts_with=[],
|
|
55
|
+
file_patterns=YAMLLINT_FILE_PATTERNS,
|
|
56
|
+
tool_type=ToolType.LINTER,
|
|
57
|
+
options={
|
|
58
|
+
"timeout": YAMLLINT_DEFAULT_TIMEOUT,
|
|
59
|
+
# Use parsable by default; aligns with parser expectations
|
|
60
|
+
"format": "parsable",
|
|
61
|
+
"config_file": None,
|
|
62
|
+
"config_data": None,
|
|
63
|
+
"strict": False,
|
|
64
|
+
"relaxed": False,
|
|
65
|
+
"no_warnings": False,
|
|
66
|
+
},
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def __post_init__(self) -> None:
|
|
71
|
+
"""Initialize the tool."""
|
|
72
|
+
super().__post_init__()
|
|
73
|
+
|
|
74
|
+
def set_options(
|
|
75
|
+
self,
|
|
76
|
+
format: str | YamllintFormat | None = None,
|
|
77
|
+
config_file: str | None = None,
|
|
78
|
+
config_data: str | None = None,
|
|
79
|
+
strict: bool | None = None,
|
|
80
|
+
relaxed: bool | None = None,
|
|
81
|
+
no_warnings: bool | None = None,
|
|
82
|
+
**kwargs,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Set Yamllint-specific options.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
format: Output format (parsable, standard, colored, github, auto)
|
|
88
|
+
config_file: Path to yamllint config file
|
|
89
|
+
config_data: Inline config data (YAML string)
|
|
90
|
+
strict: Return non-zero exit code on warnings as well as errors
|
|
91
|
+
relaxed: Use relaxed configuration
|
|
92
|
+
no_warnings: Output only error level problems
|
|
93
|
+
**kwargs: Other tool options
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: If an option value is invalid
|
|
97
|
+
"""
|
|
98
|
+
if format is not None:
|
|
99
|
+
# Accept both enum and string values for backward compatibility
|
|
100
|
+
fmt_enum = normalize_yamllint_format(format) # type: ignore[arg-type]
|
|
101
|
+
format = fmt_enum.name.lower()
|
|
102
|
+
if config_file is not None and not isinstance(config_file, str):
|
|
103
|
+
raise ValueError("config_file must be a string path")
|
|
104
|
+
if config_data is not None and not isinstance(config_data, str):
|
|
105
|
+
raise ValueError("config_data must be a YAML string")
|
|
106
|
+
if strict is not None and not isinstance(strict, bool):
|
|
107
|
+
raise ValueError("strict must be a boolean")
|
|
108
|
+
if relaxed is not None and not isinstance(relaxed, bool):
|
|
109
|
+
raise ValueError("relaxed must be a boolean")
|
|
110
|
+
if no_warnings is not None and not isinstance(no_warnings, bool):
|
|
111
|
+
raise ValueError("no_warnings must be a boolean")
|
|
112
|
+
options = {
|
|
113
|
+
"format": format,
|
|
114
|
+
"config_file": config_file,
|
|
115
|
+
"config_data": config_data,
|
|
116
|
+
"strict": strict,
|
|
117
|
+
"relaxed": relaxed,
|
|
118
|
+
"no_warnings": no_warnings,
|
|
119
|
+
}
|
|
120
|
+
options = {k: v for k, v in options.items() if v is not None}
|
|
121
|
+
super().set_options(**options, **kwargs)
|
|
122
|
+
|
|
123
|
+
def _build_command(self) -> list[str]:
|
|
124
|
+
"""Build the yamllint command.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
list[str]: Command arguments for yamllint.
|
|
128
|
+
"""
|
|
129
|
+
cmd: list[str] = ["yamllint"]
|
|
130
|
+
format_option: str = self.options.get("format", YAMLLINT_FORMATS[0])
|
|
131
|
+
cmd.extend(["--format", format_option])
|
|
132
|
+
config_file: str | None = self.options.get("config_file")
|
|
133
|
+
if config_file:
|
|
134
|
+
cmd.extend(["--config-file", config_file])
|
|
135
|
+
config_data: str | None = self.options.get("config_data")
|
|
136
|
+
if config_data:
|
|
137
|
+
cmd.extend(["--config-data", config_data])
|
|
138
|
+
if self.options.get("strict", False):
|
|
139
|
+
cmd.append("--strict")
|
|
140
|
+
if self.options.get("relaxed", False):
|
|
141
|
+
cmd.append("--relaxed")
|
|
142
|
+
if self.options.get("no_warnings", False):
|
|
143
|
+
cmd.append("--no-warnings")
|
|
144
|
+
return cmd
|
|
145
|
+
|
|
146
|
+
def check(
|
|
147
|
+
self,
|
|
148
|
+
paths: list[str],
|
|
149
|
+
) -> ToolResult:
|
|
150
|
+
"""Check files with Yamllint.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
paths: list[str]: List of file or directory paths to check.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
ToolResult: Result of the check operation.
|
|
157
|
+
"""
|
|
158
|
+
self._validate_paths(paths=paths)
|
|
159
|
+
if not paths:
|
|
160
|
+
return ToolResult(
|
|
161
|
+
name=self.name,
|
|
162
|
+
success=True,
|
|
163
|
+
output="No files to check.",
|
|
164
|
+
issues_count=0,
|
|
165
|
+
)
|
|
166
|
+
yaml_files: list[str] = walk_files_with_excludes(
|
|
167
|
+
paths=paths,
|
|
168
|
+
file_patterns=self.config.file_patterns,
|
|
169
|
+
exclude_patterns=self.exclude_patterns,
|
|
170
|
+
include_venv=self.include_venv,
|
|
171
|
+
)
|
|
172
|
+
logger.debug(f"Files to check: {yaml_files}")
|
|
173
|
+
timeout: int = self.options.get("timeout", YAMLLINT_DEFAULT_TIMEOUT)
|
|
174
|
+
# Aggregate parsed issues across files and rely on table renderers upstream
|
|
175
|
+
all_success: bool = True
|
|
176
|
+
all_issues: list = []
|
|
177
|
+
skipped_files: list[str] = []
|
|
178
|
+
total_issues: int = 0
|
|
179
|
+
for file_path in yaml_files:
|
|
180
|
+
# Use absolute path; run with the file's parent as cwd so that
|
|
181
|
+
# yamllint discovers any local .yamllint config beside the file.
|
|
182
|
+
abs_file: str = os.path.abspath(file_path)
|
|
183
|
+
cmd: list[str] = self._build_command() + [abs_file]
|
|
184
|
+
try:
|
|
185
|
+
success, output = self._run_subprocess(
|
|
186
|
+
cmd=cmd,
|
|
187
|
+
timeout=timeout,
|
|
188
|
+
cwd=self.get_cwd(paths=[abs_file]),
|
|
189
|
+
)
|
|
190
|
+
issues = parse_yamllint_output(output=output)
|
|
191
|
+
issues_count: int = len(issues)
|
|
192
|
+
# Yamllint returns 1 on errors/warnings unless --no-warnings/relaxed
|
|
193
|
+
# Use parsed issues to determine success and counts reliably.
|
|
194
|
+
if issues_count > 0:
|
|
195
|
+
all_success = False
|
|
196
|
+
total_issues += issues_count
|
|
197
|
+
if issues:
|
|
198
|
+
all_issues.extend(issues)
|
|
199
|
+
except subprocess.TimeoutExpired:
|
|
200
|
+
skipped_files.append(file_path)
|
|
201
|
+
all_success = False
|
|
202
|
+
except Exception as e:
|
|
203
|
+
# Suppress missing file noise in console output; keep as debug
|
|
204
|
+
err_msg = str(e)
|
|
205
|
+
if "No such file or directory" in err_msg:
|
|
206
|
+
# treat as skipped/missing silently for user; do not fail run
|
|
207
|
+
continue
|
|
208
|
+
# Do not add raw errors to user-facing output; mark failure only
|
|
209
|
+
all_success = False
|
|
210
|
+
# Let the unified formatter render a table from issues; no raw output
|
|
211
|
+
output = None
|
|
212
|
+
return ToolResult(
|
|
213
|
+
name=self.name,
|
|
214
|
+
success=all_success,
|
|
215
|
+
output=output,
|
|
216
|
+
issues_count=total_issues,
|
|
217
|
+
issues=all_issues,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def fix(
|
|
221
|
+
self,
|
|
222
|
+
paths: list[str],
|
|
223
|
+
) -> ToolResult:
|
|
224
|
+
"""Yamllint cannot fix issues, only report them.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
paths: list[str]: List of file or directory paths to fix.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
ToolResult: Result indicating that fixing is not supported.
|
|
231
|
+
"""
|
|
232
|
+
return ToolResult(
|
|
233
|
+
name=self.name,
|
|
234
|
+
success=False,
|
|
235
|
+
output=(
|
|
236
|
+
"Yamllint is a linter only and cannot fix issues. Use a YAML "
|
|
237
|
+
"formatter like Prettier for formatting."
|
|
238
|
+
),
|
|
239
|
+
issues_count=0,
|
|
240
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""ToolEnum for all Lintro tools, mapping to their classes."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from lintro.tools.implementations.tool_darglint import DarglintTool
|
|
6
|
+
from lintro.tools.implementations.tool_hadolint import HadolintTool
|
|
7
|
+
from lintro.tools.implementations.tool_prettier import PrettierTool
|
|
8
|
+
from lintro.tools.implementations.tool_ruff import RuffTool
|
|
9
|
+
from lintro.tools.implementations.tool_yamllint import YamllintTool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ToolEnum(Enum):
|
|
13
|
+
DARGLINT = DarglintTool
|
|
14
|
+
HADOLINT = HadolintTool
|
|
15
|
+
PRETTIER = PrettierTool
|
|
16
|
+
RUFF = RuffTool
|
|
17
|
+
YAMLLINT = YamllintTool
|
lintro/utils/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""CLI to normalize ASCII art files to a standard size.
|
|
2
|
+
|
|
3
|
+
Usage (via uv):
|
|
4
|
+
uv run python -m lintro.utils.ascii_normalize_cli --width 80 --height 20
|
|
5
|
+
|
|
6
|
+
By default processes all .txt files under lintro/ascii-art.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from lintro.utils.formatting import normalize_ascii_file_sections
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _ascii_art_dir() -> Path:
|
|
18
|
+
return Path(__file__).resolve().parents[1] / "ascii-art"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _write_sections(
|
|
22
|
+
file_path: Path,
|
|
23
|
+
sections: list[list[str]],
|
|
24
|
+
) -> None:
|
|
25
|
+
# Join sections with a single blank line between them
|
|
26
|
+
lines: list[str] = []
|
|
27
|
+
for idx, sec in enumerate(sections):
|
|
28
|
+
lines.extend(sec)
|
|
29
|
+
if idx != len(sections) - 1:
|
|
30
|
+
lines.append("")
|
|
31
|
+
file_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> int:
|
|
35
|
+
parser = argparse.ArgumentParser(description="Normalize ASCII art files.")
|
|
36
|
+
parser.add_argument("files", nargs="*", help="Specific ASCII art files to process")
|
|
37
|
+
parser.add_argument("--width", type=int, default=80)
|
|
38
|
+
parser.add_argument("--height", type=int, default=20)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--align", choices=["left", "center", "right"], default="center"
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--valign",
|
|
44
|
+
choices=["top", "middle", "bottom"],
|
|
45
|
+
default="middle",
|
|
46
|
+
)
|
|
47
|
+
args = parser.parse_args()
|
|
48
|
+
|
|
49
|
+
base_dir = _ascii_art_dir()
|
|
50
|
+
if not base_dir.exists():
|
|
51
|
+
print(f"ASCII art directory not found: {base_dir}")
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
targets: list[Path]
|
|
55
|
+
if args.files:
|
|
56
|
+
targets = [base_dir / f for f in args.files]
|
|
57
|
+
else:
|
|
58
|
+
targets = sorted(base_dir.glob("*.txt"))
|
|
59
|
+
|
|
60
|
+
updated = 0
|
|
61
|
+
for fp in targets:
|
|
62
|
+
sections = normalize_ascii_file_sections(
|
|
63
|
+
file_path=fp,
|
|
64
|
+
width=args.width,
|
|
65
|
+
height=args.height,
|
|
66
|
+
align=args.align,
|
|
67
|
+
valign=args.valign,
|
|
68
|
+
)
|
|
69
|
+
if not sections:
|
|
70
|
+
print(f"Skipping (no sections or unreadable): {fp.name}")
|
|
71
|
+
continue
|
|
72
|
+
_write_sections(
|
|
73
|
+
arg1=fp,
|
|
74
|
+
arg2=sections,
|
|
75
|
+
)
|
|
76
|
+
updated += 1
|
|
77
|
+
print(f"Normalized: {fp.name} -> {args.width}x{args.height}")
|
|
78
|
+
|
|
79
|
+
print(f"Done. Updated {updated} file(s).")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__": # pragma: no cover - exercised via CLI in practice
|
|
84
|
+
raise SystemExit(main())
|
lintro/utils/config.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Project configuration helpers for Lintro.
|
|
2
|
+
|
|
3
|
+
Reads configuration from `pyproject.toml` under the `[tool.lintro]` table.
|
|
4
|
+
Allows tool-specific defaults via `[tool.lintro.<tool>]` (e.g., `[tool.lintro.ruff]`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import tomllib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_pyproject() -> dict[str, Any]:
|
|
15
|
+
pyproject_path = Path("pyproject.toml")
|
|
16
|
+
if not pyproject_path.exists():
|
|
17
|
+
return {}
|
|
18
|
+
try:
|
|
19
|
+
with pyproject_path.open("rb") as f:
|
|
20
|
+
data = tomllib.load(f)
|
|
21
|
+
return data.get("tool", {}).get("lintro", {})
|
|
22
|
+
except Exception:
|
|
23
|
+
return {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_lintro_tool_config(tool_name: str) -> dict[str, Any]:
|
|
27
|
+
"""Load tool-specific config from pyproject.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
tool_name: Tool name (e.g., "ruff").
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A dict of options for the given tool, or an empty dict if none.
|
|
34
|
+
"""
|
|
35
|
+
cfg = _load_pyproject()
|
|
36
|
+
section = cfg.get(tool_name, {})
|
|
37
|
+
if isinstance(section, dict):
|
|
38
|
+
return section
|
|
39
|
+
return {}
|