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

feat: report progress when downloading packages #2328

Merged
merged 2 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions news/2328.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Report the progress of download and unpacking when installing packages.
25 changes: 14 additions & 11 deletions src/pdm/installers/synchronizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
from types import SimpleNamespace
from typing import TYPE_CHECKING, Any, Callable, Collection, TypeVar

from rich.progress import SpinnerColumn
from rich.progress import SpinnerColumn, TaskProgressColumn

from pdm import termui
from pdm.compat import cached_property
from pdm.environments import BaseEnvironment
from pdm.exceptions import InstallationError
from pdm.installers.manager import InstallManager
from pdm.models.candidates import Candidate, make_candidate
from pdm.models.reporter import BaseReporter, RichProgressReporter
from pdm.models.requirements import FileRequirement, Requirement, parse_requirement, strip_extras
from pdm.utils import is_editable, normalize_name

Expand Down Expand Up @@ -279,7 +280,8 @@ def create_executor(
def install_candidate(self, key: str, progress: Progress) -> Candidate:
"""Install candidate"""
can = self.candidates[key]
job = progress.add_task(f"Installing {can.format()}...", total=1)
job = progress.add_task(f"Installing {can.format()}...", text="", total=None)
can.prepare(self.environment, RichProgressReporter(progress, job))
try:
self.manager.install(can)
except Exception:
Expand All @@ -288,7 +290,8 @@ def install_candidate(self, key: str, progress: Progress) -> Candidate:
else:
progress.live.console.print(f" [success]{termui.Emoji.SUCC}[/] Install {can.format()} successful")
finally:
progress.update(job, completed=1, visible=False)
progress.update(job, visible=False)
can.prepare(self.environment, BaseReporter())
return can

def update_candidate(self, key: str, progress: Progress) -> tuple[Distribution, Candidate]:
Expand All @@ -297,9 +300,9 @@ def update_candidate(self, key: str, progress: Progress) -> tuple[Distribution,
dist = self.working_set[strip_extras(key)[0]]
dist_version = dist.version
job = progress.add_task(
f"Updating [req]{key}[/] [warning]{dist_version}[/] -> [warning]{can.version}[/]...",
total=1,
f"Updating [req]{key}[/] [warning]{dist_version}[/] -> [warning]{can.version}[/]...", text="", total=None
)
can.prepare(self.environment, RichProgressReporter(progress, job))
try:
self.manager.uninstall(dist)
self.manager.install(can)
Expand All @@ -317,7 +320,8 @@ def update_candidate(self, key: str, progress: Progress) -> tuple[Distribution,
f"-> [warning]{can.version}[/] successful",
)
finally:
progress.update(job, completed=1, visible=False)
progress.update(job, visible=False)
can.prepare(self.environment, BaseReporter())

return dist, can

Expand All @@ -326,10 +330,7 @@ def remove_distribution(self, key: str, progress: Progress) -> Distribution:
dist = self.working_set[key]
dist_version = dist.version

job = progress.add_task(
f"Removing [req]{key}[/] [warning]{dist_version}[/]...",
total=1,
)
job = progress.add_task(f"Removing [req]{key}[/] [warning]{dist_version}[/]...", text="", total=None)
try:
self.manager.uninstall(dist)
except Exception:
Expand All @@ -342,7 +343,7 @@ def remove_distribution(self, key: str, progress: Progress) -> Distribution:
f" [success]{termui.Emoji.SUCC}[/] Remove [req]{key}[/] [warning]{dist_version}[/] successful"
)
finally:
progress.update(job, completed=1, visible=False)
progress.update(job, visible=False)
return dist

def _show_headline(self, packages: dict[str, list[str]]) -> None:
Expand Down Expand Up @@ -428,6 +429,8 @@ def update_progress(future: Future | DummyFuture, kind: str, key: str) -> None:
" ",
SpinnerColumn(termui.SPINNER, speed=1, style="primary"),
"{task.description}",
"[info]{task.fields[text]}",
TaskProgressColumn("[info]{task.percentage:>3.0f}%[/]"),
) as progress:
live = progress.live
for kind, key in sequential_jobs:
Expand Down
33 changes: 26 additions & 7 deletions src/pdm/models/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from pdm.compat import importlib_metadata as im
from pdm.exceptions import BuildError, CandidateNotFound, InvalidPyVersion, PDMWarning
from pdm.models.backends import get_backend, get_backend_by_spec
from pdm.models.reporter import BaseReporter
from pdm.models.requirements import (
FileRequirement,
Requirement,
Expand Down Expand Up @@ -276,22 +277,29 @@ def format(self) -> str:
"""Format for output."""
return f"[req]{self.name}[/] [warning]{self.version}[/]"

def prepare(self, environment: BaseEnvironment) -> PreparedCandidate:
def prepare(self, environment: BaseEnvironment, reporter: BaseReporter | None = None) -> PreparedCandidate:
"""Prepare the candidate for installation."""
if self._prepared is None:
self._prepared = PreparedCandidate(self, environment)
self._prepared = PreparedCandidate(self, environment, reporter=reporter or BaseReporter())
else:
self._prepared.environment = environment
if reporter is not None:
self._prepared.reporter = reporter
return self._prepared


@dataclasses.dataclass
class PreparedCandidate:
"""A candidate that has been prepared for installation.
The metadata and built wheel are available.
"""

def __init__(self, candidate: Candidate, environment: BaseEnvironment) -> None:
self.candidate = candidate
self.environment = environment
self.req = candidate.req
candidate: Candidate
environment: BaseEnvironment
reporter: BaseReporter = dataclasses.field(default_factory=BaseReporter)

def __post_init__(self) -> None:
self.req = self.candidate.req

self.wheel: Path | None = None
self.link = self._replace_url_vars(self.candidate.link)
Expand Down Expand Up @@ -392,7 +400,9 @@ def build(self) -> Path:
build_dir = self._get_wheel_dir()
os.makedirs(build_dir, exist_ok=True)
termui.logger.info("Running PEP 517 backend to build a wheel for %s", self.link)
self.reporter.report_build_start(self.link.filename) # type: ignore[union-attr]
self.wheel = Path(builder.build(build_dir, metadata_directory=self._metadata_dir))
self.reporter.report_build_end(self.link.filename) # type: ignore[union-attr]
return self.wheel

def obtain(self, allow_all: bool = False, unpack: bool = True) -> None:
Expand Down Expand Up @@ -444,7 +454,14 @@ def _unpack(self, validate_hashes: bool = False) -> None:
download_dir = build_dir
else:
download_dir = tmpdir
result = finder.download_and_unpack(self.link, build_dir, download_dir, hash_options)
result = finder.download_and_unpack(
self.link,
build_dir,
download_dir,
hash_options,
download_reporter=self.reporter.report_download,
unpack_reporter=self.reporter.report_unpack,
)
if self.link.is_wheel:
self.wheel = result
else:
Expand Down Expand Up @@ -551,7 +568,9 @@ def _get_metadata_from_build(self, source_dir: Path, metadata_parent: str) -> im
builder = EditableBuilder if self.req.editable else WheelBuilder
try:
termui.logger.info("Running PEP 517 backend to get metadata for %s", self.link)
self.reporter.report_build_start(self.link.filename) # type: ignore[union-attr]
self._metadata_dir = builder(source_dir, self.environment).prepare_metadata(metadata_parent)
self.reporter.report_build_end(self.link.filename) # type: ignore[union-attr]
except BuildError:
termui.logger.warning("Failed to build package, try parsing project files.")
try:
Expand Down
41 changes: 41 additions & 0 deletions src/pdm/models/reporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from rich.progress import Progress, TaskID


class BaseReporter:
def report_download(self, link: Any, completed: int, total: int | None) -> None:
pass

def report_build_start(self, filename: str) -> None:
pass

def report_build_end(self, filename: str) -> None:
pass

def report_unpack(self, filename: str, completed: int, total: int | None) -> None:
pass


@dataclass
class RichProgressReporter(BaseReporter):
progress: Progress
task_id: TaskID

def report_download(self, link: Any, completed: int, total: int | None) -> None:
self.progress.update(self.task_id, completed=completed, total=total, text="Downloading...")

def report_unpack(self, filename: str, completed: int, total: int | None) -> None:
self.progress.update(self.task_id, completed=completed, total=total, text="Unpacking...")

Check warning on line 33 in src/pdm/models/reporter.py

View check run for this annotation

Codecov / codecov/patch

src/pdm/models/reporter.py#L33

Added line #L33 was not covered by tests

def report_build_start(self, filename: str) -> None:
task = self.progress._tasks[self.task_id]
task.total = None
self.progress.update(self.task_id, text="Building...")

def report_build_end(self, filename: str) -> None:
self.progress.update(self.task_id, text="")
Loading