lintro 0.6.2__py3-none-any.whl → 0.17.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.
Files changed (109) hide show
  1. lintro/__init__.py +1 -1
  2. lintro/cli.py +230 -14
  3. lintro/cli_utils/commands/__init__.py +8 -1
  4. lintro/cli_utils/commands/check.py +1 -0
  5. lintro/cli_utils/commands/config.py +325 -0
  6. lintro/cli_utils/commands/format.py +2 -2
  7. lintro/cli_utils/commands/init.py +361 -0
  8. lintro/cli_utils/commands/list_tools.py +180 -42
  9. lintro/cli_utils/commands/test.py +316 -0
  10. lintro/cli_utils/commands/versions.py +81 -0
  11. lintro/config/__init__.py +62 -0
  12. lintro/config/config_loader.py +420 -0
  13. lintro/config/lintro_config.py +189 -0
  14. lintro/config/tool_config_generator.py +403 -0
  15. lintro/enums/__init__.py +1 -0
  16. lintro/enums/darglint_strictness.py +10 -0
  17. lintro/enums/hadolint_enums.py +22 -0
  18. lintro/enums/tool_name.py +2 -0
  19. lintro/enums/tool_type.py +2 -0
  20. lintro/enums/yamllint_format.py +11 -0
  21. lintro/exceptions/__init__.py +1 -0
  22. lintro/formatters/__init__.py +1 -0
  23. lintro/formatters/core/__init__.py +1 -0
  24. lintro/formatters/core/output_style.py +11 -0
  25. lintro/formatters/core/table_descriptor.py +8 -0
  26. lintro/formatters/styles/csv.py +2 -0
  27. lintro/formatters/styles/grid.py +2 -0
  28. lintro/formatters/styles/html.py +2 -0
  29. lintro/formatters/styles/json.py +2 -0
  30. lintro/formatters/styles/markdown.py +2 -0
  31. lintro/formatters/styles/plain.py +2 -0
  32. lintro/formatters/tools/__init__.py +12 -0
  33. lintro/formatters/tools/black_formatter.py +27 -5
  34. lintro/formatters/tools/darglint_formatter.py +16 -1
  35. lintro/formatters/tools/eslint_formatter.py +108 -0
  36. lintro/formatters/tools/hadolint_formatter.py +13 -0
  37. lintro/formatters/tools/markdownlint_formatter.py +88 -0
  38. lintro/formatters/tools/prettier_formatter.py +15 -0
  39. lintro/formatters/tools/pytest_formatter.py +201 -0
  40. lintro/formatters/tools/ruff_formatter.py +26 -5
  41. lintro/formatters/tools/yamllint_formatter.py +14 -1
  42. lintro/models/__init__.py +1 -0
  43. lintro/models/core/__init__.py +1 -0
  44. lintro/models/core/tool_config.py +11 -7
  45. lintro/parsers/__init__.py +69 -9
  46. lintro/parsers/actionlint/actionlint_parser.py +1 -1
  47. lintro/parsers/bandit/__init__.py +6 -0
  48. lintro/parsers/bandit/bandit_issue.py +49 -0
  49. lintro/parsers/bandit/bandit_parser.py +99 -0
  50. lintro/parsers/black/black_issue.py +4 -0
  51. lintro/parsers/darglint/__init__.py +1 -0
  52. lintro/parsers/darglint/darglint_issue.py +11 -0
  53. lintro/parsers/eslint/__init__.py +6 -0
  54. lintro/parsers/eslint/eslint_issue.py +26 -0
  55. lintro/parsers/eslint/eslint_parser.py +63 -0
  56. lintro/parsers/markdownlint/__init__.py +6 -0
  57. lintro/parsers/markdownlint/markdownlint_issue.py +22 -0
  58. lintro/parsers/markdownlint/markdownlint_parser.py +113 -0
  59. lintro/parsers/prettier/__init__.py +1 -0
  60. lintro/parsers/prettier/prettier_issue.py +12 -0
  61. lintro/parsers/prettier/prettier_parser.py +1 -1
  62. lintro/parsers/pytest/__init__.py +21 -0
  63. lintro/parsers/pytest/pytest_issue.py +28 -0
  64. lintro/parsers/pytest/pytest_parser.py +483 -0
  65. lintro/parsers/ruff/ruff_parser.py +6 -2
  66. lintro/parsers/yamllint/__init__.py +1 -0
  67. lintro/tools/__init__.py +3 -1
  68. lintro/tools/core/__init__.py +1 -0
  69. lintro/tools/core/timeout_utils.py +112 -0
  70. lintro/tools/core/tool_base.py +286 -50
  71. lintro/tools/core/tool_manager.py +77 -24
  72. lintro/tools/core/version_requirements.py +482 -0
  73. lintro/tools/implementations/__init__.py +1 -0
  74. lintro/tools/implementations/pytest/pytest_command_builder.py +311 -0
  75. lintro/tools/implementations/pytest/pytest_config.py +200 -0
  76. lintro/tools/implementations/pytest/pytest_error_handler.py +128 -0
  77. lintro/tools/implementations/pytest/pytest_executor.py +122 -0
  78. lintro/tools/implementations/pytest/pytest_handlers.py +375 -0
  79. lintro/tools/implementations/pytest/pytest_option_validators.py +212 -0
  80. lintro/tools/implementations/pytest/pytest_output_processor.py +408 -0
  81. lintro/tools/implementations/pytest/pytest_result_processor.py +113 -0
  82. lintro/tools/implementations/pytest/pytest_utils.py +697 -0
  83. lintro/tools/implementations/tool_actionlint.py +106 -16
  84. lintro/tools/implementations/tool_bandit.py +34 -29
  85. lintro/tools/implementations/tool_black.py +236 -29
  86. lintro/tools/implementations/tool_darglint.py +183 -22
  87. lintro/tools/implementations/tool_eslint.py +374 -0
  88. lintro/tools/implementations/tool_hadolint.py +94 -25
  89. lintro/tools/implementations/tool_markdownlint.py +354 -0
  90. lintro/tools/implementations/tool_prettier.py +317 -24
  91. lintro/tools/implementations/tool_pytest.py +327 -0
  92. lintro/tools/implementations/tool_ruff.py +278 -84
  93. lintro/tools/implementations/tool_yamllint.py +448 -34
  94. lintro/tools/tool_enum.py +8 -0
  95. lintro/utils/__init__.py +1 -0
  96. lintro/utils/ascii_normalize_cli.py +5 -0
  97. lintro/utils/config.py +41 -18
  98. lintro/utils/console_logger.py +211 -25
  99. lintro/utils/path_utils.py +42 -0
  100. lintro/utils/tool_executor.py +339 -45
  101. lintro/utils/tool_utils.py +51 -24
  102. lintro/utils/unified_config.py +926 -0
  103. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/METADATA +172 -30
  104. lintro-0.17.2.dist-info/RECORD +134 -0
  105. lintro-0.6.2.dist-info/RECORD +0 -96
  106. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/WHEEL +0 -0
  107. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/entry_points.txt +0 -0
  108. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/licenses/LICENSE +0 -0
  109. {lintro-0.6.2.dist-info → lintro-0.17.2.dist-info}/top_level.txt +0 -0
lintro/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Lintro - A unified CLI core for code formatting, linting, and quality assurance."""
2
2
 
3
- __version__ = "0.6.2"
3
+ __version__ = "0.17.2"
lintro/cli.py CHANGED
@@ -1,31 +1,70 @@
1
1
  """Command-line interface for Lintro."""
2
2
 
3
3
  import click
4
+ from loguru import logger
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+ from rich.text import Text
4
9
 
5
10
  from lintro import __version__
6
11
  from lintro.cli_utils.commands.check import check_command
12
+ from lintro.cli_utils.commands.config import config_command
7
13
  from lintro.cli_utils.commands.format import format_code
14
+ from lintro.cli_utils.commands.init import init_command
8
15
  from lintro.cli_utils.commands.list_tools import list_tools_command
16
+ from lintro.cli_utils.commands.test import test_command
17
+ from lintro.cli_utils.commands.versions import versions_command
9
18
 
10
19
 
11
20
  class LintroGroup(click.Group):
12
- def format_commands(
21
+ """Custom Click group with enhanced help rendering and command chaining.
22
+
23
+ This group prints command aliases alongside their canonical names to make
24
+ the CLI help output more discoverable. It also supports command chaining
25
+ with comma-separated commands (e.g., lintro fmt , chk , tst).
26
+ """
27
+
28
+ def format_help(
13
29
  self,
14
30
  ctx: click.Context,
15
31
  formatter: click.HelpFormatter,
16
32
  ) -> None:
17
- """Render command list with aliases in the help output.
33
+ """Render help with Rich formatting.
18
34
 
19
35
  Args:
20
36
  ctx: click.Context: The Click context.
21
- formatter: click.HelpFormatter: The help formatter to write to.
37
+ formatter: click.HelpFormatter: The help formatter (unused, we use Rich).
22
38
  """
23
- # Group commands by canonical name and aliases
39
+ console = Console()
40
+
41
+ # Header panel
42
+ header = Text()
43
+ header.append("🔧 Lintro", style="bold cyan")
44
+ header.append(f" v{__version__}", style="dim")
45
+ console.print(Panel(header, border_style="cyan"))
46
+ console.print()
47
+
48
+ # Description
49
+ console.print(
50
+ "[white]Unified CLI for code formatting, linting, "
51
+ "and quality assurance.[/white]",
52
+ )
53
+ console.print()
54
+
55
+ # Usage
56
+ console.print("[bold cyan]Usage:[/bold cyan]")
57
+ console.print(" lintro [OPTIONS] COMMAND [ARGS]...")
58
+ console.print(" lintro COMMAND1 , COMMAND2 , ... [dim](chain commands)[/dim]")
59
+ console.print()
60
+
61
+ # Commands table
24
62
  commands = self.list_commands(ctx)
25
- # Map canonical name to (command, [aliases])
26
- canonical_map = {}
63
+ canonical_map: dict[str, tuple[click.Command, list[str]]] = {}
27
64
  for name in commands:
28
65
  cmd = self.get_command(ctx, name)
66
+ if cmd is None:
67
+ continue
29
68
  if not hasattr(cmd, "_canonical_name"):
30
69
  cmd._canonical_name = name
31
70
  canonical = cmd._canonical_name
@@ -33,14 +72,180 @@ class LintroGroup(click.Group):
33
72
  canonical_map[canonical] = (cmd, [])
34
73
  if name != canonical:
35
74
  canonical_map[canonical][1].append(name)
36
- rows = []
37
- for canonical, (cmd, aliases) in canonical_map.items():
38
- names = [canonical] + aliases
39
- name_str = " / ".join(names)
40
- rows.append((name_str, cmd.get_short_help_str()))
41
- if rows:
42
- with formatter.section("Commands"):
43
- formatter.write_dl(rows)
75
+
76
+ table = Table(title="Commands", show_header=True, header_style="bold cyan")
77
+ table.add_column("Command", style="cyan", no_wrap=True)
78
+ table.add_column("Alias", style="yellow", no_wrap=True)
79
+ table.add_column("Description", style="white")
80
+
81
+ for canonical, (cmd, aliases) in sorted(canonical_map.items()):
82
+ alias_str = ", ".join(aliases) if aliases else "-"
83
+ table.add_row(canonical, alias_str, cmd.get_short_help_str())
84
+
85
+ console.print(table)
86
+ console.print()
87
+
88
+ # Options
89
+ console.print("[bold cyan]Options:[/bold cyan]")
90
+ console.print(" [yellow]--version[/yellow] Show the version and exit.")
91
+ console.print(" [yellow]--help[/yellow] Show this message and exit.")
92
+ console.print()
93
+
94
+ # Examples
95
+ console.print("[bold cyan]Examples:[/bold cyan]")
96
+ console.print(" [dim]# Check all files[/dim]")
97
+ console.print(" lintro check .")
98
+ console.print()
99
+ console.print(" [dim]# Format and then check[/dim]")
100
+ console.print(" lintro fmt . , chk .")
101
+ console.print()
102
+ console.print(" [dim]# Show tool versions[/dim]")
103
+ console.print(" lintro versions")
104
+
105
+ def format_commands(
106
+ self,
107
+ ctx: click.Context,
108
+ formatter: click.HelpFormatter,
109
+ ) -> None:
110
+ """Render command list with aliases in the help output.
111
+
112
+ Args:
113
+ ctx: click.Context: The Click context.
114
+ formatter: click.HelpFormatter: The help formatter to write to.
115
+ """
116
+ # This is now handled by format_help, but keep for compatibility
117
+ pass
118
+
119
+ def invoke(
120
+ self,
121
+ ctx: click.Context,
122
+ ) -> int:
123
+ """Handle command execution with support for command chaining.
124
+
125
+ Supports chaining commands with commas, e.g.: lintro fmt , chk , tst
126
+
127
+ Args:
128
+ ctx: click.Context: The Click context.
129
+
130
+ Returns:
131
+ int: Exit code from command execution.
132
+
133
+ Raises:
134
+ KeyboardInterrupt: If the user interrupts command execution.
135
+ SystemExit: If a command exits with a non-zero exit code.
136
+ """
137
+ all_args = ctx.protected_args + ctx.args
138
+ if all_args:
139
+ # Get set of known command names/aliases
140
+ command_names = set(self.list_commands(ctx))
141
+ normalized_args: list[str] = []
142
+ saw_separator = False
143
+
144
+ for arg in all_args:
145
+ if arg == ",":
146
+ normalized_args.append(arg)
147
+ saw_separator = True
148
+ continue
149
+
150
+ if "," in arg:
151
+ # Check if this looks like comma-separated commands
152
+ raw_parts = [part.strip() for part in arg.split(",")]
153
+ # Filter out empty fragments after splitting
154
+ fragments = [part for part in raw_parts if part]
155
+ # Only split if all parts are known commands
156
+ if fragments and all(part in command_names for part in fragments):
157
+ # Split into separate tokens
158
+ for idx, part in enumerate(fragments):
159
+ if part:
160
+ normalized_args.append(part)
161
+ if idx < len(fragments) - 1:
162
+ normalized_args.append(",")
163
+ saw_separator = True
164
+ continue
165
+ # Not all parts are commands, keep as-is (e.g., --tools ruff,bandit)
166
+ normalized_args.append(arg)
167
+ continue
168
+
169
+ normalized_args.append(arg)
170
+
171
+ if saw_separator:
172
+ # Parse chained commands from normalized args
173
+ command_groups: list[list[str]] = []
174
+ current_group: list[str] = []
175
+
176
+ for arg in normalized_args:
177
+ if arg == ",":
178
+ if current_group:
179
+ command_groups.append(current_group)
180
+ current_group = []
181
+ continue
182
+ current_group.append(arg)
183
+
184
+ if current_group:
185
+ command_groups.append(current_group)
186
+
187
+ # Execute each command group
188
+ exit_codes: list[int] = []
189
+ for cmd_args in command_groups:
190
+ if not cmd_args:
191
+ continue
192
+ # Create a new context for each command
193
+ ctx_copy = self.make_context(
194
+ ctx.info_name,
195
+ cmd_args,
196
+ parent=ctx,
197
+ allow_extra_args=True,
198
+ allow_interspersed_args=False,
199
+ )
200
+ # Invoke the command
201
+ with ctx_copy.scope() as subctx:
202
+ try:
203
+ result = super().invoke(subctx)
204
+ exit_codes.append(result if isinstance(result, int) else 0)
205
+ except SystemExit as e:
206
+ exit_codes.append(
207
+ (
208
+ e.code
209
+ if isinstance(e.code, int)
210
+ else (0 if e.code is None else 1)
211
+ ),
212
+ )
213
+ except KeyboardInterrupt:
214
+ # Re-raise KeyboardInterrupt to allow normal interruption
215
+ raise
216
+ except Exception as e:
217
+ # Catch all other exceptions to allow command chain to
218
+ # continue
219
+ exit_code = getattr(e, "exit_code", 1)
220
+ exit_codes.append(exit_code)
221
+ # Log the exception with full traceback
222
+ logger.exception(
223
+ (
224
+ f"Error executing command "
225
+ f"'{' '.join(cmd_args)}': {type(e).__name__}: {e}"
226
+ ),
227
+ )
228
+ # Also echo to stderr for immediate user feedback
229
+ click.echo(
230
+ click.style(
231
+ (
232
+ f"Error executing command "
233
+ f"'{' '.join(cmd_args)}': "
234
+ f"{type(e).__name__}: {e}"
235
+ ),
236
+ fg="red",
237
+ ),
238
+ err=True,
239
+ )
240
+
241
+ # Return aggregated exit code (0 only if all succeeded)
242
+ final_exit_code = max(exit_codes) if exit_codes else 0
243
+ if final_exit_code != 0:
244
+ raise SystemExit(final_exit_code)
245
+ return 0
246
+
247
+ # Normal single command execution
248
+ return super().invoke(ctx)
44
249
 
45
250
 
46
251
  @click.group(cls=LintroGroup, invoke_without_command=True)
@@ -52,17 +257,28 @@ def cli() -> None:
52
257
 
53
258
  # Register canonical commands and set _canonical_name for help
54
259
  check_command._canonical_name = "check"
260
+ config_command._canonical_name = "config"
55
261
  format_code._canonical_name = "format"
262
+ init_command._canonical_name = "init"
263
+ test_command._canonical_name = "test"
56
264
  list_tools_command._canonical_name = "list-tools"
265
+ versions_command._canonical_name = "versions"
57
266
 
58
267
  cli.add_command(check_command, name="check")
268
+ cli.add_command(config_command, name="config")
59
269
  cli.add_command(format_code, name="format")
270
+ cli.add_command(init_command, name="init")
271
+ cli.add_command(test_command, name="test")
60
272
  cli.add_command(list_tools_command, name="list-tools")
273
+ cli.add_command(versions_command, name="versions")
61
274
 
62
275
  # Register aliases
63
276
  cli.add_command(check_command, name="chk")
277
+ cli.add_command(config_command, name="cfg")
64
278
  cli.add_command(format_code, name="fmt")
279
+ cli.add_command(test_command, name="tst")
65
280
  cli.add_command(list_tools_command, name="ls")
281
+ cli.add_command(versions_command, name="ver")
66
282
 
67
283
 
68
284
  def main() -> None:
@@ -2,6 +2,13 @@
2
2
 
3
3
  from .check import check_command
4
4
  from .format import format_code, format_code_legacy
5
+ from .init import init_command
5
6
  from .list_tools import list_tools
6
7
 
7
- __all__ = ["check_command", "format_code", "format_code_legacy", "list_tools"]
8
+ __all__ = [
9
+ "check_command",
10
+ "format_code",
11
+ "format_code_legacy",
12
+ "init_command",
13
+ "list_tools",
14
+ ]
@@ -138,6 +138,7 @@ def check_command(
138
138
  output_format=output_format,
139
139
  verbose=verbose,
140
140
  raw_output=raw_output,
141
+ output_file=output,
141
142
  )
142
143
 
143
144
  # Exit with code only; CLI uses this as process exit code and avoids any
@@ -0,0 +1,325 @@
1
+ """Config command for displaying Lintro configuration status."""
2
+
3
+ from typing import Any
4
+
5
+ import click
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from lintro.config import LintroConfig, get_config
11
+ from lintro.utils.unified_config import (
12
+ _load_native_tool_config,
13
+ get_ordered_tools,
14
+ get_tool_priority,
15
+ is_tool_injectable,
16
+ validate_config_consistency,
17
+ )
18
+
19
+
20
+ def _get_all_tool_names() -> list[str]:
21
+ """Get list of all known tool names.
22
+
23
+ Returns:
24
+ list[str]: Sorted list of tool names.
25
+ """
26
+ return [
27
+ "ruff",
28
+ "black",
29
+ "prettier",
30
+ "eslint",
31
+ "yamllint",
32
+ "markdownlint",
33
+ "darglint",
34
+ "bandit",
35
+ "hadolint",
36
+ "actionlint",
37
+ "pytest",
38
+ ]
39
+
40
+
41
+ @click.command()
42
+ @click.option(
43
+ "--verbose",
44
+ "-v",
45
+ is_flag=True,
46
+ help="Show detailed configuration including native tool configs.",
47
+ )
48
+ @click.option(
49
+ "--json",
50
+ "json_output",
51
+ is_flag=True,
52
+ help="Output configuration as JSON.",
53
+ )
54
+ def config_command(
55
+ verbose: bool,
56
+ json_output: bool,
57
+ ) -> None:
58
+ """Display Lintro configuration status.
59
+
60
+ Shows the unified configuration for all tools including:
61
+ - Config source (.lintro-config.yaml or pyproject.toml)
62
+ - Global settings (line_length, tool ordering strategy)
63
+ - Tool execution order based on configured strategy
64
+ - Per-tool effective configuration
65
+ - Configuration warnings and inconsistencies
66
+
67
+ Args:
68
+ verbose: Show detailed configuration including native tool configs.
69
+ json_output: Output configuration as JSON.
70
+ """
71
+ console = Console()
72
+ config = get_config(reload=True)
73
+
74
+ if json_output:
75
+ _output_json(config=config, verbose=verbose)
76
+ return
77
+
78
+ _output_rich(
79
+ console=console,
80
+ config=config,
81
+ verbose=verbose,
82
+ )
83
+
84
+
85
+ def _output_json(
86
+ config: LintroConfig,
87
+ verbose: bool = False,
88
+ ) -> None:
89
+ """Output configuration as JSON.
90
+
91
+ Args:
92
+ config: LintroConfig instance from get_config()
93
+ verbose: Include native configs in output when True
94
+ """
95
+ import json
96
+
97
+ # Get tool order settings
98
+ tool_order = config.execution.tool_order
99
+ if isinstance(tool_order, list):
100
+ order_strategy = "custom"
101
+ custom_order = tool_order
102
+ else:
103
+ order_strategy = tool_order or "priority"
104
+ custom_order = []
105
+
106
+ # Get list of all known tools
107
+ tool_names = _get_all_tool_names()
108
+ ordered_tools = get_ordered_tools(
109
+ tool_names=tool_names,
110
+ tool_order=config.execution.tool_order,
111
+ )
112
+
113
+ output = {
114
+ "config_source": config.config_path or "defaults",
115
+ "global_settings": {
116
+ "line_length": config.enforce.line_length,
117
+ "target_python": config.enforce.target_python,
118
+ "tool_order": order_strategy,
119
+ "custom_order": custom_order,
120
+ },
121
+ "execution": {
122
+ "enabled_tools": config.execution.enabled_tools or "all",
123
+ "fail_fast": config.execution.fail_fast,
124
+ "parallel": config.execution.parallel,
125
+ },
126
+ "tool_execution_order": [
127
+ {"tool": t, "priority": get_tool_priority(t)} for t in ordered_tools
128
+ ],
129
+ "tool_configs": {},
130
+ "warnings": validate_config_consistency(),
131
+ }
132
+
133
+ for tool_name in tool_names:
134
+ tool_config = config.get_tool_config(tool_name)
135
+ effective_ll = config.get_effective_line_length(tool_name)
136
+
137
+ tool_output: dict[str, Any] = {
138
+ "enabled": tool_config.enabled,
139
+ "is_injectable": is_tool_injectable(tool_name),
140
+ "effective_line_length": effective_ll,
141
+ "config_source": tool_config.config_source,
142
+ }
143
+ if verbose:
144
+ native = _load_native_tool_config(tool_name)
145
+ tool_output["native_config"] = native if native else None
146
+ tool_output["defaults"] = config.get_tool_defaults(tool_name) or None
147
+
148
+ output["tool_configs"][tool_name] = tool_output
149
+
150
+ print(json.dumps(output, indent=2))
151
+
152
+
153
+ def _output_rich(
154
+ console: Console,
155
+ config: LintroConfig,
156
+ verbose: bool,
157
+ ) -> None:
158
+ """Output configuration using Rich formatting.
159
+
160
+ Args:
161
+ console: Rich Console instance
162
+ config: LintroConfig instance from get_config()
163
+ verbose: Whether to show verbose output
164
+ """
165
+ # Header panel
166
+ console.print(
167
+ Panel.fit(
168
+ "[bold cyan]Lintro Configuration Report[/bold cyan]",
169
+ border_style="cyan",
170
+ ),
171
+ )
172
+ console.print()
173
+
174
+ # Config Source Section
175
+ config_source = config.config_path or "[dim]No config file (using defaults)[/dim]"
176
+ console.print(f"[bold]Config Source:[/bold] {config_source}")
177
+ console.print()
178
+
179
+ # Global Settings Section
180
+ global_table = Table(
181
+ title="Enforce Settings",
182
+ show_header=False,
183
+ box=None,
184
+ )
185
+ global_table.add_column("Setting", style="cyan", width=25)
186
+ global_table.add_column("Value", style="yellow")
187
+
188
+ line_length = config.enforce.line_length
189
+ global_table.add_row(
190
+ "line_length",
191
+ str(line_length) if line_length else "[dim]Not configured[/dim]",
192
+ )
193
+
194
+ target_python = config.enforce.target_python
195
+ global_table.add_row(
196
+ "target_python",
197
+ target_python if target_python else "[dim]Not configured[/dim]",
198
+ )
199
+
200
+ console.print(global_table)
201
+ console.print()
202
+
203
+ # Execution Settings Section
204
+ exec_table = Table(
205
+ title="Execution Settings",
206
+ show_header=False,
207
+ box=None,
208
+ )
209
+ exec_table.add_column("Setting", style="cyan", width=25)
210
+ exec_table.add_column("Value", style="yellow")
211
+
212
+ tool_order = config.execution.tool_order
213
+ if isinstance(tool_order, list):
214
+ order_strategy = "custom"
215
+ exec_table.add_row("tool_order", order_strategy)
216
+ exec_table.add_row("custom_order", ", ".join(tool_order))
217
+ else:
218
+ exec_table.add_row("tool_order", tool_order or "priority")
219
+
220
+ enabled_tools = config.execution.enabled_tools
221
+ exec_table.add_row(
222
+ "enabled_tools",
223
+ ", ".join(enabled_tools) if enabled_tools else "[dim]all[/dim]",
224
+ )
225
+ exec_table.add_row("fail_fast", str(config.execution.fail_fast))
226
+ exec_table.add_row("parallel", str(config.execution.parallel))
227
+
228
+ console.print(exec_table)
229
+ console.print()
230
+
231
+ # Tool Execution Order Section
232
+ tool_names = _get_all_tool_names()
233
+ ordered_tools = get_ordered_tools(
234
+ tool_names=tool_names,
235
+ tool_order=config.execution.tool_order,
236
+ )
237
+
238
+ order_table = Table(title="Tool Execution Order")
239
+ order_table.add_column("#", style="dim", justify="right", width=3)
240
+ order_table.add_column("Tool", style="cyan")
241
+ order_table.add_column("Priority", justify="center", style="yellow")
242
+ order_table.add_column("Type", style="green")
243
+ order_table.add_column("Enabled", justify="center")
244
+
245
+ for idx, tool_name in enumerate(ordered_tools, 1):
246
+ priority = get_tool_priority(tool_name)
247
+ injectable = is_tool_injectable(tool_name)
248
+ tool_type = "Syncable" if injectable else "Native only"
249
+ enabled = config.is_tool_enabled(tool_name)
250
+ enabled_display = "[green]✓[/green]" if enabled else "[red]✗[/red]"
251
+
252
+ order_table.add_row(
253
+ str(idx),
254
+ tool_name,
255
+ str(priority),
256
+ tool_type,
257
+ enabled_display,
258
+ )
259
+
260
+ console.print(order_table)
261
+ console.print()
262
+
263
+ # Per-Tool Configuration Section
264
+ config_table = Table(title="Per-Tool Configuration")
265
+ config_table.add_column("Tool", style="cyan")
266
+ config_table.add_column("Sync Status", justify="center")
267
+ config_table.add_column("Line Length", justify="center", style="yellow")
268
+ config_table.add_column("Config Source", style="dim")
269
+
270
+ if verbose:
271
+ config_table.add_column("Native Config", style="dim")
272
+
273
+ for tool_name in tool_names:
274
+ tool_config = config.get_tool_config(tool_name)
275
+ injectable = is_tool_injectable(tool_name)
276
+ status = (
277
+ "[green]✓ Syncable[/green]"
278
+ if injectable
279
+ else "[yellow]⚠ Native only[/yellow]"
280
+ )
281
+ effective_ll = config.get_effective_line_length(tool_name)
282
+ ll_display = str(effective_ll) if effective_ll else "[dim]default[/dim]"
283
+
284
+ cfg_source = tool_config.config_source or "[dim]auto[/dim]"
285
+
286
+ row = [tool_name, status, ll_display, cfg_source]
287
+
288
+ if verbose:
289
+ native = _load_native_tool_config(tool_name)
290
+ native_cfg = str(native) if native else "[dim]None[/dim]"
291
+ row.append(native_cfg)
292
+
293
+ config_table.add_row(*row)
294
+
295
+ console.print(config_table)
296
+ console.print()
297
+
298
+ # Warnings Section
299
+ warnings = validate_config_consistency()
300
+ if warnings:
301
+ console.print("[bold red]Configuration Warnings[/bold red]")
302
+ for warning in warnings:
303
+ console.print(f" [yellow]⚠️[/yellow] {warning}")
304
+ console.print()
305
+ console.print(
306
+ "[dim]Tools marked 'Native only' cannot be configured by Lintro. "
307
+ "Update their config files manually for consistency.[/dim]",
308
+ )
309
+ else:
310
+ console.print(
311
+ "[green]✅ All configurations are consistent![/green]",
312
+ )
313
+
314
+ console.print()
315
+
316
+ # Help text
317
+ console.print(
318
+ "[dim]Configure Lintro in .lintro-config.yaml:[/dim]",
319
+ )
320
+ console.print(
321
+ "[dim] Run 'lintro init' to create a config file[/dim]",
322
+ )
323
+ console.print(
324
+ '[dim] tool_order: "priority" | "alphabetical" | ["tool1", "tool2"][/dim]',
325
+ )
@@ -140,7 +140,7 @@ def format_code_legacy(
140
140
  None: This function does not return a value.
141
141
 
142
142
  Raises:
143
- Exception: If format fails for any reason.
143
+ RuntimeError: If format fails for any reason.
144
144
  """
145
145
  args: list[str] = []
146
146
  if paths:
@@ -163,5 +163,5 @@ def format_code_legacy(
163
163
  runner = CliRunner()
164
164
  result = runner.invoke(format_code, args)
165
165
  if result.exit_code != DEFAULT_EXIT_CODE:
166
- raise Exception(f"Format failed: {result.output}")
166
+ raise RuntimeError(f"Format failed: {result.output}")
167
167
  return None