diff --git a/docs/guide/commands.rst b/docs/guide/commands.rst index 89177a79..c8cee4d8 100644 --- a/docs/guide/commands.rst +++ b/docs/guide/commands.rst @@ -50,12 +50,20 @@ Lint one or more paths, and print a list of lint errors. .. code:: console - $ fixit lint [--diff] [PATH ...] + $ fixit lint [--diff] [--stdin --stdin-filepath] [PATH ...] .. attribute:: --diff / -d Show suggested fixes, in unified diff format, when available. +.. attribute:: --stdin + + Lint code from stdin. + +.. attribute:: --stdin-filepath + + Required with --stdin; Analyze code from stdin as if it is from this filepath. + ``fix`` ^^^^^^^ @@ -64,7 +72,8 @@ Lint one or more paths, and apply suggested fixes. .. code:: console - $ fixit fix [--interactive | --automatic [--diff]] [PATH ...] + $ fixit fix [--interactive | --automatic [--diff]] \ + [--stdin --stdin-filepath] [PATH ...] .. attribute:: --interactive / -i @@ -79,6 +88,15 @@ Lint one or more paths, and apply suggested fixes. Show applied fixes in unified diff format when applied automatically. +.. attribute:: --stdin + + Fix code from stdin and output fixed code to stdout. Ignores --interactive/--automatic/--diff. + +.. attribute:: --stdin-filepath + + Required with --stdin; Analyze code from stdin as if it is from this filepath. + + ``test`` ^^^^^^^^ diff --git a/src/fixit/cli.py b/src/fixit/cli.py index bc1c5c90..9ad2b9a0 100644 --- a/src/fixit/cli.py +++ b/src/fixit/cli.py @@ -13,7 +13,7 @@ from fixit import __version__ -from .api import fixit_paths, print_result +from .api import fixit_bytes, fixit_paths, print_result from .config import collect_rules, generate_config, parse_rule from .ftypes import Config, Options, QualifiedRule, Tags from .rule import LintRule @@ -21,6 +21,27 @@ from .util import capture +stdin_option = click.option( + "--stdin", is_flag=True, default=False, help="Lint code from stdin" +) +stdin_filepath_option = click.option( + "--stdin-filepath", + type=click.Path(path_type=Path), + default=None, + help="Analyze code from stdin as if it is from this filepath", +) + + +def generator_from_stdin( + ctx: click.Context, stdin_filepath: Optional[Path], options: Options, fix=False +): + if not stdin_filepath: + ctx.fail("--stdin-filepath is required with --stdin") + content = sys.stdin.buffer.read() + config = generate_config(stdin_filepath, options=options) + return fixit_bytes(stdin_filepath, content, autofix=fix, config=config) + + def splash( visited: Set[Path], dirty: Set[Path], autofixes: int = 0, fixed: int = 0 ) -> None: @@ -101,25 +122,33 @@ def main( @main.command() @click.pass_context @click.option("--diff", "-d", is_flag=True, help="Show diff of suggested changes") +@stdin_option +@stdin_filepath_option @click.argument("paths", nargs=-1, type=click.Path(path_type=Path)) def lint( ctx: click.Context, diff: bool, + stdin: bool, + stdin_filepath: Optional[Path], paths: Sequence[Path], ): """ - lint one or more paths and return suggestions + lint stdin or one or more paths and return suggestions """ options: Options = ctx.obj - if not paths: - paths = [Path.cwd()] + if stdin: + results = generator_from_stdin(ctx, stdin_filepath, options=options) + else: + if not paths: + paths = [Path.cwd()] + results = fixit_paths(paths, options=options) exit_code = 0 visited: Set[Path] = set() dirty: Set[Path] = set() autofixes = 0 - for result in fixit_paths(paths, options=options): + for result in results: visited.add(result.path) if print_result(result, show_diff=diff): @@ -145,11 +174,15 @@ def lint( help="how to apply fixes; interactive by default", ) @click.option("--diff", "-d", is_flag=True, help="show diff even with --automatic") +@stdin_option +@stdin_filepath_option @click.argument("paths", nargs=-1, type=click.Path(path_type=Path)) def fix( ctx: click.Context, interactive: bool, diff: bool, + stdin: bool, + stdin_filepath: Optional[Path], paths: Sequence[Path], ): """ @@ -168,10 +201,22 @@ def fix( autofixes = 0 fixed = 0 + if stdin: + generator = capture( + generator_from_stdin(ctx, stdin_filepath, options=options, fix=True) + ) + for _ in generator: + pass + if not generator.result: + raise Exception("Internal Error: fixit_bytes returned None") + print(generator.result.decode(), end="") + ctx.exit(exit_code) + # TODO: make this parallel generator = capture( fixit_paths(paths, autofix=autofix, options=options, parallel=False) ) + for result in generator: visited.add(result.path) if print_result(result, show_diff=interactive or diff): diff --git a/src/fixit/tests/smoke.py b/src/fixit/tests/smoke.py index ef746eea..c83ac486 100644 --- a/src/fixit/tests/smoke.py +++ b/src/fixit/tests/smoke.py @@ -109,6 +109,38 @@ def func(): expected_format, path.read_text(), "unexpected file output" ) + with self.subTest("linting via stdin"): + result = self.runner.invoke( + main, + ["lint", "--stdin", "--stdin-filepath", path.as_posix()], + input=content, # provide content via stdin + catch_exceptions=False, + ) + + self.assertNotEqual(result.output, "") + self.assertNotEqual(result.exit_code, 0) + self.assertRegex( + result.output, + r"file\.py@\d+:\d+ NoRedundantFString: .+ \(has autofix\)", + ) + + with self.subTest("fixing with formatting via stdin"): + result = self.runner.invoke( + main, + [ + "fix", + "--automatic", + "--stdin", + "--stdin-filepath", + path.as_posix(), + ], + input=content, + catch_exceptions=False, + ) + + self.assertEqual(result.exit_code, 0) + self.assertEqual(expected_format, result.output, "unexpected stdout") + def test_this_file_is_clean(self) -> None: path = Path(__file__).resolve().as_posix() result = self.runner.invoke(main, ["lint", path], catch_exceptions=False)