diff --git a/news/12601.feature.rst b/news/12601.feature.rst new file mode 100644 index 00000000000..fa38a4aabac --- /dev/null +++ b/news/12601.feature.rst @@ -0,0 +1 @@ +Output install progress when 'raw' progress_bar type is in use diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index b842b1b316a..96e1653bcb3 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -1,6 +1,15 @@ import functools import sys -from typing import Callable, Generator, Iterable, Iterator, Optional, Tuple +from typing import ( + Callable, + Generator, + Iterable, + Iterator, + Optional, + Sized, + Tuple, + TypeVar, +) from pip._vendor.rich.progress import ( BarColumn, @@ -16,9 +25,12 @@ ) from pip._internal.cli.spinners import RateLimiter +from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.logging import get_indentation -DownloadProgressRenderer = Callable[[Iterable[bytes]], Iterator[bytes]] +P = TypeVar("P") + +ProgressRenderer = Callable[[Iterable[P]], Iterator[P]] def _rich_progress_bar( @@ -57,11 +69,15 @@ def _rich_progress_bar( progress.update(task_id, advance=len(chunk)) +T = TypeVar("T", bound=Sized) + + def _raw_progress_bar( - iterable: Iterable[bytes], + iterable: Iterable[T], *, size: Optional[int], -) -> Generator[bytes, None, None]: + unit_size: int = 0, +) -> Generator[T, None, None]: def write_progress(current: int, total: int) -> None: sys.stdout.write("Progress %d of %d\n" % (current, total)) sys.stdout.flush() @@ -72,7 +88,7 @@ def write_progress(current: int, total: int) -> None: write_progress(current, total) for chunk in iterable: - current += len(chunk) + current += unit_size or len(chunk) if rate_limiter.ready() or current == total: write_progress(current, total) rate_limiter.reset() @@ -81,7 +97,7 @@ def write_progress(current: int, total: int) -> None: def get_download_progress_renderer( *, bar_type: str, size: Optional[int] = None -) -> DownloadProgressRenderer: +) -> ProgressRenderer[bytes]: """Get an object that can be used to render the download progress. Returns a callable, that takes an iterable to "wrap". @@ -89,6 +105,21 @@ def get_download_progress_renderer( if bar_type == "on": return functools.partial(_rich_progress_bar, bar_type=bar_type, size=size) elif bar_type == "raw": - return functools.partial(_raw_progress_bar, size=size) + return functools.partial[Iterator[bytes]](_raw_progress_bar, size=size) + else: + return iter # no-op, when passed an iterator + + +def get_install_progress_renderer( + *, bar_type: str, total: Optional[int] = None +) -> ProgressRenderer[Tuple[str, InstallRequirement]]: + """Get an object that can be used to render the install progress. + + Returns a callable, that takes an iterable to "wrap". + """ + if bar_type == "raw": + return functools.partial[Iterator[Tuple[str, InstallRequirement]]]( + _raw_progress_bar, size=total, unit_size=1 + ) else: return iter # no-op, when passed an iterator diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 6cf7571e4a6..dd218f51c80 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -458,6 +458,7 @@ def run(self, options: Values, args: List[str]) -> int: warn_script_location=warn_script_location, use_user_site=options.use_user_site, pycompile=options.compile, + progress_bar=options.progress_bar, ) lib_locations = get_lib_location_guesses( diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 16de903a44c..702a6fc0421 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -2,6 +2,7 @@ import logging from typing import Generator, List, Optional, Sequence, Tuple +from pip._internal.cli.progress_bars import get_install_progress_renderer from pip._internal.utils.logging import indent_log from .req_file import parse_requirements @@ -43,6 +44,7 @@ def install_given_reqs( warn_script_location: bool, use_user_site: bool, pycompile: bool, + progress_bar: str, ) -> List[InstallationResult]: """ Install everything in the given list. @@ -59,8 +61,17 @@ def install_given_reqs( installed = [] + show_progress = logger.getEffectiveLevel() <= logging.INFO + + items = iter(to_install.items()) + if show_progress: + renderer = get_install_progress_renderer( + bar_type=progress_bar, total=len(to_install) + ) + items = renderer(items) + with indent_log(): - for req_name, requirement in to_install.items(): + for req_name, requirement in items: if requirement.should_reinstall: logger.info("Attempting uninstall: %s", req_name) with indent_log():