diff --git a/README.rst b/README.rst index bb9d48f..6115f74 100644 --- a/README.rst +++ b/README.rst @@ -371,6 +371,14 @@ Uses the ``django-admin.py`` script to start a new project named ``foo``, withou Removes a virtualenv matching the given uuid from disk and cache index. +``fades --list-venvs`` + +List all virtualenvs, showing the information (UUID, timestamp, dependencies, interpreter, etc). + +``fades --list-venvs ipython,prospector`` + +Optionally filter the list using FILTER, FILTER can be a word string or comma separated words. + What if Python is updated in my system? --------------------------------------- diff --git a/fades/helpers.py b/fades/helpers.py index 73c5e4b..5ad0a7c 100644 --- a/fades/helpers.py +++ b/fades/helpers.py @@ -16,12 +16,15 @@ """A collection of utilities for fades.""" -import os -import sys + import json import logging +import os +import re import subprocess +import sys +from datetime import datetime from urllib import request from urllib.error import HTTPError @@ -39,6 +42,24 @@ print(json.dumps(d)) """ +LIST_VENVS_TEMPLATE = """ +Virtualenv UUID: {uid} + timestamp: {dat} + full path: {pat} + dependencies: {pac} + interpreter: {pyv} + options: {opt} +""" + +# UUID Regex, for v1 to v5. +UUID_REGEX = re.compile(( + '[a-f0-9]{8}-' + '[a-f0-9]{4}-' + '[1-5]' # Versions. + '[a-f0-9]{3}-' + '[89ab][a-f0-9]{3}-' + '[a-f0-9]{12}$' +), re.IGNORECASE) # the url to query PyPI for project versions BASE_PYPI_URL = 'https://pypi.python.org/pypi/{name}/json' @@ -256,3 +277,25 @@ def check_pypi_exists(dependencies): logger.error("%s doesn't exists in PyPI.", dependency) return False return True + + +def list_venvs(index_path, query=None, template=LIST_VENVS_TEMPLATE): + """List all venvs from an index file path and print info to stdout.""" + if os.path.isfile(index_path): + if query: + query = tuple(set(query.lower().strip().split(","))) + venv_info = "" + with open(index_path) as jotason: + for jotason_line in jotason: + if query and not any([qry in jotason_line.lower() for qry in query]): + continue + v_dct_get = json.loads(jotason_line).get + venv_info += template.format( + uid=UUID_REGEX.search(v_dct_get("metadata")["env_path"]).group(0), + pat=v_dct_get("metadata")["env_path"], + pac=v_dct_get("installed"), + pyv=v_dct_get("interpreter"), + opt=v_dct_get("options"), + dat=datetime.fromtimestamp(v_dct_get("timestamp"))) + print(venv_info) + return venv_info diff --git a/fades/main.py b/fades/main.py index 588ec18..e3c6676 100644 --- a/fades/main.py +++ b/fades/main.py @@ -125,13 +125,15 @@ def go(): 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.")) + help=("Remove a virtualenv by UUID (see --list-venvs).")) parser.add_argument('--clean-unused-venvs', action='store', help=("This option remove venvs that haven't been used for more than " "CLEAN_UNUSED_VENVS days. Appart from that, will compact usage " "stats file.\n" "When this option is present, the cleaning takes place at the " "beginning of the execution.")) + parser.add_argument('-l', '--list-venvs', metavar='FILTER', nargs='?', const=' ', + help=("List all venvs. Optionally filter the list by FILTER.")) parser.add_argument('child_program', nargs='?', default=None) parser.add_argument('child_options', nargs=argparse.REMAINDER) @@ -161,6 +163,11 @@ def go(): logger.debug("Starting fades v. %s", fades.__version__) logger.debug("Arguments: %s", args) + if args.list_venvs: + print(f"args.list_venvs:{args.list_venvs}.") + helpers.list_venvs(os.path.join(helpers.get_basedir(), 'venvs.idx'), args.list_venvs) + sys.exit(0) + # verify that the module is NOT being used from a virtualenv if detect_inside_virtualenv(sys.prefix, getattr(sys, 'real_prefix', None), getattr(sys, 'base_prefix', None)): diff --git a/man/fades.1 b/man/fades.1 index 3b3979f..b7c0dc3 100644 --- a/man/fades.1 +++ b/man/fades.1 @@ -20,6 +20,7 @@ fades - A system that automatically handles the virtualenvs in the cases normall [\fB--pip-options\fR=\fIoptions\fR] [\fB--python-options\fR=\fIoptions\fR] [\fB--check-updates\fR] +[\fB--list-venvs\fR] [\fB--clean-unused-venvs\fR=\fImax_days_to_keep\fR] [child_program [child_options]] @@ -104,7 +105,7 @@ Extra options to be supplied to pip. (this option can be used multiple times) .BR --python-options=\fIPYTHON_OPTION\fR Extra options to be supplied to python. (this option can be used multiple times) -.TP +.TP .BR --check-updates Will check for updates in PyPI to verify if there are new versions for the requested dependencies. If a new version is available for a dependency, it will use it (if the dependency was requested without version) or just inform which new version is available (if the dependency was requested with a specific version). @@ -112,6 +113,10 @@ Will check for updates in PyPI to verify if there are new versions for the reque .BR --clean-unused-venvs=\fIMAX_DAYS_TO_KEEP\fR Will remove all virtualenvs that haven't been used for more than MAX_DAYS_TO_KEEP days. +.TP +.BR --list-venvs\fIFILTER\fR +List all virtualenvs, showing information (UUID, timestamp, path, dependencies, interpreter, etc). Optionally filter the list using FILTER, FILTER can be a word string or comma separated words. + .SH EXAMPLES .TP diff --git a/tests/examples/output.txt b/tests/examples/output.txt new file mode 100644 index 0000000..03328e9 --- /dev/null +++ b/tests/examples/output.txt @@ -0,0 +1,56 @@ + +Virtualenv UUID: d1cf2e61-4ac5-467d-9b65-1a3c0e913bf3 + timestamp: 2011-06-16 00:00:00 + full path: /home/juan/.fades/d1cf2e61-4ac5-467d-9b65-1a3c0e913bf3 + dependencies: {} + interpreter: /usr/bin/python3 + options: {'pyvenv_options': [], 'virtualenv_options': []} + +Virtualenv UUID: 032e6e2c-bc7e-4a7f-ad1d-66ecb9106884 + timestamp: 2009-02-09 00:00:00 + full path: /home/juan/.fades/032e6e2c-bc7e-4a7f-ad1d-66ecb9106884 + dependencies: {} + interpreter: /usr/bin/python3 + options: {'pyvenv_options': [], 'virtualenv_options': []} + +Virtualenv UUID: 53c8f169-b105-4ed1-a7fc-25792b18fc4c + timestamp: 2001-06-17 00:00:00 + full path: /home/juan/.fades/53c8f169-b105-4ed1-a7fc-25792b18fc4c + dependencies: {} + interpreter: /usr/bin/python3 + options: {'pyvenv_options': [], 'virtualenv_options': []} + +Virtualenv UUID: 8cc6666c-fb5e-474b-a8a7-217c9558d5e2 + timestamp: 2017-08-05 00:00:00 + full path: /home/juan/.fades/8cc6666c-fb5e-474b-a8a7-217c9558d5e2 + dependencies: {} + interpreter: /usr/bin/python3 + options: {'pyvenv_options': [], 'virtualenv_options': []} + +Virtualenv UUID: 7a4a4eb8-b0d2-4d88-a890-4830278e0eb9 + timestamp: 2015-09-06 00:00:00 + full path: /home/juan/.fades/7a4a4eb8-b0d2-4d88-a890-4830278e0eb9 + dependencies: {} + interpreter: /usr/bin/python3 + options: {'pyvenv_options': [], 'virtualenv_options': []} + +Virtualenv UUID: 7c10fa5f-037e-49b1-a9dd-3462c739e239 + timestamp: 2007-01-15 00:00:00 + full path: /home/juan/.fades/7c10fa5f-037e-49b1-a9dd-3462c739e239 + dependencies: {} + interpreter: /usr/bin/python3 + options: {'pyvenv_options': [], 'virtualenv_options': []} + +Virtualenv UUID: 37de0f88-3db7-434a-a692-048aa35fbfe7 + timestamp: 2002-07-08 00:00:00 + full path: /home/juan/.fades/37de0f88-3db7-434a-a692-048aa35fbfe7 + dependencies: {} + interpreter: /usr/bin/python3 + options: {'pyvenv_options': [], 'virtualenv_options': []} + +Virtualenv UUID: 000236c6-5ddb-4422-b210-93e09fab603f + timestamp: 2017-12-06 00:00:00 + full path: /home/juan/.fades/000236c6-5ddb-4422-b210-93e09fab603f + dependencies: {} + interpreter: /usr/bin/python3 + options: {'pyvenv_options': [], 'virtualenv_options': []} diff --git a/tests/examples/venvs.idx b/tests/examples/venvs.idx new file mode 100644 index 0000000..f01bdb1 --- /dev/null +++ b/tests/examples/venvs.idx @@ -0,0 +1,8 @@ +{"timestamp": 1308193200, "installed": {}, "metadata": {"env_path": "/home/juan/.fades/d1cf2e61-4ac5-467d-9b65-1a3c0e913bf3", "env_bin_path": "/home/juan/.fades/d1cf2e61-4ac5-467d-9b65-1a3c0e913bf3/bin", "pip_installed": true}, "interpreter": "/usr/bin/python3", "options": {"pyvenv_options": [], "virtualenv_options": []}} +{"timestamp": 1234144800, "installed": {}, "metadata": {"env_path": "/home/juan/.fades/032e6e2c-bc7e-4a7f-ad1d-66ecb9106884", "env_bin_path": "/home/juan/.fades/032e6e2c-bc7e-4a7f-ad1d-66ecb9106884/bin", "pip_installed": true}, "interpreter": "/usr/bin/python3", "options": {"pyvenv_options": [], "virtualenv_options": []}} +{"timestamp": 992746800, "installed": {}, "metadata": {"env_path": "/home/juan/.fades/53c8f169-b105-4ed1-a7fc-25792b18fc4c", "env_bin_path": "/home/juan/.fades/53c8f169-b105-4ed1-a7fc-25792b18fc4c/bin", "pip_installed": true}, "interpreter": "/usr/bin/python3", "options": {"pyvenv_options": [], "virtualenv_options": []}} +{"timestamp": 1501902000, "installed": {}, "metadata": {"env_path": "/home/juan/.fades/8cc6666c-fb5e-474b-a8a7-217c9558d5e2", "env_bin_path": "/home/juan/.fades/8cc6666c-fb5e-474b-a8a7-217c9558d5e2/bin", "pip_installed": true}, "interpreter": "/usr/bin/python3", "options": {"pyvenv_options": [], "virtualenv_options": []}} +{"timestamp": 1441508400, "installed": {}, "metadata": {"env_path": "/home/juan/.fades/7a4a4eb8-b0d2-4d88-a890-4830278e0eb9", "env_bin_path": "/home/juan/.fades/7a4a4eb8-b0d2-4d88-a890-4830278e0eb9/bin", "pip_installed": true}, "interpreter": "/usr/bin/python3", "options": {"pyvenv_options": [], "virtualenv_options": []}} +{"timestamp": 1168830000, "installed": {}, "metadata": {"env_path": "/home/juan/.fades/7c10fa5f-037e-49b1-a9dd-3462c739e239", "env_bin_path": "/home/juan/.fades/7c10fa5f-037e-49b1-a9dd-3462c739e239/bin", "pip_installed": true}, "interpreter": "/usr/bin/python3", "options": {"pyvenv_options": [], "virtualenv_options": []}} +{"timestamp": 1026097200, "installed": {}, "metadata": {"env_path": "/home/juan/.fades/37de0f88-3db7-434a-a692-048aa35fbfe7", "env_bin_path": "/home/juan/.fades/37de0f88-3db7-434a-a692-048aa35fbfe7/bin", "pip_installed": true}, "interpreter": "/usr/bin/python3", "options": {"pyvenv_options": [], "virtualenv_options": []}} +{"timestamp": 1512529200, "installed": {}, "metadata": {"env_path": "/home/juan/.fades/000236c6-5ddb-4422-b210-93e09fab603f", "env_bin_path": "/home/juan/.fades/000236c6-5ddb-4422-b210-93e09fab603f/bin", "pip_installed": true}, "interpreter": "/usr/bin/python3", "options": {"pyvenv_options": [], "virtualenv_options": []}} diff --git a/tests/test_files/autogenerate_random_venvsidx.py b/tests/test_files/autogenerate_random_venvsidx.py new file mode 100644 index 0000000..b6dd0aa --- /dev/null +++ b/tests/test_files/autogenerate_random_venvsidx.py @@ -0,0 +1,42 @@ + + +"""Tiny tool to autogenerate random 'venvs.idx' content for testing purposes.""" + + +import os +from random import randint +from uuid import uuid4 +from shutil import which +import datetime +import json + + +def main(): + """Build a valid random-ish Fades venvs.idx content.""" + y3ar, h0me = datetime.date.today().year, os.path.join(os.path.expanduser("~"), ".fades") + venvs_idx = "" + for _ in range(randint(2, 9)): + random_envpath = os.path.join(h0me, str(uuid4())) + random_timestamp = int(datetime.datetime(year=randint(2000, y3ar), + month=randint(1, 12), + day=randint(1, 28)).timestamp()) + venvs_idx += json.dumps({ + "timestamp": random_timestamp, + "installed": {}, + "metadata": { + "env_path": random_envpath, + "env_bin_path": os.path.join(random_envpath, "bin"), + "pip_installed": True + }, + "interpreter": which("python3") or "/usr/bin/python3.6", + "options": { + "pyvenv_options": [], + "virtualenv_options": [] + } + }) + "\n" + print(venvs_idx.strip()) + return venvs_idx.strip() + + +if __name__ in "__main__": + main() diff --git a/tests/test_helpers.py b/tests/test_helpers.py index ef5ded1..14c0547 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -375,3 +375,26 @@ def test_redirect_response(self): exists = helpers.check_pypi_exists(deps) self.assertTrue(exists) self.assertLoggedWarning("Got a (unexpected) HTTP_STATUS") + + +class ListVenvsTestCase(unittest.TestCase): + + """Utilities to list venvs.""" + + maxDiff, __slots__ = None, () + + @unittest.skipIf('TRAVIS' in os.environ or 'APPVEYOR' in os.environ, + "Travis/AppVeyor weird scaping travis-ci.org/PyAr/fades/jobs/366371756#L764") + def test_list_venvs(self): + venvs_idx = os.path.join(PATH_TO_EXAMPLES, 'venvs.idx') + venvs_info = os.path.join(PATH_TO_EXAMPLES, 'output.txt') + str_list_venv = helpers.list_venvs(venvs_idx) + self.assertIsInstance(str_list_venv, str) + with open(venvs_info) as venvs_info_file: + self.assertEqual(str_list_venv, venvs_info_file.read()) + + def test_index_path_empty(self): + self.assertEqual(helpers.list_venvs(""), None) + + def test_index_path_not_found(self): + self.assertEqual(helpers.list_venvs("directory_does_not_exist"), None)