1
0
Fork 0
mirror of https://github.com/borgbase/vorta synced 2024-12-22 07:43:09 +00:00

Improve size column readability in archives tab (#1598)

Use one size unit for all archives. The unit is selected by a simple algorithm that picks the largest unit that can represent the smallest size with a given precision. Align all sizes in archive and source tab to the left.

* src/vorta/utils.py : Implement `find_best_size_formatting`. Add missing sizes to `sort_sizes`. Simplify `pretty_bytes` and add `fixed_unit` option for use with `find_best_size_formatting`. Implement `clamp` utility function. Add type hints and docstrings.

* src/vorta/views/archive_tab.py (ArchiveTab.populate_from_profile): Use `find_best_sizes_formatting`.

* src/vorta/views/source_tab.py (SizeItem.__init__): Set alignment to left.

* tests/test_utils.py : Add comprehensive tests for `pretty_bytes` and `find_best_sizes_formatting`.

Co-authored-by: yfprojects <62463991+real-yfprojects@users.noreply.github.com>
This commit is contained in:
Roberto Previdi 2023-02-21 22:01:49 +01:00 committed by GitHub
parent a048dad136
commit bcc126b634
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 140 additions and 12 deletions

View file

@ -2,6 +2,7 @@
import errno import errno
import fnmatch import fnmatch
import getpass import getpass
import math
import os import os
import platform import platform
import re import re
@ -9,7 +10,7 @@
import unicodedata import unicodedata
from datetime import datetime as dt from datetime import datetime as dt
from functools import reduce from functools import reduce
from typing import Any, Callable, Iterable, Tuple from typing import Any, Callable, Iterable, Optional, Tuple, TypeVar
import psutil import psutil
from paramiko import SSHException from paramiko import SSHException
from paramiko.ecdsakey import ECDSAKey from paramiko.ecdsakey import ECDSAKey
@ -226,7 +227,7 @@ def get_private_keys():
def sort_sizes(size_list): def sort_sizes(size_list):
"""Sorts sizes with extensions. Assumes that size is already in largest unit possible""" """Sorts sizes with extensions. Assumes that size is already in largest unit possible"""
final_list = [] final_list = []
for suffix in [" B", " KB", " MB", " GB", " TB"]: for suffix in [" B", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB"]:
sub_list = [ sub_list = [
float(size[: -len(suffix)]) float(size[: -len(suffix)])
for size in size_list for size in size_list
@ -240,7 +241,43 @@ def sort_sizes(size_list):
return final_list return final_list
def pretty_bytes(size, metric=True, sign=False, precision=1): Number = TypeVar("Number", int, float)
def clamp(n: Number, min_: Number, max_: Number) -> Number:
"""Restrict the number n inside a range"""
return min(max_, max(n, min_))
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
representing the smallest size in the sizes iterable.
"""
min_size = min((s for s in sizes if isinstance(s, int)), default=None)
return find_best_unit_for_size(min_size, metric=metric, precision=precision)
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
representing the passed size.
"""
if not isinstance(size, int) or size == 0: # this will also take care of the None case
return 0
power = 10**3 if metric else 2**10
n = math.floor(math.log(size * 10**precision, power))
return n
def pretty_bytes(
size: int, metric: bool = True, sign: bool = False, precision: int = 1, fixed_unit: Optional[int] = None
) -> str:
"""
Formats the size with the requested unit and precision. The find_best_size_unit function
can be used to find the correct unit for a list of sizes. If no fixed_unit is passed it will
find the biggest unit to represent the size
"""
if not isinstance(size, int): if not isinstance(size, int):
return '' return ''
prefix = '+' if sign and size > 0 else '' prefix = '+' if sign and size > 0 else ''
@ -249,15 +286,18 @@ def pretty_bytes(size, metric=True, sign=False, precision=1):
if metric if metric
else (2**10, ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']) else (2**10, ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'])
) )
n = 0 if fixed_unit is None:
while abs(round(size, precision)) >= power and n + 1 < len(units): n = find_best_unit_for_size(size, metric=metric, precision=precision)
size /= power else:
n += 1 n = fixed_unit
n = clamp(n, 0, len(units) - 1)
size /= power**n
try: try:
unit = units[n] unit = units[n]
return f'{prefix}{round(size, precision)} {unit}B' digits = f'%.{precision}f' % (round(size, precision))
except KeyError as e: return f'{prefix}{digits} {unit}B'
logger.error(e) except KeyError as error:
logger.error(error)
return "NaN" return "NaN"

View file

@ -32,7 +32,14 @@
from vorta.borg.umount import BorgUmountJob from vorta.borg.umount import BorgUmountJob
from vorta.i18n import translate from vorta.i18n import translate
from vorta.store.models import ArchiveModel, BackupProfileMixin from vorta.store.models import ArchiveModel, BackupProfileMixin
from vorta.utils import choose_file_dialog, format_archive_name, get_asset, get_mount_points, pretty_bytes from vorta.utils import (
choose_file_dialog,
find_best_unit_for_sizes,
format_archive_name,
get_asset,
get_mount_points,
pretty_bytes,
)
from vorta.views import diff_result, extract_dialog from vorta.views import diff_result, extract_dialog
from vorta.views.diff_result import DiffResultDialog, DiffTree from vorta.views.diff_result import DiffResultDialog, DiffTree
from vorta.views.extract_dialog import ExtractDialog, ExtractTree from vorta.views.extract_dialog import ExtractDialog, ExtractTree
@ -45,6 +52,10 @@
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#: The number of decimal digits to show in the size column
SIZE_DECIMAL_DIGITS = 1
class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
prune_intervals = ['hour', 'day', 'week', 'month', 'year'] prune_intervals = ['hour', 'day', 'week', 'month', 'year']
@ -243,12 +254,15 @@ def populate_from_profile(self):
sorting = self.archiveTable.isSortingEnabled() sorting = self.archiveTable.isSortingEnabled()
self.archiveTable.setSortingEnabled(False) self.archiveTable.setSortingEnabled(False)
best_unit = find_best_unit_for_sizes((a.size for a in archives), precision=SIZE_DECIMAL_DIGITS)
for row, archive in enumerate(archives): for row, archive in enumerate(archives):
self.archiveTable.insertRow(row) self.archiveTable.insertRow(row)
formatted_time = archive.time.strftime('%Y-%m-%d %H:%M') formatted_time = archive.time.strftime('%Y-%m-%d %H:%M')
self.archiveTable.setItem(row, 0, QTableWidgetItem(formatted_time)) self.archiveTable.setItem(row, 0, QTableWidgetItem(formatted_time))
self.archiveTable.setItem(row, 1, SizeItem(pretty_bytes(archive.size))) self.archiveTable.setItem(
row, 1, SizeItem(pretty_bytes(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS))
)
if archive.duration is not None: if archive.duration is not None:
formatted_duration = str(timedelta(seconds=round(archive.duration))) formatted_duration = str(timedelta(seconds=round(archive.duration)))
else: else:

View file

@ -21,6 +21,10 @@ class SourceColumn:
class SizeItem(QTableWidgetItem): class SizeItem(QTableWidgetItem):
def __init__(self, s):
super().__init__(s)
self.setTextAlignment(Qt.AlignVCenter + Qt.AlignRight)
def __lt__(self, other): def __lt__(self, other):
if other.text() == '': if other.text() == '':
return False return False

View file

@ -1,5 +1,6 @@
import uuid import uuid
from vorta.keyring.abc import VortaKeyring from vorta.keyring.abc import VortaKeyring
from vorta.utils import find_best_unit_for_sizes, pretty_bytes
def test_keyring(): def test_keyring():
@ -9,3 +10,72 @@ def test_keyring():
keyring = VortaKeyring.get_keyring() keyring = VortaKeyring.get_keyring()
keyring.set_password('vorta-repo', REPO, UNICODE_PW) keyring.set_password('vorta-repo', REPO, UNICODE_PW)
assert keyring.get_password("vorta-repo", REPO) == UNICODE_PW 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():
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_metric_fixed1():
s = pretty_bytes(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)
assert s == "1.0 MB"
def test_pretty_bytes_metric_fixed3():
s = pretty_bytes(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)
assert s == "1.0 MiB"
def test_pretty_bytes_metric_nonfixed2():
s = pretty_bytes(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)
assert s == "1000000.0 YB"