Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve size column readability in archives tab #1598

Merged
merged 18 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions src/vorta/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import errno
import fnmatch
import getpass
import math
import os
import platform
import re
import sys
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, Tuple, TypeVar
import psutil
from paramiko import SSHException
from paramiko.ecdsakey import ECDSAKey
Expand Down Expand Up @@ -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
Expand All @@ -240,7 +241,41 @@ def sort_sizes(size_list):
return final_list


def pretty_bytes(size, metric=True, sign=False, precision=1):
T = TypeVar("T")


def clamp(n: T, min_: T, max_: T) -> T:
"""Restrict the number n inside a range"""
return min(max_, max(n, min_))
hariseldon78 marked this conversation as resolved.
Show resolved Hide resolved


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: int, metric: bool = True, precision: int = 1) -> int:
hariseldon78 marked this conversation as resolved.
Show resolved Hide resolved
"""
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
real-yfprojects marked this conversation as resolved.
Show resolved Hide resolved


def pretty_bytes(size: int, metric: bool = True, sign: bool = False, precision: int = 1, fixed_unit: int = None) -> str:
hariseldon78 marked this conversation as resolved.
Show resolved Hide resolved
"""
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 ''
Expand All @@ -249,15 +284,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]
hariseldon78 marked this conversation as resolved.
Show resolved Hide resolved
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"


Expand Down
16 changes: 14 additions & 2 deletions src/vorta/views/archive_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,6 +51,8 @@

logger = logging.getLogger(__name__)

PRECISION = 1
hariseldon78 marked this conversation as resolved.
Show resolved Hide resolved


class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
prune_intervals = ['hour', 'day', 'week', 'month', 'year']
Expand Down Expand Up @@ -243,12 +252,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=PRECISION)
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=PRECISION))
)
if archive.duration is not None:
formatted_duration = str(timedelta(seconds=round(archive.duration)))
else:
Expand Down
4 changes: 4 additions & 0 deletions src/vorta/views/source_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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"