Move human formatting functions under beets.util.units

This commit is contained in:
Šarūnas Nejus 2025-07-05 20:46:27 +01:00
parent 8937978d5f
commit 9d088ab69f
No known key found for this signature in database
GPG key ID: DD28F6704DBE3435
7 changed files with 117 additions and 110 deletions

View file

@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
import beets
from beets import util
from beets.util.units import human_seconds_short, raw_seconds_short
from . import query
@ -437,14 +438,14 @@ class DurationType(Float):
def format(self, value):
if not beets.config["format_raw_length"].get(bool):
return util.human_seconds_short(value or 0.0)
return human_seconds_short(value or 0.0)
else:
return value
def parse(self, string):
try:
# Try to format back hh:ss to seconds.
return util.raw_seconds_short(string)
return raw_seconds_short(string)
except ValueError:
# Fall back to a plain float.
try:

View file

@ -435,48 +435,6 @@ def input_select_objects(prompt, objs, rep, prompt_all=None):
return []
# Human output formatting.
def human_bytes(size):
"""Formats size, a number of bytes, in a human-readable way."""
powers = ["", "K", "M", "G", "T", "P", "E", "Z", "Y", "H"]
unit = "B"
for power in powers:
if size < 1024:
return f"{size:3.1f} {power}{unit}"
size /= 1024.0
unit = "iB"
return "big"
def human_seconds(interval):
"""Formats interval, a number of seconds, as a human-readable time
interval using English words.
"""
units = [
(1, "second"),
(60, "minute"),
(60, "hour"),
(24, "day"),
(7, "week"),
(52, "year"),
(10, "decade"),
]
for i in range(len(units) - 1):
increment, suffix = units[i]
next_increment, _ = units[i + 1]
interval /= float(increment)
if interval < next_increment:
break
else:
# Last unit.
increment, suffix = units[-1]
interval /= float(increment)
return f"{interval:3.1f} {suffix}s"
# Colorization.
# ANSI terminal colorization code heavily inspired by pygments:

View file

@ -43,6 +43,7 @@ from beets.util import (
normpath,
syspath,
)
from beets.util.units import human_bytes, human_seconds, human_seconds_short
from . import _store_dict
@ -541,8 +542,8 @@ class ChangeRepresentation:
cur_length0 = item.length if item.length else 0
new_length0 = track_info.length if track_info.length else 0
# format into string
cur_length = f"({util.human_seconds_short(cur_length0)})"
new_length = f"({util.human_seconds_short(new_length0)})"
cur_length = f"({human_seconds_short(cur_length0)})"
new_length = f"({human_seconds_short(new_length0)})"
# colorize
lhs_length = ui.colorize(highlight_color, cur_length)
rhs_length = ui.colorize(highlight_color, new_length)
@ -706,14 +707,14 @@ class AlbumChange(ChangeRepresentation):
for track_info in self.match.extra_tracks:
line = f" ! {track_info.title} (#{self.format_index(track_info)})"
if track_info.length:
line += f" ({util.human_seconds_short(track_info.length)})"
line += f" ({human_seconds_short(track_info.length)})"
print_(ui.colorize("text_warning", line))
if self.match.extra_items:
print_(f"Unmatched tracks ({len(self.match.extra_items)}):")
for item in self.match.extra_items:
line = " ! {} (#{})".format(item.title, self.format_index(item))
if item.length:
line += " ({})".format(util.human_seconds_short(item.length))
line += " ({})".format(human_seconds_short(item.length))
print_(ui.colorize("text_warning", line))
@ -795,8 +796,8 @@ def summarize_items(items, singleton):
round(int(items[0].samplerate) / 1000, 1), items[0].bitdepth
)
summary_parts.append(sample_bits)
summary_parts.append(util.human_seconds_short(total_duration))
summary_parts.append(ui.human_bytes(total_filesize))
summary_parts.append(human_seconds_short(total_duration))
summary_parts.append(human_bytes(total_filesize))
return ", ".join(summary_parts)
@ -1906,7 +1907,7 @@ def show_stats(lib, query, exact):
if item.album_id:
albums.add(item.album_id)
size_str = "" + ui.human_bytes(total_size)
size_str = "" + human_bytes(total_size)
if exact:
size_str += f" ({total_size} bytes)"
@ -1918,7 +1919,7 @@ Artists: {}
Albums: {}
Album artists: {}""".format(
total_items,
ui.human_seconds(total_time),
human_seconds(total_time),
f" ({total_time:.2f} seconds)" if exact else "",
"Total size" if exact else "Approximate total size",
size_str,

View file

@ -1019,27 +1019,6 @@ def case_sensitive(path: bytes) -> bool:
return not os.path.samefile(lower_sys, upper_sys)
def raw_seconds_short(string: str) -> float:
"""Formats a human-readable M:SS string as a float (number of seconds).
Raises ValueError if the conversion cannot take place due to `string` not
being in the right format.
"""
match = re.match(r"^(\d+):([0-5]\d)$", string)
if not match:
raise ValueError("String not in M:SS format")
minutes, seconds = map(int, match.groups())
return float(minutes * 60 + seconds)
def human_seconds_short(interval):
"""Formats a number of seconds as a short human-readable M:SS
string.
"""
interval = int(interval)
return "%i:%02i" % (interval // 60, interval % 60)
def asciify_path(path: str, sep_replace: str) -> str:
"""Decodes all unicode characters in a path into ASCII equivalents.

61
beets/util/units.py Normal file
View file

@ -0,0 +1,61 @@
import re
def raw_seconds_short(string: str) -> float:
"""Formats a human-readable M:SS string as a float (number of seconds).
Raises ValueError if the conversion cannot take place due to `string` not
being in the right format.
"""
match = re.match(r"^(\d+):([0-5]\d)$", string)
if not match:
raise ValueError("String not in M:SS format")
minutes, seconds = map(int, match.groups())
return float(minutes * 60 + seconds)
def human_seconds_short(interval):
"""Formats a number of seconds as a short human-readable M:SS
string.
"""
interval = int(interval)
return "%i:%02i" % (interval // 60, interval % 60)
def human_bytes(size):
"""Formats size, a number of bytes, in a human-readable way."""
powers = ["", "K", "M", "G", "T", "P", "E", "Z", "Y", "H"]
unit = "B"
for power in powers:
if size < 1024:
return f"{size:3.1f} {power}{unit}"
size /= 1024.0
unit = "iB"
return "big"
def human_seconds(interval):
"""Formats interval, a number of seconds, as a human-readable time
interval using English words.
"""
units = [
(1, "second"),
(60, "minute"),
(60, "hour"),
(24, "day"),
(7, "week"),
(52, "year"),
(10, "decade"),
]
for i in range(len(units) - 1):
increment, suffix = units[i]
next_increment, _ = units[i + 1]
interval /= float(increment)
if interval < next_increment:
break
else:
# Last unit.
increment, suffix = units[-1]
interval /= float(increment)
return f"{interval:3.1f} {suffix}s"

View file

@ -21,7 +21,7 @@ from random import random
from beets import config, ui
from beets.test import _common
from beets.test.helper import BeetsTestCase, ItemInDBTestCase, control_stdin
from beets.test.helper import BeetsTestCase, control_stdin
class InputMethodsTest(BeetsTestCase):
@ -88,42 +88,6 @@ class InputMethodsTest(BeetsTestCase):
assert items == ["1", "3"]
class InitTest(ItemInDBTestCase):
def test_human_bytes(self):
tests = [
(0, "0.0 B"),
(30, "30.0 B"),
(pow(2, 10), "1.0 KiB"),
(pow(2, 20), "1.0 MiB"),
(pow(2, 30), "1.0 GiB"),
(pow(2, 40), "1.0 TiB"),
(pow(2, 50), "1.0 PiB"),
(pow(2, 60), "1.0 EiB"),
(pow(2, 70), "1.0 ZiB"),
(pow(2, 80), "1.0 YiB"),
(pow(2, 90), "1.0 HiB"),
(pow(2, 100), "big"),
]
for i, h in tests:
assert h == ui.human_bytes(i)
def test_human_seconds(self):
tests = [
(0, "0.0 seconds"),
(30, "30.0 seconds"),
(60, "1.0 minutes"),
(90, "1.5 minutes"),
(125, "2.1 minutes"),
(3600, "1.0 hours"),
(86400, "1.0 days"),
(604800, "1.0 weeks"),
(31449600, "1.0 years"),
(314496000, "1.0 decades"),
]
for i, h in tests:
assert h == ui.human_seconds(i)
class ParentalDirCreation(BeetsTestCase):
def test_create_yes(self):
non_exist_path = _common.os.fsdecode(

43
test/util/test_units.py Normal file
View file

@ -0,0 +1,43 @@
import pytest
from beets.util.units import human_bytes, human_seconds
@pytest.mark.parametrize(
"input_bytes,expected",
[
(0, "0.0 B"),
(30, "30.0 B"),
(pow(2, 10), "1.0 KiB"),
(pow(2, 20), "1.0 MiB"),
(pow(2, 30), "1.0 GiB"),
(pow(2, 40), "1.0 TiB"),
(pow(2, 50), "1.0 PiB"),
(pow(2, 60), "1.0 EiB"),
(pow(2, 70), "1.0 ZiB"),
(pow(2, 80), "1.0 YiB"),
(pow(2, 90), "1.0 HiB"),
(pow(2, 100), "big"),
],
)
def test_human_bytes(input_bytes, expected):
assert human_bytes(input_bytes) == expected
@pytest.mark.parametrize(
"input_seconds,expected",
[
(0, "0.0 seconds"),
(30, "30.0 seconds"),
(60, "1.0 minutes"),
(90, "1.5 minutes"),
(125, "2.1 minutes"),
(3600, "1.0 hours"),
(86400, "1.0 days"),
(604800, "1.0 weeks"),
(31449600, "1.0 years"),
(314496000, "1.0 decades"),
],
)
def test_human_seconds(input_seconds, expected):
assert human_seconds(input_seconds) == expected