diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 96f0fe278..7051138d4 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -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): diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index d420cdecc..ccd9e3fe7 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -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): diff --git a/borgmatic/borg/execute.py b/borgmatic/borg/execute.py index 064557afe..890af38f3 100644 --- a/borgmatic/borg/execute.py +++ b/borgmatic/borg/execute.py @@ -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): diff --git a/borgmatic/borg/extract.py b/borgmatic/borg/extract.py index 1b5bce80f..7d0b2a85e 100644 --- a/borgmatic/borg/extract.py +++ b/borgmatic/borg/extract.py @@ -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): diff --git a/borgmatic/borg/info.py b/borgmatic/borg/info.py index 79bfb5ac7..871174762 100644 --- a/borgmatic/borg/info.py +++ b/borgmatic/borg/info.py @@ -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( diff --git a/borgmatic/borg/init.py b/borgmatic/borg/init.py index 9ce419846..bf116876b 100644 --- a/borgmatic/borg/init.py +++ b/borgmatic/borg/init.py @@ -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( diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index 0c2b8bc52..bd18deda5 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -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( diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py index 54e4ba5a3..ef4f8b8f8 100644 --- a/borgmatic/borg/prune.py +++ b/borgmatic/borg/prune.py @@ -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): diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 31e810e2d..fe12443f9 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -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 -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' + ) 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: diff --git a/borgmatic/commands/hook.py b/borgmatic/commands/hook.py index 77cbcc974..0e915c8d2 100644 --- a/borgmatic/commands/hook.py +++ b/borgmatic/commands/hook.py @@ -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): diff --git a/borgmatic/commands/validate_config.py b/borgmatic/commands/validate_config.py index a53c146a8..606b79310 100644 --- a/borgmatic/commands/validate_config.py +++ b/borgmatic/commands/validate_config.py @@ -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): diff --git a/borgmatic/config/load.py b/borgmatic/config/load.py index 3ebe2cf14..a3df4c4e3 100644 --- a/borgmatic/config/load.py +++ b/borgmatic/config/load.py @@ -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): diff --git a/borgmatic/config/validate.py b/borgmatic/config/validate.py index 7818a748c..64b96f534 100644 --- a/borgmatic/config/validate.py +++ b/borgmatic/config/validate.py @@ -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(): diff --git a/borgmatic/logger.py b/borgmatic/logger.py new file mode 100644 index 000000000..cd71c21dc --- /dev/null +++ b/borgmatic/logger.py @@ -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 + + +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' + + +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 + return logger + + +def color_text(color, msg): + ''' + Give colored text. + ''' + return '{}{}{}'.format(color, msg, colorama.Style.RESET_ALL) diff --git a/docs/how-to/set-up-backups.md b/docs/how-to/set-up-backups.md index e8b266dc1..d68e6a513 100644 --- a/docs/how-to/set-up-backups.md +++ b/docs/how-to/set-up-backups.md @@ -187,6 +187,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 +passing `--no-color` or by setting the environment variable `PY_COLORS=False`. ## Troubleshooting diff --git a/setup.py b/setup.py index f87de9bbe..c799d7192 100644 --- a/setup.py +++ b/setup.py @@ -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, ) diff --git a/test_requirements.txt b/test_requirements.txt index 4ea06ce6e..399a28c85 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -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 diff --git a/tests/unit/test_logger.py b/tests/unit/test_logger.py new file mode 100644 index 000000000..60633d9f1 --- /dev/null +++ b/tests/unit/test_logger.py @@ -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