diff --git a/Makefile b/Makefile index 6a67f81d..3537904d 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ ifndef TEST_RUNNER # options are: nose, pytest TEST_RUNNER := pytest endif -UNIT_TEST_COVERAGE := 82 +UNIT_TEST_COVERAGE := 80 INTEGRATION_TEST_COVERAGE := 96 # Project settings diff --git a/gdm/__init__.py b/gdm/__init__.py index 679df17b..086869b3 100644 --- a/gdm/__init__.py +++ b/gdm/__init__.py @@ -15,6 +15,6 @@ exit("Python {}.{}+ is required.".format(*PYTHON_VERSION)) try: - from .commands import install + from .commands import install, uninstall except ImportError: # pragma: no cover (manual test) pass diff --git a/gdm/cli.py b/gdm/cli.py index bd547f78..a1ec314e 100644 --- a/gdm/cli.py +++ b/gdm/cli.py @@ -29,23 +29,31 @@ def main(args=None, function=None): shared = {'formatter_class': common.WideHelpFormatter, 'parents': [project, debug]} - # Build main parser + # Main parser parser = argparse.ArgumentParser(prog=CLI, description=DESCRIPTION, **shared) subs = parser.add_subparsers(help="", dest='command', metavar="") - # Build switch parser - info = "install the specified versions of all dependencies" + # Install parser + info = "get the specified versions of all dependencies" subs.add_parser('install', description=info.capitalize() + '.', help=info, **shared) + # Uninstall parser + info = "remove all installed dependencies" + subs.add_parser('uninstall', description=info.capitalize() + '.', + help=info, **shared) + # Parse arguments args = parser.parse_args(args=args) kwargs = {} if args.command == 'install': function = commands.install kwargs['root'] = args.root + elif args.command == 'uninstall': + function = commands.uninstall + kwargs['root'] = args.root if function is None: parser.print_help() sys.exit(1) diff --git a/gdm/commands.py b/gdm/commands.py index fc38700c..73ee73d0 100644 --- a/gdm/commands.py +++ b/gdm/commands.py @@ -1,9 +1,10 @@ """Functions to manage the installation of dependencies.""" import os +import shutil from . import common -from . import config +from .config import load, install_deps log = common.logger(__name__) @@ -13,7 +14,7 @@ def install(root=None): root = _find_root(root) log.info("installing dependencies...") - count = config.install_deps(root) + count = install_deps(root) if count == 1: log.info("installed 1 dependency") elif count > 1: @@ -24,6 +25,23 @@ def install(root=None): return count +def uninstall(root=None): + """Uninstall dependencies for a project.""" + root = _find_root(root) + + log.info("uninstalling dependencies...") + config = load(root) + if config: + if os.path.exists(config.location): + log.debug("deleting '%s'...", config.location) + shutil.rmtree(config.location) + log.info("dependencies uninstalled") + return True + else: + log.warn("no dependencies to uninstall") + return False + + def _find_root(root, cwd=None): if cwd is None: cwd = os.getcwd() diff --git a/gdm/config.py b/gdm/config.py index b9e4152f..ae6ba52e 100644 --- a/gdm/config.py +++ b/gdm/config.py @@ -123,12 +123,22 @@ def install_deps(self): return count -def install_deps(root, indent=0): - """Install the dependences listed in the project's configuration file.""" +def load(root): + """Load the configuration for the current project.""" + config = None for filename in os.listdir(root): if filename.lower() in Config.FILENAMES: config = Config(root, filename) log.debug("loaded config: %s", config.path) - config.indent = indent - return config.install_deps() - return 0 + break + return config + + +def install_deps(root, indent=0): + """Install the dependences listed in the project's configuration file.""" + config = load(root) + if config: + config.indent = indent + return config.install_deps() + else: + return 0 diff --git a/gdm/test/test_all.py b/gdm/test/test_all.py index 092bd4dd..ec5068ac 100644 --- a/gdm/test/test_all.py +++ b/gdm/test/test_all.py @@ -15,8 +15,7 @@ def test_install(): """Verify dependencies can be installed.""" config = Config(FILES) - if os.path.exists(config.location): - shutil.rmtree(config.location) + shutil.rmtree(config.location, ignore_errors=True) assert not os.path.exists(config.location) # clean install @@ -27,4 +26,16 @@ def test_install(): assert 'gdm_1' in os.listdir(config.location) assert 'gdm_2' in os.listdir(config.location) - shutil.rmtree(os.path.join(FILES, 'src')) + shutil.rmtree(os.path.join(FILES, 'src'), ignore_errors=True) + + +@pytest.mark.integration +def test_uninstall(): + """Verify dependencies can be uninstalled.""" + config = Config(FILES) + assert gdm.install(FILES) + assert os.path.isdir(config.location) + + assert gdm.uninstall(FILES) + + assert not os.path.isdir(config.location) diff --git a/gdm/test/test_cli.py b/gdm/test/test_cli.py index fc2ad941..ec47dfac 100644 --- a/gdm/test/test_cli.py +++ b/gdm/test/test_cli.py @@ -64,6 +64,23 @@ def test_install_root(self, mock_install): mock_install.assert_called_once_with(root='mock/path/to/root') +class TestUninstall: + + """Unit tests for the `uninstall` command.""" + + @patch('gdm.commands.uninstall') + def test_uninstall(self, mock_uninstall): + """Verify the 'uninstall' command can be run.""" + cli.main(['uninstall']) + mock_uninstall.assert_called_once_with(root=None) + + @patch('gdm.commands.uninstall') + def test_uninstall_root(self, mock_uninstall): + """Verify the project's root can be specified.""" + cli.main(['uninstall', '--root', 'mock/path/to/root']) + mock_uninstall.assert_called_once_with(root='mock/path/to/root') + + class TestLogging: """Unit tests for logging.""" diff --git a/gdm/test/test_commands.py b/gdm/test/test_commands.py index 613510fa..fc68eb66 100644 --- a/gdm/test/test_commands.py +++ b/gdm/test/test_commands.py @@ -14,6 +14,7 @@ class TestFindRoot: def test_specified(self): + os.chdir(PROJECT_PARENT) assert FILES == _find_root(FILES) def test_none(self): diff --git a/gdm/test/test_config.py b/gdm/test/test_config.py index a1e9143c..a317bace 100644 --- a/gdm/test/test_config.py +++ b/gdm/test/test_config.py @@ -1,9 +1,11 @@ """Unit tests for the `config` module.""" # pylint: disable=R0201 +import os + import pytest -from gdm.config import Source, Config, install_deps +from gdm.config import Source, Config, load, install_deps from .conftest import FILES @@ -74,6 +76,19 @@ def test_path(self): assert "mock/root/gdm.yml" == config.path +class TestLoad: + + def test_load(self): + """Verify a configuration can be loaded.""" + config = load(FILES) + assert None is not config + + def test_load_missing(self): + """Verify None is returned for a missing config.""" + config = load(os.path.dirname(FILES)) + assert None is config + + class TestInstall: @pytest.mark.integration