diff --git a/README.rst b/README.rst index 55d30ec..85e2c42 100644 --- a/README.rst +++ b/README.rst @@ -438,6 +438,12 @@ virtual environment matching the provided UUID if such environment exists (one e way to find out the environment's UUID is calling *fades* with the ``--where`` option). +Alternatively, you can pass ``--rm`` without a UUID and indicate the dependencies +instead; *fades* will then remove the virtual environment that matches those +dependencies, just like it would select it to run something:: + + fades --rm -d django + Another way to clean up the cache is to remove all venvs that haven't been used for some time. In order to do this you need to call *fades* with ``--clean-unused-venvs``. When fades it's called with this option, it runs in mantain mode, this means that fades will exit @@ -487,6 +493,10 @@ Remove a virtual environment matching the given uuid from disk and cache index:: fades --rm 89a2bf83-c280-4918-a78d-c35506efd69d +Remove the virtual environment matching the indicated dependencies from disk and cache index:: + + fades --rm -d django + Download the script from the given pastebin and executes it (previously building a virtual environment for the dependencies indicated in that pastebin, of course):: fades http://linkode.org/#4QI4TrPlGf1gK2V7jPBC47 diff --git a/fades/main.py b/fades/main.py index 7256691..dc4e043 100644 --- a/fades/main.py +++ b/fades/main.py @@ -45,6 +45,10 @@ # the rest of the module just fine logger = logging.getLogger('fades') +# sentinel used as the '--rm' value when it's given as a bare flag (no UUID), meaning +# "remove the virtualenv that matches the indicated dependencies" +REMOVE_BY_DEPS = object() + # the signals to redirect to the child process (note: only these are # allowed in Windows, see 'signal' doc). REDIRECTED_SIGNALS = [ @@ -260,8 +264,10 @@ def go(): '--python-options', action='append', default=[], help="extra options to be supplied to python (this option can be used multiple times)") parser.add_argument( - '--rm', dest='remove', metavar='UUID', - help="remove a virtualenv by UUID; see --where option to easily find out the UUID") + '--rm', dest='remove', metavar='UUID', nargs='?', const=REMOVE_BY_DEPS, default=None, + help="remove a virtualenv: by UUID if one is given (see --where to easily find out the " + "UUID), or by the indicated dependencies if no UUID is passed " + "(e.g. 'fades --rm -d django')") parser.add_argument( '--clean-unused-venvs', action='store', help="remove venvs that haven't been used for more than the indicated days and compact " @@ -360,22 +366,6 @@ def go(): usage_manager.clean_unused_venvs(max_days_to_keep) return 0 - uuid = args.remove - if uuid: - venv_data = venvscache.get_venv(uuid=uuid) - if venv_data: - # remove this venv from the cache - env_path = venv_data.get('env_path') - if env_path: - envbuilder.destroy_venv(env_path, venvscache) - else: - logger.warning( - "Invalid 'env_path' found in virtualenv metadata: %r. " - "Not removing virtualenv.", env_path) - else: - logger.warning('No virtualenv found with uuid: %s.', uuid) - return 0 - # decided which the child program really is analyzable_child_program, child_program = decide_child_program( args.executable, args.module, args.child_program) @@ -384,10 +374,6 @@ def go(): indicated_deps = consolidate_dependencies( args.ipython, analyzable_child_program, args.requirement, args.dependency) - # Check for packages updates - if args.check_updates: - helpers.check_pypi_updates(indicated_deps) - # get the interpreter version requested for the child_program interpreter, is_current = helpers.get_interpreter_version(args.python) @@ -399,8 +385,33 @@ def go(): if args.system_site_packages: options['venv_options'].append("--system-site-packages") + # remove a virtualenv, either by the given UUID or by the indicated dependencies + if args.remove is not None: + if args.remove is REMOVE_BY_DEPS: + venv_data = venvscache.get_venv(indicated_deps, interpreter, options=options) + descriptor = "the indicated dependencies" + else: + venv_data = venvscache.get_venv(uuid=args.remove) + descriptor = "uuid: {}".format(args.remove) + if venv_data: + # remove this venv from the cache + env_path = venv_data.get('env_path') + if env_path: + envbuilder.destroy_venv(env_path, venvscache) + else: + logger.warning( + "Invalid 'env_path' found in virtualenv metadata: %r. " + "Not removing virtualenv.", env_path) + else: + logger.warning('No virtualenv found with %s.', descriptor) + return 0 + + # Check for packages updates + if args.check_updates: + helpers.check_pypi_updates(indicated_deps) + create_venv = False - venv_data = venvscache.get_venv(indicated_deps, interpreter, uuid, options) + venv_data = venvscache.get_venv(indicated_deps, interpreter, options=options) if venv_data: env_path = venv_data['env_path'] # A venv was found in the cache check if its valid or re-generate it. diff --git a/man/fades.1 b/man/fades.1 index 273ab28..5235683 100644 --- a/man/fades.1 +++ b/man/fades.1 @@ -14,7 +14,7 @@ fades - A system that automatically handles the virtualenvs in the cases normall [\fB-r\fR][\fB--requirement\fR] [\fB-x\fR][\fB--exec\fR] [\fB-p\fR \fIversion\fR][\fB--python\fR=\fIversion\fR] -[\fB--rm\fR=\fIUUID\fR] +[\fB--rm\fR[=\fIUUID\fR]] [\fB--system-site-packages\fR] [\fB--venv-options\fR=\fIoptions\fR] [\fB--pip-options\fR=\fIoptions\fR] @@ -91,8 +91,8 @@ The dependencies can be indicated in multiple places (in the Python source file, Execute the \fIchild_program\fR in the context of the virtual environment. The child_program, which in this case becomes a mandatory parameter, can be just the executable name (relative to the venv's bin directory) or an absolute path. .TP -.BR --rm " " \fIUUID\fR -Remove a virtual environment by UUID. See \fB--get-venv-dir\fR option to easily find out the UUID. +.BR --rm " " [\fIUUID\fR] +Remove a virtual environment. If a UUID is given, the environment with that UUID is removed (see \fB--get-venv-dir\fR option to easily find out the UUID); if no UUID is given, the environment matching the indicated dependencies is removed (e.g. \fBfades --rm -d django\fR). .TP .BR --system-site-packages "" diff --git a/tests/test_main.py b/tests/test_main.py index 9e1d5bf..831de89 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -19,7 +19,7 @@ import os import unittest from pathlib import Path -from unittest.mock import patch +from unittest.mock import MagicMock, patch from packaging.requirements import Requirement @@ -233,6 +233,57 @@ def test_module(self): self.assertEqual(child, child_path) +class RemoveVenvTestCase(unittest.TestCase): + """Check the '--rm' option, removing by uuid or by the indicated dependencies.""" + + def _run(self, cmdline_args, found_venv): + """Run go() with the given cli args, mocking the cache and the venv destruction. + + 'found_venv' is the metadata returned by the cache lookup (or None when nothing matches). + Returns the mocked cache and destroy_venv so the test can check how they were called. + """ + venvscache = MagicMock() + venvscache.get_venv.return_value = found_venv + with patch('sys.argv', ['fades'] + cmdline_args), \ + patch('fades.main.detect_inside_virtualenv', return_value=False), \ + patch('fades.cache.VEnvsCache', return_value=venvscache), \ + patch('fades.envbuilder.UsageManager'), \ + patch('fades.helpers.get_basedir', return_value=Path('/tmp/fades-test')), \ + patch('fades.envbuilder.destroy_venv') as destroy_venv: + result = main.go() + self.assertEqual(result, 0) + return venvscache, destroy_venv + + def test_by_uuid_found(self): + venvscache, destroy_venv = self._run( + ['--rm', 'some-uuid'], found_venv={'env_path': '/path/to/venv'}) + venvscache.get_venv.assert_called_once_with(uuid='some-uuid') + destroy_venv.assert_called_once_with('/path/to/venv', venvscache) + + def test_by_uuid_not_found(self): + venvscache, destroy_venv = self._run(['--rm', 'some-uuid'], found_venv=None) + venvscache.get_venv.assert_called_once_with(uuid='some-uuid') + destroy_venv.assert_not_called() + + def test_by_uuid_invalid_env_path(self): + _, destroy_venv = self._run(['--rm', 'some-uuid'], found_venv={'env_path': None}) + destroy_venv.assert_not_called() + + def test_by_dependencies_found(self): + venvscache, destroy_venv = self._run( + ['--rm', '-d', 'foo'], found_venv={'env_path': '/path/to/venv'}) + # the lookup is done by the indicated dependencies, not by uuid + (_, kwargs) = venvscache.get_venv.call_args + self.assertNotIn('uuid', kwargs) + deps = venvscache.get_venv.call_args[0][0] + self.assertIn('foo', [req.name for req in deps[REPO_PYPI]]) + destroy_venv.assert_called_once_with('/path/to/venv', venvscache) + + def test_by_dependencies_not_found(self): + venvscache, destroy_venv = self._run(['--rm', '-d', 'foo'], found_venv=None) + destroy_venv.assert_not_called() + + # --------------------------------------- # autoimport tests