diff --git a/CHANGELOG.md b/CHANGELOG.md index 10e4d4b9..0d14a804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.10.4] 2022-11-05 + +### Improvements +- Arch + - replaced some system calls by Python calls + +### Fixes +- AppImage + - some desktop entries not being displayed on the desktop environment menu (requires the AppImage to be reinstalled) [#287](https://github.com/vinifmor/bauh/issues/287) +- Arch + - not detecting some package replacements when upgrading due to conflict information having logic operators (e.g: `at-spi2-core: 2.44.1 -> 2.46.0` should replace the installed `at-spi2-atk: 2.38`) + - not considering some conflict expressions when retrieving upgrade requirements (it could lead to a system breakage depending on the conflict) +- Debian + - not properly handling packages with names ending with `:i386` [#298](https://github.com/vinifmor/bauh/issues/298) +- Packaging + - AppImage: download certificate issue [#280](https://github.com/vinifmor/bauh/issues/280) +- GUI + - initialization panel size not based on the current display device size + - management panel: + - not fully maximizing + - not maximizing based on the current display device size (for multiple display setups) + - not respecting a minimal/maximum width based on the current display device size (for multiple display setup) + - dialogs not respecting the current display device width + - not readjusting size after maximizing/minimizing + +### Localization (i18n) +- Italian ([@albanobattistella](https://github.com/albanobattistella), [@luca-digrazia](https://github.com/luca-digrazia)) +- Turkish ([@agahemir](https://github.com/agahemir)) + + ## [0.10.3] 2022-05-30 ### Features diff --git a/README.md b/README.md index 307c1189..a3d36bf2 100644 --- a/README.md +++ b/README.md @@ -554,6 +554,7 @@ the `view` code is only attached to them (it does not know how the `gems` handle - Separate modules for each packaging technology - Memory and performance improvements - Improve user experience +- The current development changes can be checked [here](https://github.com/vinifmor/bauh/blob/staging/CHANGELOG.md) #### Donations - You can support this project through [ko-fi](https://ko-fi.com/vinifmor). diff --git a/bauh/__init__.py b/bauh/__init__.py index 206851b5..44a430fb 100644 --- a/bauh/__init__.py +++ b/bauh/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.10.3' +__version__ = '0.10.4' __app_name__ = 'bauh' import os diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 88ba7f98..8c648f93 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -528,13 +528,13 @@ class AppImageManager(SoftwareManager, SettingsController): finally: releases_con.close() - def _find_desktop_file(self, folder: str) -> str: + def _find_desktop_file(self, folder: str) -> Optional[str]: for r, d, files in os.walk(folder): for f in files: if f.endswith('.desktop'): return f - def _find_icon_file(self, folder: str) -> str: + def _find_icon_file(self, folder: str) -> Optional[str]: for f in glob.glob(folder + ('/**' if not folder.endswith('/') else '**'), recursive=True): if RE_ICON_ENDS_WITH.match(f): return f diff --git a/bauh/gems/appimage/util.py b/bauh/gems/appimage/util.py index d39fd662..2057d92c 100644 --- a/bauh/gems/appimage/util.py +++ b/bauh/gems/appimage/util.py @@ -1,15 +1,16 @@ import os import re +from typing import Optional -RE_DESKTOP_EXEC = re.compile(r'(Exec\s*=(.+)(\n?))') +RE_DESKTOP_EXEC = re.compile(r'(\n?\s*\w*Exec\s*=(.+))') RE_MANY_SPACES = re.compile(r'\s+') -def find_appimage_file(folder: str) -> str: +def find_appimage_file(folder: str) -> Optional[str]: for r, d, files in os.walk(folder): for f in files: if f.lower().endswith('.appimage'): - return '{}/{}'.format(folder, f) + return f'{folder}/{f}' def replace_desktop_entry_exec_command(desktop_entry: str, appname: str, file_path: str) -> str: @@ -23,6 +24,11 @@ def replace_desktop_entry_exec_command(desktop_entry: str, appname: str, file_pa for exec_groups in execs: full_match = exec_groups[0] + + if full_match.strip().startswith("TryExec"): # TryExec cause issues in some DE to display the app icon + final_entry = final_entry.replace(full_match, "") + continue + cmd = RE_MANY_SPACES.sub(' ', exec_groups[1].strip()) if cmd: words = cmd.split(' ') @@ -30,12 +36,12 @@ def replace_desktop_entry_exec_command(desktop_entry: str, appname: str, file_pa for idx in range(len(words)): if words[idx].lower() == treated_name: - words[idx] = '"{}"'.format(file_path) + words[idx] = f'"{file_path}"' changed = True break if not changed: - words = ['"{}"'.format(file_path)] + words = [f'"{file_path}"'] final_entry = final_entry.replace(full_match, full_match.replace(exec_groups[1], ' '.join(words))) diff --git a/bauh/gems/arch/dependencies.py b/bauh/gems/arch/dependencies.py index fe4d4164..f84ff783 100644 --- a/bauh/gems/arch/dependencies.py +++ b/bauh/gems/arch/dependencies.py @@ -2,7 +2,7 @@ import re import traceback from logging import Logger from threading import Thread -from typing import Set, List, Tuple, Dict, Iterable, Optional, Generator +from typing import Set, List, Tuple, Dict, Iterable, Optional, Generator, Pattern from bauh.api.abstract.handler import ProcessWatcher from bauh.gems.arch import pacman, message, sorting, confirmation @@ -14,12 +14,20 @@ from bauh.view.util.translation import I18n class DependenciesAnalyser: + _re_dep_operator: Optional[Pattern] = None + def __init__(self, aur_client: AURClient, i18n: I18n, logger: Logger): self.aur_client = aur_client self.i18n = i18n - self.re_dep_operator = re.compile(r'([<>=]+)') self._log = logger + @classmethod + def re_dep_operator(cls) -> Pattern: + if not cls._re_dep_operator: + cls._re_dep_operator = re.compile(r'([<>=]+)') + + return cls._re_dep_operator + def _fill_repository(self, name: str, output: List[Tuple[str, str]]): repository = pacman.read_repository_from_info(name) @@ -225,7 +233,7 @@ class DependenciesAnalyser: raise Exception(f"Could not retrieve information from providers: " f"{', '.join(data_not_found)}") - split_informed_dep = self.re_dep_operator.split(dep_exp) + split_informed_dep = self.re_dep_operator().split(dep_exp) version_required = split_informed_dep[2] exp_op = split_informed_dep[1] if split_informed_dep[1] != '=' else '==' @@ -237,7 +245,7 @@ class DependenciesAnalyser: info = missing_providers_data[p] for provided_exp in info['p']: - split_dep = self.re_dep_operator.split(provided_exp) + split_dep = self.re_dep_operator().split(provided_exp) if len(split_dep) == 3 and split_dep[0] == dep_name: version_provided = split_dep[2] @@ -253,7 +261,7 @@ class DependenciesAnalyser: return else: for _, dep_data in self.aur_client.gen_updates_data((dep_name,)): - split_informed_dep = self.re_dep_operator.split(dep_exp) + split_informed_dep = self.re_dep_operator().split(dep_exp) version_required = split_informed_dep[2] exp_op = split_informed_dep[1].strip() @@ -270,7 +278,7 @@ class DependenciesAnalyser: if dep_name == dep_exp: version_required, exp_op = None, None else: - split_informed_dep = self.re_dep_operator.split(dep_exp) + split_informed_dep = self.re_dep_operator().split(dep_exp) version_required = split_informed_dep[2] exp_op = split_informed_dep[1] if split_informed_dep[1] != '=' else '==' @@ -405,7 +413,7 @@ class DependenciesAnalyser: if dep in pkgs_data: continue if dep not in provided_map: - dep_split = self.re_dep_operator.split(dep) + dep_split = self.re_dep_operator().split(dep) dep_name = dep_split[0].strip() if dep_name not in deps_checked: diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index ffe83c18..15ce12a2 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -5,7 +5,7 @@ import shutil import traceback from io import StringIO from threading import Thread -from typing import List, Set, Tuple, Dict, Iterable, Optional, Any +from typing import List, Set, Tuple, Dict, Iterable, Optional, Any, Pattern, Collection from colorama import Fore @@ -26,6 +26,7 @@ RE_REMOVE_TRANSITIVE_DEPS = re.compile(r'removing\s([\w\-_]+)\s.+required\sby\s( RE_AVAILABLE_MIRRORS = re.compile(r'.+\s+OK\s+.+\s+(\d+:\d+)\s+.+(http.+)') RE_PACMAN_SYNC_FIRST = re.compile(r'SyncFirst\s*=\s*(.+)') RE_DESKTOP_FILES = re.compile(r'\n?([\w\-_]+)\s+(/usr/share/.+\.desktop)') +RE_IGNORED_PACKAGES: Optional[Pattern] = None def is_available() -> bool: @@ -234,22 +235,27 @@ def sign_key(key: str, root_password: Optional[str]) -> SystemProcess: def list_ignored_packages(config_path: str = '/etc/pacman.conf') -> Set[str]: ignored = set() + try: - pacman_conf = new_subprocess(['cat', config_path]) - grep = new_subprocess(['grep', '-Eo', r'\s*#*\s*ignorepkg\s*=\s*.+'], stdin=pacman_conf.stdout) - for o in grep.stdout: - if o: - line = o.decode().strip() + with open(config_path, 'r') as f: + file_content = f.read() - if not line.startswith('#'): - ignored.add(line.split('=')[1].strip()) + global RE_IGNORED_PACKAGES - pacman_conf.terminate() - grep.terminate() - return ignored - except: + if not RE_IGNORED_PACKAGES: + RE_IGNORED_PACKAGES = re.compile(r'[\s#]*ignorepkg\s*=\s*.+', re.IGNORECASE) + + for raw_line in RE_IGNORED_PACKAGES.findall(file_content): + line = raw_line.strip() + + if line and not line.startswith("#"): + ignored.add(line.split("=")[1].strip()) + except (FileNotFoundError, OSError): + pass + except Exception: traceback.print_exc() - return ignored + + return ignored def check_missing(names: Set[str]) -> Set[str]: @@ -1075,3 +1081,18 @@ def map_available_packages() -> Optional[Dict[str, Any]]: 'r': package_data[0].strip(), 'i': len(package_data) == 4 and 'installed' in package_data[3]} return res + + +def map_installed(pkgs: Optional[Collection[str]] = None) -> Optional[Dict[str, str]]: + output = run_cmd(f"pacman -Q {' '.join({*pkgs} if pkgs else '')}".strip(), print_error=False) + + if output: + res = dict() + for raw_line in output.split("\n"): + line = raw_line.strip() + + if line: + pkg_version = line.split(" ") + if len(pkg_version) == 2: + res[pkg_version[0]] = pkg_version[1] + return res diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index e4c55844..9ec192bf 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -22,7 +22,7 @@ class UpdateRequirementsContext: aur_to_update: Dict[str, ArchPackage], repo_to_install: Dict[str, ArchPackage], aur_to_install: Dict[str, ArchPackage], to_install: Dict[str, ArchPackage], pkgs_data: Dict[str, dict], cannot_upgrade: Dict[str, UpgradeRequirement], - to_remove: Dict[str, UpgradeRequirement], installed_names: Set[str], provided_map: Dict[str, Set[str]], + to_remove: Dict[str, UpgradeRequirement], installed_names: Dict[str, str], provided_map: Dict[str, Set[str]], aur_index: Set[str], arch_config: dict, remote_provided_map: Dict[str, Set[str]], remote_repo_map: Dict[str, str], root_password: Optional[str], aur_supported: bool): self.to_update = to_update @@ -33,7 +33,7 @@ class UpdateRequirementsContext: self.pkgs_data = pkgs_data self.cannot_upgrade = cannot_upgrade self.root_password = root_password - self.installed_names = installed_names + self.installed = installed_names self.provided_map = provided_map self.to_remove = to_remove self.to_install = to_install @@ -149,12 +149,21 @@ class UpdatesSummarizer: for p, data in context.pkgs_data.items(): if data['c']: for c in data['c']: - if c and c != p and c in context.installed_names: - # source = provided_map[c] - root_conflict[c] = p + if c: + name_op_exp = DependenciesAnalyser.re_dep_operator().split(c) + conflict_name = name_op_exp[0] - if (p, c) in root_conflict.items(): - mutual_conflicts[c] = p + if conflict_name != p: + conflict_version = context.installed.get(conflict_name) + + if conflict_version: + if len(name_op_exp) == 1 or match_required_version(conflict_version, + name_op_exp[1], + name_op_exp[2]): + root_conflict[conflict_name] = p + + if (p, conflict_name) in root_conflict.items(): + mutual_conflicts[conflict_name] = p if mutual_conflicts: for pkg1, pkg2 in mutual_conflicts.items(): @@ -278,8 +287,8 @@ class UpdatesSummarizer: ti = time.time() self.logger.info("Filling provided names") - if not context.installed_names: - context.installed_names = pacman.list_installed_names() + if not context.installed: + context.installed = pacman.map_installed() installed_to_ignore = set() @@ -305,8 +314,8 @@ class UpdatesSummarizer: if len(split_provided) > 1 and split_provided[0] != p: pacman.fill_provided_map(split_provided[0], pkgname, context.provided_map) - if installed_to_ignore: # filling the provided names of the installed - installed_to_query = context.installed_names.difference(installed_to_ignore) + if context.installed and installed_to_ignore: # filling the provided names of the installed + installed_to_query = {*context.installed}.difference(installed_to_ignore) if installed_to_query: context.provided_map.update(pacman.map_provided(remote=False, pkgs=installed_to_query)) @@ -374,7 +383,7 @@ class UpdatesSummarizer: remote_repo_map = pacman.map_repositories() context = UpdateRequirementsContext(to_update={}, repo_to_update={}, aur_to_update={}, repo_to_install={}, aur_to_install={}, to_install={}, pkgs_data={}, cannot_upgrade={}, - to_remove={}, installed_names=set(), provided_map={}, aur_index=set(), + to_remove={}, installed_names=dict(), provided_map={}, aur_index=set(), arch_config=arch_config, root_password=root_password, remote_provided_map=remote_provided_map, remote_repo_map=remote_repo_map, aur_supported=self.aur_supported) @@ -604,7 +613,7 @@ class UpdatesSummarizer: for r in reqs: if r in transaction_pkgs: reqs_in_transaction.add(r) - elif r in context.installed_names: + elif r in context.installed: reqs_not_in_transaction.add(r) if not reqs_not_in_transaction and not reqs_in_transaction: diff --git a/bauh/gems/debian/aptitude.py b/bauh/gems/debian/aptitude.py index e94490cf..8a3f62f1 100644 --- a/bauh/gems/debian/aptitude.py +++ b/bauh/gems/debian/aptitude.py @@ -241,7 +241,7 @@ class Aptitude: @property def re_transaction_pkg(self) -> Pattern: if self._re_transaction_pkg is None: - self._re_transaction_pkg = re.compile(r'([a-zA-Z0-9\-_@~.+]+)({\w+})?\s*\[([a-zA-Z0-9\-_@~.+:]+)' + self._re_transaction_pkg = re.compile(r'([a-zA-Z0-9\-_@~.+:]+)({\w+})?\s*\[([a-zA-Z0-9\-_@~.+:]+)' r'(\s+->\s+([a-zA-Z0-9\-_@~.+:]+))?](\s*<([\-+]?[0-9.,]+\s+\w+)>)?') return self._re_transaction_pkg diff --git a/bauh/manage.py b/bauh/manage.py index 789ea3d3..495148b3 100644 --- a/bauh/manage.py +++ b/bauh/manage.py @@ -76,7 +76,6 @@ def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logg manage_window = ManageWindow(i18n=i18n, manager=manager, icon_cache=icon_cache, - screen_size=screen_size, config=app_config, context=context, http_client=http_client, @@ -84,8 +83,7 @@ def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logg force_suggestions=force_suggestions, logger=logger) - prepare = PreparePanel(screen_size=screen_size, - context=context, + prepare = PreparePanel(context=context, manager=manager, i18n=i18n, manage_window=manage_window, diff --git a/bauh/view/core/downloader.py b/bauh/view/core/downloader.py index 1800191c..c6ef0024 100644 --- a/bauh/view/core/downloader.py +++ b/bauh/view/core/downloader.py @@ -86,7 +86,7 @@ class AdaptableFileDownloader(FileDownloader): return SimpleProcess(cmd=cmd, cwd=cwd, root_password=root_password) def _get_wget_process(self, url: str, output_path: str, cwd: str, root_password: Optional[str]) -> SimpleProcess: - cmd = ['wget', url, '-c', '--retry-connrefused', '-t', '10', '--no-config', '-nc'] + cmd = ['wget', url, '-c', '--retry-connrefused', '-t', '10', '-nc'] if not self.check_ssl: cmd.append('--no-check-certificate') diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 87dfe125..c08e58e6 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -15,6 +15,7 @@ from bauh.api.abstract.model import PackageStatus, CustomSoftwareAction from bauh.commons.html import strip_html, bold from bauh.view.qt.components import IconButton, QCustomMenuAction, QCustomToolbar from bauh.view.qt.dialog import ConfirmationDialog +from bauh.view.qt.qt_utils import get_current_screen_geometry from bauh.view.qt.view_model import PackageView from bauh.view.util.translation import I18n @@ -68,9 +69,8 @@ class PackagesTable(QTableWidget): COL_NUMBER = 9 DEFAULT_ICON_SIZE = QSize(16, 16) - def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool, screen_width: int): + def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool): super(PackagesTable, self).__init__() - self.screen_width = screen_width self.setObjectName('table_packages') self.setParent(parent) self.window = parent @@ -180,15 +180,16 @@ class PackagesTable(QTableWidget): action=custom_action) def refresh(self, pkg: PackageView): - self._update_row(pkg, update_check_enabled=False, change_update_col=False) + screen_width = get_current_screen_geometry(self.parent()).width() + self._update_row(pkg, screen_width, update_check_enabled=False, change_update_col=False) - def update_package(self, pkg: PackageView, change_update_col: bool = False): + def update_package(self, pkg: PackageView, screen_width: int, change_update_col: bool = False): if self.download_icons and pkg.model.icon_url: icon_request = QNetworkRequest(QUrl(pkg.model.icon_url)) icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) self.network_man.get(icon_request) - self._update_row(pkg, change_update_col=change_update_col) + self._update_row(pkg, screen_width, change_update_col=change_update_col) def _uninstall(self, pkg: PackageView): if ConfirmationDialog(title=self.i18n['manage_window.apps_table.row.actions.uninstall.popup.title'], @@ -255,6 +256,7 @@ class PackagesTable(QTableWidget): self.setEnabled(True) if pkgs: + screen_width = get_current_screen_geometry(self.parent()).width() self.setColumnCount(self.COL_NUMBER if update_check_enabled else self.COL_NUMBER - 1) self.setRowCount(len(pkgs)) @@ -266,16 +268,17 @@ class PackagesTable(QTableWidget): icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) self.network_man.get(icon_request) - self._update_row(pkg, update_check_enabled) + self._update_row(pkg, screen_width, update_check_enabled) self.scrollToTop() - def _update_row(self, pkg: PackageView, update_check_enabled: bool = True, change_update_col: bool = True): + def _update_row(self, pkg: PackageView, screen_width: int, + update_check_enabled: bool = True, change_update_col: bool = True): self._set_col_icon(0, pkg) - self._set_col_name(1, pkg) - self._set_col_version(2, pkg) - self._set_col_description(3, pkg) - self._set_col_publisher(4, pkg) + self._set_col_name(1, pkg, screen_width) + self._set_col_version(2, pkg, screen_width) + self._set_col_description(3, pkg, screen_width) + self._set_col_publisher(4, pkg, screen_width) self._set_col_type(5, pkg) self._set_col_installed(6, pkg) self._set_col_actions(7, pkg) @@ -355,7 +358,7 @@ class PackagesTable(QTableWidget): col_type_icon.setToolTip(icon_data['tip']) self.setCellWidget(pkg.table_index, col, col_type_icon) - def _set_col_version(self, col: int, pkg: PackageView): + def _set_col_version(self, col: int, pkg: PackageView, screen_width: int): label_version = QLabel(str(pkg.model.version if pkg.model.version else '?')) label_version.setObjectName('app_version') label_version.setAlignment(Qt.AlignCenter) @@ -383,7 +386,7 @@ class PackagesTable(QTableWidget): f"{self.i18n['version.latest']}: {pkg.model.latest_version})" label_version.setText(f"{label_version.text()} > {pkg.model.latest_version}") - if label_version.sizeHint().width() / self.screen_width > 0.22: + if label_version.sizeHint().width() / screen_width > 0.22: label_version.setText(pkg.model.latest_version) item.setToolTip(tooltip) @@ -426,7 +429,7 @@ class PackagesTable(QTableWidget): self._update_icon(col_icon, icon) self.setCellWidget(pkg.table_index, col, col_icon) - def _set_col_name(self, col: int, pkg: PackageView): + def _set_col_name(self, col: int, pkg: PackageView, screen_width: int): col_name = QLabel() col_name.setObjectName('app_name') col_name.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred) @@ -439,7 +442,7 @@ class PackagesTable(QTableWidget): col_name.setToolTip(self.i18n['app.name'].lower()) col_name.setText(name) - screen_perc = col_name.sizeHint().width() / self.screen_width + screen_perc = col_name.sizeHint().width() / screen_width if screen_perc > 0.15: max_chars = int(len(name) * 0.15 / screen_perc) - 3 @@ -454,7 +457,7 @@ class PackagesTable(QTableWidget): sizes = icon.availableSizes() return sizes[-1] if sizes else self.DEFAULT_ICON_SIZE - def _set_col_description(self, col: int, pkg: PackageView): + def _set_col_description(self, col: int, pkg: PackageView, screen_width: int): item = QLabel() item.setObjectName('app_description') @@ -468,7 +471,7 @@ class PackagesTable(QTableWidget): item.setText(desc) - current_width_perc = item.sizeHint().width() / self.screen_width + current_width_perc = item.sizeHint().width() / screen_width if current_width_perc > 0.18: max_width = int(len(desc) * 0.18 / current_width_perc) - 3 desc = desc[0:max_width] + '...' @@ -479,7 +482,7 @@ class PackagesTable(QTableWidget): self.setCellWidget(pkg.table_index, col, item) - def _set_col_publisher(self, col: int, pkg: PackageView): + def _set_col_publisher(self, col: int, pkg: PackageView, screen_width: int): item = QToolBar() publisher = pkg.model.get_publisher() @@ -494,7 +497,7 @@ class PackagesTable(QTableWidget): if publisher: lb_name.setText(publisher) - screen_perc = lb_name.sizeHint().width() / self.screen_width + screen_perc = lb_name.sizeHint().width() / screen_width if screen_perc > 0.12: max_chars = int(len(publisher) * 0.12 / screen_perc) - 3 diff --git a/bauh/view/qt/info.py b/bauh/view/qt/info.py index 618988e7..f12f2d29 100644 --- a/bauh/view/qt/info.py +++ b/bauh/view/qt/info.py @@ -1,6 +1,6 @@ from collections.abc import Iterable -from PyQt5.QtCore import QSize, Qt +from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QCursor from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \ QLineEdit, QLabel, QGridLayout, QPushButton, QPlainTextEdit, QScrollArea, QFrame, QWidget, QSizePolicy, \ @@ -8,6 +8,7 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \ from bauh.api.abstract.cache import MemoryCache from bauh.view.qt.components import new_spacer +from bauh.view.qt.qt_utils import get_current_screen_geometry from bauh.view.util.translation import I18n IGNORED_ATTRS = {'name', '__app__'} @@ -15,10 +16,9 @@ IGNORED_ATTRS = {'name', '__app__'} class InfoDialog(QDialog): - def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n, screen_size: QSize): + def __init__(self, pkg_info: dict, icon_cache: MemoryCache, i18n: I18n): super(InfoDialog, self).__init__() self.setWindowTitle(str(pkg_info['__app__'])) - self.screen_size = screen_size self.i18n = i18n layout = QVBoxLayout() self.setLayout(layout) @@ -108,7 +108,9 @@ class InfoDialog(QDialog): lower_container.layout().addWidget(self.bt_close) layout.addWidget(lower_container) self.setMinimumWidth(int(self.gbox_info.sizeHint().width() * 1.2)) - self.setMaximumHeight(int(screen_size.height() * 0.8)) + + screen_height = get_current_screen_geometry().height() + self.setMaximumHeight(int(screen_height * 0.8)) self.adjustSize() def _gen_show_button(self, idx: int, val): diff --git a/bauh/view/qt/prepare.py b/bauh/view/qt/prepare.py index b93ee42e..f1b94fc6 100644 --- a/bauh/view/qt/prepare.py +++ b/bauh/view/qt/prepare.py @@ -15,7 +15,7 @@ from bauh.api.abstract.controller import SoftwareManager, SoftwareAction from bauh.api.abstract.handler import TaskManager from bauh.api import user from bauh.view.qt.components import new_spacer, QCustomToolbar -from bauh.view.qt.qt_utils import centralize +from bauh.view.qt.qt_utils import centralize, get_current_screen_geometry from bauh.view.qt.root import RootDialog from bauh.view.qt.thread import AnimateProgress from bauh.view.util.translation import I18n @@ -145,7 +145,7 @@ class PreparePanel(QWidget, TaskManager): signal_status = pyqtSignal(int) signal_password_response = pyqtSignal(bool, str) - def __init__(self, context: ApplicationContext, manager: SoftwareManager, screen_size: QSize, + def __init__(self, context: ApplicationContext, manager: SoftwareManager, i18n: I18n, manage_window: QWidget, app_config: dict, force_suggestions: bool = False): super(PreparePanel, self).__init__(flags=Qt.CustomizeWindowHint | Qt.WindowTitleHint) self.i18n = i18n @@ -153,9 +153,7 @@ class PreparePanel(QWidget, TaskManager): self.app_config = app_config self.manage_window = manage_window self.setWindowTitle('{} ({})'.format(__app_name__, self.i18n['prepare_panel.title.start'].lower())) - self.setMinimumWidth(int(screen_size.width() * 0.5)) - self.setMinimumHeight(int(screen_size.height() * 0.35)) - self.setMaximumHeight(int(screen_size.height() * 0.95)) + self.setLayout(QVBoxLayout()) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) self.manager = manager @@ -292,6 +290,10 @@ class PreparePanel(QWidget, TaskManager): super(PreparePanel, self).show() self.prepare_thread.start() centralize(self) + screen_size = get_current_screen_geometry() + self.setMinimumWidth(int(screen_size.width() * 0.5)) + self.setMinimumHeight(int(screen_size.height() * 0.35)) + self.setMaximumHeight(int(screen_size.height() * 0.95)) def start(self, tasks: int): self.started_at = time.time() diff --git a/bauh/view/qt/qt_utils.py b/bauh/view/qt/qt_utils.py index ca092d6e..f84ea46d 100644 --- a/bauh/view/qt/qt_utils.py +++ b/bauh/view/qt/qt_utils.py @@ -1,16 +1,18 @@ -from PyQt5.QtCore import Qt +from typing import Optional, Union + +from PyQt5.QtCore import Qt, QRect, QPoint from PyQt5.QtGui import QIcon, QPixmap -from PyQt5.QtWidgets import QWidget, QApplication +from PyQt5.QtWidgets import QWidget, QApplication, QDesktopWidget from bauh.view.util import resource +desktop: Optional[QDesktopWidget] = None + def centralize(widget: QWidget): - geo = widget.frameGeometry() - screen = QApplication.desktop().screenNumber(QApplication.desktop().cursor().pos()) - center_point = QApplication.desktop().screenGeometry(screen).center() - geo.moveCenter(center_point) - widget.move(geo.topLeft()) + screen_geometry = get_current_screen_geometry() + widget.frameGeometry().moveCenter(screen_geometry.center()) + widget.move(widget.frameGeometry().topLeft()) def load_icon(path: str, width: int, height: int = None) -> QIcon: @@ -27,3 +29,13 @@ def measure_based_on_width(percent: float) -> int: def measure_based_on_height(percent: float) -> int: return round(percent * QApplication.primaryScreen().size().height()) + + +def get_current_screen_geometry(source_widget: Optional[Union[QWidget, QPoint]] = None) -> QRect: + global desktop + + if not desktop: + desktop = QDesktopWidget() + + current_screen_idx = desktop.screenNumber(source_widget if source_widget else desktop.cursor().pos()) + return desktop.screen(current_screen_idx).geometry() diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 9ff1f5b2..5b8eb18c 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -29,6 +29,7 @@ from bauh.view.core import timeshift from bauh.view.core.config import CoreConfigManager, BACKUP_REMOVE_METHODS, BACKUP_DEFAULT_REMOVE_METHOD from bauh.view.qt import commons from bauh.view.qt.commons import sort_packages +from bauh.view.qt.qt_utils import get_current_screen_geometry from bauh.view.qt.view_model import PackageView, PackageViewStatus from bauh.view.util.translation import I18n @@ -231,12 +232,12 @@ class UpgradeSelected(AsyncAction): SUMMARY_FILE = UPGRADE_LOGS_DIR + '/{}_summary.txt' def __init__(self, manager: SoftwareManager, internet_checker: InternetChecker, i18n: I18n, - screen_width: int, pkgs: List[PackageView] = None): + parent_widget: QWidget, pkgs: List[PackageView] = None): super(UpgradeSelected, self).__init__(i18n=i18n) self.pkgs = pkgs self.manager = manager self.internet_checker = internet_checker - self.screen_width = screen_width + self._parent_widget = parent_widget def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None, required_size: bool = True, display_sizes: bool = True, positive_size_symbol: bool = False) -> InputOption: @@ -495,9 +496,10 @@ class UpgradeSelected(AsyncAction): comps.insert(0, TextComponent(f'{disc_size_text} ({download_size_text})', size=14)) comps.insert(1, TextComponent('')) + screen_width = get_current_screen_geometry(self._parent_widget).width() if not self.request_confirmation(title=self.i18n['action.update.summary'].capitalize(), body='', components=comps, confirmation_label=self.i18n['proceed'].capitalize(), deny_label=self.i18n['cancel'].capitalize(), - confirmation_button=can_upgrade, min_width=int(0.45 * self.screen_width)): + confirmation_button=can_upgrade, min_width=int(0.45 * screen_width)): self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': None}) self.pkgs = None return diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index dfb90663..3f287cbe 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -2,12 +2,11 @@ import logging import operator import os.path import time -import traceback from pathlib import Path from typing import List, Type, Set, Tuple, Optional -from PyQt5.QtCore import QEvent, Qt, pyqtSignal, QSize -from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor +from PyQt5.QtCore import QEvent, Qt, pyqtSignal, QRect +from PyQt5.QtGui import QIcon, QWindowStateChangeEvent, QCursor, QMoveEvent from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHeaderView, QToolBar, \ QLabel, QPlainTextEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy, \ QMenu, QHBoxLayout @@ -34,6 +33,7 @@ from bauh.view.qt.components import new_spacer, IconButton, QtComponentsManager, from bauh.view.qt.dialog import ConfirmationDialog from bauh.view.qt.history import HistoryDialog from bauh.view.qt.info import InfoDialog +from bauh.view.qt.qt_utils import get_current_screen_geometry from bauh.view.qt.root import RootDialog from bauh.view.qt.screenshots import ScreenshotsDialog from bauh.view.qt.settings import SettingsWindow @@ -96,7 +96,7 @@ class ManageWindow(QWidget): signal_table_update = pyqtSignal() signal_stop_notifying = pyqtSignal() - def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, screen_size: QSize, config: dict, + def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, config: dict, context: ApplicationContext, http_client: HttpClient, logger: logging.Logger, icon: QIcon, force_suggestions: bool = False): super(ManageWindow, self).__init__() @@ -112,7 +112,6 @@ class ManageWindow(QWidget): self.pkgs_installed = [] # cached installed packages self.display_limit = config['ui']['table']['max_displayed'] self.icon_cache = icon_cache - self.screen_size = screen_size self.config = config self.context = context self.http_client = http_client @@ -279,8 +278,7 @@ class ManageWindow(QWidget): self.table_container.layout().setContentsMargins(0, 0, 0, 0) self.table_apps = PackagesTable(self, self.icon_cache, - download_icons=bool(self.config['download']['icons']), - screen_width=int(screen_size.width())) + download_icons=bool(self.config['download']['icons'])) self.table_apps.change_headers_policy() self.table_container.layout().addWidget(self.table_apps) @@ -333,7 +331,7 @@ class ManageWindow(QWidget): self.thread_update = self._bind_async_action(UpgradeSelected(manager=self.manager, i18n=self.i18n, internet_checker=context.internet_checker, - screen_width=screen_size.width()), + parent_widget=self), finished_call=self._finish_upgrade_selected) self.thread_refresh = self._bind_async_action(RefreshApps(i18n, self.manager), finished_call=self._finish_refresh_packages, only_finished=True) self.thread_uninstall = self._bind_async_action(UninstallPackage(self.manager, self.icon_cache, self.i18n), finished_call=self._finish_uninstall) @@ -433,8 +431,6 @@ class ManageWindow(QWidget): self.container_progress.add_space() self.layout.addWidget(self.container_progress) - qt_utils.centralize(self) - self.filter_only_apps = True self.type_filter = self.any_type_filter self.category_filter = self.any_category_filter @@ -457,11 +453,8 @@ class ManageWindow(QWidget): self.thread_load_installed = NotifyInstalledLoaded() self.thread_load_installed.signal_loaded.connect(self._finish_loading_installed) - self.setMinimumHeight(int(screen_size.height() * 0.5)) - self.setMaximumHeight(int(screen_size.height())) - self.setMinimumWidth(int(screen_size.width() * 0.5)) - self.setMaximumWidth(int(screen_size.width() - screen_size.width() * 0.015)) self._register_groups() + self._screen_geometry: Optional[QRect] = None def _register_groups(self): common_filters = (CHECK_APPS, CHECK_UPDATES, COMBO_CATEGORIES, COMBO_TYPES, INP_NAME) @@ -597,6 +590,8 @@ class ManageWindow(QWidget): self.thread_warnings.start() qt_utils.centralize(self) + self._screen_geometry = get_current_screen_geometry() + self._update_size_limits() def verify_warnings(self): self.thread_warnings.start() @@ -671,7 +666,8 @@ class ManageWindow(QWidget): if self.table_apps.isEnabled() and self.pkgs is not None and 0 <= idx < len(self.pkgs): pkg = self.pkgs[idx] pkg.status = PackageViewStatus.READY - self.table_apps.update_package(pkg) + screen_width = get_current_screen_geometry(self).width() + self.table_apps.update_package(pkg, screen_width=screen_width) def _reload_categories(self): categories = set() @@ -687,11 +683,33 @@ class ManageWindow(QWidget): if categories: self._update_categories(categories, keep_selected=True) + def _update_size_limits(self): + self.setMinimumHeight(int(self._screen_geometry.height() * 0.5)) + self.setMinimumWidth(int(self._screen_geometry.width() * 0.5)) + self.setMaximumWidth(int(self._screen_geometry.width())) + def changeEvent(self, e: QEvent): if isinstance(e, QWindowStateChangeEvent): self._maximized = self.isMaximized() self.table_apps.change_headers_policy(maximized=self._maximized) + if not self._maximized: + self._reorganize() + self.adjustSize() + + def event(self, e: QEvent) -> bool: + res = super(ManageWindow, self).event(e) + + if self.isVisible() and e.type() == 216: # drop event + current_geometry = get_current_screen_geometry() + if current_geometry != self._screen_geometry: # only if the display device has changed + self._screen_geometry = current_geometry + self._update_size_limits() + self._reorganize() + self.adjustSize() + + return res + def _handle_console(self, checked: bool): if checked: self.textarea_details.show() @@ -780,6 +798,7 @@ class ManageWindow(QWidget): util.notify_user('{} ({}) {}'.format(src_pkg.model.name, src_pkg.model.get_type(), self.i18n['uninstalled'])) if res['removed']: + screen_width = get_current_screen_geometry(self).width() for list_idx, pkg_list in enumerate((self.pkgs_available, self.pkgs, self.pkgs_installed)): if pkg_list: removed_idxs = [] @@ -796,7 +815,9 @@ class ManageWindow(QWidget): removed_idxs.append(pkgv_idx) if self.search_performed and list_idx == 1: # only for displayed - self.table_apps.update_package(pkgv, change_update_col=True) + self.table_apps.update_package(pkgv, + screen_width=screen_width, + change_update_col=True) break # as the model has been found, stops the loop @@ -1286,8 +1307,7 @@ class ManageWindow(QWidget): if pkg_info: if len(pkg_info) > 1: - dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache, - i18n=self.i18n, screen_size=self.screen_size) + dialog_info = InfoDialog(pkg_info=pkg_info, icon_cache=self.icon_cache, i18n=self.i18n) dialog_info.exec_() else: dialog.show_message(title=self.i18n['warning'].capitalize(), @@ -1462,10 +1482,11 @@ class ManageWindow(QWidget): self.pkgs_available.insert(new_idx, data[2]) # updating the respective table rows: + screen_width = get_current_screen_geometry(self).width() for displayed in self.pkgs: for model in models_updated: if displayed.model == model: - self.table_apps.update_package(displayed, change_update_col=True) + self.table_apps.update_package(displayed, screen_width=screen_width, change_update_col=True) self.update_bt_upgrade() @@ -1565,7 +1586,8 @@ class ManageWindow(QWidget): self.settings_window.handle_display() else: self.settings_window = SettingsWindow(manager=self.manager, i18n=self.i18n, window=self) - self.settings_window.setMinimumWidth(int(self.screen_size.width() / 4)) + screen_width = get_current_screen_geometry(self).width() + self.settings_window.setMinimumWidth(int(screen_width / 4)) self.settings_window.resize(self.size()) self.settings_window.adjustSize() qt_utils.centralize(self.settings_window) @@ -1628,10 +1650,11 @@ class ManageWindow(QWidget): self._update_table_indexes() self.update_bt_upgrade() else: + screen_width = get_current_screen_geometry(self).width() for pkg in self.pkgs: if pkg == res['pkg']: pkg.update_model(res['pkg'].model) - self.table_apps.update_package(pkg, change_update_col=not any([self.search_performed, self.suggestions_requested])) + self.table_apps.update_package(pkg, screen_width=screen_width, change_update_col=not any([self.search_performed, self.suggestions_requested])) self.update_bt_upgrade() break diff --git a/linux_dist/appimage/Dockerfile b/linux_dist/appimage/Dockerfile new file mode 100644 index 00000000..b6513523 --- /dev/null +++ b/linux_dist/appimage/Dockerfile @@ -0,0 +1,22 @@ +FROM ubuntu:20.04 + +ARG bauh_commit +ENV BAUH_VERSION=$bauh_commit +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get upgrade && \ + apt-get install python3-pip python3-setuptools python3-wheel wget fuse binutils coreutils desktop-file-utils fakeroot patchelf squashfs-tools strace zsync libgdk-pixbuf2.0-dev gtk-update-icon-cache -y && \ + mkdir /build && cd /build && \ + wget https://github.com/AppImageCrafters/appimage-builder/releases/download/v0.9.2/appimage-builder-0.9.2-35e3eab-x86_64.AppImage -O appimage-builder && \ + wget https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -O appimage-tool && \ + chmod +x appimage-tool && \ + chmod +x appimage-builder && \ + mv /build/appimage-builder /usr/local/bin/appimage-builder && \ + mv /build/appimage-tool /usr/local/bin/appimage-tool + +WORKDIR /build + +COPY AppImageBuilder.yml /build + +CMD [ "appimage-builder", "--skip-tests"] diff --git a/linux_dist/appimage/build.sh b/linux_dist/appimage/build.sh new file mode 100755 index 00000000..08735442 --- /dev/null +++ b/linux_dist/appimage/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker build -t bauh-appimage --build-arg bauh_commit=$BAUH_COMMIT . +docker run --cap-add=SYS_ADMIN --device /dev/fuse --mount type=bind,source="$(pwd)",target=/build bauh-appimage diff --git a/tests/common/test_util.py b/tests/common/test_util.py index 22086bd0..f883bb28 100644 --- a/tests/common/test_util.py +++ b/tests/common/test_util.py @@ -1,4 +1,3 @@ -import time from unittest import TestCase from bauh.commons.util import size_to_byte, sanitize_command_input diff --git a/tests/common/test_view_utils.py b/tests/common/test_view_utils.py index afd62be5..dc4e5e29 100644 --- a/tests/common/test_view_utils.py +++ b/tests/common/test_view_utils.py @@ -8,7 +8,7 @@ class GetHumanSizeStrTest(TestCase): def setUp(self): try: - locale.setlocale(locale.LC_NUMERIC, None) + locale.setlocale(locale.LC_NUMERIC, "C") except: print("Error: could not set locale.LC_NUMERIC to None") diff --git a/tests/gems/appimage/test_util.py b/tests/gems/appimage/test_util.py index fa445e0b..3a5b894d 100644 --- a/tests/gems/appimage/test_util.py +++ b/tests/gems/appimage/test_util.py @@ -79,6 +79,45 @@ class TestUtil(TestCase): self.assertEqual(expected, res) + def test_replace_desktop_entry_exec_command__exec_as_the_first_field(self): + desktop_entry = """ + Exec=myapp %f + Name=MyApp + Icon=MyApp + """ + + res = replace_desktop_entry_exec_command(desktop_entry=desktop_entry, + appname='myapp', + file_path='/path/to/myapp.appimage') + + expected = """ + Exec="/path/to/myapp.appimage" %f + Name=MyApp + Icon=MyApp + """ + + self.assertEqual(expected, res) + + def test_replace_desktop_entry_exec_command__try_exec_as_the_first_field(self): + desktop_entry = """ + TryExec=MyApp + Exec=myapp %f + Name=MyApp + Icon=MyApp + """ + + res = replace_desktop_entry_exec_command(desktop_entry=desktop_entry, + appname='myapp', + file_path='/path/to/myapp.appimage') + + expected = """ + Exec="/path/to/myapp.appimage" %f + Name=MyApp + Icon=MyApp + """ + + self.assertEqual(expected, res) + def test_replace_desktop_entry_exec_command__only_one_exec_field_with_spaces_and_params(self): desktop_entry = """ Name=MyApp @@ -136,7 +175,7 @@ class TestUtil(TestCase): self.assertEqual(expected, res) - def test_replace_desktop_entry_exec_command__only_one_tryexec_field_with_spaces_and_params(self): + def test_replace_desktop_entry_exec_command__must_remove_try_exec_field(self): desktop_entry = """ Name=MyApp Icon=MyApp @@ -150,12 +189,11 @@ class TestUtil(TestCase): expected = """ Name=MyApp Icon=MyApp - TryExec ="/path/to/myapp.appimage" %f --a """ self.assertEqual(expected, res) - def test_replace_desktop_entry_exec_command__exec_and_tryexec_fields(self): + def test_replace_desktop_entry_exec_command__must_replace_exec_and_remove_tryexec_fields(self): desktop_entry = """ Name=MyApp Icon=MyApp @@ -171,14 +209,13 @@ class TestUtil(TestCase): expected = """ Name=MyApp Icon=MyApp - TryExec ="/path/to/myapp.appimage" %f Exec="/path/to/myapp.appimage" --a Terminal=false """ self.assertEqual(expected, res) - def test_replace_desktop_entry_exec_command__exec_and_tryexec_fields_with_envvars_and_params(self): + def test_replace_desktop_entry_exec_command__exec_field_with_envvars_and_params(self): desktop_entry = """ Name=MyApp Icon=MyApp @@ -194,7 +231,6 @@ class TestUtil(TestCase): expected = """ Name=MyApp Icon=MyApp - TryExec=__MY_VAR=1 "/path/to/myapp.appimage" %f Exec=NEW_VAR=abc "/path/to/myapp.appimage" --a Terminal=false """ @@ -228,7 +264,6 @@ Name=RPCS3 GenericName=PlayStation 3 Emulator Comment=An open-source PlayStation 3 emulator/debugger written in C++. Icon=rpcs3 -TryExec="/path/to/rpcs3.appimage" Exec="/path/to/rpcs3.appimage" %f Terminal=false Categories=Game;Emulator; diff --git a/tests/gems/arch/resources/pacman_ign_pkgs.conf b/tests/gems/arch/resources/pacman_ign_pkgs.conf index 6e502ab0..7ea5ebdf 100644 --- a/tests/gems/arch/resources/pacman_ign_pkgs.conf +++ b/tests/gems/arch/resources/pacman_ign_pkgs.conf @@ -98,4 +98,4 @@ Include = /etc/pacman.d/mirrorlist #Server = file:///home/custompkgs ignorepkg=google-chrome # ignorepkg=abc -ignorepkg = firefox \ No newline at end of file +IgnorePkg = firefox \ No newline at end of file diff --git a/tests/gems/debian/test_aptitude.py b/tests/gems/debian/test_aptitude.py index 8015c4cb..05f775d2 100644 --- a/tests/gems/debian/test_aptitude.py +++ b/tests/gems/debian/test_aptitude.py @@ -166,3 +166,33 @@ Description: GNU C compiler ] self.assertEqual([p.__dict__ for p in expected], [p.__dict__ for p in returned]) + + def test_map_transaction_output__it_should_map_i386_packages(self): + output = "\nThe following NEW packages will be installed:\n" \ + " gcc-12-base:i386{a} [12.1.0-2distro~22.04] <+272 kB> glib-networking:i386{a} [2.72.0-1] <+242 kB>\n" \ + "\nThe following packages will be REMOVED:\n" \ + " celluloid{a} [0.21-linux+distro] <-1066 kB> libpcre3:i386{a} [2:8.39-13distro0.22.04.1] <-714 kB>" + + transaction = self.aptitude.map_transaction_output(output) + to_install = { + DebianPackage(name="gcc-12-base:i386", + version="12.1.0-2distro~22.04", + latest_version="12.1.0-2distro~22.04", + transaction_size=272000.0 + ), + DebianPackage(name="glib-networking:i386", version="2.72.0-1", + latest_version="2.72.0-1", transaction_size=242000.0) + } + self.assertEqual(to_install, {*transaction.to_install}) + + to_remove = { + DebianPackage(name="celluloid", + version="0.21-linux+distro", + latest_version="0.21-linux+distro", + transaction_size=-1066000.0 + ), + DebianPackage(name="libpcre3:i386", version="2:8.39-13distro0.22.04.1", + latest_version="2:8.39-13distro0.22.04.1", transaction_size=-714000.0) + } + + self.assertEqual(to_remove, {*transaction.to_remove})