diff --git a/news/4256.feature.rst b/news/4256.feature.rst new file mode 100644 index 000000000..03d7c95d7 --- /dev/null +++ b/news/4256.feature.rst @@ -0,0 +1 @@ +Add ``--exclude`` option to ``pip freeze`` and ``pip list`` commands to explicitly exclude packages from the output. diff --git a/news/4256.removal.rst b/news/4256.removal.rst new file mode 100644 index 000000000..6d560b7bb --- /dev/null +++ b/news/4256.removal.rst @@ -0,0 +1,2 @@ +``pip freeze`` will stop filtering the ``pip``, ``setuptools``, ``distribute`` and ``wheel`` packages from ``pip freeze`` output in a future version. +To keep the previous behavior, users should use the new ``--exclude`` option. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ec4df3fcc..6a6634fb8 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -20,6 +20,8 @@ from functools import partial from optparse import SUPPRESS_HELP, Option, OptionGroup from textwrap import dedent +from pip._vendor.packaging.utils import canonicalize_name + from pip._internal.cli.progress_bars import BAR_TYPES from pip._internal.exceptions import CommandError from pip._internal.locations import USER_CACHE_DIR, get_src_prefix @@ -133,9 +135,15 @@ def _path_option_check(option, opt, value): return os.path.expanduser(value) +def _package_name_option_check(option, opt, value): + # type: (Option, str, str) -> str + return canonicalize_name(value) + + class PipOption(Option): - TYPES = Option.TYPES + ("path",) + TYPES = Option.TYPES + ("path", "package_name") TYPE_CHECKER = Option.TYPE_CHECKER.copy() + TYPE_CHECKER["package_name"] = _package_name_option_check TYPE_CHECKER["path"] = _path_option_check @@ -866,6 +874,17 @@ def check_list_path_option(options): ) +list_exclude = partial( + PipOption, + '--exclude', + dest='excludes', + action='append', + metavar='package', + type='package_name', + help="Exclude specified package from the output", +) # type: Callable[..., Option] + + no_python_version_warning = partial( Option, '--no-python-version-warning', diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 20084a498..4d1ce69a1 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -74,6 +74,7 @@ class FreezeCommand(Command): dest='exclude_editable', action='store_true', help='Exclude editable package from output.') + self.cmd_opts.add_option(cmdoptions.list_exclude()) self.parser.insert_option_group(0, self.cmd_opts) @@ -85,6 +86,9 @@ class FreezeCommand(Command): if not options.freeze_all: skip.update(DEV_PKGS) + if options.excludes: + skip.update(options.excludes) + cmdoptions.check_list_path_option(options) if options.find_links: diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index a6dfa5fd5..27b15d70a 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -12,6 +12,7 @@ from pip._internal.exceptions import CommandError from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.utils.compat import stdlib_pkgs from pip._internal.utils.misc import ( dist_is_editable, get_installed_distributions, @@ -114,6 +115,7 @@ class ListCommand(IndexGroupCommand): help='Include editable package from output.', default=True, ) + self.cmd_opts.add_option(cmdoptions.list_exclude()) index_opts = cmdoptions.make_option_group( cmdoptions.index_group, self.parser ) @@ -147,12 +149,17 @@ class ListCommand(IndexGroupCommand): cmdoptions.check_list_path_option(options) + skip = set(stdlib_pkgs) + if options.excludes: + skip.update(options.excludes) + packages = get_installed_distributions( local_only=options.local, user_only=options.user, editables_only=options.editable, include_editables=options.include_editable, paths=options.path, + skip=skip, ) # get_not_required must be called firstly in order to find and diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 74d676aed..f0a2265f3 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -16,6 +16,7 @@ from tests.lib import ( need_mercurial, need_svn, path_to_url, + wheel, ) distribute_re = re.compile('^distribute==[0-9.]+\n', re.MULTILINE) @@ -80,6 +81,25 @@ def test_freeze_with_pip(script): assert 'pip==' in result.stdout +def test_exclude_and_normalization(script, tmpdir): + req_path = wheel.make_wheel( + name="Normalizable_Name", version="1.0").save_to_dir(tmpdir) + script.pip("install", "--no-index", req_path) + result = script.pip("freeze") + assert "Normalizable-Name" in result.stdout + result = script.pip("freeze", "--exclude", "normalizablE-namE") + assert "Normalizable-Name" not in result.stdout + + +def test_freeze_multiple_exclude_with_all(script, with_wheel): + result = script.pip('freeze', '--all') + assert 'pip==' in result.stdout + assert 'wheel==' in result.stdout + result = script.pip('freeze', '--all', '--exclude', 'pip', '--exclude', 'wheel') + assert 'pip==' not in result.stdout + assert 'wheel==' not in result.stdout + + def test_freeze_with_invalid_names(script): """ Test that invalid names produce warnings and are passed over gracefully. diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 37787246b..40dfbdea3 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -3,7 +3,7 @@ import os import pytest -from tests.lib import create_test_package_with_setup +from tests.lib import create_test_package_with_setup, wheel from tests.lib.path import Path @@ -94,6 +94,19 @@ def test_local_columns_flag(simple_script): assert 'simple 1.0' in result.stdout, str(result) +def test_multiple_exclude_and_normalization(script, tmpdir): + req_path = wheel.make_wheel( + name="Normalizable_Name", version="1.0").save_to_dir(tmpdir) + script.pip("install", "--no-index", req_path) + result = script.pip("list") + print(result.stdout) + assert "Normalizable-Name" in result.stdout + assert "pip" in result.stdout + result = script.pip("list", "--exclude", "normalizablE-namE", "--exclude", "pIp") + assert "Normalizable-Name" not in result.stdout + assert "pip" not in result.stdout + + @pytest.mark.network @pytest.mark.incompatible_with_test_venv def test_user_flag(script, data):