Repository: alanmcruickshank/sqlfluff

Issue:
Feature Request: CLI that updates while running, rather than afterward
The current CLI looks like it's frozen while linting large directories. It should spit out output as it goes rather than just waiting until the end. Potentially with some options for a progress bar or similar.

Comments:

Patch:
diff --git a/src/sqlfluff/cli/commands.py b/src/sqlfluff/cli/commands.py
index 3aac14db..c5efaddc 100644
--- a/src/sqlfluff/cli/commands.py
+++ b/src/sqlfluff/cli/commands.py
@@ -6,9 +6,9 @@ import click
 
 from ..dialects import dialect_selector
 from ..linter import Linter
-from .formatters import (format_config, format_linting_result,
-                         format_linting_violations, format_rules,
-                         format_violation)
+from .formatters import (format_config, format_rules,
+                         format_violation, format_linting_result_header,
+                         format_linting_result_footer)
 from .helpers import cli_table, get_package_version
 
 
@@ -21,7 +21,7 @@ def common_options(f):
     return f
 
 
-def get_linter(dialiect_string, rule_string, exclude_rule_string):
+def get_linter(dialiect_string, rule_string, exclude_rule_string, color=False):
     """ A generic way of getting hold of a linter """
     try:
         dialect_obj = dialect_selector(dialiect_string)
@@ -38,8 +38,9 @@ def get_linter(dialiect_string, rule_string, exclude_rule_string):
         excluded_rule_list = exclude_rule_string.split(',')
     else:
         excluded_rule_list = None
-    # Instantiate the linter and return
-    return Linter(dialect=dialect_obj, rule_whitelist=rule_list, rule_blacklist=excluded_rule_list)
+    # Instantiate the linter and return (with an output function)
+    return Linter(dialect=dialect_obj, rule_whitelist=rule_list, rule_blacklist=excluded_rule_list,
+                  output_func=lambda m: click.echo(m, color=color))
 
 
 @click.group()
@@ -56,7 +57,8 @@ def version(verbose, nocolor, dialect, rules, exclude_rules):
     color = False if nocolor else None
     if verbose > 0:
         # Instantiate the linter
-        lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules)
+        lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules,
+                         color=color)
         click.echo(format_config(lnt, verbose=verbose))
     else:
         click.echo(get_package_version(), color=color)
@@ -69,7 +71,8 @@ def rules(verbose, nocolor, dialect, rules, exclude_rules):
     # Configure Color
     color = False if nocolor else None
     # Instantiate the linter
-    lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules)
+    lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules,
+                     color=color)
     click.echo(format_rules(lnt), color=color)
 
 
@@ -95,21 +98,22 @@ def lint(verbose, nocolor, dialect, rules, exclude_rules, paths):
     # Configure Color
     color = False if nocolor else None
     # Instantiate the linter
-    lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules)
+    lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules, color=color)
     config_string = format_config(lnt, verbose=verbose)
     if len(config_string) > 0:
-        click.echo(config_string, color=color)
+        lnt.log(config_string)
     # Lint the paths
     if verbose > 1:
-        click.echo("==== logging ====")
+        lnt.log("==== logging ====")
     # add stdin if specified via lone '-'
     if ('-',) == paths:
         result = lnt.lint_string(sys.stdin.read(), name='stdin', verbosity=verbose)
     else:
+        # Output the results as we go
+        lnt.log(format_linting_result_header(verbose=verbose))
         result = lnt.lint_paths(paths, verbosity=verbose)
-    # Output the results
-    output = format_linting_result(result, verbose=verbose)
-    click.echo(output, color=color)
+        # Output the final stats
+        lnt.log(format_linting_result_footer(result, verbose=verbose))
     sys.exit(result.stats()['exit code'])
 
 
@@ -121,22 +125,21 @@ def fix(verbose, nocolor, dialect, rules, exclude_rules, force, paths):
     """ Fix SQL files """
     # Configure Color
     color = False if nocolor else None
-    # Instantiate the linter
-    lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules)
+    # Instantiate the linter (with an output function)
+    lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules, color=color)
     config_string = format_config(lnt, verbose=verbose)
     if len(config_string) > 0:
-        click.echo(config_string, color=color)
+        lnt.log(config_string)
     # Check that if fix is specified, that we have picked only a subset of rules
     if lnt.rule_whitelist is None:
-        click.echo(("The fix option is only available in combination"
-                    " with --rules. This is for your own safety!"))
+        lnt.log(("The fix option is only available in combination"
+                 " with --rules. This is for your own safety!"))
         sys.exit(1)
-    # Lint the paths (not with the fix argument at this stage)
-    result = lnt.lint_paths(paths)
+    # Lint the paths (not with the fix argument at this stage), outputting as we go.
+    lnt.log("==== finding violations ====")
+    result = lnt.lint_paths(paths, verbosity=verbose)
 
     if result.num_violations() > 0:
-        click.echo("==== violations found ====")
-        click.echo(format_linting_violations(result, verbose=verbose), color=color)
         click.echo("==== fixing violations ====")
         click.echo("{0} violations found of rule{1} {2}".format(
             result.num_violations(),
@@ -185,29 +188,25 @@ def parse(verbose, nocolor, dialect, rules, exclude_rules, path, recurse):
     if recurse == 0:
         recurse = True
     # Instantiate the linter
-    lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules)
+    lnt = get_linter(dialiect_string=dialect, rule_string=rules, exclude_rule_string=exclude_rules, color=color)
     config_string = format_config(lnt, verbose=verbose)
     if len(config_string) > 0:
-        click.echo(config_string, color=color)
+        lnt.log(config_string)
 
     nv = 0
     # A single path must be specified for this command
     for parsed, violations, time_dict in lnt.parse_path(path, verbosity=verbose, recurse=recurse):
-        click.echo('=== [\u001b[30;1m{0}\u001b[0m] ==='.format(path), color=color)
         if parsed:
-            click.echo(parsed.stringify())
+            lnt.log(parsed.stringify())
         else:
             # TODO: Make this prettier
-            click.echo('...Failed to Parse...', color=color)
+            lnt.log('...Failed to Parse...')
         nv += len(violations)
         for v in violations:
-            click.echo(
-                format_violation(v, verbose=verbose),
-                color=color
-            )
+            lnt.log(format_violation(v, verbose=verbose))
         if verbose >= 2:
-            click.echo("==== timings ====")
-            click.echo(cli_table(time_dict.items()))
+            lnt.log("==== timings ====")
+            lnt.log(cli_table(time_dict.items()))
     if nv > 0:
         sys.exit(66)
     else:
diff --git a/src/sqlfluff/cli/formatters.py b/src/sqlfluff/cli/formatters.py
index 352b3601..45a3f27a 100644
--- a/src/sqlfluff/cli/formatters.py
+++ b/src/sqlfluff/cli/formatters.py
@@ -18,6 +18,10 @@ def format_filename(filename, success=False, verbose=0, success_text='PASS'):
         + "] " + status_string)
 
 
+def format_path(path):
+    return '=== [ path: {0} ] ===\n'.format(colorize(path, 'lightgrey'))
+
+
 def format_violation(violation, verbose=0):
     if isinstance(violation, SQLBaseError):
         code, line, pos, desc = violation.get_info_tuple()
@@ -51,26 +55,37 @@ def format_fix(fix, verbose=0):
     )
 
 
-def format_violations(violations, verbose=0):
+def format_file_violations(fname, res, verbose=0):
+    text_buffer = StringIO()
+    # Success is having no violations
+    success = len(res) == 0
+
+    # Only print the filename if it's either a failure or verbosity > 1
+    if verbose > 1 or not success:
+        text_buffer.write(format_filename(fname, success=success, verbose=verbose))
+        text_buffer.write('\n')
+
+    # If we have violations, print them
+    if not success:
+        # sort by position in file
+        s = sorted(res, key=lambda v: v.char_pos())
+        for violation in s:
+            text_buffer.write(format_violation(violation, verbose=verbose))
+            text_buffer.write('\n')
+    str_buffer = text_buffer.getvalue()
+    # Remove the trailing newline if there is one
+    if len(str_buffer) > 0 and str_buffer[-1] == '\n':
+        str_buffer = str_buffer[:-1]
+    return str_buffer
+
+
+def format_path_violations(violations, verbose=0):
     # Violations should be a dict
     keys = sorted(violations.keys())
     text_buffer = StringIO()
     for key in keys:
-        # Success is having no violations
-        success = len(violations[key]) == 0
-
-        # Only print the filename if it's either a failure or verbosity > 1
-        if verbose > 1 or not success:
-            text_buffer.write(format_filename(key, success=success, verbose=verbose))
-            text_buffer.write('\n')
-
-        # If we have violations, print them
-        if not success:
-            # sort by position in file
-            s = sorted(violations[key], key=lambda v: v.char_pos())
-            for violation in s:
-                text_buffer.write(format_violation(violation, verbose=verbose))
-                text_buffer.write('\n')
+        text_buffer.write(format_file_violations(key, violations[key], verbose=verbose))
+        text_buffer.write('\n')
     str_buffer = text_buffer.getvalue()
     # Remove the trailing newline if there is one
     if len(str_buffer) > 0 and str_buffer[-1] == '\n':
@@ -101,27 +116,58 @@ def format_linting_stats(result, verbose=0):
     return text_buffer.getvalue()
 
 
+def format_linting_path(p, verbose=0):
+    text_buffer = StringIO()
+    if verbose > 0:
+        text_buffer.write(format_path(p))
+    return text_buffer.getvalue()
+
+
+def _format_path_linting_violations(result, verbose=0):
+    text_buffer = StringIO()
+    text_buffer.write(format_linting_path(result.path))
+    text_buffer.write(format_path_violations(result.violations(), verbose=verbose))
+    return text_buffer.getvalue()
+
+
 def format_linting_violations(result, verbose=0):
     """ Assume we're passed a LintingResult """
     text_buffer = StringIO()
-    for path in result.paths:
-        if verbose > 0:
-            text_buffer.write('=== [ path: {0} ] ===\n'.format(colorize(path.path, 'lightgrey')))
-        text_buffer.write(format_violations(path.violations(), verbose=verbose))
+    if hasattr(result, 'paths'):
+        # We've got a full path
+        for path in result.paths:
+            text_buffer.write(_format_path_linting_violations(path, verbose=verbose))
+    else:
+        # We've got an individual
+        text_buffer.write(_format_path_linting_violations(result, verbose=verbose))
     return text_buffer.getvalue()
 
 
-def format_linting_result(result, verbose=0):
+def format_linting_result_header(verbose=0):
     """ Assume we're passed a LintingResult """
     text_buffer = StringIO()
     if verbose >= 1:
         text_buffer.write("==== readout ====\n")
-    text_buffer.write(format_linting_violations(result, verbose=verbose))
+    return text_buffer.getvalue()
+
+
+def format_linting_result_footer(result, verbose=0):
+    """ Assume we're passed a LintingResult """
+    text_buffer = StringIO()
     text_buffer.write('\n')
     text_buffer.write(format_linting_stats(result, verbose=verbose))
     return text_buffer.getvalue()
 
 
+def format_linting_result(result, verbose=0):
+    """ Assume we're passed a LintingResult """
+    text_buffer = StringIO()
+    text_buffer.write(format_linting_result_header(verbose=verbose))
+    text_buffer.write(format_linting_violations(result, verbose=verbose))
+    text_buffer.write(format_linting_result_footer(result, verbose=verbose))
+    return text_buffer.getvalue()
+
+
 def format_config(linter, verbose=0):
     text_buffer = StringIO()
     # Only show version information if verbosity is high enough
diff --git a/src/sqlfluff/linter.py b/src/sqlfluff/linter.py
index af97e26d..4a954d74 100644
--- a/src/sqlfluff/linter.py
+++ b/src/sqlfluff/linter.py
@@ -12,6 +12,8 @@ from .parser.segments_file import FileSegment
 from .parser.segments_base import verbosity_logger, frame_msg, ParseContext
 from .rules.std import standard_rule_set
 
+from .cli.formatters import format_linting_path, format_file_violations
+
 
 class LintedFile(namedtuple('ProtoFile', ['path', 'violations', 'time_dict', 'tree'])):
     __slots__ = ()
@@ -131,7 +133,8 @@ class LintingResult(object):
 
 
 class Linter(object):
-    def __init__(self, dialect=None, sql_exts=('.sql',), rule_whitelist=None, rule_blacklist=None):
+    def __init__(self, dialect=None, sql_exts=('.sql',), rule_whitelist=None,
+                 rule_blacklist=None, output_func=None):
         # NB: dialect defaults to ansi if "none" supplied
         if isinstance(dialect, str) or dialect is None:
             dialect = dialect_selector(dialect)
@@ -141,6 +144,14 @@ class Linter(object):
         # assume that this is a list of rule codes
         self.rule_whitelist = rule_whitelist
         self.rule_blacklist = rule_blacklist
+        # Used for logging as we go
+        self.output_func = output_func
+
+    def log(self, msg):
+        if self.output_func:
+            # Check we've actually got a meaningful message
+            if msg.strip(' \n\t'):
+                self.output_func(msg)
 
     def get_ruleset(self):
         """
@@ -286,7 +297,10 @@ class Linter(object):
 
             vs += linting_errors
 
-        return LintedFile(fname, vs, time_dict, parsed)
+        res = LintedFile(fname, vs, time_dict, parsed)
+        # Do the logging as appropriate
+        self.log(format_file_violations(fname, res.violations, verbose=verbosity))
+        return res
 
     def paths_from_path(self, path):
         # take a path (potentially a directory) and return just the sql files
@@ -324,6 +338,7 @@ class Linter(object):
 
     def lint_path(self, path, verbosity=0, fix=False):
         linted_path = LintedPath(path)
+        self.log(format_linting_path(path, verbose=verbosity))
         for fname in self.paths_from_path(path):
             with open(fname, 'r') as f:
                 linted_path.add(self.lint_file(f, fname=fname, verbosity=verbosity, fix=fix))
@@ -343,5 +358,6 @@ class Linter(object):
 
     def parse_path(self, path, verbosity=0, recurse=True):
         for fname in self.paths_from_path(path):
+            self.log('=== [\u001b[30;1m{0}\u001b[0m] ==='.format(fname))
             with open(fname, 'r') as f:
                 yield self.parse_file(f, fname=fname, verbosity=verbosity, recurse=recurse)
diff --git a/src/sqlfluff/parser/grammar.py b/src/sqlfluff/parser/grammar.py
index 6823e412..ea2ed0ae 100644
--- a/src/sqlfluff/parser/grammar.py
+++ b/src/sqlfluff/parser/grammar.py
@@ -251,7 +251,7 @@ class BaseGrammar(object):
 
         # Have we been passed an empty list?
         if len(segments) == 0:
-            return MatchResult.from_empty()
+            return ((), MatchResult.from_unmatched(segments), None)
 
         # Get hold of the bracket matchers from the dialect, and append them
         # to the list of matchers.
diff --git a/src/sqlfluff/parser/lexer.py b/src/sqlfluff/parser/lexer.py
index cfe708d8..edc959ba 100644
--- a/src/sqlfluff/parser/lexer.py
+++ b/src/sqlfluff/parser/lexer.py
@@ -178,7 +178,7 @@ class Lexer(object):
         res = self.matcher.match(raw, start_pos)
         if len(res.new_string) > 0:
             raise SQLLexError(
-                "Unable to lex characters: '{0}...'".format(
+                "Unable to lex characters: '{0!r}...'".format(
                     res.new_string[:10]),
                 pos=res.new_pos
             )


Test patch:
diff --git a/test/cli_commands_test.py b/test/cli_commands_test.py
index 972ccf5b..1acf65f4 100644
--- a/test/cli_commands_test.py
+++ b/test/cli_commands_test.py
@@ -12,11 +12,24 @@ import sqlfluff
 from sqlfluff.cli.commands import lint, version, rules, fix, parse
 
 
+def invoke_assert_code(ret_code=0, args=None, kwargs=None):
+    args = args or []
+    kwargs = kwargs or {}
+    runner = CliRunner()
+    result = runner.invoke(*args, **kwargs)
+    if ret_code == 0:
+        if result.exception:
+            raise result.exception
+    assert ret_code == result.exit_code
+    return result
+
+
 def test__cli__command_directed():
     """ Basic checking of lint functionality """
-    runner = CliRunner()
-    result = runner.invoke(lint, ['-n', 'test/fixtures/linter/indentation_error_simple.sql'])
-    assert result.exit_code == 65
+    result = invoke_assert_code(
+        ret_code=65,
+        args=[lint, ['-n', 'test/fixtures/linter/indentation_error_simple.sql']]
+    )
     # We should get a readout of what the error was
     check_a = "L:   2 | P:   1 | L003"
     # NB: Skip the number at the end because it's configurable
@@ -27,10 +40,11 @@ def test__cli__command_directed():
 
 def test__cli__command_dialect():
     """ Check the script raises the right exception on an unknown dialect """
-    runner = CliRunner()
-    result = runner.invoke(lint, ['-n', '--dialect', 'faslkjh', 'test/fixtures/linter/indentation_error_simple.sql'])
     # The dialect is unknown should be a non-zero exit code
-    assert result.exit_code == 66
+    invoke_assert_code(
+        ret_code=66,
+        args=[lint, ['-n', '--dialect', 'faslkjh', 'test/fixtures/linter/indentation_error_simple.sql']]
+    )
 
 
 def test__cli__command_lint_a():
@@ -39,19 +53,14 @@ def test__cli__command_lint_a():
     The subprocess command should exit without errors, as
     no issues should be found.
     """
-    runner = CliRunner()
     # Not verbose
-    result = runner.invoke(lint, ['-n', 'test/fixtures/cli/passing_a.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['-n', 'test/fixtures/cli/passing_a.sql']])
     # Verbose
-    result = runner.invoke(lint, ['-n', '-v', 'test/fixtures/cli/passing_a.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['-n', '-v', 'test/fixtures/cli/passing_a.sql']])
     # Very Verbose
-    result = runner.invoke(lint, ['-n', '-vv', 'test/fixtures/cli/passing_a.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['-n', '-vvvv', 'test/fixtures/cli/passing_a.sql']])
     # Very Verbose (Colored)
-    result = runner.invoke(lint, ['-vv', 'test/fixtures/cli/passing_a.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['-vvvv', 'test/fixtures/cli/passing_a.sql']])
 
 
 @pytest.mark.parametrize('command', [
@@ -64,9 +73,7 @@ def test__cli__command_lint_stdin(command):
     """
     with open('test/fixtures/cli/passing_a.sql', 'r') as f:
         sql = f.read()
-    runner = CliRunner()
-    result = runner.invoke(lint, command, input=sql)
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, command], kwargs=dict(input=sql))
 
 
 def test__cli__command_lint_b():
@@ -75,18 +82,14 @@ def test__cli__command_lint_b():
     The subprocess command should exit without errors, as
     no issues should be found.
     """
-    runner = CliRunner()
-    result = runner.invoke(lint, ['-n', 'test/fixtures/cli/passing_b.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['-n', 'test/fixtures/cli/passing_b.sql']])
 
 
 def test__cli__command_parse():
     """
     Check parse command
     """
-    runner = CliRunner()
-    result = runner.invoke(parse, ['-n', 'test/fixtures/cli/passing_b.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[parse, ['-n', 'test/fixtures/cli/passing_b.sql']])
 
 
 def test__cli__command_lint_c_rules_single():
@@ -95,9 +98,7 @@ def test__cli__command_lint_c_rules_single():
     The subprocess command should exit without erros, as
     no issues should be found.
     """
-    runner = CliRunner()
-    result = runner.invoke(lint, ['-n', '--rules', 'L001', 'test/fixtures/linter/operator_errors.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['-n', '--rules', 'L001', 'test/fixtures/linter/operator_errors.sql']])
 
 
 def test__cli__command_lint_c_rules_multi():
@@ -106,9 +107,7 @@ def test__cli__command_lint_c_rules_multi():
     The subprocess command should exit without erros, as
     no issues should be found.
     """
-    runner = CliRunner()
-    result = runner.invoke(lint, ['-n', '--rules', 'L001,L002', 'test/fixtures/linter/operator_errors.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['-n', '--rules', 'L001,L002', 'test/fixtures/linter/operator_errors.sql']])
 
 
 def test__cli__command_lint_c_exclude_rules_single():
@@ -117,9 +116,7 @@ def test__cli__command_lint_c_exclude_rules_single():
     The subprocess command should exit without erros, as
     no issues should be found.
     """
-    runner = CliRunner()
-    result = runner.invoke(lint, ['-n', '--rules', 'L001,L006', '--exclude-rules', 'L006', 'test/fixtures/linter/operator_errors.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['-n', '--rules', 'L001,L006', '--exclude-rules', 'L006', 'test/fixtures/linter/operator_errors.sql']])
 
 
 def test__cli__command_lint_c_exclude_rules_multi():
@@ -128,9 +125,7 @@ def test__cli__command_lint_c_exclude_rules_multi():
     The subprocess command should exit without erros, as
     no issues should be found.
     """
-    runner = CliRunner()
-    result = runner.invoke(lint, ['-n', '--exclude-rules', 'L006,L007', 'test/fixtures/linter/operator_errors.sql'])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['-n', '--exclude-rules', 'L006,L007', 'test/fixtures/linter/operator_errors.sql']])
 
 
 def test__cli__command_versioning():
@@ -165,9 +160,7 @@ def test__cli__command_version():
 
 def test__cli__command_rules():
     """ Just check rules command for exceptions """
-    runner = CliRunner()
-    result = runner.invoke(rules)
-    assert result.exit_code == 0
+    invoke_assert_code(args=[rules])
 
 
 def generic_roundtrip_test(source_file, rulestring):
@@ -180,16 +173,12 @@ def generic_roundtrip_test(source_file, rulestring):
     with open(filepath, mode='w') as dest_file:
         for line in source_file:
             dest_file.write(line)
-    runner = CliRunner()
     # Check that we first detect the issue
-    result = runner.invoke(lint, ['--rules', rulestring, filepath])
-    assert result.exit_code == 65
+    invoke_assert_code(ret_code=65, args=[lint, ['--rules', rulestring, filepath]])
     # Fix the file (in force mode)
-    result = runner.invoke(fix, ['--rules', rulestring, '-f', filepath])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[fix, ['--rules', rulestring, '-f', filepath]])
     # Now lint the file and check for exceptions
-    result = runner.invoke(lint, ['--rules', rulestring, filepath])
-    assert result.exit_code == 0
+    invoke_assert_code(args=[lint, ['--rules', rulestring, filepath]])
     shutil.rmtree(tempdir_path)
 
 
diff --git a/test/cli_formatters_test.py b/test/cli_formatters_test.py
index 8c8ccf0b..6a29eba3 100644
--- a/test/cli_formatters_test.py
+++ b/test/cli_formatters_test.py
@@ -6,7 +6,7 @@ from sqlfluff.rules.crawler import RuleGhost
 from sqlfluff.parser import RawSegment
 from sqlfluff.parser.markers import FilePositionMarker
 from sqlfluff.errors import SQLLintError
-from sqlfluff.cli.formatters import format_filename, format_violation, format_violations
+from sqlfluff.cli.formatters import format_filename, format_violation, format_path_violations
 
 
 def escape_ansi(line):
@@ -50,7 +50,7 @@ def test__cli__formatters__violations():
                 segment=RawSegment('blah', FilePositionMarker(0, 2, 11, 3)),
                 rule=RuleGhost('C', 'DESC'))]
     }
-    f = format_violations(v)
+    f = format_path_violations(v)
     k = sorted(['foo', 'bar'])
     chk = {
         'foo': ["L:  21 | P:   3 | B | DESC", "L:  25 | P:   2 | A | DESC"],