From 6f7ec60e4d6b29f69e863da3126e473a3f6f6549 Mon Sep 17 00:00:00 2001 From: ychampion Date: Wed, 8 Jul 2026 01:16:39 +0000 Subject: [PATCH 1/4] Preserve display output under quiet mode Constraint: pip quiet mode uses logging thresholds that also suppress primary command output. Rejected: Emitting display output at warning level | it would misclassify normal output and route it through stderr. Confidence: high Scope-risk: moderate Directive: Keep install/download progress quiet-suppressible; opt in only primary display output. Tested: uv run --with pre-commit pre-commit run black --files ; uv run --with pre-commit pre-commit run ruff-check --files ; uv run --with nox nox -s test-3.12 -- -q; uv run --with nox nox -s test-3.12 -- tests/functional/test_install.py::test_install_quiet -q; source-tree quiet-mode smoke checks for list/check/hash/cache/config/debug. Not-tested: Full pip suite across every supported Python version. --- news/13018.bugfix.rst | 1 + src/pip/_internal/commands/cache.py | 14 +++--- src/pip/_internal/commands/check.py | 5 +- src/pip/_internal/commands/configuration.py | 16 +++--- src/pip/_internal/commands/debug.py | 18 ++++--- src/pip/_internal/commands/hash.py | 6 ++- src/pip/_internal/commands/help.py | 6 ++- src/pip/_internal/commands/index.py | 11 +++-- src/pip/_internal/commands/list.py | 10 ++-- src/pip/_internal/commands/search.py | 17 ++++--- src/pip/_internal/commands/show.py | 54 +++++++++++---------- src/pip/_internal/utils/misc.py | 53 +++++++++++++++++++- tests/functional/test_cache.py | 6 +++ tests/functional/test_check.py | 9 ++++ tests/functional/test_configuration.py | 3 ++ tests/functional/test_debug.py | 10 ++++ tests/functional/test_hash.py | 10 ++++ tests/functional/test_help.py | 10 ++++ tests/functional/test_index.py | 20 ++++++++ tests/functional/test_list.py | 9 ++++ tests/functional/test_show.py | 11 +++++ 21 files changed, 233 insertions(+), 66 deletions(-) create mode 100644 news/13018.bugfix.rst diff --git a/news/13018.bugfix.rst b/news/13018.bugfix.rst new file mode 100644 index 0000000000..0c0f038019 --- /dev/null +++ b/news/13018.bugfix.rst @@ -0,0 +1 @@ +Preserve primary output from display commands when ``--quiet`` is used. diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 232ace8d40..fea95fae26 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -8,7 +8,7 @@ from pip._internal.exceptions import CommandError, PipError from pip._internal.utils import filesystem from pip._internal.utils.logging import getLogger -from pip._internal.utils.misc import format_size +from pip._internal.utils.misc import format_size, write_output logger = getLogger(__name__) @@ -88,7 +88,7 @@ def get_cache_dir(self, options: Values, args: list[str]) -> None: if args: raise CommandError("Too many arguments") - logger.info(options.cache_dir) + write_output(options.cache_dir, show_on_quiet=True) def get_cache_info(self, options: Values, args: list[str]) -> None: if args: @@ -128,7 +128,7 @@ def get_cache_info(self, options: Values, args: list[str]) -> None: .strip() ) - logger.info(message) + write_output(message, show_on_quiet=True) def list_cache_items(self, options: Values, args: list[str]) -> None: if len(args) > 1: @@ -147,7 +147,7 @@ def list_cache_items(self, options: Values, args: list[str]) -> None: def format_for_human(self, files: list[str]) -> None: if not files: - logger.info("No locally built wheels cached.") + write_output("No locally built wheels cached.", show_on_quiet=True) return results = [] @@ -155,12 +155,12 @@ def format_for_human(self, files: list[str]) -> None: wheel = os.path.basename(filename) size = filesystem.format_file_size(filename) results.append(f" - {wheel} ({size})") - logger.info("Cache contents:\n") - logger.info("\n".join(sorted(results))) + write_output("Cache contents:\n", show_on_quiet=True) + write_output("\n".join(sorted(results)), show_on_quiet=True) def format_for_abspath(self, files: list[str]) -> None: if files: - logger.info("\n".join(sorted(files))) + write_output("\n".join(sorted(files)), show_on_quiet=True) def remove_cache_items(self, options: Values, args: list[str]) -> None: if len(args) > 1: diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index 516757eead..1c8c3ff565 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -40,6 +40,7 @@ def run(self, options: Values, args: list[str]) -> int: project_name, version, dependency[0], + show_on_quiet=True, ) for project_name in conflicting: @@ -52,15 +53,17 @@ def run(self, options: Values, args: list[str]) -> int: req, dep_name, dep_version, + show_on_quiet=True, ) for package in unsupported: write_output( "%s %s is not supported on this platform", package.raw_name, package.version, + show_on_quiet=True, ) if missing or conflicting or parsing_probs or unsupported: return ERROR else: - write_output("No broken requirements found.") + write_output("No broken requirements found.", show_on_quiet=True) return SUCCESS diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 4fa860ff9b..0bfed9dc4e 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -179,13 +179,13 @@ def list_values(self, options: Values, args: list[str]) -> None: for key, value in sorted(self.configuration.items()): for key, value in sorted(value.items()): - write_output("%s=%r", key, value) + write_output("%s=%r", key, value, show_on_quiet=True) def get_name(self, options: Values, args: list[str]) -> None: key = self._get_n_args(args, "get [name]", n=1) value = self.configuration.get_value(key) - write_output("%s", value) + write_output("%s", value, show_on_quiet=True) def set_name_value(self, options: Values, args: list[str]) -> None: key, value = self._get_n_args(args, "set [name] [value]", n=2) @@ -207,11 +207,13 @@ def list_config_values(self, options: Values, args: list[str]) -> None: # Iterate over config files and print if they exist, and the # key-value pairs present in them if they do for variant, files in sorted(self.configuration.iter_config_files()): - write_output("%s:", variant) + write_output("%s:", variant, show_on_quiet=True) for fname in files: with indent_log(): file_exists = os.path.exists(fname) - write_output("%s, exists: %r", fname, file_exists) + write_output( + "%s, exists: %r", fname, file_exists, show_on_quiet=True + ) if file_exists: self.print_config_file_values(variant, fname) @@ -221,15 +223,15 @@ def print_config_file_values(self, variant: Kind, fname: str) -> None: with indent_log(): if name == fname: for confname, confvalue in value.items(): - write_output("%s: %s", confname, confvalue) + write_output("%s: %s", confname, confvalue, show_on_quiet=True) def print_env_var_values(self) -> None: """Get key-values pairs present as environment variables""" - write_output("%s:", "env_var") + write_output("%s:", "env_var", show_on_quiet=True) with indent_log(): for key, value in sorted(self.configuration.get_environ_vars()): env_var = f"PIP_{key.upper()}" - write_output("%s=%r", env_var, value) + write_output("%s=%r", env_var, value, show_on_quiet=True) def open_in_editor(self, options: Values, args: list[str]) -> None: editor = self._determine_editor(options) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 7d5f9039c5..c1e7f63aa2 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -19,17 +19,17 @@ from pip._internal.metadata import get_environment from pip._internal.utils.compat import get_locale_encoding, open_text_resource from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import get_pip_version +from pip._internal.utils.misc import get_pip_version, write_output logger = logging.getLogger(__name__) def show_value(name: str, value: Any) -> None: - logger.info("%s: %s", name, value) + write_output("%s: %s", name, value, show_on_quiet=True) def show_sys_implementation() -> None: - logger.info("sys.implementation:") + write_output("sys.implementation:", show_on_quiet=True) implementation_name = sys.implementation.name with indent_log(): show_value("name", implementation_name) @@ -91,11 +91,13 @@ def show_actual_vendor_versions(vendor_txt_versions: dict[str, str]) -> None: " (CONFLICT: vendor.txt suggests version should" f" be {expected_version})" ) - logger.info("%s==%s%s", module_name, actual_version, extra_message) + write_output( + "%s==%s%s", module_name, actual_version, extra_message, show_on_quiet=True + ) def show_vendor_versions() -> None: - logger.info("vendored library versions:") + write_output("vendored library versions:", show_on_quiet=True) vendor_txt_versions = create_vendor_txt_map() with indent_log(): @@ -115,7 +117,7 @@ def show_tags(options: Values) -> None: suffix = f" (target: {formatted_target})" msg = f"Compatible tags: {len(tags)}{suffix}" - logger.info(msg) + write_output(msg, show_on_quiet=True) if options.verbose < 1 and len(tags) > tag_limit: tags_limited = True @@ -125,11 +127,11 @@ def show_tags(options: Values) -> None: with indent_log(): for tag in tags: - logger.info(str(tag)) + write_output(str(tag), show_on_quiet=True) if tags_limited: msg = f"...\n[First {tag_limit} tags shown. Pass --verbose to show all.]" - logger.info(msg) + write_output(msg, show_on_quiet=True) def ca_bundle_info(config: Configuration) -> str: diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index 271a4c91a7..2bf9c8e057 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -44,7 +44,11 @@ def run(self, options: Values, args: list[str]) -> int: algorithm = options.algorithm for path in args: write_output( - "%s:\n--hash=%s:%s", path, algorithm, _hash_of_file(path, algorithm) + "%s:\n--hash=%s:%s", + path, + algorithm, + _hash_of_file(path, algorithm), + show_on_quiet=True, ) return SUCCESS diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index 2ae658ff5e..353c6e990f 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -20,9 +20,13 @@ def run(self, options: Values, args: list[str]) -> int: ) try: - # 'pip help' with no args is handled by pip.__init__.parseopt() cmd_name = args[0] # the command we need help for except IndexError: + # 'pip help' with no args is handled by parse_command(), but + # command-level options like 'pip help --quiet' reach this point. + from pip._internal.cli.main_parser import create_main_parser + + create_main_parser().print_help() return SUCCESS if cmd_name not in commands_dict: diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index ce0bafda39..f3ef5c4d54 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -158,9 +158,12 @@ def get_available_package_versions(self, options: Values, args: list[Any]) -> No if dist is not None: structured_output["installed_version"] = str(dist.version) - write_output(json.dumps(structured_output)) + write_output(json.dumps(structured_output), show_on_quiet=True) else: - write_output(f"{query} ({latest})") - write_output("Available versions: {}".format(", ".join(formatted_versions))) - print_dist_installation_info(latest, dist) + write_output(f"{query} ({latest})", show_on_quiet=True) + write_output( + "Available versions: {}".format(", ".join(formatted_versions)), + show_on_quiet=True, + ) + print_dist_installation_info(latest, dist, show_on_quiet=True) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index e41c511fd2..6bca0bf202 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -298,11 +298,13 @@ def output_package_listing( except InvalidVersion: req_string = f"{dist.raw_name}==={dist.raw_version}" if options.verbose >= 1: - write_output("%s (%s)", req_string, dist.location) + write_output( + "%s (%s)", req_string, dist.location, show_on_quiet=True + ) else: - write_output(req_string) + write_output(req_string, show_on_quiet=True) elif options.list_format == "json": - write_output(format_for_json(packages, options)) + write_output(format_for_json(packages, options), show_on_quiet=True) def output_package_listing_columns( self, data: list[list[str]], header: list[str] @@ -318,7 +320,7 @@ def output_package_listing_columns( pkg_strings.insert(1, " ".join("-" * x for x in sizes)) for val in pkg_strings: - write_output(val) + write_output(val, show_on_quiet=True) def format_for_columns( diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index b8dbc27d3a..365ac702e6 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -113,21 +113,26 @@ def transform_hits(hits: list[dict[str, str]]) -> list[TransformedHit]: return list(packages.values()) -def print_dist_installation_info(latest: str, dist: BaseDistribution | None) -> None: +def print_dist_installation_info( + latest: str, dist: BaseDistribution | None, show_on_quiet: bool = False +) -> None: if dist is not None: with indent_log(): if dist.version == latest: - write_output("INSTALLED: %s (latest)", dist.version) + write_output( + "INSTALLED: %s (latest)", dist.version, show_on_quiet=show_on_quiet + ) else: - write_output("INSTALLED: %s", dist.version) + write_output("INSTALLED: %s", dist.version, show_on_quiet=show_on_quiet) if parse_version(latest).pre: write_output( "LATEST: %s (pre-release; install" " with `pip install --pre`)", latest, + show_on_quiet=show_on_quiet, ) else: - write_output("LATEST: %s", latest) + write_output("LATEST: %s", latest, show_on_quiet=show_on_quiet) def get_installed_distribution(name: str) -> BaseDistribution | None: @@ -167,9 +172,9 @@ def print_results( name_latest = f"{name} ({latest})" line = f"{name_latest:{name_column_width}} - {summary}" try: - write_output(line) + write_output(line, show_on_quiet=True) dist = get_installed_distribution(name) - print_dist_installation_info(latest, dist) + print_dist_installation_info(latest, dist, show_on_quiet=True) except UnicodeEncodeError: pass diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 40601a2b9d..cee838c5ff 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -187,51 +187,53 @@ def print_results( Print the information from installed distributions found. """ results_printed = False + + def output(msg: str, *args: object) -> None: + write_output(msg, *args, show_on_quiet=True) + for i, dist in enumerate(distributions): results_printed = True if i > 0: - write_output("---") + output("---") metadata_version = dist.metadata_version metadata_version_tuple = ( tuple(map(int, metadata_version.split("."))) if metadata_version else () ) - write_output("Name: %s", dist.name) - write_output("Version: %s", dist.version) - write_output("Summary: %s", dist.summary) - write_output("Home-page: %s", dist.homepage) - write_output("Author: %s", dist.author) - write_output("Author-email: %s", dist.author_email) + output("Name: %s", dist.name) + output("Version: %s", dist.version) + output("Summary: %s", dist.summary) + output("Home-page: %s", dist.homepage) + output("Author: %s", dist.author) + output("Author-email: %s", dist.author_email) if metadata_version_tuple >= (2, 4) and dist.license_expression: - write_output("License-Expression: %s", dist.license_expression) + output("License-Expression: %s", dist.license_expression) else: - write_output("License: %s", dist.license) - write_output("Location: %s", dist.location) + output("License: %s", dist.license) + output("Location: %s", dist.location) if dist.editable_project_location is not None: - write_output( - "Editable project location: %s", dist.editable_project_location - ) - write_output("Requires: %s", ", ".join(dist.requires)) - write_output("Required-by: %s", ", ".join(dist.required_by)) + output("Editable project location: %s", dist.editable_project_location) + output("Requires: %s", ", ".join(dist.requires)) + output("Required-by: %s", ", ".join(dist.required_by)) if verbose: - write_output("Metadata-Version: %s", dist.metadata_version) - write_output("Installer: %s", dist.installer) - write_output("Classifiers:") + output("Metadata-Version: %s", dist.metadata_version) + output("Installer: %s", dist.installer) + output("Classifiers:") for classifier in dist.classifiers: - write_output(" %s", classifier) - write_output("Entry-points:") + output(" %s", classifier) + output("Entry-points:") for entry in dist.entry_points: - write_output(" %s", entry.strip()) - write_output("Project-URLs:") + output(" %s", entry.strip()) + output("Project-URLs:") for project_url in dist.project_urls: - write_output(" %s", project_url) + output(" %s", project_url) if list_files: - write_output("Files:") + output("Files:") if dist.files is None: - write_output("Cannot locate RECORD or installed-files.txt") + output("Cannot locate RECORD or installed-files.txt") else: for line in dist.files: - write_output(" %s", line.strip()) + output(" %s", line.strip()) return results_printed diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index ca304e9e28..20fcd39086 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -381,8 +381,59 @@ def is_local(path: str) -> bool: return path.startswith(normalize_path(sys.prefix)) -def write_output(msg: Any, *args: Any) -> None: +def _format_output(msg: Any, *args: Any) -> str: + if args: + return str(msg) % args + return str(msg) + + +def _console_logging_accepts_info() -> bool: + if not logger.isEnabledFor(logging.INFO): + return False + + has_console_handler = False + for handler in logging.getLogger().handlers: + if getattr(handler, "console", None) is None: + continue + has_console_handler = True + if handler.level <= logging.INFO: + return True + + return not has_console_handler + + +def _write_stdout_direct(msg: Any, *args: Any) -> None: + from pip._internal.utils.logging import ( + BrokenStdoutLoggingError, + get_console, + get_indentation, + ) + + text = _format_output(msg, *args) + indentation = get_indentation() + if indentation: + prefix = " " * indentation + text = "".join(prefix + line for line in text.splitlines(True)) + + try: + try: + console = get_console() + except AssertionError: + console = None + + if console is None: + sys.stdout.write(text) + sys.stdout.write(os.linesep) + else: + console.print(text, overflow="ignore", crop=False) + except BrokenPipeError as exc: + raise BrokenStdoutLoggingError() from exc + + +def write_output(msg: Any, *args: Any, show_on_quiet: bool = False) -> None: logger.info(msg, *args) + if show_on_quiet and not _console_logging_accepts_info(): + _write_stdout_direct(msg, *args) class StreamWrapper(StringIO): diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 2388c362a5..988fb0fcc2 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -184,6 +184,12 @@ def test_cache_dir(script: PipTestEnvironment, cache_dir: str) -> None: assert os.path.normcase(cache_dir) == result.stdout.strip() +def test_cache_dir_quiet(script: PipTestEnvironment, cache_dir: str) -> None: + result = script.pip("cache", "dir", "--quiet") + + assert os.path.normcase(cache_dir) == result.stdout.strip() + + def test_cache_dir_too_many_args(script: PipTestEnvironment, cache_dir: str) -> None: result = script.pip("cache", "dir", "aaa", expect_error=True) diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py index acf99bc13c..3a733d17fb 100644 --- a/tests/functional/test_check.py +++ b/tests/functional/test_check.py @@ -25,6 +25,15 @@ def test_basic_check_clean(script: PipTestEnvironment) -> None: assert result.returncode == 0 +def test_basic_check_clean_quiet(script: PipTestEnvironment) -> None: + """Quiet mode should not suppress check's dependency report.""" + result = script.pip("check", "--quiet") + + expected_lines = ("No broken requirements found.",) + assert matches_expected_lines(result.stdout, expected_lines) + assert result.returncode == 0 + + def test_basic_check_missing_dependency(script: PipTestEnvironment) -> None: # Setup a small project pkga_path = create_test_package_with_setup( diff --git a/tests/functional/test_configuration.py b/tests/functional/test_configuration.py index f443e4c70f..f7233aedaf 100644 --- a/tests/functional/test_configuration.py +++ b/tests/functional/test_configuration.py @@ -54,6 +54,9 @@ def test_basic_modification_pipeline(self, script: PipTestEnvironment) -> None: result = script.pip("config", "get", "test.blah") assert result.stdout.strip() == "1" + result = script.pip("config", "get", "test.blah", "--quiet") + assert result.stdout.strip() == "1" + script.pip("config", "unset", "test.blah") script.pip("config", "get", "test.blah", expect_error=True) diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py index 331a2e6575..480e5777c3 100644 --- a/tests/functional/test_debug.py +++ b/tests/functional/test_debug.py @@ -39,6 +39,16 @@ def test_debug(script: PipTestEnvironment, expected_text: str) -> None: assert expected_text in stdout +def test_debug_quiet(script: PipTestEnvironment) -> None: + """ + Check that quiet mode does not suppress debug's primary output. + """ + result = script.pip("debug", "--quiet", allow_stderr_warning=True) + + assert "pip version: " in result.stdout + assert "Compatible tags: " in result.stdout + + def test_debug__library_versions(script: PipTestEnvironment) -> None: """ Check the library versions normal output. diff --git a/tests/functional/test_hash.py b/tests/functional/test_hash.py index cf993b6feb..7c0d18d2c1 100644 --- a/tests/functional/test_hash.py +++ b/tests/functional/test_hash.py @@ -15,6 +15,16 @@ def test_basic_hash(script: PipTestEnvironment, tmpdir: Path) -> None: assert expected in str(result) +def test_basic_hash_quiet(script: PipTestEnvironment, tmpdir: Path) -> None: + """Quiet mode should not suppress the generated hash.""" + expected = ( + "--hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425" + "e73043362938b9824" + ) + result = script.pip("hash", "--quiet", _hello_file(tmpdir)) + assert expected in str(result) + + def test_good_algo_option(script: PipTestEnvironment, tmpdir: Path) -> None: """Make sure the -a option works.""" expected = ( diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py index 251a4bf341..bbcc50e7f4 100644 --- a/tests/functional/test_help.py +++ b/tests/functional/test_help.py @@ -66,6 +66,16 @@ def test_help_command_should_exit_status_ok_when_no_cmd_is_specified( assert result.returncode == SUCCESS +def test_help_command_quiet_shows_general_help(script: PipTestEnvironment) -> None: + """ + Test that quiet mode does not suppress the no-argument help command. + """ + result = script.pip("help", "--quiet") + assert result.returncode == SUCCESS + assert "Usage:" in result.stdout + assert "Commands:" in result.stdout + + def test_help_command_should_exit_status_error_when_cmd_does_not_exist( script: PipTestEnvironment, ) -> None: diff --git a/tests/functional/test_index.py b/tests/functional/test_index.py index 57cfe7af05..9d9ded7189 100644 --- a/tests/functional/test_index.py +++ b/tests/functional/test_index.py @@ -142,6 +142,26 @@ def test_index_versions_all_releases_for_package(script: PipTestEnvironment) -> assert "2.0a1" in result.stdout +def test_index_versions_quiet_shows_versions(script: PipTestEnvironment) -> None: + """Test that quiet mode does not suppress index versions output.""" + wheelhouse_path = script.scratch_path / "wheelhouse" + wheelhouse_path.mkdir() + make_wheel("simple", "1.0").save_to_dir(wheelhouse_path) + + result = script.pip( + "index", + "versions", + "--quiet", + "--no-index", + "--find-links", + wheelhouse_path, + "simple", + ) + + assert "simple (1.0)" in result.stdout + assert "Available versions: 1.0" in result.stdout + + def test_index_versions_only_final_for_package(script: PipTestEnvironment) -> None: """Test that --only-final filters prereleases for specific package.""" # Create fake local package index with prerelease diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index c308c3b860..83dc792362 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -45,6 +45,15 @@ def test_basic_list(simple_script: PipTestEnvironment) -> None: assert "simple2 3.0" in result.stdout, str(result) +def test_basic_list_quiet(simple_script: PipTestEnvironment) -> None: + """ + Test that quiet mode does not suppress the list command's package output. + """ + result = simple_script.pip("list", "--quiet") + assert "simple 1.0" in result.stdout, str(result) + assert "simple2 3.0" in result.stdout, str(result) + + def test_verbose_flag(simple_script: PipTestEnvironment) -> None: """ Test the list command with the '-v' option diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index a658b829f3..ded0aee3a3 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -32,6 +32,17 @@ def test_basic_show(script: PipTestEnvironment) -> None: assert "Requires: " in lines +def test_basic_show_quiet(script: PipTestEnvironment) -> None: + """ + Test that quiet mode does not suppress the show command's package output. + """ + result = script.pip("show", "--quiet", "pip") + lines = result.stdout.splitlines() + assert "Name: pip" in lines + assert f"Version: {__version__}" in lines + assert any(line.startswith("Location: ") for line in lines) + + def test_show_without_files_does_not_read_installed_files( monkeypatch: pytest.MonkeyPatch, ) -> None: From e40cb7f47d841d47f10a8da9ec46b5c9fc12e276 Mon Sep 17 00:00:00 2001 From: ychampion Date: Wed, 8 Jul 2026 01:37:29 +0000 Subject: [PATCH 2/4] Avoid highlighting quiet-mode display output The quiet fallback writes primary display output directly when logging is too quiet. It should preserve the plain text shape of command output instead of letting Rich highlight package versions or paths under forced color. Constraint: GitHub Actions sets FORCE_COLOR, exposing Rich highlighter output in quiet-mode fallback paths.\nRejected: Loosening the list test assertion | that would hide a real output-shape regression for plain display text.\nConfidence: high\nScope-risk: narrow\nDirective: Keep direct display fallback semantically plain unless a caller explicitly passes rich renderables.\nTested: PYTHONPATH=src FORCE_COLOR=1 python3 -m pip list --quiet | python3 -c 'import sys; data=sys.stdin.buffer.read(); print(b"\\x1b[" in data); print(data[:240].decode("utf-8", "replace"))'; uv run --with pre-commit pre-commit run black --files src/pip/_internal/utils/misc.py tests/functional/test_list.py; uv run --with pre-commit pre-commit run ruff-check --files src/pip/_internal/utils/misc.py tests/functional/test_list.py; uv run --with nox nox -s test-3.12 -- tests/functional/test_list.py::test_basic_list_quiet -q; uv run --with nox nox -s test-3.12 -- tests/functional/test_list.py tests/functional/test_show.py tests/functional/test_help.py tests/functional/test_index.py tests/functional/test_check.py tests/functional/test_hash.py tests/functional/test_cache.py tests/functional/test_configuration.py tests/functional/test_debug.py tests/unit/test_command_show.py -q; uv run --with nox nox -s test-3.12 -- tests/functional/test_install.py::test_install_quiet -q\nNot-tested: Full CI matrix locally. --- src/pip/_internal/utils/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 20fcd39086..c66b63d60e 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -425,7 +425,7 @@ def _write_stdout_direct(msg: Any, *args: Any) -> None: sys.stdout.write(text) sys.stdout.write(os.linesep) else: - console.print(text, overflow="ignore", crop=False) + console.print(text, overflow="ignore", crop=False, highlight=False) except BrokenPipeError as exc: raise BrokenStdoutLoggingError() from exc From 55548c41db7eebe641f80f867c014e0ebab52cf8 Mon Sep 17 00:00:00 2001 From: ychampion Date: Wed, 8 Jul 2026 07:38:52 +0000 Subject: [PATCH 3/4] Address quiet output review feedback Constraint: pip's quiet output fix should keep primary display output visible while preserving install and download quiet behavior. Rejected: Keeping per-command show_on_quiet=True annotations | maintainer requested making visible display output the default with explicit exceptions. Confidence: high Scope-risk: moderate Directive: Use show_on_quiet=False only for output that quiet mode should suppress. Tested: uv run --with pre-commit pre-commit run black --files src/pip/_internal/utils/misc.py src/pip/_internal/utils/logging.py src/pip/_internal/commands/show.py src/pip/_internal/commands/index.py src/pip/_internal/commands/check.py src/pip/_internal/commands/configuration.py src/pip/_internal/commands/cache.py src/pip/_internal/commands/list.py src/pip/_internal/commands/search.py src/pip/_internal/commands/hash.py src/pip/_internal/commands/debug.py src/pip/_internal/commands/download.py src/pip/_internal/commands/install.py tests/functional/test_cache.py; uv run --with pre-commit pre-commit run ruff-check --files same set; uv run --with nox nox -s test-3.12 -- tests/functional/test_show.py tests/functional/test_help.py tests/functional/test_index.py tests/functional/test_check.py tests/functional/test_list.py tests/functional/test_hash.py tests/functional/test_cache.py tests/functional/test_configuration.py tests/functional/test_debug.py tests/unit/test_command_show.py -q; uv run --with nox nox -s test-3.12 -- tests/functional/test_install.py::test_install_quiet -q; source-tree quiet smoke checks for list/show/cache/dry-run install. Not-tested: Full CI matrix locally. --- src/pip/_internal/commands/cache.py | 12 ++--- src/pip/_internal/commands/check.py | 5 +- src/pip/_internal/commands/configuration.py | 16 +++--- src/pip/_internal/commands/debug.py | 16 +++--- src/pip/_internal/commands/download.py | 6 ++- src/pip/_internal/commands/hash.py | 6 +-- src/pip/_internal/commands/index.py | 11 ++-- src/pip/_internal/commands/install.py | 3 +- src/pip/_internal/commands/list.py | 10 ++-- src/pip/_internal/commands/search.py | 6 +-- src/pip/_internal/commands/show.py | 53 +++++++++--------- src/pip/_internal/utils/logging.py | 36 +++++++++++++ src/pip/_internal/utils/misc.py | 59 ++++----------------- tests/functional/test_cache.py | 1 + 14 files changed, 112 insertions(+), 128 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index fea95fae26..19f5c2f343 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -88,7 +88,7 @@ def get_cache_dir(self, options: Values, args: list[str]) -> None: if args: raise CommandError("Too many arguments") - write_output(options.cache_dir, show_on_quiet=True) + write_output(options.cache_dir) def get_cache_info(self, options: Values, args: list[str]) -> None: if args: @@ -128,7 +128,7 @@ def get_cache_info(self, options: Values, args: list[str]) -> None: .strip() ) - write_output(message, show_on_quiet=True) + write_output(message) def list_cache_items(self, options: Values, args: list[str]) -> None: if len(args) > 1: @@ -147,7 +147,7 @@ def list_cache_items(self, options: Values, args: list[str]) -> None: def format_for_human(self, files: list[str]) -> None: if not files: - write_output("No locally built wheels cached.", show_on_quiet=True) + write_output("No locally built wheels cached.") return results = [] @@ -155,12 +155,12 @@ def format_for_human(self, files: list[str]) -> None: wheel = os.path.basename(filename) size = filesystem.format_file_size(filename) results.append(f" - {wheel} ({size})") - write_output("Cache contents:\n", show_on_quiet=True) - write_output("\n".join(sorted(results)), show_on_quiet=True) + write_output("Cache contents:\n") + write_output("\n".join(sorted(results))) def format_for_abspath(self, files: list[str]) -> None: if files: - write_output("\n".join(sorted(files)), show_on_quiet=True) + write_output("\n".join(sorted(files))) def remove_cache_items(self, options: Values, args: list[str]) -> None: if len(args) > 1: diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index 1c8c3ff565..516757eead 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -40,7 +40,6 @@ def run(self, options: Values, args: list[str]) -> int: project_name, version, dependency[0], - show_on_quiet=True, ) for project_name in conflicting: @@ -53,17 +52,15 @@ def run(self, options: Values, args: list[str]) -> int: req, dep_name, dep_version, - show_on_quiet=True, ) for package in unsupported: write_output( "%s %s is not supported on this platform", package.raw_name, package.version, - show_on_quiet=True, ) if missing or conflicting or parsing_probs or unsupported: return ERROR else: - write_output("No broken requirements found.", show_on_quiet=True) + write_output("No broken requirements found.") return SUCCESS diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 0bfed9dc4e..4fa860ff9b 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -179,13 +179,13 @@ def list_values(self, options: Values, args: list[str]) -> None: for key, value in sorted(self.configuration.items()): for key, value in sorted(value.items()): - write_output("%s=%r", key, value, show_on_quiet=True) + write_output("%s=%r", key, value) def get_name(self, options: Values, args: list[str]) -> None: key = self._get_n_args(args, "get [name]", n=1) value = self.configuration.get_value(key) - write_output("%s", value, show_on_quiet=True) + write_output("%s", value) def set_name_value(self, options: Values, args: list[str]) -> None: key, value = self._get_n_args(args, "set [name] [value]", n=2) @@ -207,13 +207,11 @@ def list_config_values(self, options: Values, args: list[str]) -> None: # Iterate over config files and print if they exist, and the # key-value pairs present in them if they do for variant, files in sorted(self.configuration.iter_config_files()): - write_output("%s:", variant, show_on_quiet=True) + write_output("%s:", variant) for fname in files: with indent_log(): file_exists = os.path.exists(fname) - write_output( - "%s, exists: %r", fname, file_exists, show_on_quiet=True - ) + write_output("%s, exists: %r", fname, file_exists) if file_exists: self.print_config_file_values(variant, fname) @@ -223,15 +221,15 @@ def print_config_file_values(self, variant: Kind, fname: str) -> None: with indent_log(): if name == fname: for confname, confvalue in value.items(): - write_output("%s: %s", confname, confvalue, show_on_quiet=True) + write_output("%s: %s", confname, confvalue) def print_env_var_values(self) -> None: """Get key-values pairs present as environment variables""" - write_output("%s:", "env_var", show_on_quiet=True) + write_output("%s:", "env_var") with indent_log(): for key, value in sorted(self.configuration.get_environ_vars()): env_var = f"PIP_{key.upper()}" - write_output("%s=%r", env_var, value, show_on_quiet=True) + write_output("%s=%r", env_var, value) def open_in_editor(self, options: Values, args: list[str]) -> None: editor = self._determine_editor(options) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index c1e7f63aa2..1caa9fcad3 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -25,11 +25,11 @@ def show_value(name: str, value: Any) -> None: - write_output("%s: %s", name, value, show_on_quiet=True) + write_output("%s: %s", name, value) def show_sys_implementation() -> None: - write_output("sys.implementation:", show_on_quiet=True) + write_output("sys.implementation:") implementation_name = sys.implementation.name with indent_log(): show_value("name", implementation_name) @@ -91,13 +91,11 @@ def show_actual_vendor_versions(vendor_txt_versions: dict[str, str]) -> None: " (CONFLICT: vendor.txt suggests version should" f" be {expected_version})" ) - write_output( - "%s==%s%s", module_name, actual_version, extra_message, show_on_quiet=True - ) + write_output("%s==%s%s", module_name, actual_version, extra_message) def show_vendor_versions() -> None: - write_output("vendored library versions:", show_on_quiet=True) + write_output("vendored library versions:") vendor_txt_versions = create_vendor_txt_map() with indent_log(): @@ -117,7 +115,7 @@ def show_tags(options: Values) -> None: suffix = f" (target: {formatted_target})" msg = f"Compatible tags: {len(tags)}{suffix}" - write_output(msg, show_on_quiet=True) + write_output(msg) if options.verbose < 1 and len(tags) > tag_limit: tags_limited = True @@ -127,11 +125,11 @@ def show_tags(options: Values) -> None: with indent_log(): for tag in tags: - write_output(str(tag), show_on_quiet=True) + write_output(str(tag)) if tags_limited: msg = f"...\n[First {tag_limit} tags shown. Pass --verbose to show all.]" - write_output(msg, show_on_quiet=True) + write_output(msg) def ca_bundle_info(config: Configuration) -> str: diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 5c0a3a51de..9467f80437 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -141,6 +141,10 @@ def run(self, options: Values, args: list[str]) -> int: downloaded.append(req.name) if downloaded: - write_output("Successfully downloaded %s", " ".join(downloaded)) + write_output( + "Successfully downloaded %s", + " ".join(downloaded), + show_on_quiet=False, + ) return SUCCESS diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index 2bf9c8e057..271a4c91a7 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -44,11 +44,7 @@ def run(self, options: Values, args: list[str]) -> int: algorithm = options.algorithm for path in args: write_output( - "%s:\n--hash=%s:%s", - path, - algorithm, - _hash_of_file(path, algorithm), - show_on_quiet=True, + "%s:\n--hash=%s:%s", path, algorithm, _hash_of_file(path, algorithm) ) return SUCCESS diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index f3ef5c4d54..ce0bafda39 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -158,12 +158,9 @@ def get_available_package_versions(self, options: Values, args: list[Any]) -> No if dist is not None: structured_output["installed_version"] = str(dist.version) - write_output(json.dumps(structured_output), show_on_quiet=True) + write_output(json.dumps(structured_output)) else: - write_output(f"{query} ({latest})", show_on_quiet=True) - write_output( - "Available versions: {}".format(", ".join(formatted_versions)), - show_on_quiet=True, - ) - print_dist_installation_info(latest, dist, show_on_quiet=True) + write_output(f"{query} ({latest})") + write_output("Available versions: {}".format(", ".join(formatted_versions))) + print_dist_installation_info(latest, dist) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 47094abf7b..3b17525a7a 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -498,6 +498,7 @@ def run(self, options: Values, args: list[str]) -> int: write_output( "Would install %s", " ".join("-".join(item) for item in would_install_items), + show_on_quiet=False, ) return SUCCESS @@ -580,7 +581,7 @@ def run(self, options: Values, args: list[str]) -> int: resolver_variant=self.determine_resolver_variant(options), ) if summary := installed_packages_summary(installed, env): - write_output(summary) + write_output(summary, show_on_quiet=False) except OSError as error: show_traceback = self.verbosity >= 1 diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 6bca0bf202..e41c511fd2 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -298,13 +298,11 @@ def output_package_listing( except InvalidVersion: req_string = f"{dist.raw_name}==={dist.raw_version}" if options.verbose >= 1: - write_output( - "%s (%s)", req_string, dist.location, show_on_quiet=True - ) + write_output("%s (%s)", req_string, dist.location) else: - write_output(req_string, show_on_quiet=True) + write_output(req_string) elif options.list_format == "json": - write_output(format_for_json(packages, options), show_on_quiet=True) + write_output(format_for_json(packages, options)) def output_package_listing_columns( self, data: list[list[str]], header: list[str] @@ -320,7 +318,7 @@ def output_package_listing_columns( pkg_strings.insert(1, " ".join("-" * x for x in sizes)) for val in pkg_strings: - write_output(val, show_on_quiet=True) + write_output(val) def format_for_columns( diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 365ac702e6..577deef306 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -114,7 +114,7 @@ def transform_hits(hits: list[dict[str, str]]) -> list[TransformedHit]: def print_dist_installation_info( - latest: str, dist: BaseDistribution | None, show_on_quiet: bool = False + latest: str, dist: BaseDistribution | None, show_on_quiet: bool = True ) -> None: if dist is not None: with indent_log(): @@ -172,9 +172,9 @@ def print_results( name_latest = f"{name} ({latest})" line = f"{name_latest:{name_column_width}} - {summary}" try: - write_output(line, show_on_quiet=True) + write_output(line) dist = get_installed_distribution(name) - print_dist_installation_info(latest, dist, show_on_quiet=True) + print_dist_installation_info(latest, dist) except UnicodeEncodeError: pass diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index cee838c5ff..b68731bacf 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -188,52 +188,51 @@ def print_results( """ results_printed = False - def output(msg: str, *args: object) -> None: - write_output(msg, *args, show_on_quiet=True) - for i, dist in enumerate(distributions): results_printed = True if i > 0: - output("---") + write_output("---") metadata_version = dist.metadata_version metadata_version_tuple = ( tuple(map(int, metadata_version.split("."))) if metadata_version else () ) - output("Name: %s", dist.name) - output("Version: %s", dist.version) - output("Summary: %s", dist.summary) - output("Home-page: %s", dist.homepage) - output("Author: %s", dist.author) - output("Author-email: %s", dist.author_email) + write_output("Name: %s", dist.name) + write_output("Version: %s", dist.version) + write_output("Summary: %s", dist.summary) + write_output("Home-page: %s", dist.homepage) + write_output("Author: %s", dist.author) + write_output("Author-email: %s", dist.author_email) if metadata_version_tuple >= (2, 4) and dist.license_expression: - output("License-Expression: %s", dist.license_expression) + write_output("License-Expression: %s", dist.license_expression) else: - output("License: %s", dist.license) - output("Location: %s", dist.location) + write_output("License: %s", dist.license) + write_output("Location: %s", dist.location) if dist.editable_project_location is not None: - output("Editable project location: %s", dist.editable_project_location) - output("Requires: %s", ", ".join(dist.requires)) - output("Required-by: %s", ", ".join(dist.required_by)) + write_output( + "Editable project location: %s", dist.editable_project_location + ) + write_output("Requires: %s", ", ".join(dist.requires)) + write_output("Required-by: %s", ", ".join(dist.required_by)) if verbose: - output("Metadata-Version: %s", dist.metadata_version) - output("Installer: %s", dist.installer) - output("Classifiers:") + write_output("Metadata-Version: %s", dist.metadata_version) + write_output("Installer: %s", dist.installer) + write_output("Classifiers:") for classifier in dist.classifiers: - output(" %s", classifier) - output("Entry-points:") + write_output(" %s", classifier) + write_output("Entry-points:") for entry in dist.entry_points: - output(" %s", entry.strip()) - output("Project-URLs:") + write_output(" %s", entry.strip()) + write_output("Project-URLs:") for project_url in dist.project_urls: - output(" %s", project_url) + write_output(" %s", project_url) if list_files: - output("Files:") + write_output("Files:") if dist.files is None: - output("Cannot locate RECORD or installed-files.txt") + write_output("Cannot locate RECORD or installed-files.txt") else: for line in dist.files: - output(" %s", line.strip()) + write_output(" %s", line.strip()) return results_printed diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index e067703580..4d9e753e81 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -250,6 +250,42 @@ def handleError(self, record: logging.LogRecord) -> None: return super().handleError(record) +def _format_output_message(msg: Any, *args: Any) -> str: + # Match logging's %-style argument handling; write_output passes the + # same message and args to logger.info when the console can show INFO. + if args: + return str(msg) % args + return str(msg) + + +def should_directly_write_output(logger: logging.Logger) -> bool: + if not logger.isEnabledFor(logging.INFO): + return True + + has_console_handler = False + for handler in logging.getLogger().handlers: + if not isinstance(handler, RichPipStreamHandler): + continue + has_console_handler = True + if handler.level <= logging.INFO: + return False + + return has_console_handler + + +def write_output_direct(msg: Any, *args: Any) -> None: + text = _format_output_message(msg, *args) + indentation = get_indentation() + if indentation: + prefix = " " * indentation + text = "".join(prefix + line for line in text.splitlines(True)) + + try: + get_console().print(text, overflow="ignore", crop=False, highlight=False) + except BrokenPipeError as exc: + raise BrokenStdoutLoggingError() from exc + + class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler): def _open(self) -> TextIOWrapper: ensure_dir(os.path.dirname(self.baseFilename)) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index c66b63d60e..dceb916950 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -381,59 +381,18 @@ def is_local(path: str) -> bool: return path.startswith(normalize_path(sys.prefix)) -def _format_output(msg: Any, *args: Any) -> str: - if args: - return str(msg) % args - return str(msg) - - -def _console_logging_accepts_info() -> bool: - if not logger.isEnabledFor(logging.INFO): - return False - - has_console_handler = False - for handler in logging.getLogger().handlers: - if getattr(handler, "console", None) is None: - continue - has_console_handler = True - if handler.level <= logging.INFO: - return True - - return not has_console_handler - - -def _write_stdout_direct(msg: Any, *args: Any) -> None: - from pip._internal.utils.logging import ( - BrokenStdoutLoggingError, - get_console, - get_indentation, - ) - - text = _format_output(msg, *args) - indentation = get_indentation() - if indentation: - prefix = " " * indentation - text = "".join(prefix + line for line in text.splitlines(True)) - - try: - try: - console = get_console() - except AssertionError: - console = None - - if console is None: - sys.stdout.write(text) - sys.stdout.write(os.linesep) - else: - console.print(text, overflow="ignore", crop=False, highlight=False) - except BrokenPipeError as exc: - raise BrokenStdoutLoggingError() from exc +def write_output(msg: Any, *args: Any, show_on_quiet: bool = True) -> None: + if show_on_quiet: + from pip._internal.utils.logging import ( + should_directly_write_output, + write_output_direct, + ) + if should_directly_write_output(logger): + write_output_direct(msg, *args) + return -def write_output(msg: Any, *args: Any, show_on_quiet: bool = False) -> None: logger.info(msg, *args) - if show_on_quiet and not _console_logging_accepts_info(): - _write_stdout_direct(msg, *args) class StreamWrapper(StringIO): diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 988fb0fcc2..e289a2e3fd 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -185,6 +185,7 @@ def test_cache_dir(script: PipTestEnvironment, cache_dir: str) -> None: def test_cache_dir_quiet(script: PipTestEnvironment, cache_dir: str) -> None: + """Test that quiet mode does not suppress the cache dir output.""" result = script.pip("cache", "dir", "--quiet") assert os.path.normcase(cache_dir) == result.stdout.strip() From e1d8d8b0b4e01a69528ed455b6f619393ec68ae9 Mon Sep 17 00:00:00 2001 From: ychampion Date: Wed, 8 Jul 2026 16:39:03 +0000 Subject: [PATCH 4/4] Tighten quiet-output review follow-up Constraint: Address pip review comments without changing quiet-output behavior. Rejected: Keep the helper parameter | It only forwarded a constant default and made the call site harder to read. Confidence: high Scope-risk: narrow Directive: Keep quiet-mode bypass explicit at display-output write_output calls. Tested: uv run --with pre-commit pre-commit run black --files src/pip/_internal/commands/search.py src/pip/_internal/commands/show.py; uv run --with pre-commit pre-commit run ruff-check --files src/pip/_internal/commands/search.py src/pip/_internal/commands/show.py; uv run --with nox nox -s test-3.12 -- tests/functional/test_show.py tests/functional/test_help.py tests/functional/test_index.py tests/functional/test_check.py tests/functional/test_list.py tests/functional/test_hash.py tests/functional/test_cache.py tests/functional/test_configuration.py tests/functional/test_debug.py tests/unit/test_command_show.py -q; PYTHONPATH=src python3 -m pip show --quiet pip; PYTHONPATH=src python3 -m pip help --quiet; PYTHONPATH=src python3 -m pip cache dir --quiet; direct search helper smoke. Not-tested: Full CI matrix. --- src/pip/_internal/commands/search.py | 14 +++++--------- src/pip/_internal/commands/show.py | 1 - 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 577deef306..a65f340be6 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -113,26 +113,22 @@ def transform_hits(hits: list[dict[str, str]]) -> list[TransformedHit]: return list(packages.values()) -def print_dist_installation_info( - latest: str, dist: BaseDistribution | None, show_on_quiet: bool = True -) -> None: +def print_dist_installation_info(latest: str, dist: BaseDistribution | None) -> None: if dist is not None: with indent_log(): if dist.version == latest: - write_output( - "INSTALLED: %s (latest)", dist.version, show_on_quiet=show_on_quiet - ) + write_output("INSTALLED: %s (latest)", dist.version, show_on_quiet=True) else: - write_output("INSTALLED: %s", dist.version, show_on_quiet=show_on_quiet) + write_output("INSTALLED: %s", dist.version, show_on_quiet=True) if parse_version(latest).pre: write_output( "LATEST: %s (pre-release; install" " with `pip install --pre`)", latest, - show_on_quiet=show_on_quiet, + show_on_quiet=True, ) else: - write_output("LATEST: %s", latest, show_on_quiet=show_on_quiet) + write_output("LATEST: %s", latest, show_on_quiet=True) def get_installed_distribution(name: str) -> BaseDistribution | None: diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index b68731bacf..40601a2b9d 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -187,7 +187,6 @@ def print_results( Print the information from installed distributions found. """ results_printed = False - for i, dist in enumerate(distributions): results_printed = True if i > 0: