From c55a713beb2a1f5b73117724e5bb8b0ac2a68b0a Mon Sep 17 00:00:00 2001 From: bigtedde Date: Mon, 22 May 2023 17:48:50 -0700 Subject: [PATCH 01/12] Archive tab setting + dynamic pretty bytes --- src/vorta/store/settings.py | 8 ++++++++ src/vorta/utils.py | 21 +++++++++++++++++++++ src/vorta/views/archive_tab.py | 18 ++++++++++++++---- src/vorta/views/main_window.py | 1 + src/vorta/views/misc_tab.py | 9 ++++++++- 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/vorta/store/settings.py b/src/vorta/store/settings.py index f0559582b..c96cd396e 100644 --- a/src/vorta/store/settings.py +++ b/src/vorta/store/settings.py @@ -59,6 +59,14 @@ def get_misc_settings() -> List[Dict[str, str]]: 'label': trans_late('settings', 'Get statistics of file/folder when added'), 'tooltip': trans_late('settings', 'When adding a new source, calculate its size and the number of files.'), }, + { + 'key': 'enable_fixed_units', + 'value': False, + 'type': 'checkbox', + 'group': information, + 'label': trans_late('settings', 'Display all archive sizes in a consistent unit of measurement'), + 'tooltip': trans_late('settings', 'When enabled, replaces dynamic units of measurement based on file size'), + }, { 'key': 'use_system_keyring', 'value': True, diff --git a/src/vorta/utils.py b/src/vorta/utils.py index 7c2960730..d354e5d26 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -265,6 +265,27 @@ def find_best_unit_for_size(size: Optional[int], metric: bool = True, precision: return n +def pretty_bytes_dynamic_units(size, metric=True, sign=False, precision=1): + if not isinstance(size, int): + return '' + prefix = '+' if sign and size > 0 else '' + power, units = ( + (10**3, ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']) + if metric + else (2**10, ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']) + ) + n = 0 + while abs(round(size, precision)) >= power and n + 1 < len(units): + size /= power + n += 1 + try: + unit = units[n] + return f'{prefix}{round(size, precision)} {unit}B' + except KeyError as e: + logger.error(e) + return "NaN" + + def pretty_bytes( size: int, metric: bool = True, sign: bool = False, precision: int = 1, fixed_unit: Optional[int] = None ) -> str: diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index f5b92c8ee..67ca7e7f9 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -32,7 +32,7 @@ from vorta.borg.rename import BorgRenameJob from vorta.borg.umount import BorgUmountJob from vorta.i18n import translate -from vorta.store.models import ArchiveModel, BackupProfileMixin +from vorta.store.models import ArchiveModel, BackupProfileMixin, SettingsModel from vorta.utils import ( choose_file_dialog, find_best_unit_for_sizes, @@ -40,6 +40,7 @@ get_asset, get_mount_points, pretty_bytes, + pretty_bytes_dynamic_units, ) from vorta.views import diff_result, extract_dialog from vorta.views.diff_result import DiffResultDialog, DiffTree @@ -263,9 +264,18 @@ def populate_from_profile(self): formatted_time = archive.time.strftime('%Y-%m-%d %H:%M') self.archiveTable.setItem(row, 0, QTableWidgetItem(formatted_time)) - self.archiveTable.setItem( - row, 1, SizeItem(pretty_bytes(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS)) - ) + + if SettingsModel.get(key='enable_fixed_units').value is True: + self.archiveTable.setItem( + row, + 1, + SizeItem(pretty_bytes(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS)), + ) + else: + self.archiveTable.setItem( + row, 1, SizeItem(pretty_bytes_dynamic_units(archive.size, precision=SIZE_DECIMAL_DIGITS)) + ) + if archive.duration is not None: formatted_duration = str(timedelta(seconds=round(archive.duration))) else: diff --git a/src/vorta/views/main_window.py b/src/vorta/views/main_window.py index ca0c2426b..231471a21 100644 --- a/src/vorta/views/main_window.py +++ b/src/vorta/views/main_window.py @@ -78,6 +78,7 @@ def __init__(self, parent=None): self.repoTab.repo_changed.connect(self.archiveTab.populate_from_profile) self.repoTab.repo_changed.connect(self.scheduleTab.populate_from_profile) self.repoTab.repo_added.connect(self.archiveTab.refresh_archive_list) + self.miscTab.refresh_archive.connect(self.archiveTab.populate_from_profile) self.createStartBtn.clicked.connect(self.app.create_backup_action) self.cancelButton.clicked.connect(self.app.backup_cancelled_event.emit) diff --git a/src/vorta/views/misc_tab.py b/src/vorta/views/misc_tab.py index 087e30ef3..af1fe1818 100644 --- a/src/vorta/views/misc_tab.py +++ b/src/vorta/views/misc_tab.py @@ -1,6 +1,6 @@ import logging -from PyQt6 import uic +from PyQt6 import QtCore, uic from PyQt6.QtCore import Qt from PyQt6.QtWidgets import ( QApplication, @@ -28,6 +28,8 @@ class MiscTab(MiscTabBase, MiscTabUI, BackupProfileMixin): + refresh_archive = QtCore.pyqtSignal() + def __init__(self, parent=None): """Init.""" super().__init__(parent) @@ -100,6 +102,7 @@ def populate(self): cb.setCheckState(Qt.CheckState(setting.value)) cb.setTristate(False) cb.stateChanged.connect(lambda v, key=setting.key: self.save_setting(key, v)) + cb.stateChanged.connect(lambda v, key=setting.key: self.emit_archive_refresh(key)) tb = ToolTipButton() tb.setToolTip(setting.tooltip) @@ -124,6 +127,10 @@ def set_icons(self): for button in self.tooltip_buttons: button.setIcon(get_colored_icon('help-about')) + def emit_archive_refresh(self, key): + if key == 'enable_fixed_units': + self.refresh_archive.emit() + def save_setting(self, key, new_value): setting = SettingsModel.get(key=key) setting.value = bool(new_value) From 8074e53396c2b51eb9698666a9f5a119dbc9c962 Mon Sep 17 00:00:00 2001 From: bigtedde Date: Tue, 23 May 2023 17:39:10 -0700 Subject: [PATCH 02/12] updated setting tooltip --- src/vorta/store/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vorta/store/settings.py b/src/vorta/store/settings.py index c96cd396e..194c01425 100644 --- a/src/vorta/store/settings.py +++ b/src/vorta/store/settings.py @@ -65,7 +65,7 @@ def get_misc_settings() -> List[Dict[str, str]]: 'type': 'checkbox', 'group': information, 'label': trans_late('settings', 'Display all archive sizes in a consistent unit of measurement'), - 'tooltip': trans_late('settings', 'When enabled, replaces dynamic units of measurement based on file size'), + 'tooltip': trans_late('settings', 'Enable to replace dynamic units of measurement based on archive size'), }, { 'key': 'use_system_keyring', From 4281b2efbfb931508f79ce0b811797ddb231a746 Mon Sep 17 00:00:00 2001 From: bigtedde Date: Thu, 25 May 2023 17:23:38 -0700 Subject: [PATCH 03/12] added tests, rename pretty_bytes --- src/vorta/borg/borg_job.py | 11 ++-- src/vorta/utils.py | 8 +-- src/vorta/views/archive_tab.py | 6 +- src/vorta/views/diff_result.py | 10 ++-- src/vorta/views/extract_dialog.py | 4 +- src/vorta/views/repo_tab.py | 13 +++-- src/vorta/views/source_tab.py | 8 ++- tests/test_misc.py | 91 ++++++++++++++++++++++++------- tests/test_utils.py | 50 ++++++++++++----- 9 files changed, 144 insertions(+), 57 deletions(-) diff --git a/src/vorta/borg/borg_job.py b/src/vorta/borg/borg_job.py index d3846897a..ac0db6959 100644 --- a/src/vorta/borg/borg_job.py +++ b/src/vorta/borg/borg_job.py @@ -21,7 +21,7 @@ from vorta.keyring.abc import VortaKeyring from vorta.keyring.db import VortaDBKeyring from vorta.store.models import BackupProfileMixin, EventLogModel -from vorta.utils import borg_compat, pretty_bytes +from vorta.utils import borg_compat, pretty_bytes_fixed_units keyring_lock = Lock() db_lock = Lock() @@ -291,9 +291,12 @@ def read_async(fd): elif parsed['type'] == 'archive_progress' and not parsed.get('finished', False): msg = ( f"{translate('BorgJob','Files')}: {parsed['nfiles']}, " - f"{translate('BorgJob','Original')}: {pretty_bytes(parsed['original_size'])}, " - # f"{translate('BorgJob','Compressed')}: {pretty_bytes(parsed['compressed_size'])}, " - f"{translate('BorgJob','Deduplicated')}: {pretty_bytes(parsed['deduplicated_size'])}" # noqa: E501 + f"{translate('BorgJob','Original')}: " + f"{pretty_bytes_fixed_units(parsed['original_size'])}, " + # f"{translate('BorgJob','Compressed')}: + # f"{pretty_bytes_fixed_units(parsed['compressed_size'])}, " + f"{translate('BorgJob','Deduplicated')}: " + f"{pretty_bytes_fixed_units(parsed['deduplicated_size'])}" # noqa: E501 ) self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {msg}") except json.decoder.JSONDecodeError: diff --git a/src/vorta/utils.py b/src/vorta/utils.py index d354e5d26..b9d93a2f9 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -142,7 +142,7 @@ def get_path_datasize(path, exclude_patterns): if file_info.isDir(): data_size, files_count = get_directory_size(file_info.absoluteFilePath(), exclude_patterns) # logger.info("path (folder) %s %u elements size now=%u (%s)", - # file_info.absoluteFilePath(), files_count, data_size, pretty_bytes(data_size)) + # file_info.absoluteFilePath(), files_count, data_size, pretty_bytes_fixed_units(data_size)) else: # logger.info("path (file) %s size=%u", file_info.path(), file_info.size()) data_size = file_info.size() @@ -246,7 +246,7 @@ def clamp(n: Number, min_: Number, max_: Number) -> Number: def find_best_unit_for_sizes(sizes: Iterable[int], metric: bool = True, precision: int = 1) -> int: """ - Selects the index of the biggest unit (see the lists in the pretty_bytes function) capable of + Selects the index of the biggest unit (see the lists in the pretty_bytes_fixed_units function) capable of representing the smallest size in the sizes iterable. """ min_size = min((s for s in sizes if isinstance(s, int)), default=None) @@ -255,7 +255,7 @@ def find_best_unit_for_sizes(sizes: Iterable[int], metric: bool = True, precisio def find_best_unit_for_size(size: Optional[int], metric: bool = True, precision: int = 1) -> int: """ - Selects the index of the biggest unit (see the lists in the pretty_bytes function) capable of + Selects the index of the biggest unit (see the lists in the pretty_bytes_fixed_units function) capable of representing the passed size. """ if not isinstance(size, int) or size == 0: # this will also take care of the None case @@ -286,7 +286,7 @@ def pretty_bytes_dynamic_units(size, metric=True, sign=False, precision=1): return "NaN" -def pretty_bytes( +def pretty_bytes_fixed_units( size: int, metric: bool = True, sign: bool = False, precision: int = 1, fixed_unit: Optional[int] = None ) -> str: """ diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index 67ca7e7f9..d78c0110a 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -39,8 +39,8 @@ format_archive_name, get_asset, get_mount_points, - pretty_bytes, pretty_bytes_dynamic_units, + pretty_bytes_fixed_units, ) from vorta.views import diff_result, extract_dialog from vorta.views.diff_result import DiffResultDialog, DiffTree @@ -269,7 +269,9 @@ def populate_from_profile(self): self.archiveTable.setItem( row, 1, - SizeItem(pretty_bytes(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS)), + SizeItem( + pretty_bytes_fixed_units(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS) + ), ) else: self.archiveTable.setItem( diff --git a/src/vorta/views/diff_result.py b/src/vorta/views/diff_result.py index 5d262efa5..dea597cac 100644 --- a/src/vorta/views/diff_result.py +++ b/src/vorta/views/diff_result.py @@ -21,7 +21,7 @@ from PyQt6.QtWidgets import QApplication, QHeaderView, QMenu, QTreeView from vorta.store.models import SettingsModel -from vorta.utils import get_asset, pretty_bytes, uses_dark_mode +from vorta.utils import get_asset, pretty_bytes_fixed_units, uses_dark_mode from vorta.views.partials.treemodel import ( FileSystemItem, FileTreeModel, @@ -827,10 +827,10 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): # change type return item.data.change_type.short() elif column == 2: - return pretty_bytes(item.data.changed_size) + return pretty_bytes_fixed_units(item.data.changed_size) else: # size - return pretty_bytes(item.data.size) + return pretty_bytes_fixed_units(item.data.size) if role == Qt.ItemDataRole.ForegroundRole: # colour @@ -884,8 +884,8 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): if item.data.modified: tooltip += '\n' tooltip += modified_template.format( - pretty_bytes(item.data.modified[0]), - pretty_bytes(item.data.modified[1]), + pretty_bytes_fixed_units(item.data.modified[0]), + pretty_bytes_fixed_units(item.data.modified[1]), ) if item.data.mode_change: diff --git a/src/vorta/views/extract_dialog.py b/src/vorta/views/extract_dialog.py index 0822d0670..4849ab7e9 100644 --- a/src/vorta/views/extract_dialog.py +++ b/src/vorta/views/extract_dialog.py @@ -27,7 +27,7 @@ ) from vorta.store.models import SettingsModel -from vorta.utils import borg_compat, get_asset, pretty_bytes, uses_dark_mode +from vorta.utils import borg_compat, get_asset, pretty_bytes_fixed_units, uses_dark_mode from vorta.views.utils import get_colored_icon from .partials.treemodel import ( @@ -483,7 +483,7 @@ def data(self, index: QModelIndex, role: Union[int, Qt.ItemDataRole] = Qt.ItemDa return QLocale.system().toString(item.data.last_modified, QLocale.FormatType.ShortFormat) elif column == 2: # size - return pretty_bytes(item.data.size) + return pretty_bytes_fixed_units(item.data.size) else: # health return diff --git a/src/vorta/views/repo_tab.py b/src/vorta/views/repo_tab.py index 2f6b52fb4..809072c8a 100644 --- a/src/vorta/views/repo_tab.py +++ b/src/vorta/views/repo_tab.py @@ -6,7 +6,12 @@ from PyQt6.QtWidgets import QApplication, QLayout, QMenu, QMessageBox from vorta.store.models import ArchiveModel, BackupProfileMixin, RepoModel -from vorta.utils import borg_compat, get_asset, get_private_keys, pretty_bytes +from vorta.utils import ( + borg_compat, + get_asset, + get_private_keys, + pretty_bytes_fixed_units, +) from .repo_add_dialog import AddRepoWindow, ExistingRepoWindow from .ssh_dialog import SSHAddWindow @@ -129,21 +134,21 @@ def init_repo_stats(self): # update stats if repo.unique_csize is not None: - self.sizeCompressed.setText(pretty_bytes(repo.unique_csize)) + self.sizeCompressed.setText(pretty_bytes_fixed_units(repo.unique_csize)) self.sizeCompressed.setToolTip('') else: self.sizeCompressed.setText(na) self.sizeCompressed.setToolTip(refresh) if repo.unique_size is not None: - self.sizeDeduplicated.setText(pretty_bytes(repo.unique_size)) + self.sizeDeduplicated.setText(pretty_bytes_fixed_units(repo.unique_size)) self.sizeDeduplicated.setToolTip('') else: self.sizeDeduplicated.setText(na) self.sizeDeduplicated.setToolTip(refresh) if repo.total_size is not None: - self.sizeOriginal.setText(pretty_bytes(repo.total_size)) + self.sizeOriginal.setText(pretty_bytes_fixed_units(repo.total_size)) self.sizeOriginal.setToolTip('') else: self.sizeOriginal.setText(na) diff --git a/src/vorta/views/source_tab.py b/src/vorta/views/source_tab.py index 13a48f5e4..4a5860ac3 100644 --- a/src/vorta/views/source_tab.py +++ b/src/vorta/views/source_tab.py @@ -18,7 +18,7 @@ FilePathInfoAsync, choose_file_dialog, get_asset, - pretty_bytes, + pretty_bytes_fixed_units, sort_sizes, ) from vorta.views.utils import get_colored_icon @@ -171,7 +171,7 @@ def set_path_info(self, path, data_size, files_count): db_item.path_isdir = False self.sourceFilesWidget.item(item.row(), SourceColumn.Path).setIcon(get_colored_icon('file')) - self.sourceFilesWidget.item(item.row(), SourceColumn.Size).setText(pretty_bytes(data_size)) + self.sourceFilesWidget.item(item.row(), SourceColumn.Size).setText(pretty_bytes_fixed_units(data_size)) db_item.dir_size = data_size db_item.dir_files_count = files_count @@ -236,7 +236,9 @@ def add_source_to_table(self, source, update_data=None): else: # Use cached data from DB if source.dir_size > -1: - self.sourceFilesWidget.item(index_row, SourceColumn.Size).setText(pretty_bytes(source.dir_size)) + self.sourceFilesWidget.item(index_row, SourceColumn.Size).setText( + pretty_bytes_fixed_units(source.dir_size) + ) if source.path_isdir: self.sourceFilesWidget.item(index_row, SourceColumn.FilesCount).setText( diff --git a/tests/test_misc.py b/tests/test_misc.py index 680ac9d88..44ce9dfe6 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -34,6 +34,62 @@ def test_autostart(qapp, qtbot): assert not os.path.exists(autostart_path) +def test_enable_fixed_units(qapp, qtbot, mocker): + """ + Mocks the 'enable fixed units' setting to ensure the correct function is called when displaying the archive size. + """ + + archive_tab = qapp.main_window.archiveTab + + # set mocks + mock_setting = mocker.patch.object(vorta.views.archive_tab.SettingsModel, "get", return_value=Mock(value=True)) + mock_fixed = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes_fixed_units") + mock_dynamic = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes_dynamic_units") + + # with setting enabled, fixed units should be used and not dynamic units + archive_tab.populate_from_profile() + mock_fixed.assert_called() + mock_dynamic.assert_not_called() + + # reset mocks and disable setting + mock_setting.return_value = Mock(value=False) + mock_fixed.reset_mock() + + # with setting disabled, dynamic units should be used and not fixed units + archive_tab.populate_from_profile() + mock_dynamic.assert_called() + mock_fixed.assert_not_called() + + +def test_emit_archive_refresh(qapp, qtbot, mocker): + """ + When the 'enable fixed units' setting is changed, 'refresh_archive' in misc_tab should emit. This emit triggers + main_window to call 'archive_tab.populate_from_profile' and refresh the archive tab with new archive size units. + """ + + setting = "Display all archive sizes in a consistent unit of measurement" + + # set up mocks + mock_fixed = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes_fixed_units") + mock_dynamic = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes_dynamic_units") + + # setting is disabled by default, so this click enables the fixed units setting + # click toggle the setting, which triggers the emit that refreshes archive tab + _click_toggle_setting(setting, qapp, qtbot) + mock_fixed.assert_called() + mock_dynamic.assert_not_called() + + # reset mocks + mock_fixed.reset_mock() + mock_dynamic.reset_mock() + + # click toggle disables the fixed units setting + # emit should trigger a refresh of the archive tab to show dynamic units + _click_toggle_setting(setting, qapp, qtbot) + mock_dynamic.assert_called() + mock_fixed.assert_not_called() + + @pytest.mark.skipif(sys.platform != 'darwin', reason="Full Disk Access check only on Darwin") def test_check_full_disk_access(qapp, qtbot, mocker): """Enables/disables 'Check for Full Disk Access on startup' setting and ensures functionality""" @@ -64,23 +120,18 @@ def test_check_full_disk_access(qapp, qtbot, mocker): def _click_toggle_setting(setting, qapp, qtbot): - """Click toggle setting in the misc tab""" - - main = qapp.main_window - main.tabWidget.setCurrentIndex(4) - tab = main.miscTab - - for x in range(0, tab.checkboxLayout.count()): - item = tab.checkboxLayout.itemAt(x, QFormLayout.ItemRole.FieldRole) - if not item: - continue - checkbox = item.itemAt(0).widget() - checkbox.__class__ = QCheckBox - - if checkbox.text() == setting: - # Have to use pos to click checkbox correctly - # https://stackoverflow.com/questions/19418125/pysides-qtest-not-checking-box/24070484#24070484 - qtbot.mouseClick( - checkbox, QtCore.Qt.MouseButton.LeftButton, pos=QtCore.QPoint(2, int(checkbox.height() / 2)) - ) - break + """Toggle setting checkbox in the misc tab""" + + miscTab = qapp.main_window.miscTab + + for x in range(miscTab.checkboxLayout.count()): + item = miscTab.checkboxLayout.itemAt(x, QFormLayout.ItemRole.FieldRole) + if item is not None: + checkbox = item.itemAt(0).widget() + if checkbox.text() == setting and isinstance(checkbox, QCheckBox): + # Have to use pos to click checkbox correctly + # https://stackoverflow.com/questions/19418125/pysides-qtest-not-checking-box/24070484#24070484 + qtbot.mouseClick( + checkbox, QtCore.Qt.MouseButton.LeftButton, pos=QtCore.QPoint(2, int(checkbox.height() / 2)) + ) + break diff --git a/tests/test_utils.py b/tests/test_utils.py index 4d9c3d6f3..d1eaf2e67 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,11 @@ import uuid from vorta.keyring.abc import VortaKeyring -from vorta.utils import find_best_unit_for_sizes, pretty_bytes +from vorta.utils import ( + find_best_unit_for_sizes, + pretty_bytes_dynamic_units, + pretty_bytes_fixed_units, +) def test_keyring(): @@ -52,31 +56,51 @@ def test_best_size_unit_nonmetric2(): assert unit == 1 # 103bytes == 0.1KB -def test_pretty_bytes_metric_fixed1(): - s = pretty_bytes(1000000, metric=True, precision=0, fixed_unit=2) +def test_pretty_bytes_fixed_units_metric_fixed1(): + s = pretty_bytes_fixed_units(1000000, metric=True, precision=0, fixed_unit=2) assert s == "1 MB" -def test_pretty_bytes_metric_fixed2(): - s = pretty_bytes(1000000, metric=True, precision=1, fixed_unit=2) +def test_pretty_bytes_fixed_units_metric_fixed2(): + s = pretty_bytes_fixed_units(1000000, metric=True, precision=1, fixed_unit=2) assert s == "1.0 MB" -def test_pretty_bytes_metric_fixed3(): - s = pretty_bytes(100000, metric=True, precision=1, fixed_unit=2) +def test_pretty_bytes_fixed_units_metric_fixed3(): + s = pretty_bytes_fixed_units(100000, metric=True, precision=1, fixed_unit=2) assert s == "0.1 MB" -def test_pretty_bytes_nonmetric_fixed1(): - s = pretty_bytes(1024 * 1024, metric=False, precision=1, fixed_unit=2) +def test_pretty_bytes_fixed_units_nonmetric_fixed1(): + s = pretty_bytes_fixed_units(1024 * 1024, metric=False, precision=1, fixed_unit=2) assert s == "1.0 MiB" -def test_pretty_bytes_metric_nonfixed2(): - s = pretty_bytes(1000000, metric=True, precision=1) +def test_pretty_bytes_fixed_units_metric_nonfixed2(): + s = pretty_bytes_fixed_units(1000000, metric=True, precision=1) assert s == "1.0 MB" -def test_pretty_bytes_metric_large(): - s = pretty_bytes(10**30, metric=True, precision=1) +def test_pretty_bytes_fixed_units_metric_large(): + s = pretty_bytes_fixed_units(10**30, metric=True, precision=1) assert s == "1000000.0 YB" + + +def test_pretty_bytes_dynamic_units_metric_small(): + s = pretty_bytes_dynamic_units(1000000, metric=True, precision=1) + assert s == "1.0 MB" + + +def test_pretty_bytes_dynamic_units_metric_large(): + s = pretty_bytes_dynamic_units(10**24, metric=True, precision=1) + assert s == "1.0 YB" + + +def test_pretty_bytes_dynamic_units_nonmetric_small(): + s = pretty_bytes_dynamic_units(1024 * 1024, metric=False, precision=1) + assert s == "1.0 MiB" + + +def test_pretty_bytes_dynamic_units_nonmetric_large(): + s = pretty_bytes_dynamic_units(2**40 * 2**40, metric=False, precision=1) + assert s == "1.0 YiB" From ebb77e00541e1cc926ec77db57953c8e23bca032 Mon Sep 17 00:00:00 2001 From: bigtedde Date: Tue, 1 Aug 2023 12:10:01 -0700 Subject: [PATCH 04/12] parameterized pretty_bytes tests --- tests/test_utils.py | 136 +++++++++++++++++--------------------------- 1 file changed, 53 insertions(+), 83 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index d1eaf2e67..7b1056bdf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,6 @@ import uuid +import pytest from vorta.keyring.abc import VortaKeyring from vorta.utils import ( find_best_unit_for_sizes, @@ -17,90 +18,59 @@ def test_keyring(): assert keyring.get_password("vorta-repo", REPO) == UNICODE_PW -def test_best_size_unit_precision0(): - MB = 1000000 - sizes = [int(0.1 * MB), 100 * MB, 2000 * MB] - unit = find_best_unit_for_sizes(sizes, metric=True, precision=0) - assert unit == 1 # KB, min=100KB - - -def test_best_size_unit_precision1(): +@pytest.mark.parametrize( + "precision, expected_unit", + [ + (0, 1), # return units as "1" (represents KB), min=100KB + (1, 2), # return units as "2" (represents MB), min=0.1MB + (2, 2), # still returns KB, since 0.1MB < min=0.001 GB to allow for GB to be best_unit + ], +) +def test_best_unit_for_sizes_precision(precision, expected_unit): MB = 1000000 sizes = [int(0.1 * MB), 100 * MB, 2000 * MB] - unit = find_best_unit_for_sizes(sizes, metric=True, precision=1) - assert unit == 2 # MB, min=0.1MB - - -def test_best_size_unit_empty(): - sizes = [] - unit = find_best_unit_for_sizes(sizes, metric=True, precision=1) - assert unit == 0 # bytes - - -def test_best_size_unit_precision3(): - MB = 1000000 - sizes = [1 * MB, 100 * MB, 2000 * MB] - unit = find_best_unit_for_sizes(sizes, metric=True, precision=3) - assert unit == 3 # GB, min=0.001 GB - - -def test_best_size_unit_nonmetric1(): - sizes = [102] - unit = find_best_unit_for_sizes(sizes, metric=False, precision=1) - assert unit == 0 # 102 < 0.1KB - - -def test_best_size_unit_nonmetric2(): - sizes = [103] - unit = find_best_unit_for_sizes(sizes, metric=False, precision=1) - assert unit == 1 # 103bytes == 0.1KB - - -def test_pretty_bytes_fixed_units_metric_fixed1(): - s = pretty_bytes_fixed_units(1000000, metric=True, precision=0, fixed_unit=2) - assert s == "1 MB" - - -def test_pretty_bytes_fixed_units_metric_fixed2(): - s = pretty_bytes_fixed_units(1000000, metric=True, precision=1, fixed_unit=2) - assert s == "1.0 MB" + best_unit = find_best_unit_for_sizes(sizes, metric=True, precision=precision) + assert best_unit == expected_unit -def test_pretty_bytes_fixed_units_metric_fixed3(): - s = pretty_bytes_fixed_units(100000, metric=True, precision=1, fixed_unit=2) - assert s == "0.1 MB" - - -def test_pretty_bytes_fixed_units_nonmetric_fixed1(): - s = pretty_bytes_fixed_units(1024 * 1024, metric=False, precision=1, fixed_unit=2) - assert s == "1.0 MiB" - - -def test_pretty_bytes_fixed_units_metric_nonfixed2(): - s = pretty_bytes_fixed_units(1000000, metric=True, precision=1) - assert s == "1.0 MB" - - -def test_pretty_bytes_fixed_units_metric_large(): - s = pretty_bytes_fixed_units(10**30, metric=True, precision=1) - assert s == "1000000.0 YB" - - -def test_pretty_bytes_dynamic_units_metric_small(): - s = pretty_bytes_dynamic_units(1000000, metric=True, precision=1) - assert s == "1.0 MB" - - -def test_pretty_bytes_dynamic_units_metric_large(): - s = pretty_bytes_dynamic_units(10**24, metric=True, precision=1) - assert s == "1.0 YB" - - -def test_pretty_bytes_dynamic_units_nonmetric_small(): - s = pretty_bytes_dynamic_units(1024 * 1024, metric=False, precision=1) - assert s == "1.0 MiB" - - -def test_pretty_bytes_dynamic_units_nonmetric_large(): - s = pretty_bytes_dynamic_units(2**40 * 2**40, metric=False, precision=1) - assert s == "1.0 YiB" +@pytest.mark.parametrize( + "sizes, expected_unit", + [ + ([], 0), # no sizes given but should still return "0" (represents bytes) as best representation + ([102], 0), # non-metric size 102 < 0.1KB (102 < 0.1 * 1024), so it will return 0 instead of 1 + ([103], 1), # non-metric size 103 > 0.1KB (103 < 0.1 * 1024), so it will return 1 + ], +) +def test_best_unit_for_sizes_nonmetric(sizes, expected_unit): + best_unit = find_best_unit_for_sizes(sizes, metric=False, precision=1) + assert best_unit == expected_unit + + +@pytest.mark.parametrize( + "size, metric, precision, fixed_unit, expected_output", + [ + (10**5, True, 1, 2, "0.1 MB"), # 100KB, metric, precision 1, fixed unit "2" (MB) + (10**6, True, 0, 2, "1 MB"), # 1MB, metric, precision 0, fixed unit "2" (MB) + (10**6, True, 1, 2, "1.0 MB"), # 1MB, metric, precision 1, fixed unit "2" (MB) + (10**6, True, 1, None, "1.0 MB"), # 1MB, metric, precision 1, no fixed unit + (10**30, True, 1, None, "1000000.0 YB"), # test with large number, metric, precision 1, no fixed units + (1024 * 1024, False, 1, 2, "1.0 MiB"), # 1MiB, nonmetric, precision 1, fixed unit "2" (MiB) + ], +) +def test_pretty_bytes_fixed_units(size, metric, precision, fixed_unit, expected_output): + output = pretty_bytes_fixed_units(size, metric=metric, precision=precision, fixed_unit=fixed_unit) + assert output == expected_output + + +@pytest.mark.parametrize( + "size, metric, expected_output", + [ + (10**6, True, "1.0 MB"), # 1MB, metric + (10**24, True, "1.0 YB"), # large size (1YB), metric + (1024 * 1024, False, "1.0 MiB"), # 1MiB, nonmetric + (2**40 * 2**40, False, "1.0 YiB"), # large size (1YiB), nonmetric + ], +) +def test_pretty_bytes_dynamic_units(size, metric, expected_output): + output = pretty_bytes_dynamic_units(size, metric=metric, precision=1) + assert output == expected_output From cc148200e8e25c29d504f5468921d08b3c3e54b8 Mon Sep 17 00:00:00 2001 From: bigtedde Date: Tue, 1 Aug 2023 12:41:50 -0700 Subject: [PATCH 05/12] removed duplicate code --- src/vorta/utils.py | 24 ++++++------------------ src/vorta/views/archive_tab.py | 15 +++++---------- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/vorta/utils.py b/src/vorta/utils.py index 6aac5e349..243b92b6d 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -24,6 +24,8 @@ # Used to store whether a user wanted to override the # default directory for the --development flag DEFAULT_DIR_FLAG = object() +METRIC_UNITS = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] +NONMETRIC_UNITS = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'] borg_compat = BorgCompatibility() _network_status_monitor = None @@ -140,14 +142,10 @@ def get_network_status_monitor(): def get_path_datasize(path, exclude_patterns): file_info = QFileInfo(path) - data_size = 0 if file_info.isDir(): data_size, files_count = get_directory_size(file_info.absoluteFilePath(), exclude_patterns) - # logger.info("path (folder) %s %u elements size now=%u (%s)", - # file_info.absoluteFilePath(), files_count, data_size, pretty_bytes_fixed_units(data_size)) else: - # logger.info("path (file) %s size=%u", file_info.path(), file_info.size()) data_size = file_info.size() files_count = 1 @@ -272,15 +270,9 @@ def pretty_bytes_dynamic_units(size, metric=True, sign=False, precision=1): if not isinstance(size, int): return '' prefix = '+' if sign and size > 0 else '' - power, units = ( - (10**3, ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']) - if metric - else (2**10, ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']) - ) - n = 0 - while abs(round(size, precision)) >= power and n + 1 < len(units): - size /= power - n += 1 + power, units = (10**3, METRIC_UNITS) if metric else (2**10, NONMETRIC_UNITS) + n = find_best_unit_for_size(size, metric=metric, precision=precision) + size /= power**n try: unit = units[n] return f'{prefix}{round(size, precision)} {unit}B' @@ -300,11 +292,7 @@ def pretty_bytes_fixed_units( if not isinstance(size, int): return '' prefix = '+' if sign and size > 0 else '' - power, units = ( - (10**3, ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']) - if metric - else (2**10, ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']) - ) + power, units = (10**3, METRIC_UNITS) if metric else (2**10, NONMETRIC_UNITS) if fixed_unit is None: n = find_best_unit_for_size(size, metric=metric, precision=precision) else: diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index 2401e4649..ea72fd126 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -293,18 +293,13 @@ def populate_from_profile(self): formatted_time = archive.time.strftime('%Y-%m-%d %H:%M') self.archiveTable.setItem(row, 0, QTableWidgetItem(formatted_time)) + # format units based on user settings for 'dynamic' or 'fixed' units if SettingsModel.get(key='enable_fixed_units').value is True: - self.archiveTable.setItem( - row, - 1, - SizeItem( - pretty_bytes_fixed_units(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS) - ), - ) + size = pretty_bytes_fixed_units(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS) else: - self.archiveTable.setItem( - row, 1, SizeItem(pretty_bytes_dynamic_units(archive.size, precision=SIZE_DECIMAL_DIGITS)) - ) + size = pretty_bytes_dynamic_units(archive.size, precision=SIZE_DECIMAL_DIGITS) + + self.archiveTable.setItem(row, 1, SizeItem(size)) if archive.duration is not None: formatted_duration = str(timedelta(seconds=round(archive.duration))) From 303a4bbf84c6d55276e1fd8f4b490f42e0e83eaa Mon Sep 17 00:00:00 2001 From: bigtedde Date: Wed, 2 Aug 2023 13:55:15 -0700 Subject: [PATCH 06/12] combined pretty_bytes _fixed and _dynamic into one function --- src/vorta/borg/borg_job.py | 8 ++--- src/vorta/utils.py | 21 ++--------- src/vorta/views/archive_tab.py | 10 ++---- src/vorta/views/diff_result.py | 10 +++--- src/vorta/views/extract_dialog.py | 4 +-- src/vorta/views/repo_tab.py | 8 ++--- src/vorta/views/source_tab.py | 8 ++--- tests/test_misc.py | 60 ++++++++++++------------------- tests/test_utils.py | 16 ++++----- 9 files changed, 54 insertions(+), 91 deletions(-) diff --git a/src/vorta/borg/borg_job.py b/src/vorta/borg/borg_job.py index 4f3230c41..01948c923 100644 --- a/src/vorta/borg/borg_job.py +++ b/src/vorta/borg/borg_job.py @@ -21,7 +21,7 @@ from vorta.keyring.abc import VortaKeyring from vorta.keyring.db import VortaDBKeyring from vorta.store.models import BackupProfileMixin, EventLogModel -from vorta.utils import borg_compat, pretty_bytes_fixed_units +from vorta.utils import borg_compat, pretty_bytes keyring_lock = Lock() db_lock = Lock() @@ -293,11 +293,11 @@ def read_async(fd): msg = ( f"{translate('BorgJob','Files')}: {parsed['nfiles']}, " f"{translate('BorgJob','Original')}: " - f"{pretty_bytes_fixed_units(parsed['original_size'])}, " + f"{pretty_bytes(parsed['original_size'])}, " # f"{translate('BorgJob','Compressed')}: - # f"{pretty_bytes_fixed_units(parsed['compressed_size'])}, " + # f"{pretty_bytes(parsed['compressed_size'])}, " f"{translate('BorgJob','Deduplicated')}: " - f"{pretty_bytes_fixed_units(parsed['deduplicated_size'])}" # noqa: E501 + f"{pretty_bytes(parsed['deduplicated_size'])}" # noqa: E501 ) self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {msg}") except json.decoder.JSONDecodeError: diff --git a/src/vorta/utils.py b/src/vorta/utils.py index 243b92b6d..033b486a5 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -247,7 +247,7 @@ def clamp(n: Number, min_: Number, max_: Number) -> Number: def find_best_unit_for_sizes(sizes: Iterable[int], metric: bool = True, precision: int = 1) -> int: """ - Selects the index of the biggest unit (see the lists in the pretty_bytes_fixed_units function) capable of + Selects the index of the biggest unit (see the lists in the pretty_bytes function) capable of representing the smallest size in the sizes iterable. """ min_size = min((s for s in sizes if isinstance(s, int)), default=None) @@ -256,7 +256,7 @@ def find_best_unit_for_sizes(sizes: Iterable[int], metric: bool = True, precisio def find_best_unit_for_size(size: Optional[int], metric: bool = True, precision: int = 1) -> int: """ - Selects the index of the biggest unit (see the lists in the pretty_bytes_fixed_units function) capable of + Selects the index of the biggest unit (see the lists in the pretty_bytes function) capable of representing the passed size. """ if not isinstance(size, int) or size == 0: # this will also take care of the None case @@ -266,22 +266,7 @@ def find_best_unit_for_size(size: Optional[int], metric: bool = True, precision: return n -def pretty_bytes_dynamic_units(size, metric=True, sign=False, precision=1): - if not isinstance(size, int): - return '' - prefix = '+' if sign and size > 0 else '' - power, units = (10**3, METRIC_UNITS) if metric else (2**10, NONMETRIC_UNITS) - n = find_best_unit_for_size(size, metric=metric, precision=precision) - size /= power**n - try: - unit = units[n] - return f'{prefix}{round(size, precision)} {unit}B' - except KeyError as e: - logger.error(e) - return "NaN" - - -def pretty_bytes_fixed_units( +def pretty_bytes( size: int, metric: bool = True, sign: bool = False, precision: int = 1, fixed_unit: Optional[int] = None ) -> str: """ diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index ea72fd126..c29218c97 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -41,8 +41,7 @@ format_archive_name, get_asset, get_mount_points, - pretty_bytes_dynamic_units, - pretty_bytes_fixed_units, + pretty_bytes, ) from vorta.views import diff_result, extract_dialog from vorta.views.diff_result import DiffResultDialog, DiffTree @@ -294,11 +293,8 @@ def populate_from_profile(self): self.archiveTable.setItem(row, 0, QTableWidgetItem(formatted_time)) # format units based on user settings for 'dynamic' or 'fixed' units - if SettingsModel.get(key='enable_fixed_units').value is True: - size = pretty_bytes_fixed_units(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS) - else: - size = pretty_bytes_dynamic_units(archive.size, precision=SIZE_DECIMAL_DIGITS) - + fixed_unit = best_unit if SettingsModel.get(key='enable_fixed_units').value else None + size = pretty_bytes(archive.size, fixed_unit=fixed_unit, precision=SIZE_DECIMAL_DIGITS) self.archiveTable.setItem(row, 1, SizeItem(size)) if archive.duration is not None: diff --git a/src/vorta/views/diff_result.py b/src/vorta/views/diff_result.py index dea597cac..5d262efa5 100644 --- a/src/vorta/views/diff_result.py +++ b/src/vorta/views/diff_result.py @@ -21,7 +21,7 @@ from PyQt6.QtWidgets import QApplication, QHeaderView, QMenu, QTreeView from vorta.store.models import SettingsModel -from vorta.utils import get_asset, pretty_bytes_fixed_units, uses_dark_mode +from vorta.utils import get_asset, pretty_bytes, uses_dark_mode from vorta.views.partials.treemodel import ( FileSystemItem, FileTreeModel, @@ -827,10 +827,10 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): # change type return item.data.change_type.short() elif column == 2: - return pretty_bytes_fixed_units(item.data.changed_size) + return pretty_bytes(item.data.changed_size) else: # size - return pretty_bytes_fixed_units(item.data.size) + return pretty_bytes(item.data.size) if role == Qt.ItemDataRole.ForegroundRole: # colour @@ -884,8 +884,8 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): if item.data.modified: tooltip += '\n' tooltip += modified_template.format( - pretty_bytes_fixed_units(item.data.modified[0]), - pretty_bytes_fixed_units(item.data.modified[1]), + pretty_bytes(item.data.modified[0]), + pretty_bytes(item.data.modified[1]), ) if item.data.mode_change: diff --git a/src/vorta/views/extract_dialog.py b/src/vorta/views/extract_dialog.py index 4849ab7e9..0822d0670 100644 --- a/src/vorta/views/extract_dialog.py +++ b/src/vorta/views/extract_dialog.py @@ -27,7 +27,7 @@ ) from vorta.store.models import SettingsModel -from vorta.utils import borg_compat, get_asset, pretty_bytes_fixed_units, uses_dark_mode +from vorta.utils import borg_compat, get_asset, pretty_bytes, uses_dark_mode from vorta.views.utils import get_colored_icon from .partials.treemodel import ( @@ -483,7 +483,7 @@ def data(self, index: QModelIndex, role: Union[int, Qt.ItemDataRole] = Qt.ItemDa return QLocale.system().toString(item.data.last_modified, QLocale.FormatType.ShortFormat) elif column == 2: # size - return pretty_bytes_fixed_units(item.data.size) + return pretty_bytes(item.data.size) else: # health return diff --git a/src/vorta/views/repo_tab.py b/src/vorta/views/repo_tab.py index c7f7afad5..19b420600 100644 --- a/src/vorta/views/repo_tab.py +++ b/src/vorta/views/repo_tab.py @@ -10,7 +10,7 @@ borg_compat, get_asset, get_private_keys, - pretty_bytes_fixed_units, + pretty_bytes, ) from .repo_add_dialog import AddRepoWindow, ExistingRepoWindow @@ -136,21 +136,21 @@ def init_repo_stats(self): # update stats if repo.unique_csize is not None: - self.sizeCompressed.setText(pretty_bytes_fixed_units(repo.unique_csize)) + self.sizeCompressed.setText(pretty_bytes(repo.unique_csize)) self.sizeCompressed.setToolTip('') else: self.sizeCompressed.setText(na) self.sizeCompressed.setToolTip(refresh) if repo.unique_size is not None: - self.sizeDeduplicated.setText(pretty_bytes_fixed_units(repo.unique_size)) + self.sizeDeduplicated.setText(pretty_bytes(repo.unique_size)) self.sizeDeduplicated.setToolTip('') else: self.sizeDeduplicated.setText(na) self.sizeDeduplicated.setToolTip(refresh) if repo.total_size is not None: - self.sizeOriginal.setText(pretty_bytes_fixed_units(repo.total_size)) + self.sizeOriginal.setText(pretty_bytes(repo.total_size)) self.sizeOriginal.setToolTip('') else: self.sizeOriginal.setText(na) diff --git a/src/vorta/views/source_tab.py b/src/vorta/views/source_tab.py index 4a5860ac3..13a48f5e4 100644 --- a/src/vorta/views/source_tab.py +++ b/src/vorta/views/source_tab.py @@ -18,7 +18,7 @@ FilePathInfoAsync, choose_file_dialog, get_asset, - pretty_bytes_fixed_units, + pretty_bytes, sort_sizes, ) from vorta.views.utils import get_colored_icon @@ -171,7 +171,7 @@ def set_path_info(self, path, data_size, files_count): db_item.path_isdir = False self.sourceFilesWidget.item(item.row(), SourceColumn.Path).setIcon(get_colored_icon('file')) - self.sourceFilesWidget.item(item.row(), SourceColumn.Size).setText(pretty_bytes_fixed_units(data_size)) + self.sourceFilesWidget.item(item.row(), SourceColumn.Size).setText(pretty_bytes(data_size)) db_item.dir_size = data_size db_item.dir_files_count = files_count @@ -236,9 +236,7 @@ def add_source_to_table(self, source, update_data=None): else: # Use cached data from DB if source.dir_size > -1: - self.sourceFilesWidget.item(index_row, SourceColumn.Size).setText( - pretty_bytes_fixed_units(source.dir_size) - ) + self.sourceFilesWidget.item(index_row, SourceColumn.Size).setText(pretty_bytes(source.dir_size)) if source.path_isdir: self.sourceFilesWidget.item(index_row, SourceColumn.FilesCount).setText( diff --git a/tests/test_misc.py b/tests/test_misc.py index 44ce9dfe6..63fde354a 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -35,59 +35,43 @@ def test_autostart(qapp, qtbot): def test_enable_fixed_units(qapp, qtbot, mocker): - """ - Mocks the 'enable fixed units' setting to ensure the correct function is called when displaying the archive size. - """ + """Tests the 'enable fixed units' setting to ensure the archive tab sizes are displayed correctly.""" - archive_tab = qapp.main_window.archiveTab + tab = qapp.main_window.archiveTab # set mocks mock_setting = mocker.patch.object(vorta.views.archive_tab.SettingsModel, "get", return_value=Mock(value=True)) - mock_fixed = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes_fixed_units") - mock_dynamic = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes_dynamic_units") + mock_pretty_bytes = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes") - # with setting enabled, fixed units should be used and not dynamic units - archive_tab.populate_from_profile() - mock_fixed.assert_called() - mock_dynamic.assert_not_called() + # with setting enabled, fixed units should be decided and passed to pretty_bytes as an 'int' + tab.populate_from_profile() + mock_pretty_bytes.assert_called() + kwargs_list = mock_pretty_bytes.call_args_list[0].kwargs + assert 'fixed_unit' in kwargs_list + assert isinstance(kwargs_list['fixed_unit'], int) - # reset mocks and disable setting + # disable setting and reset mock mock_setting.return_value = Mock(value=False) - mock_fixed.reset_mock() + mock_pretty_bytes.reset_mock() - # with setting disabled, dynamic units should be used and not fixed units - archive_tab.populate_from_profile() - mock_dynamic.assert_called() - mock_fixed.assert_not_called() + # with setting disabled, pretty_bytes should be called with fixed units set to 'None' + tab.populate_from_profile() + mock_pretty_bytes.assert_called() + kwargs_list = mock_pretty_bytes.call_args_list[0].kwargs + assert 'fixed_unit' in kwargs_list + assert kwargs_list['fixed_unit'] is None def test_emit_archive_refresh(qapp, qtbot, mocker): - """ - When the 'enable fixed units' setting is changed, 'refresh_archive' in misc_tab should emit. This emit triggers - main_window to call 'archive_tab.populate_from_profile' and refresh the archive tab with new archive size units. - """ + """Test that an emit occurs when `enable fixed units` setting is changed""" setting = "Display all archive sizes in a consistent unit of measurement" + mock_pretty_bytes = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes") - # set up mocks - mock_fixed = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes_fixed_units") - mock_dynamic = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes_dynamic_units") - - # setting is disabled by default, so this click enables the fixed units setting - # click toggle the setting, which triggers the emit that refreshes archive tab - _click_toggle_setting(setting, qapp, qtbot) - mock_fixed.assert_called() - mock_dynamic.assert_not_called() - - # reset mocks - mock_fixed.reset_mock() - mock_dynamic.reset_mock() - - # click toggle disables the fixed units setting - # emit should trigger a refresh of the archive tab to show dynamic units + # click the setting, see that 'pretty_bytes' is called to recalculate the archive unit size + mock_pretty_bytes.reset_mock() _click_toggle_setting(setting, qapp, qtbot) - mock_dynamic.assert_called() - mock_fixed.assert_not_called() + mock_pretty_bytes.assert_called() @pytest.mark.skipif(sys.platform != 'darwin', reason="Full Disk Access check only on Darwin") diff --git a/tests/test_utils.py b/tests/test_utils.py index 7b1056bdf..4ac9f4004 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,8 +4,7 @@ from vorta.keyring.abc import VortaKeyring from vorta.utils import ( find_best_unit_for_sizes, - pretty_bytes_dynamic_units, - pretty_bytes_fixed_units, + pretty_bytes, ) @@ -52,13 +51,12 @@ def test_best_unit_for_sizes_nonmetric(sizes, expected_unit): (10**5, True, 1, 2, "0.1 MB"), # 100KB, metric, precision 1, fixed unit "2" (MB) (10**6, True, 0, 2, "1 MB"), # 1MB, metric, precision 0, fixed unit "2" (MB) (10**6, True, 1, 2, "1.0 MB"), # 1MB, metric, precision 1, fixed unit "2" (MB) - (10**6, True, 1, None, "1.0 MB"), # 1MB, metric, precision 1, no fixed unit - (10**30, True, 1, None, "1000000.0 YB"), # test with large number, metric, precision 1, no fixed units (1024 * 1024, False, 1, 2, "1.0 MiB"), # 1MiB, nonmetric, precision 1, fixed unit "2" (MiB) ], ) def test_pretty_bytes_fixed_units(size, metric, precision, fixed_unit, expected_output): - output = pretty_bytes_fixed_units(size, metric=metric, precision=precision, fixed_unit=fixed_unit) + # test pretty bytes when specifying a fixed unit of measurement + output = pretty_bytes(size, metric=metric, precision=precision, fixed_unit=fixed_unit) assert output == expected_output @@ -66,11 +64,13 @@ def test_pretty_bytes_fixed_units(size, metric, precision, fixed_unit, expected_ "size, metric, expected_output", [ (10**6, True, "1.0 MB"), # 1MB, metric - (10**24, True, "1.0 YB"), # large size (1YB), metric + (10**24, True, "1.0 YB"), # 1YB, metric + (10**30, True, "1000000.0 YB"), # test huge number, metric (1024 * 1024, False, "1.0 MiB"), # 1MiB, nonmetric - (2**40 * 2**40, False, "1.0 YiB"), # large size (1YiB), nonmetric + (2**40 * 2**40, False, "1.0 YiB"), # 1YiB, nonmetric ], ) def test_pretty_bytes_dynamic_units(size, metric, expected_output): - output = pretty_bytes_dynamic_units(size, metric=metric, precision=1) + # test pretty bytes when NOT specifying a fixed unit of measurement + output = pretty_bytes(size, metric=metric, precision=1) assert output == expected_output From fb04a67a8115671c3ce8cb0aeac5a74ebe7a10ca Mon Sep 17 00:00:00 2001 From: bigtedde Date: Wed, 2 Aug 2023 14:00:33 -0700 Subject: [PATCH 07/12] reverted pretty_bytes calls in 2 files --- src/vorta/borg/borg_job.py | 9 +++------ src/vorta/views/repo_tab.py | 7 +------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/vorta/borg/borg_job.py b/src/vorta/borg/borg_job.py index 01948c923..9cf6a1551 100644 --- a/src/vorta/borg/borg_job.py +++ b/src/vorta/borg/borg_job.py @@ -292,12 +292,9 @@ def read_async(fd): elif parsed['type'] == 'archive_progress' and not parsed.get('finished', False): msg = ( f"{translate('BorgJob','Files')}: {parsed['nfiles']}, " - f"{translate('BorgJob','Original')}: " - f"{pretty_bytes(parsed['original_size'])}, " - # f"{translate('BorgJob','Compressed')}: - # f"{pretty_bytes(parsed['compressed_size'])}, " - f"{translate('BorgJob','Deduplicated')}: " - f"{pretty_bytes(parsed['deduplicated_size'])}" # noqa: E501 + f"{translate('BorgJob','Original')}: {pretty_bytes(parsed['original_size'])}, " + # f"{translate('BorgJob','Compressed')}: {pretty_bytes(parsed['compressed_size'])}, " + f"{translate('BorgJob','Deduplicated')}: {pretty_bytes(parsed['deduplicated_size'])}" # noqa: E501 ) self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {msg}") except json.decoder.JSONDecodeError: diff --git a/src/vorta/views/repo_tab.py b/src/vorta/views/repo_tab.py index 19b420600..96b614b02 100644 --- a/src/vorta/views/repo_tab.py +++ b/src/vorta/views/repo_tab.py @@ -6,12 +6,7 @@ from PyQt6.QtWidgets import QApplication, QLayout, QMenu, QMessageBox from vorta.store.models import ArchiveModel, BackupProfileMixin, RepoModel -from vorta.utils import ( - borg_compat, - get_asset, - get_private_keys, - pretty_bytes, -) +from vorta.utils import borg_compat, get_asset, get_private_keys, pretty_bytes from .repo_add_dialog import AddRepoWindow, ExistingRepoWindow from .ssh_dialog import SSHAddWindow From 6f17d337bde0de29a5a89e21c2de2f84135f0ff5 Mon Sep 17 00:00:00 2001 From: bigtedde Date: Wed, 2 Aug 2023 14:04:55 -0700 Subject: [PATCH 08/12] reworded 'dynamic' to 'nonfixed' --- tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4ac9f4004..db575216a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -70,7 +70,7 @@ def test_pretty_bytes_fixed_units(size, metric, precision, fixed_unit, expected_ (2**40 * 2**40, False, "1.0 YiB"), # 1YiB, nonmetric ], ) -def test_pretty_bytes_dynamic_units(size, metric, expected_output): +def test_pretty_bytes_nonfixed_units(size, metric, expected_output): # test pretty bytes when NOT specifying a fixed unit of measurement output = pretty_bytes(size, metric=metric, precision=1) assert output == expected_output From 1179cff50165a55301d6098d3d1c9d8c0d1c6f6c Mon Sep 17 00:00:00 2001 From: bigtedde Date: Wed, 9 Aug 2023 12:59:00 -0700 Subject: [PATCH 09/12] streamlined setting and testing --- src/vorta/views/misc_tab.py | 7 ++----- tests/unit/test_misc.py | 18 +++++------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/vorta/views/misc_tab.py b/src/vorta/views/misc_tab.py index 07be5e451..83c47be64 100644 --- a/src/vorta/views/misc_tab.py +++ b/src/vorta/views/misc_tab.py @@ -103,7 +103,8 @@ def populate(self): cb.setCheckState(Qt.CheckState(setting.value)) cb.setTristate(False) cb.stateChanged.connect(lambda v, key=setting.key: self.save_setting(key, v)) - cb.stateChanged.connect(lambda v, key=setting.key: self.emit_archive_refresh(key)) + if setting.key == 'enable_fixed_units': + cb.stateChanged.connect(self.refresh_archive.emit) tb = ToolTipButton() tb.setToolTip(setting.tooltip) @@ -128,10 +129,6 @@ def set_icons(self): for button in self.tooltip_buttons: button.setIcon(get_colored_icon('help-about')) - def emit_archive_refresh(self, key): - if key == 'enable_fixed_units': - self.refresh_archive.emit() - def save_setting(self, key, new_value): setting = SettingsModel.get(key=key) setting.value = bool(new_value) diff --git a/tests/unit/test_misc.py b/tests/unit/test_misc.py index 63fde354a..69d5e890c 100644 --- a/tests/unit/test_misc.py +++ b/tests/unit/test_misc.py @@ -36,14 +36,14 @@ def test_autostart(qapp, qtbot): def test_enable_fixed_units(qapp, qtbot, mocker): """Tests the 'enable fixed units' setting to ensure the archive tab sizes are displayed correctly.""" - tab = qapp.main_window.archiveTab + setting = "Display all archive sizes in a consistent unit of measurement" # set mocks mock_setting = mocker.patch.object(vorta.views.archive_tab.SettingsModel, "get", return_value=Mock(value=True)) mock_pretty_bytes = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes") - # with setting enabled, fixed units should be decided and passed to pretty_bytes as an 'int' + # with setting enabled, fixed units should be determined and passed to pretty_bytes as an 'int' tab.populate_from_profile() mock_pretty_bytes.assert_called() kwargs_list = mock_pretty_bytes.call_args_list[0].kwargs @@ -61,17 +61,9 @@ def test_enable_fixed_units(qapp, qtbot, mocker): assert 'fixed_unit' in kwargs_list assert kwargs_list['fixed_unit'] is None - -def test_emit_archive_refresh(qapp, qtbot, mocker): - """Test that an emit occurs when `enable fixed units` setting is changed""" - - setting = "Display all archive sizes in a consistent unit of measurement" - mock_pretty_bytes = mocker.patch.object(vorta.views.archive_tab, "pretty_bytes") - - # click the setting, see that 'pretty_bytes' is called to recalculate the archive unit size - mock_pretty_bytes.reset_mock() - _click_toggle_setting(setting, qapp, qtbot) - mock_pretty_bytes.assert_called() + # use the qt bot to click the setting and see that the refresh_archive emit works as intended. + with qtbot.waitSignal(qapp.main_window.miscTab.refresh_archive, timeout=5000): + _click_toggle_setting(setting, qapp, qtbot) @pytest.mark.skipif(sys.platform != 'darwin', reason="Full Disk Access check only on Darwin") From 3386cd955c232d30516bf480c1b3c7fab42efe09 Mon Sep 17 00:00:00 2001 From: bigtedde Date: Wed, 9 Aug 2023 13:18:14 -0700 Subject: [PATCH 10/12] minor changes to _click_toggle_setting --- tests/unit/test_misc.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_misc.py b/tests/unit/test_misc.py index 69d5e890c..7b0625adb 100644 --- a/tests/unit/test_misc.py +++ b/tests/unit/test_misc.py @@ -11,7 +11,6 @@ def test_autostart(qapp, qtbot): """Check if file exists only on Linux, otherwise just check it doesn't crash""" - setting = "Automatically start Vorta at login" _click_toggle_setting(setting, qapp, qtbot) @@ -69,7 +68,6 @@ def test_enable_fixed_units(qapp, qtbot, mocker): @pytest.mark.skipif(sys.platform != 'darwin', reason="Full Disk Access check only on Darwin") def test_check_full_disk_access(qapp, qtbot, mocker): """Enables/disables 'Check for Full Disk Access on startup' setting and ensures functionality""" - setting = "Check for Full Disk Access on startup" # Set mocks for setting enabled @@ -97,7 +95,6 @@ def test_check_full_disk_access(qapp, qtbot, mocker): def _click_toggle_setting(setting, qapp, qtbot): """Toggle setting checkbox in the misc tab""" - miscTab = qapp.main_window.miscTab for x in range(miscTab.checkboxLayout.count()): @@ -107,7 +104,6 @@ def _click_toggle_setting(setting, qapp, qtbot): if checkbox.text() == setting and isinstance(checkbox, QCheckBox): # Have to use pos to click checkbox correctly # https://stackoverflow.com/questions/19418125/pysides-qtest-not-checking-box/24070484#24070484 - qtbot.mouseClick( - checkbox, QtCore.Qt.MouseButton.LeftButton, pos=QtCore.QPoint(2, int(checkbox.height() / 2)) - ) + pos = QtCore.QPoint(2, int(checkbox.height() / 2)) + qtbot.mouseClick(checkbox, QtCore.Qt.MouseButton.LeftButton, pos=pos) break From 4733c851523735c56e7e86257bc2b1bce1433f2d Mon Sep 17 00:00:00 2001 From: bigtedde Date: Thu, 10 Aug 2023 10:24:32 -0700 Subject: [PATCH 11/12] updated verbiage for setting/tooltip --- src/vorta/store/settings.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vorta/store/settings.py b/src/vorta/store/settings.py index 194c01425..40324e9b7 100644 --- a/src/vorta/store/settings.py +++ b/src/vorta/store/settings.py @@ -64,8 +64,12 @@ def get_misc_settings() -> List[Dict[str, str]]: 'value': False, 'type': 'checkbox', 'group': information, - 'label': trans_late('settings', 'Display all archive sizes in a consistent unit of measurement'), - 'tooltip': trans_late('settings', 'Enable to replace dynamic units of measurement based on archive size'), + 'label': trans_late('settings', 'Use the same unit of measurement for archive sizes'), + 'tooltip': trans_late( + 'settings', + 'When enabled, all archive sizes will use the same unit of measurement, ' + 'such as KB or MB. This can make archive sizes easier to compare.', + ), }, { 'key': 'use_system_keyring', From c4a58cb40e1f73946ad14548dc6c3f148b768e76 Mon Sep 17 00:00:00 2001 From: bigtedde Date: Thu, 10 Aug 2023 12:00:08 -0700 Subject: [PATCH 12/12] test fix to match new setting label --- tests/unit/test_misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_misc.py b/tests/unit/test_misc.py index 7b0625adb..eb1a5ac1f 100644 --- a/tests/unit/test_misc.py +++ b/tests/unit/test_misc.py @@ -36,7 +36,7 @@ def test_autostart(qapp, qtbot): def test_enable_fixed_units(qapp, qtbot, mocker): """Tests the 'enable fixed units' setting to ensure the archive tab sizes are displayed correctly.""" tab = qapp.main_window.archiveTab - setting = "Display all archive sizes in a consistent unit of measurement" + setting = "Use the same unit of measurement for archive sizes" # set mocks mock_setting = mocker.patch.object(vorta.views.archive_tab.SettingsModel, "get", return_value=Mock(value=True))