Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add machine readable download progress option #12084

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d7a76d3
Add machine readable download progress option
joeyballentine Jun 13, 2023
13c0431
Adds news file
joeyballentine Jun 13, 2023
0045fd8
Fix mypy error
joeyballentine Jun 13, 2023
5f7ab61
lint
joeyballentine Jun 13, 2023
5127329
line length
joeyballentine Jun 13, 2023
813aac6
add newline
joeyballentine Jun 13, 2023
98742ea
use json
joeyballentine Jun 13, 2023
9080d82
fix import sorting
joeyballentine Jun 13, 2023
70ae242
format
joeyballentine Jun 13, 2023
13c11ee
PR suggestions
joeyballentine Jun 13, 2023
d0d96d9
use logger
joeyballentine Jun 13, 2023
f4ca354
add docs to user guide
joeyballentine Jun 13, 2023
dfc4d98
add test
joeyballentine Jun 13, 2023
d1d6a69
fix test, maybe
joeyballentine Jun 13, 2023
5f7782a
maybe this time
joeyballentine Jun 14, 2023
71f3444
attempting just using a network package
joeyballentine Jun 14, 2023
14eb09d
lint
joeyballentine Jun 14, 2023
7567e2a
PR suggestions
joeyballentine Jun 15, 2023
c0fbf13
I didn't see the comment about using -u
joeyballentine Jun 15, 2023
31dee38
lint + add a test (that may or may not work)
joeyballentine Jun 15, 2023
d73336b
remove bad test
joeyballentine Jun 15, 2023
a3d3ebf
Merge branch 'main' into machine-readable-progress
joeyballentine Jun 16, 2023
15f0c8a
Update with more realistic example
joeyballentine Jun 24, 2023
23f95d2
update null verbiage
joeyballentine Jun 24, 2023
fd16cbc
error suggestion
joeyballentine Jun 24, 2023
e88444e
Merge remote-tracking branch 'origin/machine-readable-progress' into …
joeyballentine Jun 24, 2023
d81de49
ruff
joeyballentine Jun 24, 2023
0d6bfc3
Merge branch 'main' into machine-readable-progress
joeyballentine Jun 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/html/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,64 @@ We are using `freeze`_ here which outputs installed packages in requirements for

reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'])

Since pip's progress bar gets hidden when running in a subprocess, you can use
the ``--progress-bar=json`` option for easily parsable progress information::

import subprocess
import sys
import json

python_path = sys.executable

process = subprocess.Popen(
[
python_path,
"-m",
"pip",
"install",
"numpy",
"opencv-python",
"scipy",
"--progress-bar=json",
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
while True:
nextline = process.stdout.readline()
if nextline == b"" and process.poll() is not None:
break
line = nextline.decode("utf-8").strip()

if "Progress:" in line:
json_line = line.replace("Progress:", "").strip()
parsed = json.loads(json_line)
current, total = parsed["current"], parsed["total"]
if total is not None and total > 0:
percent = current / total * 100
print(f"Download at: {percent}%")


Which will give the following output after it processes each download chunk:

``Progress: {"current": ######, "total": ######}``

Here, ``Progress:`` indicates it is download progress. The rest of the message is JSON
with the ``current`` number of bytes downloaded and ``total`` .whl size as key/value pairs.
joeyballentine marked this conversation as resolved.
Show resolved Hide resolved
Note: ``total`` is may be null if the size of the package cannot be determined. This means
that it will either be null during the duration of the download, or not be null at all.

This can be used to build your own progress bar, or report progress in other ways.
joeyballentine marked this conversation as resolved.
Show resolved Hide resolved
In the code example above, we just print the current parsed status of the download.
This feature cannot be used unless pip is invoked in a subprocess.

NOTE: For this to work properly, you may need to run python with the ``-u`` flag
to ensure that the output is unbuffered.

NOTE: Relying on the exact form of pip's output is unsupported, and so should not be used in
production applications unless you are willing to adapt when pip's output changes.


If you don't want to use pip's command line functionality, but are rather
trying to implement code that works with Python packages, their metadata, or
PyPI, then you should consider other, supported, packages that offer this type
Expand Down
1 change: 1 addition & 0 deletions news/11508.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a new progress_bar type that allows machine-readable (json) download progress
7 changes: 5 additions & 2 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,12 @@ class PipOption(Option):
"--progress-bar",
dest="progress_bar",
type="choice",
choices=["on", "off"],
choices=["on", "off", "json"],
default="on",
help="Specify whether the progress bar should be used [on, off] (default: on)",
help=(
"Specify whether the progress bar should be used"
" [on, off, json] (default: on)"
),
)

log: Callable[..., Option] = partial(
Expand Down
52 changes: 52 additions & 0 deletions src/pip/_internal/cli/progress_bars.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import functools
import json
import logging
import sys
from typing import Callable, Generator, Iterable, Iterator, Optional, Tuple

from pip._vendor.rich.progress import (
Expand All @@ -14,8 +17,11 @@
TransferSpeedColumn,
)

from pip._internal.cli.spinners import RateLimiter
from pip._internal.utils.logging import get_indentation

logger = logging.getLogger(__name__)

DownloadProgressRenderer = Callable[[Iterable[bytes]], Iterator[bytes]]


Expand Down Expand Up @@ -55,6 +61,37 @@ def _rich_progress_bar(
progress.update(task_id, advance=len(chunk))


class _MachineReadableProgress:
def __init__(
self,
iterable: Iterable[bytes],
size: Optional[int],
# Copying the default from spinners.py
min_update_interval_seconds: float = 0.125,
) -> None:
self._iterable = iter(iterable)
self._size = size
self._progress = 0
self._rate_limiter = RateLimiter(min_update_interval_seconds)

def __iter__(self) -> Iterator[bytes]:
return self

def __next__(self) -> bytes:
chunk = next(self._iterable)
self._progress += len(chunk)
progress_info = {
"current": self._progress,
"total": self._size,
}
if not self._rate_limiter.ready():
return chunk
sys.stdout.write(f"Progress: {json.dumps(progress_info)}\n")
sys.stdout.flush()
self._rate_limiter.reset()
return chunk


def get_download_progress_renderer(
*, bar_type: str, size: Optional[int] = None
) -> DownloadProgressRenderer:
Expand All @@ -64,5 +101,20 @@ def get_download_progress_renderer(
"""
if bar_type == "on":
return functools.partial(_rich_progress_bar, bar_type=bar_type, size=size)
elif bar_type == "json":
# We don't want regular users to use this progress_bar type
# so only use if not a TTY
if sys.stdout.isatty():
logger.warning(
"""Using json progress bar type outside a subprocess is not recommended.
Using normal progress bar instead."""
)
return functools.partial(_rich_progress_bar, bar_type="on", size=size)

# Mimic log level
if logger.getEffectiveLevel() <= logging.INFO:
return functools.partial(_MachineReadableProgress, size=size)

return iter
else:
return iter # no-op, when passed an iterator
Loading