%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /lib/python3/dist-packages/nala/
Upload File :
Create Path :
Current File : //lib/python3/dist-packages/nala/summary.py

#                 __
#    ____ _____  |  | _____
#   /    \\__  \ |  | \__  \
#  |   |  \/ __ \|  |__/ __ \_
#  |___|  (____  /____(____  /
#       \/     \/          \/
#
# Copyright (C) 2021, 2022 Blake Lee
#
# This file is part of nala
#
# nala is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# nala is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with nala.  If not, see <https://www.gnu.org/licenses/>.
"""Module for printing the transaction summary."""
from __future__ import annotations

from dataclasses import dataclass
from itertools import zip_longest
from typing import Generator, Iterable

from nala import _, color, console
from nala.cache import Cache
from nala.options import arguments
from nala.rich import HORIZONTALS, OVERFLOW, Column, Group, Table, Text, Tree, from_ansi
from nala.utils import NalaPackage, PackageHandler, dprint, unit_str

# NOTE: The following are the headers for the transaction summary.
# NOTE: Package:        Version:     Size:
# NOTE: ansible-core    2.12.4-1    1.2 MB
PACKAGE, VERSION, SIZE, OLD_VERSION, NEW_VERSION, EITHER = _(
	"Package/Version/Size/Old Version/New Version/Either"
).split("/")
# NOTE: Verb Tenses are [ "Present/Present Participle/Past" ]
# NOTE: This ends up looking like [ "Auto-Purge 20 Packages" ]
_AUTO_PURGE, _AUTO_PURGING, _AUTO_PURGED = _(
	"Auto-Purge/Auto-Purging/Auto-Purged",
).split("/")
# NOTE: Verb Tenses are [ "Present/Present Participle/Past" ]
# NOTE: This ends up looking like [ "Auto-Remove 20 Packages" ]
_AUTO_REMOVE, _AUTO_REMOVING, _AUTO_REMOVED = _(
	"Auto-Remove/Auto-Removing/Auto-Removed",
).split("/")
# NOTE: Verb Tenses are [ "Present/Present Participle/Past" ]
# NOTE: This ends up looking like [ "Remove 20 Packages" ]
_REMOVE, _REMOVING, _REMOVED = _(
	"Remove/Removing/Removed",
).split("/")
# NOTE: Verb Tenses are [ "Present/Present Participle/Past" ]
# NOTE: This ends up looking like [ "Purge 20 Packages" ]
_PURGE, _PURGING, _PURGED = _(
	"Purge/Purging/Purged",
).split("/")
# NOTE: Verb Tenses are [ "Present/Present Participle/Past" ]
# NOTE: This ends up looking like [ "Install 20 Packages" ]
_INSTALL, _INSTALLING, _INSTALLED = _(
	"Install/Installing/Installed",
).split("/")
# NOTE: Verb Tenses are [ "Present/Present Participle/Past" ]
# NOTE: This ends up looking like [ "Reinstall 20 Packages" ]
_REINSTALL, _REINSTALLING, _REINSTALLED = _(
	"Reinstall/Reinstalling/Reinstalled",
).split("/")
# NOTE: Verb Tenses are [ "Present/Present Participle/Past" ]
# NOTE: This ends up looking like [ "Upgrade 20 Packages" ]
_UPGRADE, _UPGRADING, _UPGRADED = _(
	"Upgrade/Upgrading/Upgraded",
).split("/")
# NOTE: Verb Tenses are [ "Present/Present Participle/Past" ]
# NOTE: This ends up looking like [ "Downgrade 20 Packages" ]
_DOWNGRADE, _DOWNGRADING, _DOWNGRADED = _(
	"Downgrade/Downgrading/Downgraded",
).split("/")
# NOTE: Verb Tenses are [ "Present/Present Participle/Past" ]
# NOTE: This ends up looking like [ "Configure 20 Packages" ]
_CONFIGURE, _CONFIGURING, _CONFIGURED = _(
	"Configure/Configuring/Configured",
).split("/")

SUMMARY_LAYOUT = ("left_adjust", "right_adjust", "left_adjust")
UPGRADE_LAYOUT = ("pkg_blue", "old_version", "new_version", "pkg_size")
DOWNGRADE_LAYOUT = ("pkg_yellow", "old_version", "new_version", "pkg_size")
DEFAULT_LAYOUT = ("pkg_green", "version", "pkg_size")
EXTRA_LAYOUT = ("pkg_magenta", "version", "pkg_size")
REMOVE_LAYOUT = ("pkg_red", "version", "pkg_size")

COLUMN_MAP: dict[str, dict[str, str | int]] = {
	"left_adjust": {"overflow": OVERFLOW},
	"right_adjust": {"justify": "right", "overflow": OVERFLOW},
	"pkg_green": {
		"header": f"{PACKAGE}:",
		"style": "bold green",
		"overflow": OVERFLOW,
		"ratio": 2,
	},
	"pkg_red": {
		"header": f"{PACKAGE}:",
		"style": "bold red",
		"overflow": OVERFLOW,
		"ratio": 2,
	},
	"pkg_blue": {
		"header": f"{PACKAGE}:",
		"style": "bold blue",
		"overflow": OVERFLOW,
		"ratio": 3,
	},
	"pkg_yellow": {
		"header": f"{PACKAGE}:",
		"style": "bold orange_red1",
		"overflow": OVERFLOW,
		"ratio": 2,
	},
	"pkg_magenta": {
		"header": f"{PACKAGE}:",
		"style": "bold magenta",
		"overflow": OVERFLOW,
		"ratio": 2,
	},
	"pkg_size": {
		"header": f"{SIZE}:",
		"justify": "right",
		"overflow": OVERFLOW,
		"ratio": 2,
	},
	"version": {"header": f"{VERSION}:", "overflow": "fold", "ratio": 2},
	"old_version": {"header": f"{OLD_VERSION}:", "overflow": "fold", "ratio": 2},
	"new_version": {"header": f"{NEW_VERSION}:", "overflow": "fold", "ratio": 2},
}
ROW_MAP: dict[str, str] = {
	"pkg_green": "name",
	"pkg_red": "name",
	"pkg_blue": "name",
	"pkg_yellow": "name",
	"pkg_magenta": "name",
	"pkg_size": "unit_size",
	"version": "version",
	"old_version": "old_version",
	"new_version": "version",
}


def get_columns(column_keys: Iterable[str]) -> Generator[Column, None, None]:
	"""Get the columns from our column map."""
	for key in column_keys:
		yield Column(**COLUMN_MAP[key])  # type: ignore[arg-type]


def get_rows(pkg: NalaPackage, layout: Iterable[str]) -> Generator[Text, None, None]:
	"""Get the rows from our row map."""
	for key in layout:
		if key == "new_version":
			yield from_ansi(version_diff(pkg))
			continue
		yield from_ansi(getattr(pkg, ROW_MAP[key]))


def version_diff(pkg: NalaPackage) -> str:
	"""Return a colored diff of the new version."""
	if pkg.old_version:
		for i, char in enumerate(zip_longest(pkg.old_version, pkg.version)):
			if char[0] != char[1]:
				return f"{pkg.version[:i]}{color(pkg.version[i:], 'YELLOW')}"
	return pkg.version


@dataclass
class PackageHeaders:
	"""Tuple for package headers."""

	layout: tuple[str, ...]
	title: str
	summary: str = ""


@dataclass
class Headers:  # pylint: disable=too-many-instance-attributes
	"""Tuple for headers."""

	deleting: PackageHeaders
	auto_removing: PackageHeaders
	installing: PackageHeaders
	reinstalling: PackageHeaders
	upgrading: PackageHeaders
	downgrading: PackageHeaders
	configuring: PackageHeaders
	recommending: PackageHeaders
	suggesting: PackageHeaders
	not_needed: PackageHeaders | None = None


def auto_remove_header(history: bool) -> tuple[str, str]:
	"""Get the auto remove header."""
	if history and arguments.is_purge():
		# NOTE: `/` will convert to a space for example in Irish it may be:
		# NOTE: "{package_total}/bpacáistí/a uasghrádú" -> "20 bpacáistí a uasghrádú"
		return _AUTO_PURGED, _AUTO_PURGED
	if history:
		return _AUTO_REMOVED, _AUTO_REMOVED
	if arguments.is_purge():
		return _AUTO_PURGING, _AUTO_PURGE
	return _AUTO_REMOVING, _AUTO_REMOVE


def remove_header(history: bool) -> tuple[str, str]:
	"""Get the remove header."""
	if history and arguments.is_purge():
		return _PURGED, _PURGED
	if history:
		return _REMOVED, _REMOVED
	if arguments.is_purge():
		return _PURGING, _PURGE
	return _REMOVING, _REMOVE


def get_headers() -> Headers:
	"""Get the headers for the summary table."""
	return Headers(
		PackageHeaders(REMOVE_LAYOUT, *remove_header(history=False)),
		PackageHeaders(REMOVE_LAYOUT, *auto_remove_header(history=False)),
		PackageHeaders(DEFAULT_LAYOUT, _INSTALLING, _INSTALL),
		PackageHeaders(DEFAULT_LAYOUT, _REINSTALLING, _REINSTALL),
		PackageHeaders(UPGRADE_LAYOUT, _UPGRADING, _UPGRADE),
		PackageHeaders(DOWNGRADE_LAYOUT, _DOWNGRADING, _DOWNGRADE),
		PackageHeaders(EXTRA_LAYOUT, _CONFIGURING, _CONFIGURE),
		PackageHeaders(EXTRA_LAYOUT, _("Recommended, Will Not Be Installed")),
		PackageHeaders(EXTRA_LAYOUT, _("Suggested, Will Not Be Installed")),
		PackageHeaders(REMOVE_LAYOUT, _("Auto-Removable, Will Not Be Removed")),
	)


def get_history_headers() -> Headers:
	"""Get the headers for the summary table."""
	return Headers(
		PackageHeaders(REMOVE_LAYOUT, *remove_header(history=True)),
		PackageHeaders(REMOVE_LAYOUT, *auto_remove_header(history=True)),
		PackageHeaders(DEFAULT_LAYOUT, _INSTALLED, _INSTALLED),
		PackageHeaders(DEFAULT_LAYOUT, _REINSTALLED, _REINSTALLED),
		PackageHeaders(UPGRADE_LAYOUT, _UPGRADED, _UPGRADED),
		PackageHeaders(DOWNGRADE_LAYOUT, _DOWNGRADED, _DOWNGRADED),
		PackageHeaders(EXTRA_LAYOUT, _CONFIGURED, _CONFIGURED),
		PackageHeaders(EXTRA_LAYOUT, _("Recommended, Will Not Be Installed")),
		PackageHeaders(EXTRA_LAYOUT, _("Suggested, Will Not Be Installed")),
	)


def gen_printers(
	nala_pkgs: PackageHandler, headers: Headers
) -> Generator[tuple[list[NalaPackage], PackageHeaders], None, None]:
	"""Generate the printers."""
	yield from (  # type: ignore[misc]
		(nala_pkgs.not_needed, headers.not_needed),
		(nala_pkgs.delete_pkgs + nala_pkgs.delete_config, headers.deleting),
		(
			nala_pkgs.autoremove_pkgs + nala_pkgs.autoremove_config,
			headers.auto_removing,
		),
		(nala_pkgs.install_pkgs, headers.installing),
		(nala_pkgs.reinstall_pkgs, headers.reinstalling),
		(nala_pkgs.upgrade_pkgs, headers.upgrading),
		(nala_pkgs.downgrade_pkgs, headers.downgrading),
		(nala_pkgs.configure_pkgs, headers.configuring),
		(nala_pkgs.recommend_pkgs, headers.recommending),
		(nala_pkgs.suggest_pkgs, headers.suggesting),
	)


def gen_package_table(
	nala_packages: list[NalaPackage] | list[NalaPackage | list[NalaPackage]],
	pkg_headers: PackageHeaders,
) -> Table:
	"""Print package transactions in a pretty format."""
	package_table = Table(
		*get_columns(pkg_headers.layout), padding=(0, 1), box=None, expand=True
	)

	# Add our packages
	or_deps: list[list[NalaPackage]] = []
	for pkg in nala_packages:
		if isinstance(pkg, list):
			or_deps.append(pkg)
			continue
		if pkg.old_version:
			package_table.add_row(*get_rows(pkg, pkg_headers.layout))
			continue
		package_table.add_row(*get_rows(pkg, pkg_headers.layout))

	# Add any or_deps
	for pkg in or_deps:
		package_table.add_row(*summary_or_depends(pkg))

	return package_table


def summary_or_depends(pkg: list[NalaPackage]) -> tuple[Tree, Group, Group]:
	"""Format Recommend and Suggests or dependencies."""
	pkg_tree = Tree(f"[default]{EITHER}[/default]", guide_style="default")
	_children = [pkg_tree.add(npkg.name) for npkg in pkg]
	return (
		pkg_tree,
		Group("", *(npkg.version for npkg in pkg)),
		Group("", *(npkg.unit_size for npkg in pkg)),
	)


def print_update_summary(nala_pkgs: PackageHandler, cache: Cache | None = None) -> None:
	"""Print our transaction summary."""
	dprint("Printing Update Summary")
	headers = get_headers() if cache else get_history_headers()

	main_table = Table.grid(expand=True)
	summary_header = Table(_("Summary"), padding=0, box=HORIZONTALS, expand=True)
	summary_table = Table.grid(*get_columns(SUMMARY_LAYOUT), padding=(0, 1))

	for pkg_set, header in gen_printers(nala_pkgs, headers):
		if not pkg_set:
			continue
		package_table = Table(header.title, padding=0, box=HORIZONTALS, expand=True)
		package_table.add_row(gen_package_table(pkg_set, header))
		main_table.add_row(package_table)
		# We don't need empty rows from these in the summary
		if nala_pkgs.no_summary(pkg_set):
			continue
		# NOTE: This ends up looking like [ "Configure 20 Packages" ]
		summary_table.add_row(header.summary, f"{len(pkg_set)}", _("Packages"))

	summary_header.add_row(summary_table)
	main_table.add_row(summary_header)
	console.print(main_table)

	if cache:
		footer_table = Table(
			*get_columns(SUMMARY_LAYOUT), box=None, show_footer=True, show_header=False
		)
		if (download := cache.required_download) > 0:
			footer_table.add_row(_("Total download size"), unit_str(download))
		if (space := cache.required_space) < 0:
			footer_table.add_row(_("Disk space to free"), unit_str(-space))
		if space > 0:
			footer_table.add_row(_("Disk space required"), unit_str(space))
		console.print(footer_table)

	if cache and arguments.download_only:
		print(_("Nala will only download the packages"))

Zerion Mini Shell 1.0