Skip to content

Commit a63d6b2

Browse files
jvllmramyreese
andauthored
make format of fixit's terminal output configurable (#437)
* format of fixit's terminal output can be configured * bind output-format and output-template to cwd; provide presets * fix linting and typechecking errors * refactor output format and config handling for cwd, more tests, reword docs * Note plans to change default output format in future release --------- Co-authored-by: Amethyst Reese <[email protected]>
1 parent 75b8b87 commit a63d6b2

File tree

9 files changed

+310
-21
lines changed

9 files changed

+310
-21
lines changed

docs/guide/commands.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ The following options are available for all commands:
4343
two tags.
4444

4545

46+
.. attribute:: --output-format / -o FORMAT_TYPE
47+
48+
Override how Fixit prints violations to the terminal.
49+
50+
See :attr:`output-format` for available formats.
51+
52+
.. attribute:: --output-template TEMPLATE
53+
54+
Override the python formatting template to use with ``output-format = 'custom'``.
55+
4656
``lint``
4757
^^^^^^^^
4858

@@ -72,7 +82,7 @@ the input read from STDIN, and the fixed output printed to STDOUT (ignoring
7282
$ fixit fix [--interactive | --automatic [--diff]] [PATH ...]
7383
7484
.. attribute:: --interactive / -i
75-
85+
7686
Interactively prompt the user to apply or decline suggested fixes for
7787
each auto-fix available. *default*
7888

docs/guide/configuration.rst

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ The main configuration table.
9999

100100
.. code-block:: toml
101101
102-
root = True
102+
root = true
103103
enable-root-import = "src"
104104
enable = ["orange.rules"]
105105
@@ -129,7 +129,7 @@ The main configuration table.
129129
.. code-block:: toml
130130
131131
python-version = "3.10"
132-
132+
133133
Defaults to the currently active version of Python.
134134
Set to empty string ``""`` to disable target version checking.
135135

@@ -151,6 +151,51 @@ The main configuration table.
151151
Alternative formatting styles can be added by implementing the
152152
:class:`~fixit.Formatter` interface.
153153

154+
.. attribute:: output-format
155+
:type: str
156+
157+
Choose one of the presets for terminal output formatting.
158+
This option is inferred based on the current working directory or from
159+
an explicity specified config file -- subpath overrides will be ignored.
160+
161+
Can be one of:
162+
163+
- ``custom``: Specify your own format using the :attr:`output-template`
164+
option below.
165+
- ``fixit``: Fixit's default output format.
166+
- ``vscode``: A format that provides clickable paths for Visual Studio Code.
167+
168+
.. note::
169+
170+
The default output format is planned to change to ``vscode`` in
171+
the next feature release, expected as part of ``v2.3`` or ``v3.0``.
172+
If you are sensitive to output formats changing, specify your preferred
173+
format in your project configs accordingly.
174+
175+
.. attribute:: output-template
176+
:type: str
177+
178+
Sets the format of output printed to terminal.
179+
Python formatting is used in the background to fill in data.
180+
Only active with :attr:`output-format` set to ``custom``.
181+
182+
This option is inferred based on the current working directory or from
183+
an explicity specified config file -- subpath overrides will be ignored.
184+
185+
Supported variables:
186+
187+
- ``message``: Message emitted by the applied rule.
188+
189+
- ``path``: Path to affected file.
190+
191+
- ``result``: Raw :class:`~fixit.Result` object.
192+
193+
- ``rule_name``: Name of the applied rule.
194+
195+
- ``start_col``: Start column of affected code.
196+
197+
- ``start_line``: Start line of affected code.
198+
154199

155200
.. _rule-options:
156201

docs/guide/integrations.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,9 @@ your repository.
7575
7676
To read more about how you can customize your pre-commit configuration,
7777
see the `pre-commit docs <https://pre-commit.com/#pre-commit-configyaml---hooks>`__.
78+
79+
80+
VSCode
81+
^^^^^^
82+
For better integration with Visual Studio Code setting ``output-format`` can be set to ``vscode``.
83+
That way VSCode opens the editor at the right position when clicking on code locations in Fixit's terminal output.

src/fixit/api.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,26 @@
1717
from .config import collect_rules, generate_config
1818
from .engine import LintRunner
1919
from .format import format_module
20-
from .ftypes import Config, FileContent, LintViolation, Options, Result, STDIN
20+
from .ftypes import (
21+
Config,
22+
FileContent,
23+
LintViolation,
24+
Options,
25+
OutputFormat,
26+
Result,
27+
STDIN,
28+
)
2129

2230
LOG = logging.getLogger(__name__)
2331

2432

2533
def print_result(
26-
result: Result, *, show_diff: bool = False, stderr: bool = False
34+
result: Result,
35+
*,
36+
show_diff: bool = False,
37+
stderr: bool = False,
38+
output_format: OutputFormat = OutputFormat.fixit,
39+
output_template: str = "",
2740
) -> int:
2841
"""
2942
Print linting results in a simple format designed for human eyes.
@@ -46,11 +59,24 @@ def print_result(
4659
message = result.violation.message
4760
if result.violation.autofixable:
4861
message += " (has autofix)"
49-
click.secho(
50-
f"{path}@{start_line}:{start_col} {rule_name}: {message}",
51-
fg="yellow",
52-
err=stderr,
53-
)
62+
63+
if output_format == OutputFormat.fixit:
64+
line = f"{path}@{start_line}:{start_col} {rule_name}: {message}"
65+
elif output_format == OutputFormat.vscode:
66+
line = f"{path}:{start_line}:{start_col} {rule_name}: {message}"
67+
elif output_format == OutputFormat.custom:
68+
line = output_template.format(
69+
message=message,
70+
path=path,
71+
result=result,
72+
rule_name=rule_name,
73+
start_col=start_col,
74+
start_line=start_line,
75+
)
76+
else:
77+
raise NotImplementedError(f"output-format = {output_format!r}")
78+
click.secho(line, fg="yellow", err=stderr)
79+
5480
if show_diff and result.violation.diff:
5581
echo_color_precomputed_diff(result.violation.diff)
5682
return True

src/fixit/cli.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from .api import fixit_paths, print_result
1717
from .config import collect_rules, generate_config, parse_rule
18-
from .ftypes import Config, LSPOptions, Options, QualifiedRule, Tags
18+
from .ftypes import Config, LSPOptions, Options, OutputFormat, QualifiedRule, Tags
1919
from .rule import LintRule
2020
from .testing import generate_lint_rule_test_cases
2121
from .util import capture
@@ -72,12 +72,28 @@ def f(v: int) -> str:
7272
default="",
7373
help="Override configured rules",
7474
)
75+
@click.option(
76+
"--output-format",
77+
"-o",
78+
type=click.Choice([o.name for o in OutputFormat], case_sensitive=False),
79+
show_choices=True,
80+
default=None,
81+
help="Select output format type",
82+
)
83+
@click.option(
84+
"--output-template",
85+
type=str,
86+
default="",
87+
help="Python format template to use with output format 'custom'",
88+
)
7589
def main(
7690
ctx: click.Context,
7791
debug: Optional[bool],
7892
config_file: Optional[Path],
7993
tags: str,
8094
rules: str,
95+
output_format: Optional[OutputFormat],
96+
output_template: str,
8197
) -> None:
8298
level = logging.WARNING
8399
if debug is not None:
@@ -95,6 +111,8 @@ def main(
95111
if r
96112
}
97113
),
114+
output_format=output_format,
115+
output_template=output_template,
98116
)
99117

100118

@@ -121,10 +139,15 @@ def lint(
121139
visited: Set[Path] = set()
122140
dirty: Set[Path] = set()
123141
autofixes = 0
142+
config = generate_config(options=options)
124143
for result in fixit_paths(paths, options=options):
125144
visited.add(result.path)
126-
127-
if print_result(result, show_diff=diff):
145+
if print_result(
146+
result,
147+
show_diff=diff,
148+
output_format=config.output_format,
149+
output_template=config.output_template,
150+
):
128151
dirty.add(result.path)
129152
if result.violation:
130153
exit_code |= 1
@@ -179,11 +202,18 @@ def fix(
179202
generator = capture(
180203
fixit_paths(paths, autofix=autofix, options=options, parallel=False)
181204
)
205+
config = generate_config(options=options)
182206
for result in generator:
183207
visited.add(result.path)
184208
# for STDIN, we need STDOUT to equal the fixed content, so
185209
# move everything else to STDERR
186-
if print_result(result, show_diff=interactive or diff, stderr=is_stdin):
210+
if print_result(
211+
result,
212+
show_diff=interactive or diff,
213+
stderr=is_stdin,
214+
output_format=config.output_format,
215+
output_template=config.output_template,
216+
):
187217
dirty.add(result.path)
188218
if autofix and result.violation and result.violation.autofixable:
189219
autofixes += 1

src/fixit/config.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import platform
1111
import sys
1212
from contextlib import contextmanager, ExitStack
13+
1314
from pathlib import Path
1415
from types import ModuleType
1516
from typing import (
@@ -37,6 +38,7 @@
3738
is_collection,
3839
is_sequence,
3940
Options,
41+
OutputFormat,
4042
QualifiedRule,
4143
QualifiedRuleRegex,
4244
RawConfig,
@@ -55,6 +57,7 @@
5557
FIXIT_CONFIG_FILENAMES = ("fixit.toml", ".fixit.toml", "pyproject.toml")
5658
FIXIT_LOCAL_MODULE = "fixit.local"
5759

60+
5861
log = logging.getLogger(__name__)
5962

6063

@@ -402,6 +405,8 @@ def merge_configs(
402405
rule_options: RuleOptionsTable = {}
403406
target_python_version: Optional[Version] = Version(platform.python_version())
404407
target_formatter: Optional[str] = None
408+
output_format: OutputFormat = OutputFormat.fixit
409+
output_template: str = ""
405410

406411
def process_subpath(
407412
subpath: Path,
@@ -483,6 +488,17 @@ def process_subpath(
483488
else:
484489
enable_root_import = True
485490

491+
if value := data.pop("output-format", ""):
492+
try:
493+
output_format = OutputFormat(value)
494+
except ValueError as e:
495+
raise ConfigError(
496+
"output-format: unknown value {value!r}", config=config
497+
) from e
498+
499+
if value := data.pop("output-template", ""):
500+
output_template = value
501+
486502
process_subpath(
487503
config.path.parent,
488504
enable=get_sequence(config, "enable"),
@@ -524,16 +540,21 @@ def process_subpath(
524540
options=rule_options,
525541
python_version=target_python_version,
526542
formatter=target_formatter,
543+
output_format=output_format,
544+
output_template=output_template,
527545
)
528546

529547

530548
def generate_config(
531-
path: Path, root: Optional[Path] = None, *, options: Optional[Options] = None
549+
path: Optional[Path] = None,
550+
root: Optional[Path] = None,
551+
*,
552+
options: Optional[Options] = None,
532553
) -> Config:
533554
"""
534555
Given a file path, walk upwards looking for and applying cascading configs
535556
"""
536-
path = path.resolve()
557+
path = (path or Path.cwd()).resolve()
537558

538559
if root is not None:
539560
root = root.resolve()
@@ -554,4 +575,10 @@ def generate_config(
554575
config.enable = list(options.rules)
555576
config.disable = []
556577

578+
if options.output_format:
579+
config.output_format = options.output_format
580+
581+
if options.output_template:
582+
config.output_template = options.output_template
583+
557584
return config

src/fixit/ftypes.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import platform
77
import re
88
from dataclasses import dataclass, field
9+
from enum import Enum
910
from pathlib import Path
1011
from typing import (
1112
Any,
@@ -52,6 +53,13 @@
5253
VisitHook = Callable[[str], ContextManager[None]]
5354

5455

56+
class OutputFormat(str, Enum):
57+
custom = "custom"
58+
fixit = "fixit"
59+
# json = "json" # TODO
60+
vscode = "vscode"
61+
62+
5563
@dataclass(frozen=True)
5664
class Invalid:
5765
code: str
@@ -177,10 +185,12 @@ class Options:
177185
Command-line options to affect runtime behavior
178186
"""
179187

180-
debug: Optional[bool]
181-
config_file: Optional[Path]
188+
debug: Optional[bool] = None
189+
config_file: Optional[Path] = None
182190
tags: Optional[Tags] = None
183191
rules: Sequence[QualifiedRule] = ()
192+
output_format: Optional[OutputFormat] = None
193+
output_template: str = ""
184194

185195

186196
@dataclass
@@ -223,6 +233,10 @@ class Config:
223233
# post-run processing
224234
formatter: Optional[str] = None
225235

236+
# output formatting options
237+
output_format: OutputFormat = OutputFormat.fixit
238+
output_template: str = ""
239+
226240
def __post_init__(self) -> None:
227241
self.path = self.path.resolve()
228242
self.root = self.root.resolve()

0 commit comments

Comments
 (0)