Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
57 changes: 34 additions & 23 deletions fades/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default=None is not used, right? Or have a behaviour that escapes me?

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 "
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand All @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions man/fades.1
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message in ArgumentParser mentions --where, which I think it's better that --get-venv-dir (in any case, we should be consistent)


.TP
.BR --system-site-packages ""
Expand Down
53 changes: 52 additions & 1 deletion tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Loading