Colors and tty (#53)

This commit is contained in:
Christophe Nanteuil 2022-08-30 17:09:12 +02:00 committed by GitHub
parent e63f4962fa
commit 5c7f2b7154
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 191 additions and 140 deletions

View File

@ -14,14 +14,14 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest] # , windows-latest] # see https://github.com/tox-dev/tox/issues/1570
tox:
- env: py36
python-version: '3.6'
- env: py37
python-version: '3.7'
- env: py38
python-version: '3.8'
- env: py39
python-version: '3.9'
- env: py310
python-version: '3.10'
include:
- tox:
env: flake8,mypy,black,pylint

137
pogrep.py
View File

@ -4,12 +4,11 @@
__version__ = "0.1.2"
import argparse
import curses
import glob
import os
import sys
from textwrap import fill
from typing import Sequence, NamedTuple, List, Tuple
from typing import Sequence, NamedTuple, List, Tuple, Optional
from shutil import get_terminal_size
import regex
@ -17,25 +16,54 @@ import polib
from tabulate import tabulate
def get_colors():
"""Just returns the CSI codes for red, green, magenta, and reset color."""
try:
curses.setupterm()
fg_color = curses.tigetstr("setaf") or curses.tigetstr("setf") or ""
red = str(curses.tparm(fg_color, 1), "ascii")
green = str(curses.tparm(fg_color, 2), "ascii")
magenta = str(curses.tparm(fg_color, 5), "ascii")
no_color = str(curses.tigetstr("sgr0"), "ascii")
except curses.error:
red, green, magenta = "", "", ""
no_color = ""
return red, green, magenta, no_color
class GrepColors:
"""Hightlights various components of matched text"""
def __init__(self):
self.colors = { # Default values from grep source code
"mt": "01;31", # both ms/mc
"ms": "01;31", # selected matched text - default: bold red
"mc": "01;31", # context matched text - default: bold red
"fn": "35", # filename - default: magenta
"ln": "32", # line number - default: green
"bn": "32", # byte(sic) offset - default: green
"se": "36", # separator - default: cyan
"sl": "", # selected lines - default: color pair
"cx": "", # context lines - default: color pair
# "rv": None, # -v reverses sl / cx
# "ne": None, # no EL on SGR
}
NO_COLOR = "\33[m\33[K"
def start(self, sgr_chain):
"""Select graphic rendition to highlight the output"""
return "\33[" + self.colors[sgr_chain] + "m\33[K"
def get_from_env_variables(self, grep_envvar):
"""Get color values from GREP_COLOR and GREP_COLORS"""
# old variable, less priority
try:
gc_from_env = os.environ["GREP_COLOR"]
for k in ("mt", "ms", "mc"):
self.colors[k] = gc_from_env
except KeyError:
pass
# new variable, high priority
last_value = ""
try:
for entry in reversed(grep_envvar.split(":")):
key, value = entry.split("=")
if value:
self.colors[key] = value
last_value = value
else:
self.colors[key] = last_value
except (ValueError, KeyError):
return
RED, GREEN, MAGENTA, NO_COLOR = get_colors()
def colorize(text, pattern, prefixes=()):
def colorize(text, pattern, grep_colors, prefixes=()):
"""Add CSI color codes to make pattern red in text.
Optionally also highlight prefixes (as (line, file) tuples) in
@ -45,15 +73,28 @@ def colorize(text, pattern, prefixes=()):
file.py:30:foo bar baz, with the following colors:
| M ||G| |R|
"""
result = regex.sub(pattern, RED + r"\g<0>" + NO_COLOR, text)
result = regex.sub(
pattern, grep_colors.start("ms") + r"\g<0>" + grep_colors.NO_COLOR, text
)
for pnum, pfile in prefixes:
prefix = " " + pfile + pnum
prefix_colored = regex.escape(
regex.sub(pattern, RED + r"\g<0>" + NO_COLOR, prefix)
regex.sub(
pattern,
grep_colors.start("ms") + r"\g<0>" + grep_colors.NO_COLOR,
prefix,
)
)
if regex.escape(RED) in prefix_colored:
if regex.escape(grep_colors.start("ms")) in prefix_colored:
prefix = prefix_colored
prefix_replace = " " + MAGENTA + pfile + GREEN + pnum + NO_COLOR
prefix_replace = (
" "
+ grep_colors.start("fn")
+ pfile
+ grep_colors.start("ln")
+ pnum
+ grep_colors.NO_COLOR
)
result = regex.sub(prefix, prefix_replace, result, count=1)
return result
@ -62,7 +103,7 @@ class Match(NamedTuple):
"""Represents a string found in a po file."""
file: str
line: int
line: Optional[int] # types-polib states that linenum may be None
msgid: str
msgstr: str
@ -80,7 +121,7 @@ def find_in_po(
try:
pofile = polib.pofile(filename)
except OSError:
errors.append("{} doesn't seem to be a .po file".format(filename))
errors.append(f"{filename} doesn't seem to be a .po file")
continue
for entry in pofile:
if entry.msgstr and (
@ -98,12 +139,16 @@ def display_results(
pattern: str,
line_number: bool,
files_with_matches: bool,
grep_colors: GrepColors,
):
"""Display matches as a colorfull table."""
files = {match.file for match in matches}
if files_with_matches: # Just print filenames
for file in files:
print(MAGENTA + file + NO_COLOR)
if grep_colors:
print(grep_colors.start("fn") + file + grep_colors.NO_COLOR)
else:
print(file)
return
prefixes = []
table = []
@ -124,7 +169,14 @@ def display_results(
fill(match.msgstr, width=(term_width - 7) // 2),
]
)
print(colorize(tabulate(table, tablefmt="fancy_grid"), pattern, prefixes))
if grep_colors:
print(
colorize(
tabulate(table, tablefmt="fancy_grid"), pattern, grep_colors, prefixes
)
)
else:
print(tabulate(table, tablefmt="fancy_grid"))
def process_path(path: Sequence[str], recursive: bool) -> List[str]:
@ -145,13 +197,11 @@ def process_path(path: Sequence[str], recursive: bool) -> List[str]:
if recursive:
files.extend(glob.glob(elt + os.sep + "**/*.po", recursive=True))
else:
print(
"{}: {}: Is a directory".format(sys.argv[0], elt), file=sys.stderr
)
print(f"{sys.argv[0]}: {elt}: Is a directory", file=sys.stderr)
sys.exit(1)
else:
print(
"{}: {}: No such file or directory".format(sys.argv[0], elt),
f"{sys.argv[0]}: {elt}: No such file or directory",
file=sys.stderr,
)
sys.exit(1)
@ -229,6 +279,18 @@ def parse_args() -> argparse.Namespace:
"matches GLOB. "
"Ignore any redundant trailing slashes in GLOB.",
)
parser.add_argument(
"--color",
"--colour",
choices=["never", "always", "auto"],
default="auto",
help="Surround the matched (non-empty) strings, matching lines, "
"context lines, file names, line numbers, byte offsets, and separators "
"(for fields and groups of context lines) with escape sequences to "
"display them in color on the terminal. The colors are defined by the "
"environment variable GREP_COLORS. The deprecated environment variable "
"GREP_COLOR is still supported, but its setting does not have priority.",
)
parser.add_argument("pattern")
parser.add_argument("path", nargs="*")
return parser.parse_args()
@ -237,6 +299,18 @@ def parse_args() -> argparse.Namespace:
def main():
"""Command line entry point."""
args = parse_args()
if args.color == "auto":
args.color = sys.stdout.isatty()
else:
args.color = args.color != "never"
if args.color:
grep_colors = GrepColors()
try:
grep_colors.get_from_env_variables(grep_envvar=os.environ["GREP_COLORS"])
except KeyError:
pass
else:
grep_colors = None
if args.fixed_strings:
args.pattern = regex.escape(args.pattern)
if args.word_regexp:
@ -255,6 +329,7 @@ def main():
args.pattern,
args.line_number,
args.files_with_matches,
grep_colors,
)

View File

@ -1,12 +1,4 @@
-r requirements.txt
black
flake8
isort
mypy
pip-tools
polib
pylint
pytest
regex
tabulate
tox

View File

@ -4,109 +4,65 @@
#
# pip-compile requirements-dev.in
#
appdirs==1.4.4
# via
# black
# virtualenv
astroid==2.5.1
# via pylint
attrs==20.3.0
attrs==22.1.0
# via pytest
black==20.8b1
# via -r requirements-dev.in
click==7.1.2
# via
# black
# pip-tools
distlib==0.3.1
build==0.8.0
# via pip-tools
click==8.1.3
# via pip-tools
distlib==0.3.6
# via virtualenv
filelock==3.0.12
filelock==3.8.0
# via
# tox
# virtualenv
flake8==3.8.4
# via -r requirements-dev.in
iniconfig==1.1.1
# via pytest
isort==5.7.0
packaging==21.3
# via
# -r requirements-dev.in
# pylint
lazy-object-proxy==1.5.2
# via astroid
mccabe==0.6.1
# via
# flake8
# pylint
mypy-extensions==0.4.3
# via
# black
# mypy
mypy==0.812
# build
# pytest
# tox
pep517==0.13.0
# via build
pip-tools==6.8.0
# via -r requirements-dev.in
packaging==20.9
platformdirs==2.5.2
# via virtualenv
pluggy==1.0.0
# via
# pytest
# tox
pathspec==0.8.1
# via black
pip-tools==5.5.0
# via -r requirements-dev.in
pluggy==0.13.1
polib==1.1.1
# via -r requirements.txt
py==1.11.0
# via
# pytest
# tox
polib==1.1.0
# via
# -r requirements-dev.in
# -r requirements.txt
py==1.10.0
# via
# pytest
# tox
pycodestyle==2.6.0
# via flake8
pyflakes==2.2.0
# via flake8
pylint==2.7.2
# via -r requirements-dev.in
pyparsing==2.4.7
pyparsing==3.0.9
# via packaging
pytest==6.2.2
pytest==7.1.2
# via -r requirements-dev.in
regex==2020.11.13
# via
# -r requirements-dev.in
# -r requirements.txt
# black
six==1.15.0
# via
# tox
# virtualenv
tabulate==0.8.9
# via
# -r requirements-dev.in
# -r requirements.txt
toml==0.10.2
# via
# black
# pylint
# pytest
# tox
tox==3.22.0
# via -r requirements-dev.in
typed-ast==1.4.2
# via
# black
# mypy
typing-extensions==3.7.4.3
# via
# black
# mypy
virtualenv==20.4.2
# via -r requirements.txt
six==1.16.0
# via tox
wrapt==1.12.1
# via astroid
tabulate==0.8.9
# via -r requirements.txt
toml==0.10.2
# via tox
tomli==2.0.1
# via
# build
# pep517
# pytest
tox==3.25.1
# via -r requirements-dev.in
virtualenv==20.16.3
# via tox
wheel==0.37.1
# via pip-tools
# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools

View File

@ -4,7 +4,7 @@
#
# pip-compile setup.py
#
polib==1.1.0
polib==1.1.1
# via pogrep (setup.py)
regex==2020.11.13
# via pogrep (setup.py)

View File

@ -1,6 +1,10 @@
import pytest
from test.support import change_cwd
from pogrep import RED, GREEN, MAGENTA, colorize, NO_COLOR, process_path, find_in_po
try:
from test.support.os_helper import change_cwd
except ImportError:
from test.support import change_cwd
from pogrep import GrepColors, colorize, process_path, find_in_po
POText = """
msgid ""
@ -79,24 +83,36 @@ culpa qui officia deserunt mollit anim id est laborum."""
TEST_PREFIXES = [["25:", "glossary.po:"], ["42:", "consectetur:"], ["42:", ""]]
@pytest.mark.skipif(RED == "", reason="No curses support in this test env.")
def test_pattern():
assert RED + "fugiat" + NO_COLOR in colorize(
text=TEST_TEXT, pattern="fugiat", prefixes=[]
grep_colors = GrepColors()
grep_colors.get_from_env_variables("ms=99")
assert "\33[99m\33[K" + "fugiat" + grep_colors.NO_COLOR in colorize(
text=TEST_TEXT, pattern="fugiat", grep_colors=grep_colors, prefixes=[]
)
assert "\33[99m\33[K" not in colorize(
text=TEST_TEXT, pattern="hello", grep_colors=grep_colors, prefixes=[]
)
assert RED not in colorize(text=TEST_TEXT, pattern="hello", prefixes=[])
@pytest.mark.skipif(RED == "", reason="No curses support in this test env.")
def test_prefixes():
text = " 42:" + TEST_TEXT
assert GREEN + "42:" + NO_COLOR in colorize(
text=text, pattern="fugiat", prefixes=TEST_PREFIXES
grep_colors = GrepColors()
grep_colors.get_from_env_variables("ln=99:fn=88:ms=77")
assert "\33[99m\33[K" + "42:" + grep_colors.NO_COLOR in colorize(
text=text, pattern="fugiat", grep_colors=grep_colors, prefixes=TEST_PREFIXES
)
text = " consectetur:" + text[1:]
result = colorize(text=text, pattern="consectetur", prefixes=TEST_PREFIXES)
assert MAGENTA + "consectetur:" + GREEN + "42:" + NO_COLOR in result
assert RED + "consectetur" + NO_COLOR in result
result = colorize(
text=text,
pattern="consectetur",
grep_colors=grep_colors,
prefixes=TEST_PREFIXES,
)
assert (
"\33[88m\33[K" + "consectetur:" + "\33[99m\33[K" + "42:" + grep_colors.NO_COLOR
in result
)
assert "\33[77m\33[K" + "consectetur" + grep_colors.NO_COLOR in result
@pytest.fixture
@ -141,3 +157,11 @@ def test_empty_and_recursive(few_files):
"library/lib1.po",
"venv/file1.po",
}
def test_read_grep_colors_envvar():
grep_colors = GrepColors()
grep_colors.get_from_env_variables("ms=:ln=99:fn=88")
assert grep_colors.start("fn") == "\33[88m\33[K"
assert grep_colors.start("ln") == "\33[99m\33[K"
assert grep_colors.start("ms") == "\33[99m\33[K"

View File

@ -2,7 +2,7 @@
max-line-length = 88
[tox]
envlist = py36, py37, py38, py39, flake8, mypy, black, pylint
envlist = py37, py38, py39, py310, flake8, mypy, black, pylint
isolated_build = True
skip_missing_interpreters = True
@ -19,8 +19,12 @@ deps = black
commands = black --check --diff tests/ pogrep.py
[testenv:mypy]
deps = mypy
deps =
mypy
types-polib
types-tabulate
commands = mypy --ignore-missing-imports pogrep.py
[testenv:pylint]
deps = pylint
commands = pylint --disable format pogrep.py