Skip to content

Commit

Permalink
Change linting highlighting implementation a bit
Browse files Browse the repository at this point in the history
- split out code into `darker.highlighting.*` modules
- register custom lexers as setuptools plugin entry points
- refer to built-in and custom Pygments lexers with names instead of
  object instances
- use `PythonLexer` directly instead of using the legacy
  `Python3Lexer` backwards-compatibility name
- separate dummy and real `colorize()` implementations (for use
  without and with Pygments, respectively)
- no need for "fake Pygments" module
- more unit tests
  • Loading branch information
akaihola committed Dec 29, 2020
1 parent bda7193 commit e7f8ba8
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 133 deletions.
11 changes: 4 additions & 7 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ disallow_any_explicit = False
[mypy-darker.config]
disallow_subclassing_any = False

[mypy-darker.highlighting.lexers]
disallow_any_unimported = False
disallow_subclassing_any = False

[mypy-darker.tests.conftest]
disallow_any_unimported = False

Expand All @@ -44,13 +48,6 @@ disallow_any_explicit = False
disallow_any_decorated = False
disallow_untyped_defs = False

[mypy-darker.fake_pygments]
disallow_any_explicit = False

[mypy-darker.highlighting]
disallow_any_unimported = False
disallow_subclassing_any = False

[mypy-darker.utils]
warn_return_any = False

Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ where = src
[options.entry_points]
console_scripts =
darker = darker.__main__:main
pygments.lexers =
lint_location = darker.highlighting.lexers:LocationLexer
lint_description = darker.highlighting.lexers:DescriptionLexer

[options.extras_require]
isort =
Expand Down
65 changes: 0 additions & 65 deletions src/darker/fake_pygments.py

This file was deleted.

12 changes: 12 additions & 0 deletions src/darker/highlighting/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Highlighting of terminal output"""

try:
import pygments # noqa: F401
except ImportError:
from darker.highlighting import without_pygments

colorize = without_pygments.colorize
else:
from darker.highlighting import with_pygments

colorize = with_pygments.colorize
60 changes: 13 additions & 47 deletions src/darker/highlighting.py → src/darker/highlighting/lexers.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,16 @@
"""Highlighting of terminal output"""

import sys
from typing import Generator, Tuple, Union, cast

try:
from pygments import highlight
from pygments.formatters.terminal import TerminalFormatter
from pygments.lexer import Lexer, RegexLexer, bygroups, combined
from pygments.lexers import get_lexer_by_name
from pygments.lexers.python import Python3Lexer
from pygments.token import Error, Number, String, Text, _TokenType

HAS_PYGMENTS = True
except ImportError:
HAS_PYGMENTS = False

from darker.fake_pygments import (
Error,
Lexer,
Number,
Python3Lexer,
RegexLexer,
String,
TerminalFormatter,
Text,
_TokenType,
bygroups,
combined,
get_lexer_by_name,
highlight,
)


def colorize(output: str, lexer: Union[str, Lexer]) -> str:
"""Return the output highlighted for terminal if Pygments is available"""
if not HAS_PYGMENTS or not sys.stdout.isatty():
return output
if isinstance(lexer, str):
lexer = get_lexer_by_name(lexer)
highlighted = highlight(output, lexer, TerminalFormatter())
if "\n" not in output:
# see https://github.com/pygments/pygments/issues/1107
highlighted = highlighted.rstrip("\n")
return cast(str, highlighted)
"""Custom Pygments lexers for highlighting linter output"""

from typing import Generator, Tuple

from pygments.lexer import Lexer, RegexLexer, bygroups, combined
from pygments.lexers.python import PythonLexer
from pygments.token import Error, Number, String, Text, _TokenType


class LocationLexer(Lexer):
"""Lexer for linter output ``path:line:col:` prefix"""
"""Lexer for linter output ``path:line:col:`` prefix"""

aliases = ["lint_location"]

def get_tokens_unprocessed(
self, text: str
Expand All @@ -72,12 +36,14 @@ class DescriptionLexer(RegexLexer):
"""

aliases = "lint_description"

# Make normal text in linter messages look like strings in source code.
# This is a decent choice since it lets source code stand out fairly well.
message = String

# Customize the Python lexer
tokens = Python3Lexer.tokens.copy()
tokens = PythonLexer.tokens.copy()

# Move the main Python lexer into a separate state
tokens["python"] = tokens["root"]
Expand Down
20 changes: 20 additions & 0 deletions src/darker/highlighting/with_pygments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Linter output highlighting helper to be used when Pygments is installed"""

import sys
from typing import cast

from pygments import highlight
from pygments.formatters.terminal import TerminalFormatter
from pygments.lexers import get_lexer_by_name


def colorize(output: str, lexer_name: str) -> str:
"""Return the output highlighted for terminal if Pygments is available"""
if not highlight or not sys.stdout.isatty():
return output
lexer = get_lexer_by_name(lexer_name)
highlighted = highlight(output, lexer, TerminalFormatter())
if "\n" not in output:
# see https://github.com/pygments/pygments/issues/1107
highlighted = highlighted.rstrip("\n")
return cast(str, highlighted)
6 changes: 6 additions & 0 deletions src/darker/highlighting/without_pygments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Linter output highlighting helper to be used when Pygments is not installed"""


def colorize(output: str, lexer_name: str) -> str: # pylint: disable=unused-argument
"""Return the output unaltered"""
return output
8 changes: 3 additions & 5 deletions src/darker/linting.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from typing import Set, Tuple

from darker.git import WORKTREE, EditedLinenumsDiffer, RevisionRange
from darker.highlighting import DescriptionLexer, LocationLexer, colorize
from darker.highlighting import colorize

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -83,8 +83,6 @@ def run_linter(
# assert needed for MyPy (see https://stackoverflow.com/q/57350490/15770)
assert linter_process.stdout is not None
edited_linenums_differ = EditedLinenumsDiffer(git_root, revrange)
location_lexer = LocationLexer()
description_lexer = DescriptionLexer()
prev_path, prev_linenum = None, 0
for line in linter_process.stdout:
path_in_repo, linter_error_linenum, location, description = _parse_linter_line(
Expand All @@ -99,5 +97,5 @@ def run_linter(
if path_in_repo != prev_path or linter_error_linenum > prev_linenum + 1:
print()
prev_path, prev_linenum = path_in_repo, linter_error_linenum
print(colorize(location, location_lexer), end=" ")
print(colorize(description, description_lexer))
print(colorize(location, "lint_location"), end=" ")
print(colorize(description, "lint_description"))
64 changes: 55 additions & 9 deletions src/darker/tests/test_highlighting.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,79 @@
"""Unit tests for :mod:`darker.highlighting`"""

import sys
from unittest.mock import Mock, patch

import pytest
from pygments.token import Token

from darker import highlighting
from darker.highlighting import Python3Lexer
from darker.highlighting import lexers, with_pygments, without_pygments


def test_colorize_import_without_pygments():
"""Dummy ``colorize()`` is used if Pygments isn't available"""
modules = sys.modules.copy()
del modules["darker.highlighting"]
# cause an ImportError for `import pygments`:
modules["pygments"] = None # type: ignore[assignment]
with patch.dict(sys.modules, modules, clear=True):
# pylint: disable=import-outside-toplevel

from darker.highlighting import colorize

assert colorize == without_pygments.colorize


def test_colorize_import_with_pygments():
"""The real ``colorize()`` is used if Pygments is available"""
assert "pygments" in sys.modules
modules = sys.modules.copy()
del modules["darker.highlighting"]
with patch.dict(sys.modules, modules, clear=True):
# pylint: disable=import-outside-toplevel

from darker.highlighting import colorize

assert colorize == with_pygments.colorize


def test_without_pygments_colorize():
"""``colorize()`` does nothing when Pygments isn't available"""
result = without_pygments.colorize("print(42)", "python")

assert result == "print(42)"


@pytest.mark.parametrize(
"text, lexer, tty, expect",
[
(
"except RuntimeError:",
Python3Lexer(),
"python",
True,
"\x1b[34mexcept\x1b[39;49;00m \x1b[36mRuntimeError\x1b[39;49;00m:",
),
("except RuntimeError:", Python3Lexer(), False, "except RuntimeError:"),
("a = 1", Python3Lexer(), True, "a = \x1b[34m1\x1b[39;49;00m"),
("a = 1\n", Python3Lexer(), True, "a = \x1b[34m1\x1b[39;49;00m\n"),
("except RuntimeError:", "python", False, "except RuntimeError:"),
("a = 1", "python", True, "a = \x1b[34m1\x1b[39;49;00m"),
("a = 1\n", "python", True, "a = \x1b[34m1\x1b[39;49;00m\n"),
(
"- a\n+ b\n",
"diff",
True,
"\x1b[91m- a\x1b[39;49;00m\n\x1b[32m+ b\x1b[39;49;00m\n",
),
(
"- a\n+ b\n",
"diff",
True,
"\x1b[91m- a\x1b[39;49;00m\n\x1b[32m+ b\x1b[39;49;00m\n",
),
],
)
def test_colorize(text, lexer, tty, expect):
"""``colorize()`` produces correct highlighted terminal output"""
with patch("sys.stdout.isatty", Mock(return_value=tty)):

result = highlighting.colorize(text, lexer)
result = with_pygments.colorize(text, lexer)

assert result == expect

Expand Down Expand Up @@ -61,7 +107,7 @@ def test_colorize(text, lexer, tty, expect):
)
def test_location_lexer(text, expect):
"""Linter "path:linenum:colnum:" prefixes are lexed correctly"""
location_lexer = highlighting.LocationLexer()
location_lexer = lexers.LocationLexer()

result = list(location_lexer.get_tokens_unprocessed(text))

Expand Down Expand Up @@ -217,7 +263,7 @@ def test_location_lexer(text, expect):
)
def test_description_lexer(text, expect):
"""The description parts of linter output are lexed correctly"""
description_lexer = highlighting.DescriptionLexer()
description_lexer = lexers.DescriptionLexer()

result = list(description_lexer.get_tokens_unprocessed(text))

Expand Down

0 comments on commit e7f8ba8

Please sign in to comment.