add --color option

This commit is contained in:
Anthony Sottile 2021-11-05 20:05:53 -04:00
parent 05cae7e046
commit 848003cc05
10 changed files with 181 additions and 2 deletions

View file

@ -40,6 +40,8 @@ Index of Options
- :option:`flake8 --quiet` - :option:`flake8 --quiet`
- :option:`flake8 --color`
- :option:`flake8 --count` - :option:`flake8 --count`
- :option:`flake8 --diff` - :option:`flake8 --diff`
@ -181,6 +183,35 @@ Options and their Descriptions
quiet = 1 quiet = 1
.. option:: --color
:ref:`Go back to index <top>`
Whether to use color in output. Defaults to ``auto``.
Possible options are ``auto``, ``always``, and ``never``.
This **can** be specified in config files.
When color is enabled, the following substitutions are enabled:
- ``%(bold)s``
- ``%(black)s``
- ``%(red)s``
- ``%(green)s``
- ``%(yellow)s``
- ``%(blue)s``
- ``%(magenta)s``
- ``%(cyan)s``
- ``%(white)s``
- ``%(reset)s``
Example config file usage:
.. code-block:: ini
color = never
.. option:: --count .. option:: --count

View file

@ -0,0 +1,59 @@
"""ctypes hackery to enable color processing on windows.
See: https://github.com/pre-commit/pre-commit/blob/cb40e96/pre_commit/color.py
"""
import sys
if sys.platform == "win32": # pragma: no cover (windows)
def _enable() -> None:
from ctypes import POINTER
from ctypes import windll
from ctypes import WinError
from ctypes import WINFUNCTYPE
from ctypes.wintypes import BOOL
from ctypes.wintypes import DWORD
from ctypes.wintypes import HANDLE
STD_ERROR_HANDLE = -12
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
def bool_errcheck(result, func, args):
if not result:
raise WinError()
return args
GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(
("GetStdHandle", windll.kernel32),
((1, "nStdHandle"),),
)
GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
("GetConsoleMode", windll.kernel32),
((1, "hConsoleHandle"), (2, "lpMode")),
)
GetConsoleMode.errcheck = bool_errcheck
SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
("SetConsoleMode", windll.kernel32),
((1, "hConsoleHandle"), (1, "dwMode")),
)
SetConsoleMode.errcheck = bool_errcheck
# As of Windows 10, the Windows console supports (some) ANSI escape
# sequences, but it needs to be enabled using `SetConsoleMode` first.
#
# More info on the escape sequences supported:
# https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx
stderr = GetStdHandle(STD_ERROR_HANDLE)
flags = GetConsoleMode(stderr)
SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
try:
_enable()
except OSError:
terminal_supports_color = False
else:
terminal_supports_color = True
else: # pragma: win32 no cover
terminal_supports_color = True

View file

@ -8,6 +8,8 @@ from typing import Optional
from typing import Tuple from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from flake8.formatting import _windows_color
if TYPE_CHECKING: if TYPE_CHECKING:
from flake8.statistics import Statistics from flake8.statistics import Statistics
from flake8.style_guide import Violation from flake8.style_guide import Violation
@ -51,6 +53,11 @@ class BaseFormatter:
self.filename = options.output_file self.filename = options.output_file
self.output_fd: Optional[IO[str]] = None self.output_fd: Optional[IO[str]] = None
self.newline = "\n" self.newline = "\n"
self.color = options.color == "always" or (
options.color == "auto"
and sys.stdout.isatty()
and _windows_color.terminal_supports_color
)
self.after_init() self.after_init()
def after_init(self) -> None: def after_init(self) -> None:

View file

@ -8,6 +8,20 @@ from flake8.formatting import base
if TYPE_CHECKING: if TYPE_CHECKING:
from flake8.style_guide import Violation from flake8.style_guide import Violation
COLORS = {
"bold": "\033[1m",
"black": "\033[30m",
"red": "\033[31m",
"green": "\033[32m",
"yellow": "\033[33m",
"blue": "\033[34m",
"magenta": "\033[35m",
"cyan": "\033[36m",
"white": "\033[37m",
"reset": "\033[m",
}
COLORS_OFF = {k: "" for k in COLORS}
class SimpleFormatter(base.BaseFormatter): class SimpleFormatter(base.BaseFormatter):
"""Simple abstraction for Default and Pylint formatter commonality. """Simple abstraction for Default and Pylint formatter commonality.
@ -39,6 +53,7 @@ class SimpleFormatter(base.BaseFormatter):
"path": error.filename, "path": error.filename,
"row": error.line_number, "row": error.line_number,
"col": error.column_number, "col": error.column_number,
**(COLORS if self.color else COLORS_OFF),
} }
@ -49,7 +64,11 @@ class Default(SimpleFormatter):
format string. format string.
""" """
error_format = "%(path)s:%(row)d:%(col)d: %(code)s %(text)s" error_format = (
"%(bold)s%(path)s%(reset)s"
"%(cyan)s:%(reset)s%(row)d%(cyan)s:%(reset)s%(col)d%(cyan)s:%(reset)s "
"%(bold)s%(red)s%(code)s%(reset)s %(text)s"
)
def after_init(self) -> None: def after_init(self) -> None:
"""Check for a custom format string.""" """Check for a custom format string."""

View file

@ -91,6 +91,7 @@ def register_default_options(option_manager):
The default options include: The default options include:
- ``-q``/``--quiet`` - ``-q``/``--quiet``
- ``--color``
- ``--count`` - ``--count``
- ``--diff`` - ``--diff``
- ``--exclude`` - ``--exclude``
@ -118,7 +119,6 @@ def register_default_options(option_manager):
""" """
add_option = option_manager.add_option add_option = option_manager.add_option
# pep8 options
add_option( add_option(
"-q", "-q",
"--quiet", "--quiet",
@ -128,6 +128,13 @@ def register_default_options(option_manager):
help="Report only file names, or nothing. This option is repeatable.", help="Report only file names, or nothing. This option is repeatable.",
) )
add_option(
"--color",
choices=("auto", "always", "never"),
default="auto",
help="Whether to use color in output. Defaults to `%(default)s`.",
)
add_option( add_option(
"--count", "--count",
action="store_true", action="store_true",

View file

@ -269,6 +269,12 @@ class PluginManager: # pylint: disable=too-few-public-methods
"flake8>=3.7 (which implements per-file-ignores itself)." "flake8>=3.7 (which implements per-file-ignores itself)."
) )
continue continue
elif entry_point.name == "flake8-colors":
LOG.warning(
"flake8-colors plugin is incompatible with "
"flake8>=4.1 (which implements colors itself)."
)
continue
self._load_plugin_from_entrypoint(entry_point) self._load_plugin_from_entrypoint(entry_point)
def _load_plugin_from_entrypoint(self, entry_point, local=False): def _load_plugin_from_entrypoint(self, entry_point, local=False):

View file

@ -1,15 +1,18 @@
"""Tests for the BaseFormatter object.""" """Tests for the BaseFormatter object."""
import argparse import argparse
import sys
from unittest import mock from unittest import mock
import pytest import pytest
from flake8 import style_guide from flake8 import style_guide
from flake8.formatting import _windows_color
from flake8.formatting import base from flake8.formatting import base
def options(**kwargs): def options(**kwargs):
"""Create an argparse.Namespace instance.""" """Create an argparse.Namespace instance."""
kwargs.setdefault("color", "auto")
kwargs.setdefault("output_file", None) kwargs.setdefault("output_file", None)
kwargs.setdefault("tee", False) kwargs.setdefault("tee", False)
return argparse.Namespace(**kwargs) return argparse.Namespace(**kwargs)
@ -136,6 +139,49 @@ def test_write_produces_stdout(capsys):
assert capsys.readouterr().out == f"{line}\n{source}\n" assert capsys.readouterr().out == f"{line}\n{source}\n"
def test_color_always_is_true():
"""Verify that color='always' sets it to True."""
formatter = base.BaseFormatter(options(color="always"))
assert formatter.color is True
def _mock_isatty(val):
attrs = {"isatty.return_value": val}
return mock.patch.object(sys, "stdout", **attrs)
def _mock_windows_color(val):
return mock.patch.object(_windows_color, "terminal_supports_color", val)
def test_color_auto_is_true_for_tty():
"""Verify that color='auto' sets it to True for a tty."""
with _mock_isatty(True), _mock_windows_color(True):
formatter = base.BaseFormatter(options(color="auto"))
assert formatter.color is True
def test_color_auto_is_false_without_tty():
"""Verify that color='auto' sets it to False without a tty."""
with _mock_isatty(False), _mock_windows_color(True):
formatter = base.BaseFormatter(options(color="auto"))
assert formatter.color is False
def test_color_auto_is_false_if_not_supported_on_windows():
"""Verify that color='auto' is False if not supported on windows."""
with _mock_isatty(True), _mock_windows_color(False):
formatter = base.BaseFormatter(options(color="auto"))
assert formatter.color is False
def test_color_never_is_false():
"""Verify that color='never' sets it to False despite a tty."""
with _mock_isatty(True), _mock_windows_color(True):
formatter = base.BaseFormatter(options(color="never"))
assert formatter.color is False
class AfterInitFormatter(base.BaseFormatter): class AfterInitFormatter(base.BaseFormatter):
"""Subclass for testing after_init.""" """Subclass for testing after_init."""

View file

@ -7,6 +7,7 @@ from flake8.formatting import default
def options(**kwargs): def options(**kwargs):
"""Create an argparse.Namespace instance.""" """Create an argparse.Namespace instance."""
kwargs.setdefault("color", "auto")
kwargs.setdefault("output_file", None) kwargs.setdefault("output_file", None)
kwargs.setdefault("tee", False) kwargs.setdefault("tee", False)
return argparse.Namespace(**kwargs) return argparse.Namespace(**kwargs)

View file

@ -7,6 +7,7 @@ from flake8.formatting import default
def options(**kwargs): def options(**kwargs):
"""Create an argparse.Namespace instance.""" """Create an argparse.Namespace instance."""
kwargs.setdefault("color", "auto")
kwargs.setdefault("output_file", None) kwargs.setdefault("output_file", None)
kwargs.setdefault("tee", False) kwargs.setdefault("tee", False)
return argparse.Namespace(**kwargs) return argparse.Namespace(**kwargs)

View file

@ -127,6 +127,8 @@ commands =
# Once Flake8 3.0 is released and in a good state, we can use both and it will # Once Flake8 3.0 is released and in a good state, we can use both and it will
# work well \o/ # work well \o/
ignore = D203, W503, E203, N818 ignore = D203, W503, E203, N818
per-file-ignores =
src/flake8/formatting/_windows_color.py: N806
exclude = exclude =
.tox, .tox,
.git, .git,