Skip to content

Commit 44cc6c8

Browse files
committed
Adds int_or_interval format parser
Accepts either int or interval, first tries parsing int then tries parsing as interval if that fails. Returns a timedelta for easy date math later. Now allows intervals of length 0 as a 0-length timedelta is perfectly fine to work with.
1 parent 7ec02dc commit 44cc6c8

File tree

3 files changed

+65
-11
lines changed

3 files changed

+65
-11
lines changed

src/borg/helpers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from .misc import ChunkIteratorFileWrapper, open_item, chunkit, iter_separated, ErrorIgnoringTextIOWrapper
2929
from .parseformat import bin_to_hex, hex_to_bin, safe_encode, safe_decode
3030
from .parseformat import text_to_json, binary_to_json, remove_surrogates, join_cmd
31-
from .parseformat import eval_escapes, decode_dict, positive_int_validator, interval
31+
from .parseformat import eval_escapes, decode_dict, positive_int_validator, interval, int_or_interval
3232
from .parseformat import PathSpec, SortBySpec, ChunkerParams, FilesCacheMode, partial_format, DatetimeWrapper
3333
from .parseformat import format_file_size, parse_file_size, FileSize
3434
from .parseformat import sizeof_fmt, sizeof_fmt_iec, sizeof_fmt_decimal, Location, text_validator

src/borg/helpers/parseformat.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import uuid
1313
from typing import ClassVar, Any, TYPE_CHECKING, Literal
1414
from collections import OrderedDict
15-
from datetime import datetime, timezone
15+
from datetime import datetime, timezone, timedelta
1616
from functools import partial
1717
from string import Formatter
1818

@@ -154,12 +154,24 @@ def interval(s):
154154
except ValueError:
155155
seconds = -1
156156

157-
if seconds <= 0:
158-
raise argparse.ArgumentTypeError(f'Invalid number "{number}": expected positive integer')
157+
if seconds < 0:
158+
raise argparse.ArgumentTypeError(f'Invalid number "{number}": expected nonnegative integer')
159159

160160
return seconds
161161

162162

163+
def int_or_interval(s):
164+
try:
165+
return int(s)
166+
except ValueError:
167+
pass
168+
169+
try:
170+
return timedelta(seconds=interval(s))
171+
except argparse.ArgumentTypeError as e:
172+
raise argparse.ArgumentTypeError(f"Value is neither an integer nor an interval: {e}")
173+
174+
163175
def ChunkerParams(s):
164176
params = s.strip().split(",")
165177
count = len(params)

src/borg/testsuite/helpers/parseformat_test.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import base64
22
import os
3+
import re
34
from argparse import ArgumentTypeError
4-
from datetime import datetime, timezone
5+
from datetime import datetime, timedelta, timezone
56

67
import pytest
78

@@ -16,6 +17,7 @@
1617
format_file_size,
1718
parse_file_size,
1819
interval,
20+
int_or_interval,
1921
partial_format,
2022
clean_lines,
2123
format_line,
@@ -351,6 +353,7 @@ def test_format_timedelta():
351353
@pytest.mark.parametrize(
352354
"timeframe, num_secs",
353355
[
356+
("0S", 0),
354357
("5S", 5),
355358
("2M", 2 * 60),
356359
("1H", 60 * 60),
@@ -367,9 +370,9 @@ def test_interval(timeframe, num_secs):
367370
@pytest.mark.parametrize(
368371
"invalid_interval, error_tuple",
369372
[
370-
("H", ('Invalid number "": expected positive integer',)),
371-
("-1d", ('Invalid number "-1": expected positive integer',)),
372-
("food", ('Invalid number "foo": expected positive integer',)),
373+
("H", ('Invalid number "": expected nonnegative integer',)),
374+
("-1d", ('Invalid number "-1": expected nonnegative integer',)),
375+
("food", ('Invalid number "foo": expected nonnegative integer',)),
373376
],
374377
)
375378
def test_interval_time_unit(invalid_interval, error_tuple):
@@ -378,10 +381,49 @@ def test_interval_time_unit(invalid_interval, error_tuple):
378381
assert exc.value.args == error_tuple
379382

380383

381-
def test_interval_number():
384+
@pytest.mark.parametrize(
385+
"invalid_input, error_regex",
386+
[
387+
("x", r'^Unexpected time unit "x": choose from'),
388+
("-1t", r'^Unexpected time unit "t": choose from'),
389+
("fool", r'^Unexpected time unit "l": choose from'),
390+
("abc", r'^Unexpected time unit "c": choose from'),
391+
(" abc ", r'^Unexpected time unit " ": choose from'),
392+
],
393+
)
394+
def test_interval_invalid_time_format(invalid_input, error_regex):
395+
with pytest.raises(ArgumentTypeError) as exc:
396+
interval(invalid_input)
397+
assert re.search(error_regex, exc.value.args[0])
398+
399+
400+
@pytest.mark.parametrize(
401+
"input, result",
402+
[
403+
("0", 0),
404+
("5", 5),
405+
(" 999 ", 999),
406+
("0S", timedelta(seconds=0)),
407+
("5S", timedelta(seconds=5)),
408+
("1m", timedelta(days=31)),
409+
],
410+
)
411+
def test_int_or_interval(input, result):
412+
assert int_or_interval(input) == result
413+
414+
415+
@pytest.mark.parametrize(
416+
"invalid_input, error_regex",
417+
[
418+
("H", r"Value is neither an integer nor an interval:"),
419+
("-1d", r"Value is neither an integer nor an interval:"),
420+
("food", r"Value is neither an integer nor an interval:"),
421+
],
422+
)
423+
def test_int_or_interval_time_unit(invalid_input, error_regex):
382424
with pytest.raises(ArgumentTypeError) as exc:
383-
interval("5")
384-
assert exc.value.args == ('Unexpected time unit "5": choose from y, m, w, d, H, M, S',)
425+
int_or_interval(invalid_input)
426+
assert re.search(error_regex, exc.value.args[0])
385427

386428

387429
def test_parse_timestamp():

0 commit comments

Comments
 (0)