Integrate colorama for coloured output #164

Merged
witten merged 5 commits from :feature/coloured-output-first-step into master 2019-05-13 19:50:37 +00:00
18 changed files with 142 additions and 18 deletions

View File

@ -3,13 +3,14 @@ import os
import subprocess
from borgmatic.borg import extract
from borgmatic.logger import get_logger
DEFAULT_CHECKS = ('repository', 'archives')
DEFAULT_PREFIX = '{hostname}-'
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def _parse_checks(consistency_config):

View File

@ -5,9 +5,10 @@ import os
import tempfile
from borgmatic.borg.execute import execute_command
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def _expand_directory(directory):

View File

@ -1,8 +1,9 @@
import logging
import subprocess
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def execute_command(full_command, capture_output=False):

View File

@ -2,8 +2,10 @@ import logging
import sys
import subprocess
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg', remote_path=None):

View File

@ -1,9 +1,10 @@
import logging
from borgmatic.borg.execute import execute_command
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def display_archives_info(

View File

@ -1,8 +1,9 @@
import logging
import subprocess
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def initialize_repository(

View File

@ -1,9 +1,10 @@
import logging
from borgmatic.borg.execute import execute_command
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def list_archives(

View File

@ -1,8 +1,10 @@
import logging
import subprocess
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def _make_prune_flags(retention_config):

View File

@ -1,5 +1,6 @@
from argparse import ArgumentParser
import collections
import colorama
import json
import logging
import os
@ -20,12 +21,12 @@ from borgmatic.borg import (
)
from borgmatic.commands import hook
from borgmatic.config import checks, collect, convert, validate
from borgmatic.logger import should_do_markup, get_logger
from borgmatic.signals import configure_signals
from borgmatic.verbosity import verbosity_to_log_level

Super minor: I tend to alpha order imports within each group, just to make visual scanning easier. E.g., from borgmatic.logger would go on the line after from borgmatic.config.

Super minor: I tend to alpha order imports within each group, just to make visual scanning easier. E.g., `from borgmatic.logger` would go on the line after `from borgmatic.config`.

I'll fix. I'd recommend integrating https://isort.readthedocs.io/en/latest/ so that the tooling mandates and saves you time checking it yourself. Here's a configuration I use myself that plays well with Black: https://git.coop/decentral1se/pypkgtemplate/blob/master/%7B%7Bcookiecutter.package%7D%7D/setup.cfg#L7.

I'll fix. I'd recommend integrating https://isort.readthedocs.io/en/latest/ so that the tooling mandates and saves you time checking it yourself. Here's a configuration I use myself that plays well with Black: https://git.coop/decentral1se/pypkgtemplate/blob/master/%7B%7Bcookiecutter.package%7D%7D/setup.cfg#L7.

Point taken. I filed witten/borgmatic#169 for that.

Point taken. I filed https://projects.torsion.org/witten/borgmatic/issues/169 for that.
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
@ -169,6 +170,9 @@ def parse_arguments(*arguments):
action='store_true',
help='Go through the motions, but do not actually write to any repositories',
)
common_group.add_argument(
'-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'

May seem silly, but I think I'd prefer American spelling for this. I don't have any good reason except perhaps it's just more common among users: https://en.wikipedia.org/wiki/Comparison_of_American_and_British_English#Demographics

But if you like, you can include flags for both (e.g. --no-color and --no-colour) to be more inclusive!

May seem silly, but I think I'd prefer American spelling for this. I don't have any good reason except perhaps it's just more common among users: https://en.wikipedia.org/wiki/Comparison_of_American_and_British_English#Demographics But if you like, you can include flags for both (e.g. `--no-color` and `--no-colour`) to be more inclusive!
)
common_group.add_argument(
'-v',
'--verbosity',
@ -472,6 +476,8 @@ def main(): # pragma: no cover
logger.critical(error)
exit_with_help_link()
colorama.init(autoreset=True, strip=not should_do_markup(args.no_color))
logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s')
if args.version:

View File

@ -1,8 +1,9 @@
import logging
import subprocess
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def execute_hook(commands, config_filename, description, dry_run):

View File

@ -3,8 +3,9 @@ import sys
import logging
from borgmatic.config import collect, validate
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def parse_arguments(*arguments):

View File

@ -1,10 +1,11 @@
import os
import logging
import ruamel.yaml
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def load_configuration(filename):

View File

@ -6,9 +6,10 @@ import pykwalify.errors
import ruamel.yaml
from borgmatic.config import load
from borgmatic.logger import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def schema_filename():

75
borgmatic/logger.py Normal file
View File

@ -0,0 +1,75 @@
import logging
import os
import sys
import colorama
def to_bool(arg):
'''
Return a boolean value based on `arg`.
'''
if arg is None or isinstance(arg, bool):
return arg
if isinstance(arg, str):
arg = arg.lower()
if arg in ('yes', 'on', '1', 'true', 1):
return True
return False

It'd be good to have a simple automated test or three for this function, given that it's pure logic.

It'd be good to have a simple automated test or three for this function, given that it's pure logic.

Makes sense! Also, to note, I took this implementation from the Ansible utility, so it is well battle tested for this use case.

Makes sense! Also, to note, I took this implementation from the Ansible utility, so it is well battle tested for this use case.
def should_do_markup(no_colour):
'''
Determine if we should enable colorama marking up.
'''
if no_colour:
return False
py_colors = os.environ.get('PY_COLORS', None)
if py_colors is not None:
return to_bool(py_colors)
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'

Nice to have the auto-detection here!

Nice to have the auto-detection here!
class BorgmaticLogger(logging.Logger):
def warn(self, msg, *args, **kwargs):
return super(BorgmaticLogger, self).warn(
color_text(colorama.Fore.YELLOW, msg), *args, **kwargs
)
def info(self, msg, *args, **kwargs):
return super(BorgmaticLogger, self).info(
color_text(colorama.Fore.GREEN, msg), *args, **kwargs
)
def debug(self, msg, *args, **kwargs):
return super(BorgmaticLogger, self).debug(
color_text(colorama.Fore.CYAN, msg), *args, **kwargs
)
def critical(self, msg, *args, **kwargs):
return super(BorgmaticLogger, self).critical(
color_text(colorama.Fore.RED, msg), *args, **kwargs
)
def get_logger(name=None):
'''
Build a logger with the given name.
'''
logging.setLoggerClass(BorgmaticLogger)
logger = logging.getLogger(name)
logger.propagate = False

Given that these *_text() functions are only used in one place, I might be inclined to just inline them there. Do not feel strongly though.

Given that these `*_text()` functions are only used in one place, I might be inclined to just inline them there. Do not feel strongly though.
return logger
def color_text(color, msg):
'''
Give colored text.
'''
return '{}{}{}'.format(color, msg, colorama.Style.RESET_ALL)

View File

@ -179,6 +179,12 @@ sudo systemctl start borgmatic.timer
Feel free to modify the timer file based on how frequently you'd like
borgmatic to run.
## Colored Output
Borgmatic uses [colorama](https://pypi.org/project/colorama/) to produce
colored terminal output by default. It is disabled when a non-interactive
terminal is detected (like a cron job). Otherwise, it can be disabled by

Given that borgmatic is used so often from cron jobs, a reader's first reaction might be: "Wait.. Does that mean I need to specify --no-colour if I want to call borgmatic from a cron job?" To get ahead of that concern, it might be good to state here that the "by default" color won't happen on a non-interactive terminal (like a cron job).

Given that borgmatic is used so often from cron jobs, a reader's first reaction might be: "Wait.. Does that mean I need to specify `--no-colour` if I want to call borgmatic from a cron job?" To get ahead of that concern, it might be good to state here that the "by default" color won't happen on a non-interactive terminal (like a cron job).
passing `--no-color` or by setting the environment variable `PY_COLORS=False`.
## Troubleshooting

View File

@ -30,6 +30,11 @@ setup(
]
},
obsoletes=['atticmatic'],
install_requires=('pykwalify>=1.6.0,<14.06', 'ruamel.yaml>0.15.0,<0.16.0', 'setuptools'),
install_requires=(
'pykwalify>=1.6.0,<14.06',
'ruamel.yaml>0.15.0,<0.16.0',
'setuptools',
'colorama>=0.4.1,<0.5',
),
include_package_data=True,
)

View File

@ -3,6 +3,7 @@ atomicwrites==1.2.1
attrs==18.2.0
black==18.9b0; python_version >= '3.6'
Click==7.0
colorama==0.4.1
coverage==4.5.1
docopt==0.6.2
flake8==3.5.0

17
tests/unit/test_logger.py Normal file
View File

@ -0,0 +1,17 @@
import pytest
from borgmatic.logger import to_bool
@pytest.mark.parametrize('bool_val', (True, 'yes', 'on', '1', 'true', 'True', 1))
def test_logger_to_bool_is_true(bool_val):
assert to_bool(bool_val)
@pytest.mark.parametrize('bool_val', (False, 'no', 'off', '0', 'false', 'False', 0))
def test_logger_to_bool_is_false(bool_val):
assert not to_bool(bool_val)
def test_logger_to_bool_returns_none():
assert to_bool(None) is None