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: improve operation messages and make them stateful #9245

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/poetry/console/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,13 @@ def handle(self) -> int:
return 0

log_install = (
"<b>Installing</> the current project:"
"<b>{verb}</> the current project:"
f" <c1>{self.poetry.package.pretty_name}</c1>"
f" (<{{tag}}>{self.poetry.package.pretty_version}</>)"
)
overwrite = self.io.output.is_decorated() and not self.io.is_debug()
self.line("")
self.write(log_install.format(tag="c2"))
self.write(log_install.format(verb="Installing", tag="c2"))
if not overwrite:
self.line("")

Expand Down Expand Up @@ -209,7 +209,7 @@ def handle(self) -> int:
return 0

if overwrite:
self.overwrite(log_install.format(tag="success"))
self.overwrite(log_install.format(verb="Installed", tag="success"))
self.line("")

return 0
93 changes: 31 additions & 62 deletions src/poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import itertools
import json
import threading
import warnings

from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import wait
Expand Down Expand Up @@ -252,7 +253,7 @@ def _write(self, operation: Operation, line: str) -> None:

def _execute_operation(self, operation: Operation) -> None:
try:
op_message = self.get_operation_message(operation)
op_message = operation.get_message()
if self.supports_fancy_output():
if id(operation) not in self._sections and self._should_write_operation(
operation
Expand Down Expand Up @@ -298,9 +299,10 @@ def _execute_operation(self, operation: Operation) -> None:
if not self.supports_fancy_output():
io = self._io
else:
operation.error = True
message = (
" <error>-</error>"
f" {self.get_operation_message(operation, error=True)}:"
f" {operation.get_message()}:"
" <error>Failed</error>"
)
self._write(operation, message)
Expand Down Expand Up @@ -354,9 +356,10 @@ def _execute_operation(self, operation: Operation) -> None:

except KeyboardInterrupt:
try:
operation.warning = True
message = (
" <warning>-</warning>"
f" {self.get_operation_message(operation, warning=True)}:"
f" {operation.get_message()}:"
" <warning>Cancelled</warning>"
)
if not self.supports_fancy_output():
Expand All @@ -370,7 +373,7 @@ def _execute_operation(self, operation: Operation) -> None:
def _do_execute_operation(self, operation: Operation) -> int:
method = operation.job_type

operation_message = self.get_operation_message(operation)
operation_message = operation.get_message()
if operation.skipped:
if self.supports_fancy_output():
self._write(
Expand All @@ -393,8 +396,8 @@ def _do_execute_operation(self, operation: Operation) -> int:
if result != 0:
return result

operation_message = self.get_operation_message(operation, done=True)
message = f" <fg=green;options=bold>-</> {operation_message}"
operation.done = True
message = f" <fg=green;options=bold>-</> {operation.get_message()}"
self._write(operation, message)

self._increment_operations_count(operation, True)
Expand Down Expand Up @@ -431,53 +434,19 @@ def get_operation_message(
error: bool = False,
warning: bool = False,
) -> str:
base_tag = "fg=default"
operation_color = "c2"
source_operation_color = "c2"
package_color = "c1"

if error:
operation_color = "error"
elif warning:
operation_color = "warning"
elif done:
operation_color = "success"

if operation.skipped:
base_tag = "fg=default;options=dark"
operation_color += "_dark"
source_operation_color += "_dark"
package_color += "_dark"

if isinstance(operation, Install):
return (
f"<{base_tag}>Installing"
f" <{package_color}>{operation.package.name}</{package_color}>"
f" (<{operation_color}>{operation.package.full_pretty_version}</>)</>"
)

if isinstance(operation, Uninstall):
return (
f"<{base_tag}>Removing"
f" <{package_color}>{operation.package.name}</{package_color}>"
f" (<{operation_color}>{operation.package.full_pretty_version}</>)</>"
)

if isinstance(operation, Update):
initial_version = (initial_pkg := operation.initial_package).version
target_version = (target_pkg := operation.target_package).version
update_kind = (
"Updating" if target_version >= initial_version else "Downgrading"
)
return (
f"<{base_tag}>{update_kind}"
f" <{package_color}>{initial_pkg.name}</{package_color}> "
f"(<{source_operation_color}>"
f"{initial_pkg.full_pretty_version}"
f"</{source_operation_color}> -> <{operation_color}>"
f"{target_pkg.full_pretty_version}</>)</>"
)
return ""
warnings.warn(
"'Executor.get_operation_message()' and its boolean parameters "
"are deprecated, please use 'Operation.get_message()' instead.",
DeprecationWarning,
stacklevel=2,
)
new_state = {"done": done, "error": error, "warning": warning}
old_state = {attr: getattr(operation, attr) for attr in new_state}
op_state = vars(operation)
op_state.update(new_state)
message = operation.get_message()
op_state.update(old_state)
return message

def _display_summary(self, operations: list[Operation]) -> None:
installs = 0
Expand Down Expand Up @@ -527,8 +496,8 @@ def _execute_update(self, operation: Install | Update) -> int:
return status_code

def _execute_uninstall(self, operation: Uninstall) -> int:
op_msg = self.get_operation_message(operation)
message = f" <fg=blue;options=bold>-</> {op_msg}: <info>Removing...</info>"
op_msg = operation.get_message()
message = f" <fg=blue;options=bold>-</> {op_msg}: <info>In progress...</info>"
self._write(operation, message)

return self._remove(operation.package)
Expand All @@ -553,10 +522,10 @@ def _install(self, operation: Install | Update) -> int:
else:
archive = self._download(operation)

operation_message = self.get_operation_message(operation)
operation_message = operation.get_message()
message = (
f" <fg=blue;options=bold>-</> {operation_message}:"
" <info>Installing...</info>"
" <info>In progress...</info>"
)
self._write(operation, message)

Expand Down Expand Up @@ -600,7 +569,7 @@ def _prepare_archive(
self, operation: Install | Update, *, output_dir: Path | None = None
) -> Path:
package = operation.package
operation_message = self.get_operation_message(operation)
operation_message = operation.get_message()

message = (
f" <fg=blue;options=bold>-</> {operation_message}:"
Expand Down Expand Up @@ -639,7 +608,7 @@ def _prepare_git_archive(self, operation: Install | Update) -> Path:
if cached_archive is not None:
return cached_archive

operation_message = self.get_operation_message(operation)
operation_message = operation.get_message()

message = (
f" <fg=blue;options=bold>-</> {operation_message}: <info>Cloning...</info>"
Expand Down Expand Up @@ -682,7 +651,7 @@ def _install_directory_without_wheel_installer(
from poetry.pyproject.toml import PyProjectTOML

package = operation.package
operation_message = self.get_operation_message(operation)
operation_message = operation.get_message()

message = (
f" <fg=blue;options=bold>-</> {operation_message}:"
Expand Down Expand Up @@ -773,7 +742,7 @@ def _download_link(self, operation: Install | Update, link: Link) -> Path:

if archive.suffix != ".whl":
message = (
f" <fg=blue;options=bold>-</> {self.get_operation_message(operation)}:"
f" <fg=blue;options=bold>-</> {operation.get_message()}:"
" <info>Preparing...</info>"
)
self._write(operation, message)
Expand Down Expand Up @@ -821,7 +790,7 @@ def _download_archive(
downloader = Downloader(url, dest, self._authenticator)
wheel_size = downloader.total_size

operation_message = self.get_operation_message(operation)
operation_message = operation.get_message()
message = (
f" <fg=blue;options=bold>-</> {operation_message}: <info>Downloading...</>"
)
Expand Down
2 changes: 1 addition & 1 deletion src/poetry/installation/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ def _write_lock_file(self, repo: LockfileRepository, force: bool = False) -> Non

if updated_lock:
self._io.write_line("")
self._io.write_line("<info>Writing lock file</>")
self._io.write_line("<info>Lock file written</>")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have this stateful as well? Like, Writing lock file changing to Lock file written once the operation is done?

Copy link
Member Author

@bswck bswck Mar 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well then, I guess that a new IO section would be needed and the

if self._should_write(lock):
self._write_lock_data(lock)
return True

snippet would probably have to be rewritten to

     if self._should_write(lock): 
         section = ...
         section.write_line("<info>Writing lock file</>")
         self._write_lock_data(lock) 
         section.write_line("<info>Lock file written</>")  # or <success>Lock file written</>
         return True 

Is it worth the cost? It would require introducing a side effect in _write_lock_file or adding some extra logic to set_lock_data. IO writes are quite fast in contrary to installation operations; the only advantage it would bring is that it could inform about a potentially (but very rarely) erroneous action taken (in case the write fails) useful as a context for a traceback following the message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then maybe we could do something like Writing lock file... Done. where Done. appears after the operation is finished?

Copy link
Member Author

@bswck bswck Mar 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then maybe we could do something like Writing lock file... Done. where Done. appears after the operation is finished?

I think that would be inconsistent with the rest of the UI. Also, would it address the issue with additional logic/necessity of introducing new _write_lock_file side effects somehow?


def _execute(self, operations: list[Operation]) -> int:
return self._executor.execute(operations)
Expand Down
14 changes: 13 additions & 1 deletion src/poetry/installation/operations/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,21 @@ def package(self) -> Package:
def job_type(self) -> str:
return "install"

@property
def message_verb(self) -> str:
return "Installed" if self.done else "Installing"

def get_message(self) -> str:
return (
f"<{self._message_base_tag}>{self.message_verb} "
f"<{self._message_package_color}>{self.package.name}"
f"</{self._message_package_color}> "
f"(<{self._message_color}>{self.package.full_pretty_version}</>)</>"
)

def __str__(self) -> str:
return (
"Installing"
f"{self.message_verb}"
f" {self.package.pretty_name} ({self.format_version(self.package)})"
)

Expand Down
27 changes: 27 additions & 0 deletions src/poetry/installation/operations/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,46 @@


if TYPE_CHECKING:
from typing import ClassVar

from poetry.core.packages.package import Package


T = TypeVar("T", bound="Operation")


class Operation:
_message_default_color: ClassVar[str] = "c2"
_message_package_color: ClassVar[str] = "c1"

def __init__(self, reason: str | None = None, priority: float = 0) -> None:
self._reason = reason

self.error = False
self.warning = False
self.done = False
self._skipped = False
self._skip_reason: str | None = None
self._priority = priority

@property
def _message_base_tag(self) -> str:
return "fg=default" + ";options=dark" * self.skipped

@property
def _message_color(self) -> str:
color = self._message_default_color
if self.error:
color = "error"
elif self.warning:
color = "warning"
elif self.done:
color = "success"
return color + "_dark" * self.skipped

def get_message(self) -> str:
return ""

@property
def job_type(self) -> str:
raise NotImplementedError
Expand Down
16 changes: 14 additions & 2 deletions src/poetry/installation/operations/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,22 @@ def package(self) -> Package:
def job_type(self) -> str:
return "uninstall"

@property
def message_verb(self) -> str:
return "Removed" if self.done else "Removing"

def get_message(self) -> str:
return (
f"<{self._message_base_tag}>{self.message_verb}"
f" <{self._message_package_color}>{self.package.name}"
f"</{self._message_package_color}>"
f" (<{self._message_color}>{self.package.full_pretty_version}</>)</>"
)

def __str__(self) -> str:
return (
"Uninstalling"
f" {self.package.pretty_name} ({self.format_version(self._package)})"
f"{self.message_verb} "
f"{self.package.pretty_name} ({self.format_version(self._package)})"
)

def __repr__(self) -> str:
Expand Down
34 changes: 29 additions & 5 deletions src/poetry/installation/operations/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@


if TYPE_CHECKING:
from typing import ClassVar

from poetry.core.packages.package import Package


class Update(Operation):
_message_source_operation_color: ClassVar[str] = "c2"

def __init__(
self,
initial: Package,
Expand Down Expand Up @@ -38,18 +42,38 @@ def package(self) -> Package:
def job_type(self) -> str:
return "update"

@property
def message_verb(self) -> str:
initial_version = self.initial_package.version
target_version = self.target_package.version
if target_version >= initial_version:
return "Updated" if self.done else "Updating"
return "Downgraded" if self.done else "Downgrading"

def get_message(self) -> str:
return (
f"<{self._message_base_tag}>{self.message_verb}"
f" <{self._message_package_color}>"
f"{self.initial_package.name}</{self._message_package_color}> "
f"(<{self._message_source_operation_color}>"
f"{self.initial_package.full_pretty_version}"
f"</{self._message_source_operation_color}> -> <{self._message_color}>"
f"{self.target_package.full_pretty_version}</>)</>"
)

def __str__(self) -> str:
init_version = self.format_version(self.initial_package)
initial_version = self.format_version(self.initial_package)
target_version = self.format_version(self.target_package)
return (
f"Updating {self.initial_package.pretty_name} ({init_version}) "
f"to {self.target_package.pretty_name} ({target_version})"
f"{self.message_verb} {self.initial_package.pretty_name} "
f"({initial_version}) to {self.target_package.pretty_name} "
f"({target_version})"
)

def __repr__(self) -> str:
init_version = self.format_version(self.initial_package)
initial_version = self.format_version(self.initial_package)
target_version = self.format_version(self.target_package)
return (
f"<Update {self.initial_package.pretty_name} ({init_version}) "
f"<Update {self.initial_package.pretty_name} ({initial_version}) "
f"to {self.target_package.pretty_name} ({target_version})>"
)