diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index be28f6891..30cabf42f 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -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: diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index f1aac766f..b7033e41b 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -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: diff --git a/beets/ui/commands.py b/beets/ui/commands.py index fb9ca8b89..3117262f1 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -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, diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 4572b27f9..c1c76c860 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -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. diff --git a/beets/util/units.py b/beets/util/units.py new file mode 100644 index 000000000..d07d42546 --- /dev/null +++ b/beets/util/units.py @@ -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" diff --git a/test/test_ui_init.py b/test/test_ui_init.py index a6f06c494..df21b300c 100644 --- a/test/test_ui_init.py +++ b/test/test_ui_init.py @@ -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( diff --git a/test/util/test_units.py b/test/util/test_units.py new file mode 100644 index 000000000..26f4d3eca --- /dev/null +++ b/test/util/test_units.py @@ -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