Example #1
0
def _handle_files_with_tmp_or_prs_errors(lint_result: LintingResult) -> int:
    """Discard lint fixes for files with templating or parse errors.

    Returns 1 if there are any files with templating or parse errors after
    filtering, else 0. (Intended as a process exit code.)
    """
    total_errors, num_filtered_errors = lint_result.count_tmp_prs_errors()
    lint_result.discard_fixes_for_lint_errors_in_files_with_tmp_or_prs_errors()
    if total_errors:
        click.echo(
            colorize(f"  [{total_errors} templating/parsing errors found]",
                     Color.red))
        if num_filtered_errors < total_errors:
            color = Color.red if num_filtered_errors else Color.green
            click.echo(
                colorize(
                    f"  [{num_filtered_errors} templating/parsing errors "
                    f'remaining after "ignore"]',
                    color,
                ))
    return 1 if num_filtered_errors else 0
Example #2
0
def get_config(
    extra_config_path: Optional[str] = None,
    ignore_local_config: bool = False,
    **kwargs,
) -> FluffConfig:
    """Get a config object from kwargs."""
    if "dialect" in kwargs:
        try:
            # We're just making sure it exists at this stage.
            # It will be fetched properly in the linter.
            dialect_selector(kwargs["dialect"])
        except SQLFluffUserError as err:
            click.echo(
                colorize(
                    f"Error loading dialect '{kwargs['dialect']}': {str(err)}",
                    color=Color.red,
                ))
            sys.exit(66)
        except KeyError:
            click.echo(
                colorize(f"Error: Unknown dialect '{kwargs['dialect']}'",
                         color=Color.red))
            sys.exit(66)
    # Instantiate a config object (filtering out the nulls)
    overrides = {k: kwargs[k] for k in kwargs if kwargs[k] is not None}
    try:
        return FluffConfig.from_root(
            extra_config_path=extra_config_path,
            ignore_local_config=ignore_local_config,
            overrides=overrides,
        )
    except SQLFluffUserError as err:  # pragma: no cover
        click.echo(
            colorize(
                f"Error loading config: {str(err)}",
                color=Color.red,
            ))
        sys.exit(66)
Example #3
0
def parse(path,
          code_only,
          format,
          profiler,
          bench,
          nofail,
          logger=None,
          **kwargs):
    """Parse SQL files and just spit out the result.

    PATH is the path to a sql file or directory to lint. This can be either a
    file ('path/to/file.sql'), a path ('directory/of/sql/files'), a single ('-')
    character to indicate reading from *stdin* or a dot/blank ('.'/' ') which will
    be interpreted like passing the current working directory as a path argument.
    """
    # Initialise the benchmarker
    bencher = BenchIt()  # starts the timer
    c = get_config(**kwargs)
    # We don't want anything else to be logged if we want json or yaml output
    non_human_output = format in ("json", "yaml")
    lnt, formatter = get_linter_and_formatter(c, silent=non_human_output)
    verbose = c.get("verbose")
    recurse = c.get("recurse")

    formatter.dispatch_config(lnt)

    # Set up logging.
    set_logging_level(verbosity=verbose,
                      logger=logger,
                      stderr_output=non_human_output)

    # TODO: do this better
    nv = 0
    if profiler:
        # Set up the profiler if required
        try:
            import cProfile
        except ImportError:
            click.echo("The cProfiler is not available on your platform.")
            sys.exit(1)
        pr = cProfile.Profile()
        pr.enable()

    bencher("Parse setup")
    try:
        # handle stdin if specified via lone '-'
        if "-" == path:
            # put the parser result in a list to iterate later
            result = [
                lnt.parse_string(sys.stdin.read(),
                                 "stdin",
                                 recurse=recurse,
                                 config=lnt.config),
            ]
        else:
            # A single path must be specified for this command
            # TODO: Remove verbose
            result = lnt.parse_path(path, recurse=recurse)

        # iterative print for human readout
        if format == "human":
            for parsed_string in result:
                if parsed_string.tree:
                    click.echo(
                        parsed_string.tree.stringify(code_only=code_only))
                else:
                    # TODO: Make this prettier
                    click.echo("...Failed to Parse...")
                nv += len(parsed_string.violations)
                if parsed_string.violations:
                    click.echo("==== parsing violations ====")
                for v in parsed_string.violations:
                    click.echo(format_violation(v))
                if (parsed_string.violations
                        and parsed_string.config.get("dialect") == "ansi"):
                    click.echo(format_dialect_warning())
                if verbose >= 2:
                    click.echo("==== timings ====")
                    click.echo(cli_table(parsed_string.time_dict.items()))
                bencher("Output details for file")
        else:
            # collect result and print as single payload
            # will need to zip in the file paths
            filepaths = ["stdin"] if "-" == path else lnt.paths_from_path(path)
            result = [
                dict(
                    filepath=filepath,
                    segments=parsed.as_record(code_only=code_only,
                                              show_raw=True)
                    if parsed else None,
                ) for filepath, (parsed, _, _, _, _) in zip(filepaths, result)
            ]

            if format == "yaml":
                # For yaml dumping always dump double quoted strings if they contain tabs or newlines.
                yaml.add_representer(str, quoted_presenter)

                click.echo(yaml.dump(result))
            elif format == "json":
                click.echo(json.dumps(result))
    except IOError:
        click.echo(
            colorize(
                "The path {0!r} could not be accessed. Check it exists.".
                format(path),
                "red",
            ))
        sys.exit(1)

    if profiler:
        pr.disable()
        profiler_buffer = StringIO()
        ps = pstats.Stats(pr, stream=profiler_buffer).sort_stats("cumulative")
        ps.print_stats()
        click.echo("==== profiler stats ====")
        # Only print the first 50 lines of it
        click.echo("\n".join(profiler_buffer.getvalue().split("\n")[:50]))

    if bench:
        click.echo("\n\n==== bencher stats ====")
        bencher.display()

    if nv > 0 and not nofail:
        sys.exit(66)
    else:
        sys.exit(0)
Example #4
0
 def filter(self, record):
     """Filter any warnings (or above) to turn them red."""
     if record.levelno >= logging.WARNING:
         record.msg = colorize(record.msg, "red") + " "
     return True
Example #5
0
def fix(force,
        paths,
        parallel,
        bench=False,
        fixed_suffix="",
        logger=None,
        **kwargs):
    """Fix SQL files.

    PATH is the path to a sql file or directory to lint. This can be either a
    file ('path/to/file.sql'), a path ('directory/of/sql/files'), a single ('-')
    character to indicate reading from *stdin* or a dot/blank ('.'/' ') which will
    be interpreted like passing the current working directory as a path argument.
    """
    # some quick checks
    fixing_stdin = ("-", ) == paths

    c = get_config(**kwargs)
    lnt, formatter = get_linter_and_formatter(c, silent=fixing_stdin)
    verbose = c.get("verbose")

    bencher = BenchIt()

    formatter.dispatch_config(lnt)

    # Set up logging.
    set_logging_level(verbosity=verbose,
                      logger=logger,
                      stderr_output=fixing_stdin)

    # handle stdin case. should output formatted sql to stdout and nothing else.
    if fixing_stdin:
        stdin = sys.stdin.read()
        # TODO: Remove verbose
        result = lnt.lint_string_wrapped(stdin, fname="stdin", fix=True)
        stdout = result.paths[0].files[0].fix_string()[0]
        click.echo(stdout, nl=False)
        sys.exit()

    # Lint the paths (not with the fix argument at this stage), outputting as we go.
    click.echo("==== finding fixable violations ====")
    try:
        result = lnt.lint_paths(paths,
                                fix=True,
                                ignore_non_existent_files=False,
                                parallel=parallel)
    except IOError:
        click.echo(
            colorize(
                "The path(s) {0!r} could not be accessed. Check it/they exist(s)."
                .format(paths),
                "red",
            ))
        sys.exit(1)

    # NB: We filter to linting violations here, because they're
    # the only ones which can be potentially fixed.
    if result.num_violations(types=SQLLintError, fixable=True) > 0:
        click.echo("==== fixing violations ====")
        click.echo("{0} fixable linting violations found".format(
            result.num_violations(types=SQLLintError, fixable=True)))
        if force:
            click.echo(colorize("FORCE MODE", "red") + ": Attempting fixes...")
            # TODO: Remove verbose
            success = do_fixes(
                lnt,
                result,
                formatter,
                types=SQLLintError,
                fixed_file_suffix=fixed_suffix,
            )
            if not success:
                sys.exit(1)
        else:
            click.echo("Are you sure you wish to attempt to fix these? [Y/n] ",
                       nl=False)
            c = click.getchar().lower()
            click.echo("...")
            if c in ("y", "\r", "\n"):
                click.echo("Attempting fixes...")
                # TODO: Remove verbose
                success = do_fixes(
                    lnt,
                    result,
                    formatter,
                    types=SQLLintError,
                    fixed_file_suffix=fixed_suffix,
                )
                if not success:
                    sys.exit(1)
                else:
                    click.echo("All Finished 📜 🎉!")
            elif c == "n":
                click.echo("Aborting...")
            else:
                click.echo("Invalid input, please enter 'Y' or 'N'")
                click.echo("Aborting...")
    else:
        click.echo("==== no fixable linting violations found ====")
        if result.num_violations(types=SQLLintError, fixable=False) > 0:
            click.echo("  [{0} unfixable linting violations found]".format(
                result.num_violations(types=SQLLintError, fixable=False)))
        click.echo("All Finished 📜 🎉!")

    if bench:
        click.echo("\n\n==== bencher stats ====")
        bencher.display()

    sys.exit(0)
Example #6
0
def lint(paths,
         parallel,
         format,
         nofail,
         disregard_sqlfluffignores,
         logger=None,
         **kwargs):
    """Lint SQL files via passing a list of files or using stdin.

    PATH is the path to a sql file or directory to lint. This can be either a
    file ('path/to/file.sql'), a path ('directory/of/sql/files'), a single ('-')
    character to indicate reading from *stdin* or a dot/blank ('.'/' ') which will
    be interpreted like passing the current working directory as a path argument.

    Linting SQL files:

        sqlfluff lint path/to/file.sql
        sqlfluff lint directory/of/sql/files

    Linting a file via stdin (note the lone '-' character):

        cat path/to/file.sql | sqlfluff lint -
        echo 'select col from tbl' | sqlfluff lint -

    """
    c = get_config(**kwargs)
    non_human_output = format in ("json", "yaml")
    lnt, formatter = get_linter_and_formatter(c, silent=non_human_output)
    verbose = c.get("verbose")

    formatter.dispatch_config(lnt)

    # Set up logging.
    set_logging_level(verbosity=verbose,
                      logger=logger,
                      stderr_output=non_human_output)
    # add stdin if specified via lone '-'
    if ("-", ) == paths:
        # TODO: Remove verbose
        result = lnt.lint_string_wrapped(sys.stdin.read(), fname="stdin")
    else:
        # Output the results as we go
        if verbose >= 1:
            click.echo(format_linting_result_header())
        try:
            # TODO: Remove verbose
            result = lnt.lint_paths(
                paths,
                ignore_non_existent_files=False,
                ignore_files=not disregard_sqlfluffignores,
                parallel=parallel,
            )
        except IOError:
            click.echo(
                colorize(
                    "The path(s) {0!r} could not be accessed. Check it/they exist(s)."
                    .format(paths),
                    "red",
                ))
            sys.exit(1)
        # Output the final stats
        if verbose >= 1:
            click.echo(format_linting_stats(result, verbose=verbose))

    if format == "json":
        click.echo(json.dumps(result.as_records()))
    elif format == "yaml":
        click.echo(yaml.dump(result.as_records()))

    if not nofail:
        if not non_human_output:
            click.echo("All Finished 📜 🎉!")
        sys.exit(result.stats()["exit code"])
    else:
        sys.exit(0)
Example #7
0
def parse(
    path,
    code_only,
    include_meta,
    format,
    profiler,
    bench,
    nofail,
    logger=None,
    **kwargs,
):
    """Parse SQL files and just spit out the result.

    PATH is the path to a sql file or directory to lint. This can be either a
    file ('path/to/file.sql'), a path ('directory/of/sql/files'), a single ('-')
    character to indicate reading from *stdin* or a dot/blank ('.'/' ') which will
    be interpreted like passing the current working directory as a path argument.
    """
    c = get_config(**kwargs)
    # We don't want anything else to be logged if we want json or yaml output
    non_human_output = format in ("json", "yaml")
    lnt, formatter = get_linter_and_formatter(c, silent=non_human_output)
    verbose = c.get("verbose")
    recurse = c.get("recurse")

    formatter.dispatch_config(lnt)

    # Set up logging.
    set_logging_level(verbosity=verbose, logger=logger, stderr_output=non_human_output)

    # TODO: do this better
    nv = 0
    if profiler:
        # Set up the profiler if required
        try:
            import cProfile
        except ImportError:  # pragma: no cover
            click.echo("The cProfiler is not available on your platform.")
            sys.exit(1)
        pr = cProfile.Profile()
        pr.enable()

    try:
        t0 = time.monotonic()
        # handle stdin if specified via lone '-'
        if "-" == path:
            # put the parser result in a list to iterate later
            result = [
                lnt.parse_string(
                    sys.stdin.read(), "stdin", recurse=recurse, config=lnt.config
                ),
            ]
        else:
            # A single path must be specified for this command
            result = lnt.parse_path(path, recurse=recurse)
        total_time = time.monotonic() - t0

        # iterative print for human readout
        if format == "human":
            timing = TimingSummary()
            for parsed_string in result:
                timing.add(parsed_string.time_dict)
                if parsed_string.tree:
                    click.echo(parsed_string.tree.stringify(code_only=code_only))
                else:
                    # TODO: Make this prettier
                    click.echo("...Failed to Parse...")  # pragma: no cover
                nv += len(parsed_string.violations)
                if parsed_string.violations:
                    click.echo("==== parsing violations ====")  # pragma: no cover
                for v in parsed_string.violations:
                    click.echo(format_violation(v))  # pragma: no cover
                if (
                    parsed_string.violations
                    and parsed_string.config.get("dialect") == "ansi"
                ):
                    click.echo(format_dialect_warning())  # pragma: no cover
                if verbose >= 2:
                    click.echo("==== timings ====")
                    click.echo(cli_table(parsed_string.time_dict.items()))
            if verbose >= 2 or bench:
                click.echo("==== overall timings ====")
                click.echo(cli_table([("Clock time", total_time)]))
                timing_summary = timing.summary()
                for step in timing_summary:
                    click.echo(f"=== {step} ===")
                    click.echo(cli_table(timing_summary[step].items()))
        else:
            result = [
                dict(
                    filepath=linted_result.fname,
                    segments=linted_result.tree.as_record(
                        code_only=code_only, show_raw=True, include_meta=include_meta
                    )
                    if linted_result.tree
                    else None,
                )
                for linted_result in result
            ]

            if format == "yaml":
                # For yaml dumping always dump double quoted strings if they contain tabs or newlines.
                yaml.add_representer(str, quoted_presenter)

                click.echo(yaml.dump(result))
            elif format == "json":
                click.echo(json.dumps(result))
    except OSError:  # pragma: no cover
        click.echo(
            colorize(
                f"The path {path!r} could not be accessed. Check it exists.",
                "red",
            ),
            err=True,
        )
        sys.exit(1)

    if profiler:
        pr.disable()
        profiler_buffer = StringIO()
        ps = pstats.Stats(pr, stream=profiler_buffer).sort_stats("cumulative")
        ps.print_stats()
        click.echo("==== profiler stats ====")
        # Only print the first 50 lines of it
        click.echo("\n".join(profiler_buffer.getvalue().split("\n")[:50]))

    if nv > 0 and not nofail:
        sys.exit(66)  # pragma: no cover
    else:
        sys.exit(0)
Example #8
0
def fix(force, paths, processes, bench=False, fixed_suffix="", logger=None, **kwargs):
    """Fix SQL files.

    PATH is the path to a sql file or directory to lint. This can be either a
    file ('path/to/file.sql'), a path ('directory/of/sql/files'), a single ('-')
    character to indicate reading from *stdin* or a dot/blank ('.'/' ') which will
    be interpreted like passing the current working directory as a path argument.
    """
    # some quick checks
    fixing_stdin = ("-",) == paths

    config = get_config(**kwargs)
    lnt, formatter = get_linter_and_formatter(config, silent=fixing_stdin)
    verbose = config.get("verbose")
    exit_code = 0

    formatter.dispatch_config(lnt)

    # Set up logging.
    set_logging_level(verbosity=verbose, logger=logger, stderr_output=fixing_stdin)

    # handle stdin case. should output formatted sql to stdout and nothing else.
    if fixing_stdin:
        stdin = sys.stdin.read()

        result = lnt.lint_string_wrapped(stdin, fname="stdin", fix=True)
        templater_error = result.num_violations(types=SQLTemplaterError) > 0
        unfixable_error = result.num_violations(types=SQLLintError, fixable=False) > 0

        if result.num_violations(types=SQLLintError, fixable=True) > 0:
            stdout = result.paths[0].files[0].fix_string()[0]
        else:
            stdout = stdin

        if templater_error:
            click.echo(
                colorize("Fix aborted due to unparseable template variables.", "red"),
                err=True,
            )
            click.echo(
                colorize("Use '--ignore templating' to attempt to fix anyway.", "red"),
                err=True,
            )
        if unfixable_error:
            click.echo(colorize("Unfixable violations detected.", "red"), err=True)

        click.echo(stdout, nl=False)
        sys.exit(1 if templater_error or unfixable_error else 0)

    # Lint the paths (not with the fix argument at this stage), outputting as we go.
    click.echo("==== finding fixable violations ====")
    try:
        result = lnt.lint_paths(
            paths, fix=True, ignore_non_existent_files=False, processes=processes
        )
    except OSError:
        click.echo(
            colorize(
                "The path(s) {!r} could not be accessed. Check it/they exist(s).".format(
                    paths
                ),
                "red",
            ),
            err=True,
        )
        sys.exit(1)

    # NB: We filter to linting violations here, because they're
    # the only ones which can be potentially fixed.
    if result.num_violations(types=SQLLintError, fixable=True) > 0:
        click.echo("==== fixing violations ====")
        click.echo(
            "{} fixable linting violations found".format(
                result.num_violations(types=SQLLintError, fixable=True)
            )
        )
        if force:
            click.echo(colorize("FORCE MODE", "red") + ": Attempting fixes...")
            success = do_fixes(
                lnt,
                result,
                formatter,
                types=SQLLintError,
                fixed_file_suffix=fixed_suffix,
            )
            if not success:
                sys.exit(1)  # pragma: no cover
        else:
            click.echo(
                "Are you sure you wish to attempt to fix these? [Y/n] ", nl=False
            )
            c = click.getchar().lower()
            click.echo("...")
            if c in ("y", "\r", "\n"):
                click.echo("Attempting fixes...")
                success = do_fixes(
                    lnt,
                    result,
                    formatter,
                    types=SQLLintError,
                    fixed_file_suffix=fixed_suffix,
                )
                if not success:
                    sys.exit(1)  # pragma: no cover
                else:
                    _completion_message(config)
            elif c == "n":
                click.echo("Aborting...")
                exit_code = 1
            else:  # pragma: no cover
                click.echo("Invalid input, please enter 'Y' or 'N'")
                click.echo("Aborting...")
                exit_code = 1
    else:
        click.echo("==== no fixable linting violations found ====")
        _completion_message(config)

    if result.num_violations(types=SQLLintError, fixable=False) > 0:
        click.echo(
            "  [{} unfixable linting violations found]".format(
                result.num_violations(types=SQLLintError, fixable=False)
            )
        )
        exit_code = 1

    if result.num_violations(types=SQLTemplaterError) > 0:
        click.echo(
            "  [{} templating errors found]".format(
                result.num_violations(types=SQLTemplaterError)
            )
        )
        exit_code = 1

    if bench:
        click.echo("==== overall timings ====")
        click.echo(cli_table([("Clock time", result.total_time)]))
        timing_summary = result.timing_summary()
        for step in timing_summary:
            click.echo(f"=== {step} ===")
            click.echo(cli_table(timing_summary[step].items()))

    sys.exit(exit_code)
Example #9
0
def lint(
    paths,
    processes,
    format,
    annotation_level,
    nofail,
    disregard_sqlfluffignores,
    logger=None,
    bench=False,
    **kwargs,
):
    """Lint SQL files via passing a list of files or using stdin.

    PATH is the path to a sql file or directory to lint. This can be either a
    file ('path/to/file.sql'), a path ('directory/of/sql/files'), a single ('-')
    character to indicate reading from *stdin* or a dot/blank ('.'/' ') which will
    be interpreted like passing the current working directory as a path argument.

    Linting SQL files:

        sqlfluff lint path/to/file.sql
        sqlfluff lint directory/of/sql/files

    Linting a file via stdin (note the lone '-' character):

        cat path/to/file.sql | sqlfluff lint -
        echo 'select col from tbl' | sqlfluff lint -

    """
    config = get_config(**kwargs)
    non_human_output = format != "human"
    lnt, formatter = get_linter_and_formatter(config, silent=non_human_output)
    verbose = config.get("verbose")

    formatter.dispatch_config(lnt)

    # Set up logging.
    set_logging_level(verbosity=verbose, logger=logger, stderr_output=non_human_output)
    # add stdin if specified via lone '-'
    if ("-",) == paths:
        result = lnt.lint_string_wrapped(sys.stdin.read(), fname="stdin")
    else:
        # Output the results as we go
        if verbose >= 1:
            click.echo(format_linting_result_header())
        try:
            result = lnt.lint_paths(
                paths,
                ignore_non_existent_files=False,
                ignore_files=not disregard_sqlfluffignores,
                processes=processes,
            )
        except OSError:
            click.echo(
                colorize(
                    "The path(s) {!r} could not be accessed. Check it/they exist(s).".format(
                        paths
                    ),
                    "red",
                )
            )
            sys.exit(1)
        # Output the final stats
        if verbose >= 1:
            click.echo(format_linting_stats(result, verbose=verbose))

    if format == "json":
        click.echo(json.dumps(result.as_records()))
    elif format == "yaml":
        click.echo(yaml.dump(result.as_records()))
    elif format == "github-annotation":
        github_result = []
        for record in result.as_records():
            filepath = record["filepath"]
            for violation in record["violations"]:
                # NOTE: The output format is designed for this GitHub action:
                # https://github.com/yuzutech/annotations-action
                # It is similar, but not identical, to the native GitHub format:
                # https://docs.github.com/en/rest/reference/checks#annotations-items
                github_result.append(
                    {
                        "file": filepath,
                        "line": violation["line_no"],
                        "start_column": violation["line_pos"],
                        "end_column": violation["line_pos"],
                        "title": "SQLFluff",
                        "message": f"{violation['code']}: {violation['description']}",
                        "annotation_level": annotation_level,
                    }
                )
        click.echo(json.dumps(github_result))

    if bench:
        click.echo("==== overall timings ====")
        click.echo(cli_table([("Clock time", result.total_time)]))
        timing_summary = result.timing_summary()
        for step in timing_summary:
            click.echo(f"=== {step} ===")
            click.echo(cli_table(timing_summary[step].items()))

    if not nofail:
        if not non_human_output:
            _completion_message(config)
        sys.exit(result.stats()["exit code"])
    else:
        sys.exit(0)
Example #10
0
def parse(
    path: str,
    code_only: bool,
    include_meta: bool,
    format: str,
    write_output: Optional[str],
    profiler: bool,
    bench: bool,
    nofail: bool,
    logger: Optional[logging.Logger] = None,
    extra_config_path: Optional[str] = None,
    ignore_local_config: bool = False,
    **kwargs,
) -> NoReturn:
    """Parse SQL files and just spit out the result.

    PATH is the path to a sql file or directory to lint. This can be either a
    file ('path/to/file.sql'), a path ('directory/of/sql/files'), a single ('-')
    character to indicate reading from *stdin* or a dot/blank ('.'/' ') which will
    be interpreted like passing the current working directory as a path argument.
    """
    c = get_config(extra_config_path, ignore_local_config, **kwargs)
    # We don't want anything else to be logged if we want json or yaml output
    # unless we're writing to a file.
    non_human_output = (format != FormatType.human.value) or (write_output
                                                              is not None)
    lnt, formatter = get_linter_and_formatter(c, silent=non_human_output)
    verbose = c.get("verbose")
    recurse = c.get("recurse")

    progress_bar_configuration.disable_progress_bar = True

    formatter.dispatch_config(lnt)

    # Set up logging.
    set_logging_level(verbosity=verbose,
                      logger=logger,
                      stderr_output=non_human_output)

    # TODO: do this better

    if profiler:
        # Set up the profiler if required
        try:
            import cProfile
        except ImportError:  # pragma: no cover
            click.echo("The cProfiler is not available on your platform.")
            sys.exit(1)
        pr = cProfile.Profile()
        pr.enable()

    try:
        t0 = time.monotonic()

        # handle stdin if specified via lone '-'
        if "-" == path:
            parsed_strings = [
                lnt.parse_string(
                    sys.stdin.read(),
                    "stdin",
                    recurse=recurse,
                    config=lnt.config,
                ),
            ]
        else:
            # A single path must be specified for this command
            parsed_strings = list(lnt.parse_path(path, recurse=recurse))

        total_time = time.monotonic() - t0
        violations_count = 0

        # iterative print for human readout
        if format == FormatType.human.value:
            violations_count = _print_out_violations_and_timing(
                bench, code_only, total_time, verbose, parsed_strings)
        else:
            parsed_strings_dict = [
                dict(
                    filepath=linted_result.fname,
                    segments=linted_result.tree.as_record(
                        code_only=code_only,
                        show_raw=True,
                        include_meta=include_meta)
                    if linted_result.tree else None,
                ) for linted_result in parsed_strings
            ]

            if format == FormatType.yaml.value:
                # For yaml dumping always dump double quoted strings if they contain
                # tabs or newlines.
                yaml.add_representer(str, quoted_presenter)
                file_output = yaml.dump(parsed_strings_dict, sort_keys=False)
            elif format == FormatType.json.value:
                file_output = json.dumps(parsed_strings_dict)

            # Dump the output to stdout or to file as appropriate.
            dump_file_payload(write_output, file_output)

    except OSError:  # pragma: no cover
        click.echo(
            colorize(
                f"The path '{path}' could not be accessed. Check it exists.",
                Color.red,
            ),
            err=True,
        )
        sys.exit(1)

    if profiler:
        pr.disable()
        profiler_buffer = StringIO()
        ps = pstats.Stats(pr, stream=profiler_buffer).sort_stats("cumulative")
        ps.print_stats()
        click.echo("==== profiler stats ====")
        # Only print the first 50 lines of it
        click.echo("\n".join(profiler_buffer.getvalue().split("\n")[:50]))

    if violations_count > 0 and not nofail:
        sys.exit(66)  # pragma: no cover
    else:
        sys.exit(0)
Example #11
0
def fix(
    force: bool,
    paths: Tuple[str],
    processes: int,
    bench: bool = False,
    fixed_suffix: str = "",
    logger: Optional[logging.Logger] = None,
    disable_progress_bar: Optional[bool] = False,
    extra_config_path: Optional[str] = None,
    ignore_local_config: bool = False,
    **kwargs,
) -> NoReturn:
    """Fix SQL files.

    PATH is the path to a sql file or directory to lint. This can be either a
    file ('path/to/file.sql'), a path ('directory/of/sql/files'), a single ('-')
    character to indicate reading from *stdin* or a dot/blank ('.'/' ') which will
    be interpreted like passing the current working directory as a path argument.
    """
    # some quick checks
    fixing_stdin = ("-", ) == paths

    config = get_config(extra_config_path, ignore_local_config, **kwargs)
    fix_even_unparsable = config.get("fix_even_unparsable")
    lnt, formatter = get_linter_and_formatter(config, silent=fixing_stdin)

    verbose = config.get("verbose")
    progress_bar_configuration.disable_progress_bar = disable_progress_bar

    exit_code = 0

    formatter.dispatch_config(lnt)

    # Set up logging.
    set_logging_level(verbosity=verbose,
                      logger=logger,
                      stderr_output=fixing_stdin)

    # handle stdin case. should output formatted sql to stdout and nothing else.
    if fixing_stdin:
        stdin = sys.stdin.read()

        result = lnt.lint_string_wrapped(stdin, fname="stdin", fix=True)
        templater_error = result.num_violations(types=SQLTemplaterError) > 0
        unfixable_error = result.num_violations(types=SQLLintError,
                                                fixable=False) > 0
        if not fix_even_unparsable:
            exit_code = _handle_files_with_tmp_or_prs_errors(result)

        if result.num_violations(types=SQLLintError, fixable=True) > 0:
            stdout = result.paths[0].files[0].fix_string()[0]
        else:
            stdout = stdin

        if templater_error:
            click.echo(
                colorize(
                    "Fix aborted due to unparseable template variables.",
                    Color.red,
                ),
                err=True,
            )
            click.echo(
                colorize(
                    "Use --fix-even-unparsable' to attempt to fix the SQL anyway.",
                    Color.red,
                ),
                err=True,
            )

        if unfixable_error:
            click.echo(colorize("Unfixable violations detected.", Color.red),
                       err=True)

        click.echo(stdout, nl=False)
        sys.exit(1 if templater_error or unfixable_error else exit_code)

    # Lint the paths (not with the fix argument at this stage), outputting as we go.
    click.echo("==== finding fixable violations ====")
    try:
        result = lnt.lint_paths(
            paths,
            fix=True,
            ignore_non_existent_files=False,
            processes=processes,
        )
    except OSError:
        click.echo(
            colorize(
                f"The path(s) '{paths}' could not be accessed. Check it/they exist(s).",
                Color.red,
            ),
            err=True,
        )
        sys.exit(1)

    if not fix_even_unparsable:
        exit_code = _handle_files_with_tmp_or_prs_errors(result)

    # NB: We filter to linting violations here, because they're
    # the only ones which can be potentially fixed.
    if result.num_violations(types=SQLLintError, fixable=True) > 0:
        click.echo("==== fixing violations ====")
        click.echo(
            f"{result.num_violations(types=SQLLintError, fixable=True)} fixable "
            "linting violations found")
        if force:
            click.echo(
                f"{colorize('FORCE MODE', Color.red)}: Attempting fixes...")
            success = do_fixes(
                lnt,
                result,
                formatter,
                types=SQLLintError,
                fixed_file_suffix=fixed_suffix,
            )
            if not success:
                sys.exit(1)  # pragma: no cover
        else:
            click.echo("Are you sure you wish to attempt to fix these? [Y/n] ",
                       nl=False)
            c = click.getchar().lower()
            click.echo("...")
            if c in ("y", "\r", "\n"):
                click.echo("Attempting fixes...")
                success = do_fixes(
                    lnt,
                    result,
                    formatter,
                    types=SQLLintError,
                    fixed_file_suffix=fixed_suffix,
                )
                if not success:
                    sys.exit(1)  # pragma: no cover
                else:
                    _completion_message(config)
            elif c == "n":
                click.echo("Aborting...")
                exit_code = 1
            else:  # pragma: no cover
                click.echo("Invalid input, please enter 'Y' or 'N'")
                click.echo("Aborting...")
                exit_code = 1
    else:
        click.echo("==== no fixable linting violations found ====")
        _completion_message(config)

    error_types = [
        (
            dict(types=SQLLintError, fixable=False),
            "  [{} unfixable linting violations found]",
            1,
        ),
    ]
    for num_violations_kwargs, message_format, error_level in error_types:
        num_violations = result.num_violations(**num_violations_kwargs)
        if num_violations > 0:
            click.echo(message_format.format(num_violations))
            exit_code = max(exit_code, error_level)

    if bench:
        click.echo("==== overall timings ====")
        click.echo(cli_table([("Clock time", result.total_time)]))
        timing_summary = result.timing_summary()
        for step in timing_summary:
            click.echo(f"=== {step} ===")
            click.echo(cli_table(timing_summary[step].items()))

    sys.exit(exit_code)