mirror of
https://github.com/borgbase/vorta
synced 2024-12-21 23:33:13 +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:
parent
a048dad136
commit
bcc126b634
4 changed files with 140 additions and 12 deletions
|
@ -2,6 +2,7 @@
|
|||
import errno
|
||||
import fnmatch
|
||||
import getpass
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
|
@ -9,7 +10,7 @@
|
|||
import unicodedata
|
||||
from datetime import datetime as dt
|
||||
from functools import reduce
|
||||
from typing import Any, Callable, Iterable, Tuple
|
||||
from typing import Any, Callable, Iterable, Optional, Tuple, TypeVar
|
||||
import psutil
|
||||
from paramiko import SSHException
|
||||
from paramiko.ecdsakey import ECDSAKey
|
||||
|
@ -226,7 +227,7 @@ def get_private_keys():
|
|||
def sort_sizes(size_list):
|
||||
"""Sorts sizes with extensions. Assumes that size is already in largest unit possible"""
|
||||
final_list = []
|
||||
for suffix in [" B", " KB", " MB", " GB", " TB"]:
|
||||
for suffix in [" B", " KB", " MB", " GB", " TB", " PB", " EB", " ZB", " YB"]:
|
||||
sub_list = [
|
||||
float(size[: -len(suffix)])
|
||||
for size in size_list
|
||||
|
@ -240,7 +241,43 @@ def sort_sizes(size_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):
|
||||
return ''
|
||||
prefix = '+' if sign and size > 0 else ''
|
||||
|
@ -249,15 +286,18 @@ def pretty_bytes(size, metric=True, sign=False, precision=1):
|
|||
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
|
||||
if fixed_unit is None:
|
||||
n = find_best_unit_for_size(size, metric=metric, precision=precision)
|
||||
else:
|
||||
n = fixed_unit
|
||||
n = clamp(n, 0, len(units) - 1)
|
||||
size /= power**n
|
||||
try:
|
||||
unit = units[n]
|
||||
return f'{prefix}{round(size, precision)} {unit}B'
|
||||
except KeyError as e:
|
||||
logger.error(e)
|
||||
digits = f'%.{precision}f' % (round(size, precision))
|
||||
return f'{prefix}{digits} {unit}B'
|
||||
except KeyError as error:
|
||||
logger.error(error)
|
||||
return "NaN"
|
||||
|
||||
|
||||
|
|
|
@ -32,7 +32,14 @@
|
|||
from vorta.borg.umount import BorgUmountJob
|
||||
from vorta.i18n import translate
|
||||
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.diff_result import DiffResultDialog, DiffTree
|
||||
from vorta.views.extract_dialog import ExtractDialog, ExtractTree
|
||||
|
@ -45,6 +52,10 @@
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#: The number of decimal digits to show in the size column
|
||||
SIZE_DECIMAL_DIGITS = 1
|
||||
|
||||
|
||||
class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
|
||||
prune_intervals = ['hour', 'day', 'week', 'month', 'year']
|
||||
|
||||
|
@ -243,12 +254,15 @@ def populate_from_profile(self):
|
|||
|
||||
sorting = self.archiveTable.isSortingEnabled()
|
||||
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):
|
||||
self.archiveTable.insertRow(row)
|
||||
|
||||
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)))
|
||||
self.archiveTable.setItem(
|
||||
row, 1, SizeItem(pretty_bytes(archive.size, fixed_unit=best_unit, precision=SIZE_DECIMAL_DIGITS))
|
||||
)
|
||||
if archive.duration is not None:
|
||||
formatted_duration = str(timedelta(seconds=round(archive.duration)))
|
||||
else:
|
||||
|
|
|
@ -21,6 +21,10 @@ class SourceColumn:
|
|||
|
||||
|
||||
class SizeItem(QTableWidgetItem):
|
||||
def __init__(self, s):
|
||||
super().__init__(s)
|
||||
self.setTextAlignment(Qt.AlignVCenter + Qt.AlignRight)
|
||||
|
||||
def __lt__(self, other):
|
||||
if other.text() == '':
|
||||
return False
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import uuid
|
||||
from vorta.keyring.abc import VortaKeyring
|
||||
from vorta.utils import find_best_unit_for_sizes, pretty_bytes
|
||||
|
||||
|
||||
def test_keyring():
|
||||
|
@ -9,3 +10,72 @@ def test_keyring():
|
|||
keyring = VortaKeyring.get_keyring()
|
||||
keyring.set_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"
|
||||
|
|
Loading…
Reference in a new issue