From 04c0b0e6eb5798240cbaff49479be7892eb34453 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Mon, 8 Apr 2019 13:00:09 -0400 Subject: [PATCH 01/40] Add 'pip cache' command. --- src/pip/_internal/commands/__init__.py | 4 + src/pip/_internal/commands/cache.py | 106 +++++++++++++++++++++++++ src/pip/_internal/utils/filesystem.py | 14 +++- tests/functional/test_cache.py | 68 ++++++++++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 src/pip/_internal/commands/cache.py create mode 100644 tests/functional/test_cache.py diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 2a311f8fc89..8507b6ef9f0 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -64,6 +64,10 @@ 'pip._internal.commands.search', 'SearchCommand', 'Search PyPI for packages.', )), + ('cache', CommandInfo( + 'pip._internal.commands.cache', 'CacheCommand', + "Inspect and manage pip's caches.", + )), ('wheel', CommandInfo( 'pip._internal.commands.wheel', 'WheelCommand', 'Build wheels from your requirements.', diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py new file mode 100644 index 00000000000..0fe3352d22f --- /dev/null +++ b/src/pip/_internal/commands/cache.py @@ -0,0 +1,106 @@ +from __future__ import absolute_import + +import logging +import os +import textwrap + +from pip._internal.cli.base_command import Command +from pip._internal.exceptions import CommandError +from pip._internal.utils.filesystem import find_files + +logger = logging.getLogger(__name__) + + +class CacheCommand(Command): + """ + Inspect and manage pip's caches. + + Subcommands: + info: + Show information about the caches. + list [name]: + List filenames of packages stored in the cache. + remove : + Remove one or more package from the cache. + `pattern` can be a glob expression or a package name. + purge: + Remove all items from the cache. + """ + actions = ['info', 'list', 'remove', 'purge'] + name = 'cache' + usage = """ + %prog """ + summary = "View and manage which packages are available in pip's caches." + + def __init__(self, *args, **kw): + super(CacheCommand, self).__init__(*args, **kw) + + def run(self, options, args): + if not args: + raise CommandError('Please provide a subcommand.') + + if args[0] not in self.actions: + raise CommandError('Invalid subcommand: %s' % args[0]) + + self.wheel_dir = os.path.join(options.cache_dir, 'wheels') + + method = getattr(self, 'action_%s' % args[0]) + return method(options, args[1:]) + + def action_info(self, options, args): + format_args = (options.cache_dir, len(self.find_wheels('*.whl'))) + result = textwrap.dedent( + """\ + Cache info: + Location: %s + Packages: %s""" % format_args + ) + logger.info(result) + + def action_list(self, options, args): + if args and args[0]: + pattern = args[0] + else: + pattern = '*' + + files = self.find_wheels(pattern) + wheels = map(self._wheel_info, files) + wheels = sorted(set(wheels)) + + if not wheels: + logger.info('Nothing is currently cached.') + return + + result = 'Current cache contents:\n' + for wheel in wheels: + result += ' - %s\n' % wheel + logger.info(result.strip()) + + def action_remove(self, options, args): + if not args: + raise CommandError('Please provide a pattern') + + files = self.find_wheels(args[0]) + if not files: + raise CommandError('No matching packages') + + wheels = map(self._wheel_info, files) + result = 'Removing cached wheels for:\n' + for wheel in wheels: + result += '- %s\n' % wheel + + for filename in files: + os.unlink(filename) + logger.debug('Removed %s', filename) + logger.info('Removed %s files', len(files)) + + def action_purge(self, options, args): + return self.action_remove(options, '*') + + def _wheel_info(self, path): + filename = os.path.splitext(os.path.basename(path))[0] + name, version = filename.split('-')[0:2] + return '%s-%s' % (name, version) + + def find_wheels(self, pattern): + return find_files(self.wheel_dir, pattern + '-*.whl') diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 36578fb6244..2772e0880e4 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -1,4 +1,5 @@ import errno +import fnmatch import os import os.path import random @@ -17,7 +18,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: - from typing import Any, BinaryIO, Iterator + from typing import Any, BinaryIO, Iterator, List class NamedTemporaryFileResult(BinaryIO): @property @@ -176,3 +177,14 @@ def _test_writable_dir_win(path): raise EnvironmentError( 'Unexpected condition testing for writable directory' ) + + +def find_files(path, pattern): + # type: (str, str) -> List[str] + """Returns a list of absolute paths of files beneath path, recursively, + with filenames which match the UNIX-style shell glob pattern.""" + result = [] # type: List[str] + for root, dirs, files in os.walk(path): + matches = fnmatch.filter(files, pattern) + result.extend(os.path.join(root, f) for f in matches) + return result diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py new file mode 100644 index 00000000000..34cd95b836d --- /dev/null +++ b/tests/functional/test_cache.py @@ -0,0 +1,68 @@ +import os +import shutil + +from pip._internal.utils import appdirs + + +def _cache_dir(script): + results = script.run( + 'python', '-c', + 'from pip._internal.locations import USER_CACHE_DIR;' + 'print(USER_CACHE_DIR)' + ) + return str(results.stdout).strip() + + +def test_cache_info(script, monkeypatch): + result = script.pip('cache', 'info') + cache_dir = _cache_dir(script) + + assert 'Location: %s' % cache_dir in result.stdout + assert 'Packages: ' in result.stdout + + +def test_cache_list(script, monkeypatch): + cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) + with open(os.path.join(destination, 'yyy-1.2.3.whl'), 'w'): + pass + with open(os.path.join(destination, 'zzz-4.5.6.whl'), 'w'): + pass + result = script.pip('cache', 'list') + assert 'yyy-1.2.3' in result.stdout + assert 'zzz-4.5.6' in result.stdout + shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) + + +def test_cache_list_with_pattern(script, monkeypatch): + cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) + with open(os.path.join(destination, 'yyy-1.2.3.whl'), 'w'): + pass + with open(os.path.join(destination, 'zzz-4.5.6.whl'), 'w'): + pass + result = script.pip('cache', 'list', 'zzz') + assert 'yyy-1.2.3' not in result.stdout + assert 'zzz-4.5.6' in result.stdout + shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) + + +def test_cache_remove(script, monkeypatch): + cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) + with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3.whl'), 'w'): + pass + with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6.whl'), 'w'): + pass + + script.pip('cache', 'remove', expect_error=True) + result = script.pip('cache', 'remove', 'zzz', '--verbose') + assert 'yyy-1.2.3' not in result.stdout + assert 'zzz-4.5.6' in result.stdout + shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) From b0e7b66326b22984c62965c861190da88c1a79db Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Mon, 7 Oct 2019 23:03:03 -0400 Subject: [PATCH 02/40] [commands/cache] Refactor + fix linting failures. --- src/pip/_internal/commands/cache.py | 112 ++++++++++++++++++---------- tests/functional/test_cache.py | 2 - 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 0fe3352d22f..7d5fb85ca64 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -5,50 +5,76 @@ import textwrap from pip._internal.cli.base_command import Command -from pip._internal.exceptions import CommandError +from pip._internal.cli.status_codes import ERROR, SUCCESS +from pip._internal.exceptions import CommandError, PipError from pip._internal.utils.filesystem import find_files +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List + logger = logging.getLogger(__name__) class CacheCommand(Command): - """ - Inspect and manage pip's caches. - - Subcommands: - info: - Show information about the caches. - list [name]: - List filenames of packages stored in the cache. - remove : - Remove one or more package from the cache. - `pattern` can be a glob expression or a package name. - purge: - Remove all items from the cache. + """Inspect and manage pip's caches. + + Subcommands: + + info: Show information about the caches. + list: List filenames of packages stored in the cache. + remove: Remove one or more package from the cache. + purge: Remove all items from the cache. + + can be a glob expression or a package name. """ actions = ['info', 'list', 'remove', 'purge'] - name = 'cache' usage = """ - %prog """ - summary = "View and manage which packages are available in pip's caches." + %prog info + %prog list [name] + %prog remove + %prog purge + """ def __init__(self, *args, **kw): + # type: (*Any, **Any) -> None super(CacheCommand, self).__init__(*args, **kw) def run(self, options, args): - if not args: - raise CommandError('Please provide a subcommand.') - - if args[0] not in self.actions: - raise CommandError('Invalid subcommand: %s' % args[0]) - - self.wheel_dir = os.path.join(options.cache_dir, 'wheels') - - method = getattr(self, 'action_%s' % args[0]) - return method(options, args[1:]) - - def action_info(self, options, args): - format_args = (options.cache_dir, len(self.find_wheels('*.whl'))) + # type: (Values, List[Any]) -> int + handlers = { + "info": self.get_cache_info, + "list": self.list_cache_items, + "remove": self.remove_cache_items, + "purge": self.purge_cache, + } + + # Determine action + if not args or args[0] not in handlers: + logger.error("Need an action ({}) to perform.".format( + ", ".join(sorted(handlers))) + ) + return ERROR + + action = args[0] + + # Error handling happens here, not in the action-handlers. + try: + handlers[action](options, args[1:]) + except PipError as e: + logger.error(e.args[0]) + return ERROR + + return SUCCESS + + def get_cache_info(self, options, args): + # type: (Values, List[Any]) -> None + format_args = ( + options.cache_dir, + len(self._find_wheels(options, '*.whl')) + ) result = textwrap.dedent( """\ Cache info: @@ -57,15 +83,16 @@ def action_info(self, options, args): ) logger.info(result) - def action_list(self, options, args): + def list_cache_items(self, options, args): + # type: (Values, List[Any]) -> None if args and args[0]: pattern = args[0] else: pattern = '*' - files = self.find_wheels(pattern) - wheels = map(self._wheel_info, files) - wheels = sorted(set(wheels)) + files = self._find_wheels(options, pattern) + wheels_ = map(self._wheel_info, files) + wheels = sorted(set(wheels_)) if not wheels: logger.info('Nothing is currently cached.') @@ -76,11 +103,12 @@ def action_list(self, options, args): result += ' - %s\n' % wheel logger.info(result.strip()) - def action_remove(self, options, args): + def remove_cache_items(self, options, args): + # type: (Values, List[Any]) -> None if not args: raise CommandError('Please provide a pattern') - files = self.find_wheels(args[0]) + files = self._find_wheels(options, args[0]) if not files: raise CommandError('No matching packages') @@ -94,13 +122,17 @@ def action_remove(self, options, args): logger.debug('Removed %s', filename) logger.info('Removed %s files', len(files)) - def action_purge(self, options, args): - return self.action_remove(options, '*') + def purge_cache(self, options, args): + # type: (Values, List[Any]) -> None + return self.remove_cache_items(options, ['*']) def _wheel_info(self, path): + # type: (str) -> str filename = os.path.splitext(os.path.basename(path))[0] name, version = filename.split('-')[0:2] return '%s-%s' % (name, version) - def find_wheels(self, pattern): - return find_files(self.wheel_dir, pattern + '-*.whl') + def _find_wheels(self, options, pattern): + # type: (Values, str) -> List[str] + wheel_dir = os.path.join(options.cache_dir, 'wheels') + return find_files(wheel_dir, pattern + '-*.whl') diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 34cd95b836d..05fcb4505d1 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,8 +1,6 @@ import os import shutil -from pip._internal.utils import appdirs - def _cache_dir(script): results = script.run( From b9b29b8c10a18843dfbb0c23a33c073b8de603c2 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Mon, 7 Oct 2019 23:31:01 -0400 Subject: [PATCH 03/40] [commands/cache] fix 'pip cache info'; don't hide python/abi/platform tags. --- src/pip/_internal/commands/cache.py | 11 ++--------- tests/functional/test_cache.py | 3 +++ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 7d5fb85ca64..ee669016728 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -73,7 +73,7 @@ def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None format_args = ( options.cache_dir, - len(self._find_wheels(options, '*.whl')) + len(self._find_wheels(options, '*')) ) result = textwrap.dedent( """\ @@ -112,11 +112,6 @@ def remove_cache_items(self, options, args): if not files: raise CommandError('No matching packages') - wheels = map(self._wheel_info, files) - result = 'Removing cached wheels for:\n' - for wheel in wheels: - result += '- %s\n' % wheel - for filename in files: os.unlink(filename) logger.debug('Removed %s', filename) @@ -128,9 +123,7 @@ def purge_cache(self, options, args): def _wheel_info(self, path): # type: (str) -> str - filename = os.path.splitext(os.path.basename(path))[0] - name, version = filename.split('-')[0:2] - return '%s-%s' % (name, version) + return os.path.basename(path) def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 05fcb4505d1..5c97cfe1903 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -16,6 +16,9 @@ def test_cache_info(script, monkeypatch): cache_dir = _cache_dir(script) assert 'Location: %s' % cache_dir in result.stdout + # TODO(@duckinator): This should probably test that the number of + # packages is actually correct, but I'm not sure how to do that + # without pretty much re-implementing the entire cache info command. assert 'Packages: ' in result.stdout From c59ced69422cc4831a2c84662bc0c1d6dc7f6ae5 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Mon, 7 Oct 2019 23:50:47 -0400 Subject: [PATCH 04/40] [commands/cache] More refactoring of cache command --- src/pip/_internal/commands/cache.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index ee669016728..7ab7e6c88f4 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -30,7 +30,7 @@ class CacheCommand(Command): can be a glob expression or a package name. """ - actions = ['info', 'list', 'remove', 'purge'] + usage = """ %prog info %prog list [name] @@ -71,17 +71,15 @@ def run(self, options, args): def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None - format_args = ( - options.cache_dir, - len(self._find_wheels(options, '*')) - ) - result = textwrap.dedent( - """\ + num_packages = len(self._find_wheels(options, '*')) + + results = textwrap.dedent("""\ Cache info: Location: %s - Packages: %s""" % format_args + Packages: %s""" % (options.cache_dir, num_packages) ) - logger.info(result) + + logger.info(results) def list_cache_items(self, options, args): # type: (Values, List[Any]) -> None @@ -91,8 +89,7 @@ def list_cache_items(self, options, args): pattern = '*' files = self._find_wheels(options, pattern) - wheels_ = map(self._wheel_info, files) - wheels = sorted(set(wheels_)) + wheels = sorted(set(map(lambda f: os.path.basename(f), files))) if not wheels: logger.info('Nothing is currently cached.') @@ -121,10 +118,6 @@ def purge_cache(self, options, args): # type: (Values, List[Any]) -> None return self.remove_cache_items(options, ['*']) - def _wheel_info(self, path): - # type: (str) -> str - return os.path.basename(path) - def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] wheel_dir = os.path.join(options.cache_dir, 'wheels') From 8ae71adbea407f4cca5baba31e9e6687571cc8f4 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Mon, 7 Oct 2019 23:59:30 -0400 Subject: [PATCH 05/40] [commands/cache] Add docs for 'pip cache' command. --- docs/man/commands/cache.rst | 20 ++++++++++++++++++++ src/pip/_internal/commands/cache.py | 15 ++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 docs/man/commands/cache.rst diff --git a/docs/man/commands/cache.rst b/docs/man/commands/cache.rst new file mode 100644 index 00000000000..b0241c8135e --- /dev/null +++ b/docs/man/commands/cache.rst @@ -0,0 +1,20 @@ +:orphan: + +========== +pip-cache +========== + +Description +*********** + +.. pip-command-description:: cache + +Usage +***** + +.. pip-command-usage:: cache + +Options +******* + +.. pip-command-options:: cache diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 7ab7e6c88f4..8dadc6a0e33 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -19,14 +19,19 @@ class CacheCommand(Command): - """Inspect and manage pip's caches. + """ + Inspect and manage pip's caches. Subcommands: - info: Show information about the caches. - list: List filenames of packages stored in the cache. - remove: Remove one or more package from the cache. - purge: Remove all items from the cache. + info: + Show information about the caches. + list: + List filenames of packages stored in the cache. + remove: + Remove one or more package from the cache. + purge: + Remove all items from the cache. can be a glob expression or a package name. """ From d57dcd934e11e069bf4bd37e8dff8d777b452217 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 8 Oct 2019 00:00:24 -0400 Subject: [PATCH 06/40] [commands/cache] Add news file for cache command. --- news/6391.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6391.feature diff --git a/news/6391.feature b/news/6391.feature new file mode 100644 index 00000000000..73d6dffca8a --- /dev/null +++ b/news/6391.feature @@ -0,0 +1 @@ +Add ``pip cache`` command for inspecting/managing pip's cache. From 50604be6c4908f481e4ca1ea17cfaa9ec06ca417 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 9 Oct 2019 19:58:23 -0400 Subject: [PATCH 07/40] [commands/cache] Raise errors if wrong number of args. Also add tests for purge_cache, since I apparently forgot those before. --- src/pip/_internal/commands/cache.py | 11 ++++++++- tests/functional/test_cache.py | 36 +++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 8dadc6a0e33..8afe5be53d5 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -88,7 +88,10 @@ def get_cache_info(self, options, args): def list_cache_items(self, options, args): # type: (Values, List[Any]) -> None - if args and args[0]: + if len(args) > 1: + raise CommandError('Too many arguments') + + if args: pattern = args[0] else: pattern = '*' @@ -107,6 +110,9 @@ def list_cache_items(self, options, args): def remove_cache_items(self, options, args): # type: (Values, List[Any]) -> None + if len(args) > 1: + raise CommandError('Too many arguments') + if not args: raise CommandError('Please provide a pattern') @@ -121,6 +127,9 @@ def remove_cache_items(self, options, args): def purge_cache(self, options, args): # type: (Values, List[Any]) -> None + if args: + raise CommandError('Too many arguments') + return self.remove_cache_items(options, ['*']) def _find_wheels(self, options, pattern): diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 5c97cfe1903..6388155df2a 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -3,12 +3,12 @@ def _cache_dir(script): - results = script.run( + result = script.run( 'python', '-c', 'from pip._internal.locations import USER_CACHE_DIR;' 'print(USER_CACHE_DIR)' ) - return str(results.stdout).strip() + return result.stdout.strip() def test_cache_info(script, monkeypatch): @@ -37,6 +37,11 @@ def test_cache_list(script, monkeypatch): shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) +def test_cache_list_too_many_args(script, monkeypatch): + script.pip('cache', 'list', 'aaa', 'bbb', + expect_error=True) + + def test_cache_list_with_pattern(script, monkeypatch): cache_dir = _cache_dir(script) wheel_cache_dir = os.path.join(cache_dir, 'wheels') @@ -67,3 +72,30 @@ def test_cache_remove(script, monkeypatch): assert 'yyy-1.2.3' not in result.stdout assert 'zzz-4.5.6' in result.stdout shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) + + +def test_cache_remove_too_many_args(script, monkeypatch): + result = script.pip('cache', 'remove', 'aaa', 'bbb', + expect_error=True) + + +def test_cache_purge(script, monkeypatch): + cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) + with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3.whl'), 'w'): + pass + with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6.whl'), 'w'): + pass + + result = script.pip('cache', 'purge', 'aaa', '--verbose', + expect_error=True) + assert 'yyy-1.2.3' not in result.stdout + assert 'zzz-4.5.6' not in result.stdout + + result = script.pip('cache', 'purge', '--verbose') + assert 'yyy-1.2.3' in result.stdout + assert 'zzz-4.5.6' in result.stdout + + shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) From 9563dfb526db14b1c9a00c66a25e1a7ec4d64e78 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 9 Oct 2019 20:00:34 -0400 Subject: [PATCH 08/40] [commands/cache] Refactor get_cache_info(). --- src/pip/_internal/commands/cache.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 8afe5be53d5..8e6f12f4c94 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -78,13 +78,16 @@ def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None num_packages = len(self._find_wheels(options, '*')) - results = textwrap.dedent("""\ + message = textwrap.dedent(""" Cache info: - Location: %s - Packages: %s""" % (options.cache_dir, num_packages) - ) - - logger.info(results) + Location: {location} + Packages: {package_count} + """).format( + location=options.cache_dir, + package_count=num_packages, + ).strip() + + logger.info(message) def list_cache_items(self, options, args): # type: (Values, List[Any]) -> None From 6fb1ee7a3d9c19995262f1961c7fb39fd459396e Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 10 Oct 2019 04:30:08 -0400 Subject: [PATCH 09/40] [commands/cache] fix linting error. --- tests/functional/test_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 6388155df2a..745a6e2a772 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -75,8 +75,8 @@ def test_cache_remove(script, monkeypatch): def test_cache_remove_too_many_args(script, monkeypatch): - result = script.pip('cache', 'remove', 'aaa', 'bbb', - expect_error=True) + script.pip('cache', 'remove', 'aaa', 'bbb', + expect_error=True) def test_cache_purge(script, monkeypatch): From c838a6717859cfcf698ce49b0387dbd54d62a686 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 10 Oct 2019 05:27:51 -0400 Subject: [PATCH 10/40] [commands/cache] Change pattern suffix from -*.whl to *.whl. --- src/pip/_internal/commands/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 8e6f12f4c94..daa9809ff89 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -138,4 +138,4 @@ def purge_cache(self, options, args): def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] wheel_dir = os.path.join(options.cache_dir, 'wheels') - return find_files(wheel_dir, pattern + '-*.whl') + return find_files(wheel_dir, pattern + '*.whl') From 61dd0bc16c6d85567e5c29af92d65b9e58ec0388 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 10 Oct 2019 05:42:28 -0400 Subject: [PATCH 11/40] [commands/cache] Use location of wheel cache dir specifically. --- src/pip/_internal/commands/cache.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index daa9809ff89..09d7d455d9b 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -83,7 +83,7 @@ def get_cache_info(self, options, args): Location: {location} Packages: {package_count} """).format( - location=options.cache_dir, + location=self._wheels_cache_dir(options), package_count=num_packages, ).strip() @@ -135,7 +135,10 @@ def purge_cache(self, options, args): return self.remove_cache_items(options, ['*']) + def _wheels_cache_dir(self, options): + return os.path.join(options.cache_dir, 'wheels') + def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] - wheel_dir = os.path.join(options.cache_dir, 'wheels') + wheel_dir = self._wheels_cache_dir(options) return find_files(wheel_dir, pattern + '*.whl') From 94a6593a5995373d3195367b6441316d03465d09 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 10 Oct 2019 05:46:57 -0400 Subject: [PATCH 12/40] [commands/cache] Add HTML docs for `pip cache`. --- docs/html/reference/index.rst | 1 + docs/html/reference/pip_cache.rst | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/html/reference/pip_cache.rst diff --git a/docs/html/reference/index.rst b/docs/html/reference/index.rst index f3312948193..d21b7a9801a 100644 --- a/docs/html/reference/index.rst +++ b/docs/html/reference/index.rst @@ -13,6 +13,7 @@ Reference Guide pip_list pip_show pip_search + pip_cache pip_check pip_config pip_wheel diff --git a/docs/html/reference/pip_cache.rst b/docs/html/reference/pip_cache.rst new file mode 100644 index 00000000000..d56d1b016ba --- /dev/null +++ b/docs/html/reference/pip_cache.rst @@ -0,0 +1,22 @@ + +.. _`pip cache`: + +pip cache +------------ + +.. contents:: + +Usage +***** + +.. pip-command-usage:: cache + +Description +*********** + +.. pip-command-description:: cache + +Options +******* + +.. pip-command-options:: cache From 61a0adcfe79043c17e905ae2e5d75b4ce8eaddd5 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 10 Oct 2019 05:51:06 -0400 Subject: [PATCH 13/40] [commands/cache] Add missing type annotation. --- src/pip/_internal/commands/cache.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 09d7d455d9b..72273265566 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -136,6 +136,7 @@ def purge_cache(self, options, args): return self.remove_cache_items(options, ['*']) def _wheels_cache_dir(self, options): + # type: (Values) -> str return os.path.join(options.cache_dir, 'wheels') def _find_wheels(self, options, pattern): From 554133a90eb9611b3c828b5b65a5f91fc6fe0a01 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Mon, 6 Jan 2020 15:51:37 -0500 Subject: [PATCH 14/40] [commands/cache] Add file size information. --- src/pip/_internal/commands/cache.py | 25 +++++++++----- src/pip/_internal/utils/filesystem.py | 50 ++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 72273265566..80ea5c8f9a2 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -4,10 +4,10 @@ import os import textwrap +import pip._internal.utils.filesystem as filesystem from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import CommandError, PipError -from pip._internal.utils.filesystem import find_files from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -78,13 +78,18 @@ def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None num_packages = len(self._find_wheels(options, '*')) + cache_location = self._wheels_cache_dir(options) + cache_size = filesystem.friendly_directory_size(cache_location) + message = textwrap.dedent(""" Cache info: Location: {location} Packages: {package_count} + Size: {size} """).format( - location=self._wheels_cache_dir(options), + location=cache_location, package_count=num_packages, + size=cache_size, ).strip() logger.info(message) @@ -100,16 +105,18 @@ def list_cache_items(self, options, args): pattern = '*' files = self._find_wheels(options, pattern) - wheels = sorted(set(map(lambda f: os.path.basename(f), files))) - if not wheels: + if not files: logger.info('Nothing is currently cached.') return - result = 'Current cache contents:\n' - for wheel in wheels: - result += ' - %s\n' % wheel - logger.info(result.strip()) + results = [] + for filename in files: + wheel = os.path.basename(filename) + size = filesystem.friendly_file_size(filename) + results.append(' - {} ({})'.format(wheel, size)) + logger.info('Current cache contents:\n') + logger.info('\n'.join(sorted(results))) def remove_cache_items(self, options, args): # type: (Values, List[Any]) -> None @@ -142,4 +149,4 @@ def _wheels_cache_dir(self, options): def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] wheel_dir = self._wheels_cache_dir(options) - return find_files(wheel_dir, pattern + '*.whl') + return filesystem.find_files(wheel_dir, pattern + '*.whl') diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 2772e0880e4..7e369e4a843 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -18,7 +18,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: - from typing import Any, BinaryIO, Iterator, List + from typing import Any, BinaryIO, Iterator, List, Union class NamedTemporaryFileResult(BinaryIO): @property @@ -188,3 +188,51 @@ def find_files(path, pattern): matches = fnmatch.filter(files, pattern) result.extend(os.path.join(root, f) for f in matches) return result + + +def _friendly_size(size): + # type: (Union[float, int]) -> str + suffix = 'B' + if size > 1000: + size /= 1000 + suffix = 'KB' + + if size > 1000: + size /= 1000 + suffix = 'MB' + + if size > 1000: + size /= 1000 + suffix = 'GB' + + size = round(size, 1) + + return '{} {}'.format(size, suffix) + + +def file_size(path): + # type: (str) -> Union[int, float] + # If it's a symlink, return 0. + if os.path.islink(path): + return 0 + return os.path.getsize(path) + + +def friendly_file_size(path): + # type: (str) -> str + return _friendly_size(file_size(path)) + + +def directory_size(path): + # type: (str) -> Union[int, float] + size = 0.0 + for root, _dirs, files in os.walk(path): + for filename in files: + file_path = os.path.join(root, filename) + size += file_size(file_path) + return size + + +def friendly_directory_size(path): + # type: (str) -> str + return _friendly_size(directory_size(path)) From 2d978309a2f439fe8b9cfe9624f6daeccb772005 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Mon, 13 Jan 2020 17:53:16 -0500 Subject: [PATCH 15/40] [commands/cache] Minor clean-up. - Consistently use singular 'cache' (not plural 'caches'). - Remove unnecessary uses of the word 'currently'. - Use 'file(s)' instead of 'files', to account for case of only one file. - Use .format() when appropriate. - Minor cleanup of `pip cache`-related files in docs/. --- docs/html/reference/pip_cache.rst | 2 +- docs/man/commands/cache.rst | 4 ++-- src/pip/_internal/commands/__init__.py | 2 +- src/pip/_internal/commands/cache.py | 10 +++++----- tests/functional/test_cache.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/html/reference/pip_cache.rst b/docs/html/reference/pip_cache.rst index d56d1b016ba..8ad99f65cba 100644 --- a/docs/html/reference/pip_cache.rst +++ b/docs/html/reference/pip_cache.rst @@ -2,7 +2,7 @@ .. _`pip cache`: pip cache ------------- +--------- .. contents:: diff --git a/docs/man/commands/cache.rst b/docs/man/commands/cache.rst index b0241c8135e..8f8e197f922 100644 --- a/docs/man/commands/cache.rst +++ b/docs/man/commands/cache.rst @@ -1,8 +1,8 @@ :orphan: -========== +========= pip-cache -========== +========= Description *********** diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 8507b6ef9f0..48e288ab345 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -66,7 +66,7 @@ )), ('cache', CommandInfo( 'pip._internal.commands.cache', 'CacheCommand', - "Inspect and manage pip's caches.", + "Inspect and manage pip's cache.", )), ('wheel', CommandInfo( 'pip._internal.commands.wheel', 'WheelCommand', diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 80ea5c8f9a2..9a473aeefb9 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -20,12 +20,12 @@ class CacheCommand(Command): """ - Inspect and manage pip's caches. + Inspect and manage pip's cache. Subcommands: info: - Show information about the caches. + Show information about the cache. list: List filenames of packages stored in the cache. remove: @@ -107,7 +107,7 @@ def list_cache_items(self, options, args): files = self._find_wheels(options, pattern) if not files: - logger.info('Nothing is currently cached.') + logger.info('Nothing cached.') return results = [] @@ -115,7 +115,7 @@ def list_cache_items(self, options, args): wheel = os.path.basename(filename) size = filesystem.friendly_file_size(filename) results.append(' - {} ({})'.format(wheel, size)) - logger.info('Current cache contents:\n') + logger.info('Cache contents:\n') logger.info('\n'.join(sorted(results))) def remove_cache_items(self, options, args): @@ -133,7 +133,7 @@ def remove_cache_items(self, options, args): for filename in files: os.unlink(filename) logger.debug('Removed %s', filename) - logger.info('Removed %s files', len(files)) + logger.info('Removed %s file(s)', len(files)) def purge_cache(self, options, args): # type: (Values, List[Any]) -> None diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 745a6e2a772..642630b201a 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -15,7 +15,7 @@ def test_cache_info(script, monkeypatch): result = script.pip('cache', 'info') cache_dir = _cache_dir(script) - assert 'Location: %s' % cache_dir in result.stdout + assert 'Location: {}'.format(cache_dir) in result.stdout # TODO(@duckinator): This should probably test that the number of # packages is actually correct, but I'm not sure how to do that # without pretty much re-implementing the entire cache info command. From 6fa8498e18e2b054eafd93965c10c1c424889cef Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 14 Jan 2020 14:36:58 -0500 Subject: [PATCH 16/40] [commands/cache] Avoid use of "(s)" suffix. As @hugovk pointed out, it can cause problems sometimes: https://github.com/pypa/pip/pull/6391#discussion_r366259867 --- src/pip/_internal/commands/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 9a473aeefb9..9e106ad7e31 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -133,7 +133,7 @@ def remove_cache_items(self, options, args): for filename in files: os.unlink(filename) logger.debug('Removed %s', filename) - logger.info('Removed %s file(s)', len(files)) + logger.info('Files removed: %s', len(files)) def purge_cache(self, options, args): # type: (Values, List[Any]) -> None From d74895a224594440f7208ec4f8b336377c1b711f Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Sun, 23 Feb 2020 13:49:14 -0500 Subject: [PATCH 17/40] [commands/cache] Normalize path in test. --- tests/functional/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 642630b201a..b6a3c55e988 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -15,7 +15,7 @@ def test_cache_info(script, monkeypatch): result = script.pip('cache', 'info') cache_dir = _cache_dir(script) - assert 'Location: {}'.format(cache_dir) in result.stdout + assert 'Location: {}'.format(os.path.normcase(cache_dir)) in result.stdout # TODO(@duckinator): This should probably test that the number of # packages is actually correct, but I'm not sure how to do that # without pretty much re-implementing the entire cache info command. From 10d13762ebab7fa731153fe59fe94d8fae21157a Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Sun, 23 Feb 2020 17:01:44 -0500 Subject: [PATCH 18/40] [commands/cache] Be explicit about `pip cache` only working on the wheel cache. --- news/6391.feature | 2 +- src/pip/_internal/commands/__init__.py | 2 +- src/pip/_internal/commands/cache.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/news/6391.feature b/news/6391.feature index 73d6dffca8a..e13df852713 100644 --- a/news/6391.feature +++ b/news/6391.feature @@ -1 +1 @@ -Add ``pip cache`` command for inspecting/managing pip's cache. +Add ``pip cache`` command for inspecting/managing pip's wheel cache. diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 48e288ab345..b43a96c13f3 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -66,7 +66,7 @@ )), ('cache', CommandInfo( 'pip._internal.commands.cache', 'CacheCommand', - "Inspect and manage pip's cache.", + "Inspect and manage pip's wheel cache.", )), ('wheel', CommandInfo( 'pip._internal.commands.wheel', 'WheelCommand', diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 9e106ad7e31..455064a0d24 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -20,7 +20,7 @@ class CacheCommand(Command): """ - Inspect and manage pip's cache. + Inspect and manage pip's wheel cache. Subcommands: From d9dc76e9094ba1e06224d140ffc5712488114d9f Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 4 Mar 2020 12:03:36 -0500 Subject: [PATCH 19/40] [commands/cache] Correct argument name in documentation for `pip cache list`. --- src/pip/_internal/commands/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 455064a0d24..8ef507941d5 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -38,7 +38,7 @@ class CacheCommand(Command): usage = """ %prog info - %prog list [name] + %prog list [] %prog remove %prog purge """ From f22f69e9bdb20677ff88fadd94941d455fccc6eb Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 4 Mar 2020 12:09:19 -0500 Subject: [PATCH 20/40] [utils/filesystem] Convert `size` to float, for consistent behavior between Py2 and Py3. --- src/pip/_internal/utils/filesystem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 7e369e4a843..adfcd8f106f 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -192,6 +192,7 @@ def find_files(path, pattern): def _friendly_size(size): # type: (Union[float, int]) -> str + size = float(size) # for consistent behavior between Python 2 and Python 3. suffix = 'B' if size > 1000: size /= 1000 From 03d5ec10f20406f4ba94e5dd2f81f8759f9cace3 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Wed, 4 Mar 2020 12:12:39 -0500 Subject: [PATCH 21/40] [utils/filesystem] Reformat comment to keep lines <79 characters long. --- src/pip/_internal/utils/filesystem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index adfcd8f106f..ea573dbe9ee 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -192,7 +192,11 @@ def find_files(path, pattern): def _friendly_size(size): # type: (Union[float, int]) -> str - size = float(size) # for consistent behavior between Python 2 and Python 3. + + # Explicitly convert `size` to a float, for consistent behavior + # between Python 2 and Python 3. + size = float(size) + suffix = 'B' if size > 1000: size /= 1000 From 735375fb6bd99d150d833684a219c4f17eadb64e Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 5 Mar 2020 08:25:09 -0500 Subject: [PATCH 22/40] [commands/cache] Reformat documentation. Co-Authored-By: Pradyun Gedam --- src/pip/_internal/commands/cache.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 8ef507941d5..fc46f9d6b12 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -24,14 +24,10 @@ class CacheCommand(Command): Subcommands: - info: - Show information about the cache. - list: - List filenames of packages stored in the cache. - remove: - Remove one or more package from the cache. - purge: - Remove all items from the cache. + info: Show information about the cache. + list: List filenames of packages stored in the cache. + remove: Remove one or more package from the cache. + purge: Remove all items from the cache. can be a glob expression or a package name. """ From 8cd8c91491c904b2536bc79ec16ae6ac396b5818 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 5 Mar 2020 08:25:41 -0500 Subject: [PATCH 23/40] [commands/cache] Reformat (more) documentation. Co-Authored-By: Pradyun Gedam --- src/pip/_internal/commands/cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index fc46f9d6b12..4d557a0f92c 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -20,9 +20,9 @@ class CacheCommand(Command): """ - Inspect and manage pip's wheel cache. + Inspect and manage pip's wheel cache. - Subcommands: + Subcommands: info: Show information about the cache. list: List filenames of packages stored in the cache. From 63ba6cce4ad4f77696eeca8a13dcccb8af9c02c4 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 5 Mar 2020 08:36:35 -0500 Subject: [PATCH 24/40] [command/cache, utils/filesystem] Use existing format_size; remove _friendly_size; rename friendly_*_size to format_*_size for consistency. --- src/pip/_internal/commands/cache.py | 4 ++-- src/pip/_internal/utils/filesystem.py | 34 ++++----------------------- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 4d557a0f92c..2f1ad977519 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -75,7 +75,7 @@ def get_cache_info(self, options, args): num_packages = len(self._find_wheels(options, '*')) cache_location = self._wheels_cache_dir(options) - cache_size = filesystem.friendly_directory_size(cache_location) + cache_size = filesystem.format_directory_size(cache_location) message = textwrap.dedent(""" Cache info: @@ -109,7 +109,7 @@ def list_cache_items(self, options, args): results = [] for filename in files: wheel = os.path.basename(filename) - size = filesystem.friendly_file_size(filename) + size = filesystem.format_file_size(filename) results.append(' - {} ({})'.format(wheel, size)) logger.info('Cache contents:\n') logger.info('\n'.join(sorted(results))) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index ea573dbe9ee..d97992acb48 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -15,6 +15,7 @@ from pip._vendor.six import PY2 from pip._internal.utils.compat import get_path_uid +from pip._internal.utils.misc import format_size from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: @@ -190,31 +191,6 @@ def find_files(path, pattern): return result -def _friendly_size(size): - # type: (Union[float, int]) -> str - - # Explicitly convert `size` to a float, for consistent behavior - # between Python 2 and Python 3. - size = float(size) - - suffix = 'B' - if size > 1000: - size /= 1000 - suffix = 'KB' - - if size > 1000: - size /= 1000 - suffix = 'MB' - - if size > 1000: - size /= 1000 - suffix = 'GB' - - size = round(size, 1) - - return '{} {}'.format(size, suffix) - - def file_size(path): # type: (str) -> Union[int, float] # If it's a symlink, return 0. @@ -223,9 +199,9 @@ def file_size(path): return os.path.getsize(path) -def friendly_file_size(path): +def format_file_size(path): # type: (str) -> str - return _friendly_size(file_size(path)) + return format_size(file_size(path)) def directory_size(path): @@ -238,6 +214,6 @@ def directory_size(path): return size -def friendly_directory_size(path): +def format_directory_size(path): # type: (str) -> str - return _friendly_size(directory_size(path)) + return format_size(directory_size(path)) From ed9f885bd7b99ed09f37d30c64779885b22aae66 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 5 Mar 2020 10:43:44 -0500 Subject: [PATCH 25/40] [commands/cache] Reformat output of `pip cache info` Co-Authored-By: Pradyun Gedam --- src/pip/_internal/commands/cache.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 2f1ad977519..e0d751d8d03 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -78,10 +78,9 @@ def get_cache_info(self, options, args): cache_size = filesystem.format_directory_size(cache_location) message = textwrap.dedent(""" - Cache info: - Location: {location} - Packages: {package_count} - Size: {size} + Location: {location} + Size: {size} + Number of wheels: {package_count} """).format( location=cache_location, package_count=num_packages, From f8b67c8bf14c1cc1afe15eefeef416af8f21bae3 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 5 Mar 2020 11:10:17 -0500 Subject: [PATCH 26/40] [commands/cache] Fix test_cache_info test. --- tests/functional/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index b6a3c55e988..1b2df3f049e 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -19,7 +19,7 @@ def test_cache_info(script, monkeypatch): # TODO(@duckinator): This should probably test that the number of # packages is actually correct, but I'm not sure how to do that # without pretty much re-implementing the entire cache info command. - assert 'Packages: ' in result.stdout + assert 'Number of wheels: ' in result.stdout def test_cache_list(script, monkeypatch): From 8b518b258df5a923d27fb7537baae31680474268 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 5 Mar 2020 11:10:39 -0500 Subject: [PATCH 27/40] [commands/cache] Make filenames more realistic in tests. --- tests/functional/test_cache.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 1b2df3f049e..2cd02829880 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -27,9 +27,9 @@ def test_cache_list(script, monkeypatch): wheel_cache_dir = os.path.join(cache_dir, 'wheels') destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) - with open(os.path.join(destination, 'yyy-1.2.3.whl'), 'w'): + with open(os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl'), 'w'): pass - with open(os.path.join(destination, 'zzz-4.5.6.whl'), 'w'): + with open(os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl'), 'w'): pass result = script.pip('cache', 'list') assert 'yyy-1.2.3' in result.stdout @@ -47,9 +47,9 @@ def test_cache_list_with_pattern(script, monkeypatch): wheel_cache_dir = os.path.join(cache_dir, 'wheels') destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) - with open(os.path.join(destination, 'yyy-1.2.3.whl'), 'w'): + with open(os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl'), 'w'): pass - with open(os.path.join(destination, 'zzz-4.5.6.whl'), 'w'): + with open(os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl'), 'w'): pass result = script.pip('cache', 'list', 'zzz') assert 'yyy-1.2.3' not in result.stdout @@ -62,9 +62,9 @@ def test_cache_remove(script, monkeypatch): wheel_cache_dir = os.path.join(cache_dir, 'wheels') destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) - with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3.whl'), 'w'): + with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3-py3-none-any.whl'), 'w'): pass - with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6.whl'), 'w'): + with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6-py27-none-any.whl'), 'w'): pass script.pip('cache', 'remove', expect_error=True) @@ -84,9 +84,9 @@ def test_cache_purge(script, monkeypatch): wheel_cache_dir = os.path.join(cache_dir, 'wheels') destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) - with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3.whl'), 'w'): + with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3-py3-none-any.whl'), 'w'): pass - with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6.whl'), 'w'): + with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6-py27-none-any.whl'), 'w'): pass result = script.pip('cache', 'purge', 'aaa', '--verbose', From d57407a37d788719f93757b7345acce7fda185b0 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 5 Mar 2020 13:44:25 -0500 Subject: [PATCH 28/40] [commands/cache] Make _find_wheels(), and this `pip cache {list,remove}` behave more predictably. --- src/pip/_internal/commands/cache.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index e0d751d8d03..af97b40217c 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -144,4 +144,23 @@ def _wheels_cache_dir(self, options): def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] wheel_dir = self._wheels_cache_dir(options) - return filesystem.find_files(wheel_dir, pattern + '*.whl') + + # The wheel filename format, as specified in PEP 427, is: + # {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl + # + # Additionally, non-alphanumeric values in the distribution are + # normalized to underscores (_), meaning hyphens can never occur + # before `-{version}`. + # + # Given that information: + # - If the pattern we're given contains a hyphen (-), the user is + # providing at least the version. Thus, we can just append `*.whl` + # to match the rest of it. + # - If the pattern we're given doesn't contain a hyphen (-), the + # user is only providing the name. Thus, we append `-*.whl` to + # match the hyphen before the version, followed by anything else. + # + # PEP 427: https://www.python.org/dev/peps/pep-0427/ + pattern = pattern + ("*.whl" if "-" in pattern else "-*.whl") + + return filesystem.find_files(wheel_dir, pattern) From e1fde1facaf44ab37882b74d14ab305d422bcd22 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 5 Mar 2020 13:47:25 -0500 Subject: [PATCH 29/40] [commands/cache] Remove unnecessary re-definition of __init__. --- src/pip/_internal/commands/cache.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index af97b40217c..fc63d5eeceb 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -39,10 +39,6 @@ class CacheCommand(Command): %prog purge """ - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(CacheCommand, self).__init__(*args, **kw) - def run(self, options, args): # type: (Values, List[Any]) -> int handlers = { From e804aa56aff9048c58b6099af121aa6cebb7a299 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Thu, 5 Mar 2020 13:49:50 -0500 Subject: [PATCH 30/40] [commands/cache] Have `pip cache info` raise an exception if it gets any arguments. --- src/pip/_internal/commands/cache.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index fc63d5eeceb..7e3f72e080b 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -68,6 +68,9 @@ def run(self, options, args): def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None + if args: + raise CommandError('Too many arguments') + num_packages = len(self._find_wheels(options, '*')) cache_location = self._wheels_cache_dir(options) From 6e425d80093b5e440eeb7a6797f13318ae8dc7b2 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Fri, 6 Mar 2020 16:12:55 -0500 Subject: [PATCH 31/40] [tests/functional/cache] Refactor to be less redundant. --- tests/functional/test_cache.py | 88 +++++++++++++++------------------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 2cd02829880..00ed6a73eaa 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -11,83 +11,73 @@ def _cache_dir(script): return result.stdout.strip() -def test_cache_info(script, monkeypatch): - result = script.pip('cache', 'info') +def _wheel_cache_contents(script): cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) - assert 'Location: {}'.format(os.path.normcase(cache_dir)) in result.stdout - # TODO(@duckinator): This should probably test that the number of - # packages is actually correct, but I'm not sure how to do that - # without pretty much re-implementing the entire cache info command. - assert 'Number of wheels: ' in result.stdout + files = [ + ('yyy-1.2.3', os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl')), + ('zzz-4.5.6', os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl')), + ] + + for _name, filename in files: + with open(filename, 'w'): + pass + return files -def test_cache_list(script, monkeypatch): + +def test_cache_info(script): cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') - destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') - os.makedirs(destination) - with open(os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl'), 'w'): - pass - with open(os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl'), 'w'): - pass + cache_files = _wheel_cache_contents(script) + + result = script.pip('cache', 'info') + + assert 'Location: {}'.format(os.path.normcase(cache_dir)) in result.stdout + assert 'Number of wheels: {}'.format(len(cache_files)) in result.stdout + + +def test_cache_list(script): + cache_files = _wheel_cache_contents(script) + packages = [name for (name, _path) in cache_files] result = script.pip('cache', 'list') - assert 'yyy-1.2.3' in result.stdout - assert 'zzz-4.5.6' in result.stdout - shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) + for package in packages: + assert package in result.stdout + # assert 'yyy-1.2.3' in result.stdout + # assert 'zzz-4.5.6' in result.stdout -def test_cache_list_too_many_args(script, monkeypatch): +def test_cache_list_too_many_args(script): script.pip('cache', 'list', 'aaa', 'bbb', expect_error=True) -def test_cache_list_with_pattern(script, monkeypatch): - cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') - destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') - os.makedirs(destination) - with open(os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl'), 'w'): - pass - with open(os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl'), 'w'): - pass +def test_cache_list_with_pattern(script): + cache_files = _wheel_cache_contents(script) + result = script.pip('cache', 'list', 'zzz') assert 'yyy-1.2.3' not in result.stdout assert 'zzz-4.5.6' in result.stdout - shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) def test_cache_remove(script, monkeypatch): - cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') - destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') - os.makedirs(destination) - with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3-py3-none-any.whl'), 'w'): - pass - with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6-py27-none-any.whl'), 'w'): - pass + cache_files = _wheel_cache_contents(script) script.pip('cache', 'remove', expect_error=True) result = script.pip('cache', 'remove', 'zzz', '--verbose') assert 'yyy-1.2.3' not in result.stdout assert 'zzz-4.5.6' in result.stdout - shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) -def test_cache_remove_too_many_args(script, monkeypatch): +def test_cache_remove_too_many_args(script): script.pip('cache', 'remove', 'aaa', 'bbb', expect_error=True) -def test_cache_purge(script, monkeypatch): - cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') - destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') - os.makedirs(destination) - with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3-py3-none-any.whl'), 'w'): - pass - with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6-py27-none-any.whl'), 'w'): - pass +def test_cache_purge(script): + cache_files = _wheel_cache_contents(script) result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) @@ -97,5 +87,3 @@ def test_cache_purge(script, monkeypatch): result = script.pip('cache', 'purge', '--verbose') assert 'yyy-1.2.3' in result.stdout assert 'zzz-4.5.6' in result.stdout - - shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) From 274b295bd8181444e2d87d0d722858955c841bfd Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Fri, 6 Mar 2020 17:09:40 -0500 Subject: [PATCH 32/40] [tests/functional/cache] Make fixtures feel less magical. It bothered me that _whether or not a function had a certain argument_ dictated the contents of a directory. Pytest fixtures are inherently kinda magical, but that was a bit much for me. --- tests/functional/test_cache.py | 58 ++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 00ed6a73eaa..a3fd5e5a489 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,8 +1,11 @@ import os -import shutil +from glob import glob +import pytest -def _cache_dir(script): + +@pytest.fixture +def cache_dir(script): result = script.run( 'python', '-c', 'from pip._internal.locations import USER_CACHE_DIR;' @@ -11,9 +14,23 @@ def _cache_dir(script): return result.stdout.strip() -def _wheel_cache_contents(script): - cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') +@pytest.fixture +def wheel_cache_dir(cache_dir): + return os.path.join(cache_dir, 'wheels') + + +@pytest.fixture +def wheel_cache_files(wheel_cache_dir): + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + filenames = glob(os.path.join(destination, '*.whl')) + files = [] + for filename in filenames: + files.append(os.path.join(destination, filename)) + return files + + +@pytest.fixture +def populate_wheel_cache(wheel_cache_dir): destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) @@ -29,24 +46,20 @@ def _wheel_cache_contents(script): return files -def test_cache_info(script): - cache_dir = _cache_dir(script) - cache_files = _wheel_cache_contents(script) - +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_info(script, wheel_cache_dir, wheel_cache_files): result = script.pip('cache', 'info') - assert 'Location: {}'.format(os.path.normcase(cache_dir)) in result.stdout - assert 'Number of wheels: {}'.format(len(cache_files)) in result.stdout + assert 'Location: {}'.format(os.path.normcase(wheel_cache_dir)) in result.stdout + assert 'Number of wheels: {}'.format(len(wheel_cache_files)) in result.stdout +@pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list(script): - cache_files = _wheel_cache_contents(script) - packages = [name for (name, _path) in cache_files] result = script.pip('cache', 'list') - for package in packages: - assert package in result.stdout - # assert 'yyy-1.2.3' in result.stdout - # assert 'zzz-4.5.6' in result.stdout + + assert 'yyy-1.2.3' in result.stdout + assert 'zzz-4.5.6' in result.stdout def test_cache_list_too_many_args(script): @@ -54,17 +67,15 @@ def test_cache_list_too_many_args(script): expect_error=True) +@pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list_with_pattern(script): - cache_files = _wheel_cache_contents(script) - result = script.pip('cache', 'list', 'zzz') assert 'yyy-1.2.3' not in result.stdout assert 'zzz-4.5.6' in result.stdout -def test_cache_remove(script, monkeypatch): - cache_files = _wheel_cache_contents(script) - +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_remove(script): script.pip('cache', 'remove', expect_error=True) result = script.pip('cache', 'remove', 'zzz', '--verbose') assert 'yyy-1.2.3' not in result.stdout @@ -76,9 +87,8 @@ def test_cache_remove_too_many_args(script): expect_error=True) +@pytest.mark.usefixtures("populate_wheel_cache") def test_cache_purge(script): - cache_files = _wheel_cache_contents(script) - result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) assert 'yyy-1.2.3' not in result.stdout From c6b5a52a5ab794cfc433b93e9c4a54c6fdff50d6 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Fri, 6 Mar 2020 21:26:04 -0500 Subject: [PATCH 33/40] [tests/functional/test_cache] Always call normcase on cache dir; fix line length problems. --- tests/functional/test_cache.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index a3fd5e5a489..be4e5589e36 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -16,7 +16,7 @@ def cache_dir(script): @pytest.fixture def wheel_cache_dir(cache_dir): - return os.path.join(cache_dir, 'wheels') + return os.path.normcase(os.path.join(cache_dir, 'wheels')) @pytest.fixture @@ -36,7 +36,7 @@ def populate_wheel_cache(wheel_cache_dir): files = [ ('yyy-1.2.3', os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl')), - ('zzz-4.5.6', os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl')), + ('zzz-4.5.6', os.path.join(destination, 'zzz-4.5.6-py3-none-any.whl')), ] for _name, filename in files: @@ -50,8 +50,9 @@ def populate_wheel_cache(wheel_cache_dir): def test_cache_info(script, wheel_cache_dir, wheel_cache_files): result = script.pip('cache', 'info') - assert 'Location: {}'.format(os.path.normcase(wheel_cache_dir)) in result.stdout - assert 'Number of wheels: {}'.format(len(wheel_cache_files)) in result.stdout + assert 'Location: {}'.format(wheel_cache_dir) in result.stdout + num_wheels = len(wheel_cache_files) + assert 'Number of wheels: {}'.format(num_wheels) in result.stdout @pytest.mark.usefixtures("populate_wheel_cache") From 32ce3bacbe0893bc0ceb326b8df98ac9300a6915 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 10 Mar 2020 12:44:33 -0400 Subject: [PATCH 34/40] [tests/functional/cache] Rewrite all of the pip cache {list,remove} tests. --- tests/functional/test_cache.py | 106 +++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index be4e5589e36..9311c6ffcf8 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,4 +1,5 @@ import os +import re from glob import glob import pytest @@ -37,6 +38,8 @@ def populate_wheel_cache(wheel_cache_dir): files = [ ('yyy-1.2.3', os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl')), ('zzz-4.5.6', os.path.join(destination, 'zzz-4.5.6-py3-none-any.whl')), + ('zzz-4.5.7', os.path.join(destination, 'zzz-4.5.7-py3-none-any.whl')), + ('zzz-7.8.9', os.path.join(destination, 'zzz-7.8.9-py3-none-any.whl')), ] for _name, filename in files: @@ -46,6 +49,38 @@ def populate_wheel_cache(wheel_cache_dir): return files +def list_matches_wheel(wheel_name, lines): + """Returns True if any line in `lines`, which should be the output of + a `pip cache list` call, matches `wheel_name`. + + E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with + `- foo-1.2.3-py3-none-any.whl `.""" + expected = ' - {}-py3-none-any.whl '.format(wheel_name) + return any(map(lambda l: l.startswith(expected), lines)) + + +@pytest.fixture +def remove_matches_wheel(wheel_cache_dir): + """Returns True if any line in `lines`, which should be the output of + a `pip cache remove`/`pip cache purge` call, matches `wheel_name`. + + E.g., If wheel_name is `foo-1.2.3`, it searches for a line equal to + `Removed /arbitrary/pathname/foo-1.2.3-py3-none-any.whl`. + """ + + def _remove_matches_wheel(wheel_name, lines): + wheel_filename = '{}-py3-none-any.whl'.format(wheel_name) + + # The "/arbitrary/pathname/" bit is an implementation detail of how + # the `populate_wheel_cache` fixture is implemented. + expected = 'Removed {}/arbitrary/pathname/{}'.format( + wheel_cache_dir, wheel_filename, + ) + return expected in lines + + return _remove_matches_wheel + + @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_info(script, wheel_cache_dir, wheel_cache_files): result = script.pip('cache', 'info') @@ -57,37 +92,86 @@ def test_cache_info(script, wheel_cache_dir, wheel_cache_files): @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list(script): + """Running `pip cache list` should return exactly what the + populate_wheel_cache fixture adds.""" result = script.pip('cache', 'list') - - assert 'yyy-1.2.3' in result.stdout - assert 'zzz-4.5.6' in result.stdout + lines = result.stdout.splitlines() + assert list_matches_wheel('yyy-1.2.3', lines) + assert list_matches_wheel('zzz-4.5.6', lines) + assert list_matches_wheel('zzz-4.5.7', lines) + assert list_matches_wheel('zzz-7.8.9', lines) def test_cache_list_too_many_args(script): + """Passing `pip cache list` too many arguments should cause an error.""" script.pip('cache', 'list', 'aaa', 'bbb', expect_error=True) @pytest.mark.usefixtures("populate_wheel_cache") -def test_cache_list_with_pattern(script): - result = script.pip('cache', 'list', 'zzz') - assert 'yyy-1.2.3' not in result.stdout - assert 'zzz-4.5.6' in result.stdout +def test_cache_list_name_match(script): + """Running `pip cache list zzz` should list zzz-4.5.6, zzz-4.5.7, + zzz-7.8.9, but nothing else.""" + result = script.pip('cache', 'list', 'zzz', '--verbose') + lines = result.stdout.splitlines() + + assert not list_matches_wheel('yyy-1.2.3', lines) + assert list_matches_wheel('zzz-4.5.6', lines) + assert list_matches_wheel('zzz-4.5.7', lines) + assert list_matches_wheel('zzz-7.8.9', lines) @pytest.mark.usefixtures("populate_wheel_cache") -def test_cache_remove(script): +def test_cache_list_name_and_version_match(script): + """Running `pip cache list zzz-4.5.6` should list zzz-4.5.6, but + nothing else.""" + result = script.pip('cache', 'list', 'zzz-4.5.6', '--verbose') + lines = result.stdout.splitlines() + + assert not list_matches_wheel('yyy-1.2.3', lines) + assert list_matches_wheel('zzz-4.5.6', lines) + assert not list_matches_wheel('zzz-4.5.7', lines) + assert not list_matches_wheel('zzz-7.8.9', lines) + + +@pytest.mark.usefixture("populate_wheel_cache") +def test_cache_remove_no_arguments(script): + """Running `pip cache remove` with no arguments should cause an error.""" script.pip('cache', 'remove', expect_error=True) - result = script.pip('cache', 'remove', 'zzz', '--verbose') - assert 'yyy-1.2.3' not in result.stdout - assert 'zzz-4.5.6' in result.stdout def test_cache_remove_too_many_args(script): + """Passing `pip cache remove` too many arguments should cause an error.""" script.pip('cache', 'remove', 'aaa', 'bbb', expect_error=True) +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_remove_name_match(script, remove_matches_wheel): + """Running `pip cache remove zzz` should remove zzz-4.5.6 and zzz-7.8.9, + but nothing else.""" + result = script.pip('cache', 'remove', 'zzz', '--verbose') + lines = result.stdout.splitlines() + + assert not remove_matches_wheel('yyy-1.2.3', lines) + assert remove_matches_wheel('zzz-4.5.6', lines) + assert remove_matches_wheel('zzz-4.5.7', lines) + assert remove_matches_wheel('zzz-7.8.9', lines) + + +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_remove_name_and_version_match(script, remove_matches_wheel): + """Running `pip cache remove zzz-4.5.6` should remove zzz-4.5.6, but + nothing else.""" + result = script.pip('cache', 'remove', 'zzz-4.5.6', '--verbose') + lines = result.stdout.splitlines() + + assert not remove_matches_wheel('yyy-1.2.3', lines) + assert remove_matches_wheel('zzz-4.5.6', lines) + assert not remove_matches_wheel('zzz-4.5.7', lines) + assert not remove_matches_wheel('zzz-7.8.9', lines) + + @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_purge(script): result = script.pip('cache', 'purge', 'aaa', '--verbose', From ba7c3ac9ec450452ec14e872bc9d6551dffc3d3b Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 10 Mar 2020 14:53:24 -0400 Subject: [PATCH 35/40] [tests/functional/test_cache] Add test `pip cache list` with an empty cache. --- tests/functional/test_cache.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 9311c6ffcf8..55080dbf4f2 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,5 +1,6 @@ import os import re +import shutil from glob import glob import pytest @@ -23,6 +24,10 @@ def wheel_cache_dir(cache_dir): @pytest.fixture def wheel_cache_files(wheel_cache_dir): destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + + if not os.path.exists(destination): + return [] + filenames = glob(os.path.join(destination, '*.whl')) files = [] for filename in filenames: @@ -49,6 +54,12 @@ def populate_wheel_cache(wheel_cache_dir): return files +@pytest.fixture +def empty_wheel_cache(wheel_cache_dir): + if os.path.exists(wheel_cache_dir): + shutil.rmtree(wheel_cache_dir) + + def list_matches_wheel(wheel_name, lines): """Returns True if any line in `lines`, which should be the output of a `pip cache list` call, matches `wheel_name`. @@ -102,6 +113,14 @@ def test_cache_list(script): assert list_matches_wheel('zzz-7.8.9', lines) +@pytest.mark.usefixtures("empty_wheel_cache") +def test_cache_list_with_empty_cache(script): + """Running `pip cache list` with an empty cache should print + "Nothing cached." and exit.""" + result = script.pip('cache', 'list') + assert result.stdout == "Nothing cached.\n" + + def test_cache_list_too_many_args(script): """Passing `pip cache list` too many arguments should cause an error.""" script.pip('cache', 'list', 'aaa', 'bbb', From a20b28d0080cc69ff0fd901c15be5a7c11faeeee Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 10 Mar 2020 15:04:07 -0400 Subject: [PATCH 36/40] [tests/functional/test_cache] Split apart tests for `pip cache purge`. --- tests/functional/test_cache.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 55080dbf4f2..37a0de0737e 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -192,12 +192,23 @@ def test_cache_remove_name_and_version_match(script, remove_matches_wheel): @pytest.mark.usefixtures("populate_wheel_cache") -def test_cache_purge(script): +def test_cache_purge(script, remove_matches_wheel): + result = script.pip('cache', 'purge', '--verbose') + lines = result.stdout.splitlines() + + assert remove_matches_wheel('yyy-1.2.3', lines) + assert remove_matches_wheel('zzz-4.5.6', lines) + assert remove_matches_wheel('zzz-4.5.7', lines) + assert remove_matches_wheel('zzz-7.8.9', lines) + + +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_purge_too_many_args(script, wheel_cache_files): result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) - assert 'yyy-1.2.3' not in result.stdout - assert 'zzz-4.5.6' not in result.stdout + assert result.stdout == '' + assert result.stderr == 'ERROR: Too many arguments\n' - result = script.pip('cache', 'purge', '--verbose') - assert 'yyy-1.2.3' in result.stdout - assert 'zzz-4.5.6' in result.stdout + # Make sure nothing was deleted. + for filename in wheel_cache_files: + assert os.path.exists(filename) From 88582379038dbb91c13d5bc1b78ed5585e994cfb Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 10 Mar 2020 15:08:18 -0400 Subject: [PATCH 37/40] [tests/functional/test_cache] Refactor list_matches_wheel() and remove_matches_wheel(). --- tests/functional/test_cache.py | 69 +++++++++++++++++----------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 37a0de0737e..aa8dc13082f 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -60,26 +60,29 @@ def empty_wheel_cache(wheel_cache_dir): shutil.rmtree(wheel_cache_dir) -def list_matches_wheel(wheel_name, lines): - """Returns True if any line in `lines`, which should be the output of +def list_matches_wheel(wheel_name, result): + """Returns True if any line in `result`, which should be the output of a `pip cache list` call, matches `wheel_name`. E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with `- foo-1.2.3-py3-none-any.whl `.""" + lines = result.stdout.splitlines() expected = ' - {}-py3-none-any.whl '.format(wheel_name) return any(map(lambda l: l.startswith(expected), lines)) @pytest.fixture def remove_matches_wheel(wheel_cache_dir): - """Returns True if any line in `lines`, which should be the output of + """Returns True if any line in `result`, which should be the output of a `pip cache remove`/`pip cache purge` call, matches `wheel_name`. E.g., If wheel_name is `foo-1.2.3`, it searches for a line equal to `Removed /arbitrary/pathname/foo-1.2.3-py3-none-any.whl`. """ - def _remove_matches_wheel(wheel_name, lines): + def _remove_matches_wheel(wheel_name, result): + lines = result.stdout.splitlines() + wheel_filename = '{}-py3-none-any.whl'.format(wheel_name) # The "/arbitrary/pathname/" bit is an implementation detail of how @@ -106,11 +109,11 @@ def test_cache_list(script): """Running `pip cache list` should return exactly what the populate_wheel_cache fixture adds.""" result = script.pip('cache', 'list') - lines = result.stdout.splitlines() - assert list_matches_wheel('yyy-1.2.3', lines) - assert list_matches_wheel('zzz-4.5.6', lines) - assert list_matches_wheel('zzz-4.5.7', lines) - assert list_matches_wheel('zzz-7.8.9', lines) + + assert list_matches_wheel('yyy-1.2.3', result) + assert list_matches_wheel('zzz-4.5.6', result) + assert list_matches_wheel('zzz-4.5.7', result) + assert list_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("empty_wheel_cache") @@ -132,12 +135,11 @@ def test_cache_list_name_match(script): """Running `pip cache list zzz` should list zzz-4.5.6, zzz-4.5.7, zzz-7.8.9, but nothing else.""" result = script.pip('cache', 'list', 'zzz', '--verbose') - lines = result.stdout.splitlines() - assert not list_matches_wheel('yyy-1.2.3', lines) - assert list_matches_wheel('zzz-4.5.6', lines) - assert list_matches_wheel('zzz-4.5.7', lines) - assert list_matches_wheel('zzz-7.8.9', lines) + assert not list_matches_wheel('yyy-1.2.3', result) + assert list_matches_wheel('zzz-4.5.6', result) + assert list_matches_wheel('zzz-4.5.7', result) + assert list_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("populate_wheel_cache") @@ -145,12 +147,11 @@ def test_cache_list_name_and_version_match(script): """Running `pip cache list zzz-4.5.6` should list zzz-4.5.6, but nothing else.""" result = script.pip('cache', 'list', 'zzz-4.5.6', '--verbose') - lines = result.stdout.splitlines() - assert not list_matches_wheel('yyy-1.2.3', lines) - assert list_matches_wheel('zzz-4.5.6', lines) - assert not list_matches_wheel('zzz-4.5.7', lines) - assert not list_matches_wheel('zzz-7.8.9', lines) + assert not list_matches_wheel('yyy-1.2.3', result) + assert list_matches_wheel('zzz-4.5.6', result) + assert not list_matches_wheel('zzz-4.5.7', result) + assert not list_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixture("populate_wheel_cache") @@ -170,12 +171,11 @@ def test_cache_remove_name_match(script, remove_matches_wheel): """Running `pip cache remove zzz` should remove zzz-4.5.6 and zzz-7.8.9, but nothing else.""" result = script.pip('cache', 'remove', 'zzz', '--verbose') - lines = result.stdout.splitlines() - assert not remove_matches_wheel('yyy-1.2.3', lines) - assert remove_matches_wheel('zzz-4.5.6', lines) - assert remove_matches_wheel('zzz-4.5.7', lines) - assert remove_matches_wheel('zzz-7.8.9', lines) + assert not remove_matches_wheel('yyy-1.2.3', result) + assert remove_matches_wheel('zzz-4.5.6', result) + assert remove_matches_wheel('zzz-4.5.7', result) + assert remove_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("populate_wheel_cache") @@ -183,27 +183,28 @@ def test_cache_remove_name_and_version_match(script, remove_matches_wheel): """Running `pip cache remove zzz-4.5.6` should remove zzz-4.5.6, but nothing else.""" result = script.pip('cache', 'remove', 'zzz-4.5.6', '--verbose') - lines = result.stdout.splitlines() - assert not remove_matches_wheel('yyy-1.2.3', lines) - assert remove_matches_wheel('zzz-4.5.6', lines) - assert not remove_matches_wheel('zzz-4.5.7', lines) - assert not remove_matches_wheel('zzz-7.8.9', lines) + assert not remove_matches_wheel('yyy-1.2.3', result) + assert remove_matches_wheel('zzz-4.5.6', result) + assert not remove_matches_wheel('zzz-4.5.7', result) + assert not remove_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_purge(script, remove_matches_wheel): + """Running `pip cache purge` should remove all cached wheels.""" result = script.pip('cache', 'purge', '--verbose') - lines = result.stdout.splitlines() - assert remove_matches_wheel('yyy-1.2.3', lines) - assert remove_matches_wheel('zzz-4.5.6', lines) - assert remove_matches_wheel('zzz-4.5.7', lines) - assert remove_matches_wheel('zzz-7.8.9', lines) + assert remove_matches_wheel('yyy-1.2.3', result) + assert remove_matches_wheel('zzz-4.5.6', result) + assert remove_matches_wheel('zzz-4.5.7', result) + assert remove_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_purge_too_many_args(script, wheel_cache_files): + """Running `pip cache purge aaa` should raise an error and remove no + cached wheels.""" result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) assert result.stdout == '' From b7239f5deedbfd030bc9b247214bf8bf590a561a Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 10 Mar 2020 15:40:25 -0400 Subject: [PATCH 38/40] [tests/functional/test_cache] Remove unused import. --- tests/functional/test_cache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index aa8dc13082f..c0112929d9e 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,5 +1,4 @@ import os -import re import shutil from glob import glob From 0c4eafad6263fe7e6755635e041cecc390d1a57e Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Tue, 10 Mar 2020 16:00:28 -0400 Subject: [PATCH 39/40] [tests/functional/test_cache] Fix test on Python 2.7. --- tests/functional/test_cache.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index c0112929d9e..1ab7fa6457e 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -207,7 +207,10 @@ def test_cache_purge_too_many_args(script, wheel_cache_files): result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) assert result.stdout == '' - assert result.stderr == 'ERROR: Too many arguments\n' + + # This would be `result.stderr == ...`, but Pip prints deprecation + # warnings on Python 2.7, so we check if the _line_ is in stderr. + assert 'ERROR: Too many arguments' in result.stderr.splitlines() # Make sure nothing was deleted. for filename in wheel_cache_files: From b988417b4f0746c70d04f5a78cb92cfe15e338aa Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash Date: Fri, 13 Mar 2020 17:56:03 -0400 Subject: [PATCH 40/40] [tests/functional/test_cache] Use os.path.join() instead of hard-coding the path separator. --- tests/functional/test_cache.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 1ab7fa6457e..a464ece7945 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -86,9 +86,10 @@ def _remove_matches_wheel(wheel_name, result): # The "/arbitrary/pathname/" bit is an implementation detail of how # the `populate_wheel_cache` fixture is implemented. - expected = 'Removed {}/arbitrary/pathname/{}'.format( - wheel_cache_dir, wheel_filename, + path = os.path.join( + wheel_cache_dir, 'arbitrary', 'pathname', wheel_filename, ) + expected = 'Removed {}'.format(path) return expected in lines return _remove_matches_wheel