Skip to content

Commit

Permalink
Merge ce4cbf8 into 3b19f6c
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra committed Mar 16, 2018
2 parents 3b19f6c + ce4cbf8 commit 5d859ca
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 36 deletions.
74 changes: 54 additions & 20 deletions backuppy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import yaml

from backuppy import task
from backuppy.config import from_json, from_yaml
from backuppy.task import backup

FORMAT_JSON_EXTENSIONS = ('json',)
FORMAT_YAML_EXTENSIONS = ('yml', 'yaml')
Expand All @@ -31,17 +31,19 @@ class ConfigurationAction(argparse.Action):

def __init__(self, *args, **kwargs):
"""Initialize a new instance."""
argparse.Action.__init__(self, *args, required=True, help='The path to the back-up configuration file.',
**kwargs)
kwargs.setdefault('required', True)
kwargs.setdefault('help', 'The path to the back-up configuration file.')
argparse.Action.__init__(self, *args, **kwargs)

def __call__(self, parser, namespace, values, option_string=None):
"""Invoke the action."""
configuration_file_path = values

verbose = None
if namespace.quiet:
verbose = False
if namespace.verbose:
verbose = True
configuration_file_path = values
with open(configuration_file_path) as f:
if any(map(f.name.endswith, FORMAT_JSON_EXTENSIONS)):
configuration_factory = from_json
Expand All @@ -55,7 +57,7 @@ def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, configuration)


def add_configuration_to_args(parser):
def add_configuration_to_parser(parser):
"""Add configuration options to a parser.
:param parser: argparse.ArgumentParser
Expand All @@ -80,6 +82,41 @@ def add_verbose_to_args(parser):
return parser


def add_backup_command_to_parser(parser):
"""Add the back-up command to a parser.
:param parser: argparse.ArgumentParser
:return: argparse.ArgumentParser
"""
backup_parser = parser.add_parser('backup', help='Starts a back-up.')
backup_parser.set_defaults(func=lambda parsed_args: task.backup(parsed_args.configuration))
add_configuration_to_parser(backup_parser)
return parser


def add_init_command_to_parser(parser):
"""Add the configuration initialization command to a parser.
:param parser: argparse.ArgumentParser
:return: argparse.ArgumentParser
"""
init_parser = parser.add_parser('init', help='Initializes a new back-up configuration.')
init_parser.set_defaults(func=lambda parsed_args: init())
return parser


def add_commands_to_parser(parser):
"""Add Backuppy commands to a parser.
:param parser: argparse.ArgumentParser
:return: argparse.ArgumentParser
"""
subparsers = parser.add_subparsers()
add_backup_command_to_parser(subparsers)
add_init_command_to_parser(subparsers)
return parser


def ask_confirm(value_label, question=None, default=None):
"""Ask for a confirmation.
Expand Down Expand Up @@ -237,22 +274,19 @@ def _file_path_validator(path):

def main(args):
"""Provide the CLI entry point."""
parser = argparse.ArgumentParser(
description='Backuppy backs up and restores your data using rsync.')

subparsers = parser.add_subparsers()

backup_parser = subparsers.add_parser('backup', help='Starts a back-up.')
backup_parser.set_defaults(func=lambda subparser_cli_args: backup(
subparser_cli_args['configuration']))
add_configuration_to_args(backup_parser)

init_parser = subparsers.add_parser(
'init', help='Initializes a new back-up configuration.')
init_parser.set_defaults(func=lambda subparser_cli_args: init())
parser = argparse.ArgumentParser(description='Backuppy backs up and restores your data using rsync.')
add_commands_to_parser(parser)

if args:
cli_args = parser.parse_args(args)
cli_args.func(vars(cli_args))
parsed_args = parser.parse_args(args)
try:
parsed_args.func(parsed_args)
except KeyboardInterrupt:
# Quit gracefully.
print('Quitting...')
except BaseException:
configuration = parsed_args.configuration
configuration.logger.exception('A fatal error occurred.')
configuration.notifier.alert('A fatal error occurred. Details have been logged as per your configuration.')
else:
parser.print_help()
5 changes: 3 additions & 2 deletions backuppy/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Provides configuration components."""
import json
import logging
import os
from logging import getLogger, config as logging_config
from logging import config as logging_config

import yaml

Expand All @@ -27,7 +28,7 @@ def __init__(self, name, working_directory=None, verbose=False):
self._source = None
self._target = None
self._notifier = None
self._logger = getLogger('backuppy')
self._logger = logging.getLogger('backuppy')

@property
def verbose(self):
Expand Down
77 changes: 64 additions & 13 deletions backuppy/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import json
import os
import subprocess
from logging import Logger
from tempfile import NamedTemporaryFile
from unittest import TestCase

from parameterized import parameterized

from backuppy.cli import main, FORMAT_JSON_EXTENSIONS, FORMAT_YAML_EXTENSIONS, ask_confirm, ask_option, ask_any
from backuppy.config import from_json, from_yaml
from backuppy.location import PathSource, PathTarget
from backuppy.tests import CONFIGURATION_PATH

try:
from unittest.mock import patch
from unittest.mock import patch, call, Mock
except ImportError:
from mock import patch
from mock import patch, call, Mock

try:
from tempfile import TemporaryDirectory
except ImportError:
from backports.tempfile import TemporaryDirectory

from backuppy.cli import main, FORMAT_JSON_EXTENSIONS, FORMAT_YAML_EXTENSIONS, ask_confirm, ask_option, ask_any
from backuppy.config import from_json, from_yaml
from backuppy.location import PathSource, PathTarget
from backuppy.tests import CONFIGURATION_PATH


class AskConfirmTest(TestCase):
@parameterized.expand([
Expand Down Expand Up @@ -90,17 +93,16 @@ def test_ask_any_with_validator(self, m_input):

def _validator(value):
return value + 'Baz'

actual = ask_any('Foo', validator=_validator)
self.assertEquals(actual, 'BarBaz')


class CliTest(TestCase):
def test_help_appears_in_readme(self):
"""Assert that the CLI command's help output in README.md is up-to-date."""
cli_help = subprocess.check_output(
['backuppy', '--help']).decode('utf-8')
readme_path = os.path.join(os.path.dirname(
os.path.dirname(os.path.dirname(__file__))), 'README.md')
cli_help = subprocess.check_output(['backuppy', '--help']).decode('utf-8')
readme_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'README.md')
with open(readme_path) as f:
self.assertIn(cli_help, f.read())

Expand All @@ -117,16 +119,65 @@ def test_backup_with_json(self):
args = ['backup', '-c', configuration_file_path]
main(args)

def test_backup_with_yaml(self):
@patch('sys.stdout')
@patch('sys.stderr')
def test_backup_with_yaml(self, m_stdout, m_stderr):
configuration_file_path = '%s/backuppy.yml' % CONFIGURATION_PATH
args = ['backup', '-c', configuration_file_path]
main(args)

def test_backup_without_arguments(self):
@patch('sys.stdout')
@patch('sys.stderr')
def test_backup_without_arguments(self, m_stdout, m_stderr):
args = ['backup']
with self.assertRaises(SystemExit):
main(args)

@patch('sys.stdout')
@patch('sys.stderr')
@patch('backuppy.task.backup')
def test_keyboard_interrupt_in_command_should_exit_gracefully(self, m_backup, m_stderr, m_stdout):
m_backup.side_effect = KeyboardInterrupt
configuration_file_path = '%s/backuppy.json' % CONFIGURATION_PATH
args = ['backup', '-c', configuration_file_path]
main(args)
m_stdout.write.assert_has_calls([call('Quitting...')])
m_stderr.write.assert_not_called()

@parameterized.expand([
(ValueError,),
(RuntimeError,),
(AttributeError,),
(ImportError,),
(NotImplementedError,),
])
@patch('sys.stdout')
@patch('sys.stderr')
@patch('backuppy.task.backup')
@patch('logging.getLogger')
def test_error_in_command(self, error_type, m_get_logger, m_backup, m_stderr, m_stdout):
m_backup.side_effect = error_type
m_logger = Mock(Logger)
m_get_logger.return_value = m_logger
with open('%s/backuppy.json' % CONFIGURATION_PATH) as f:
configuration = json.load(f)
configuration['notifications'] = [
{
'type': 'stdio',
}
]
with NamedTemporaryFile(mode='w+t', suffix='.json') as f:
json.dump(configuration, f)
f.seek(0)
args = ['backup', '-c', f.name]
main(args)
m_get_logger.assert_called_with('backuppy')
self.assertTrue(m_logger.exception.called)
m_stderr.write.assert_has_calls([
call(
'\x1b[0;41m \x1b[0;1;31m A fatal error occurred. Details have been logged as per your configuration.\x1b[0m'),
])


class CliInitTest(TestCase):
@parameterized.expand([
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ coverage ~= 4.4.1
coveralls ~= 1.2.0
flake8 ~= 3.3.0
m2r ~= 0.1.13
mock ~= 2.0.0; python_version < "3.3"
mock ~= 2.0.0
nose2 ~= 0.6.5
parameterized ~= 0.6.1
pydocstyle ~= 2.1.1
Expand Down

0 comments on commit 5d859ca

Please sign in to comment.