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:
parent
a048dad136
commit
bcc126b634
4 changed files with 140 additions and 12 deletions
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue