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.
This commit is contained in:
Hugo Wallenburg 2025-04-19 20:43:10 +02:00
parent 6959b69f9b
commit bcd9f84643
No known key found for this signature in database
3 changed files with 73 additions and 11 deletions

View file

@ -27,7 +27,7 @@ from .misc import sysinfo, log_multi, consume
from .misc import ChunkIteratorFileWrapper, open_item, chunkit, iter_separated, ErrorIgnoringTextIOWrapper
from .parseformat import octal_int, bin_to_hex, hex_to_bin, safe_encode, safe_decode
from .parseformat import text_to_json, binary_to_json, remove_surrogates, join_cmd
from .parseformat import eval_escapes, decode_dict, interval
from .parseformat import eval_escapes, decode_dict, interval, int_or_interval
from .parseformat import (
PathSpec,
FilesystemPathSpec,

View file

@ -12,7 +12,7 @@ import uuid
from pathlib import Path
from typing import ClassVar, Any, TYPE_CHECKING, Literal
from collections import OrderedDict
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from functools import partial
from hashlib import sha256
from string import Formatter
@ -159,12 +159,27 @@ def interval(s):
except ValueError:
seconds = -1
if seconds <= 0:
raise ArgumentTypeError(f'Invalid number "{number}": expected positive integer')
if seconds < 0:
raise ArgumentTypeError(f'Invalid number "{number}": expected nonnegative integer')
return seconds
def int_or_interval(s):
if isinstance(s, (int, timedelta)):
return s
try:
return int(s)
except ValueError:
pass
try:
return timedelta(seconds=interval(s))
except ArgumentTypeError as e:
raise ArgumentTypeError(f"Value is neither an integer nor an interval: {e}")
class CompressionSpec:
def __init__(self, s):
if isinstance(s, CompressionSpec):

View file

@ -1,7 +1,8 @@
import base64
import os
from datetime import datetime, timezone
import re
from datetime import datetime, timedelta, timezone
import pytest
@ -17,6 +18,7 @@ from ...helpers.parseformat import (
format_file_size,
parse_file_size,
interval,
int_or_interval,
partial_format,
clean_lines,
format_line,
@ -388,6 +390,7 @@ def test_format_timedelta():
@pytest.mark.parametrize(
"timeframe, num_secs",
[
("0S", 0),
("5S", 5),
("2M", 2 * 60),
("1H", 60 * 60),
@ -404,9 +407,9 @@ def test_interval(timeframe, num_secs):
@pytest.mark.parametrize(
"invalid_interval, error_tuple",
[
("H", ('Invalid number "": expected positive integer',)),
("-1d", ('Invalid number "-1": expected positive integer',)),
("food", ('Invalid number "foo": expected positive integer',)),
("H", ('Invalid number "": expected nonnegative integer',)),
("-1d", ('Invalid number "-1": expected nonnegative integer',)),
("food", ('Invalid number "foo": expected nonnegative integer',)),
],
)
def test_interval_time_unit(invalid_interval, error_tuple):
@ -415,10 +418,54 @@ def test_interval_time_unit(invalid_interval, error_tuple):
assert exc.value.args == error_tuple
def test_interval_number():
@pytest.mark.parametrize(
"invalid_input, error_regex",
[
("x", r'^Unexpected time unit "x": choose from'),
("-1t", r'^Unexpected time unit "t": choose from'),
("fool", r'^Unexpected time unit "l": choose from'),
("abc", r'^Unexpected time unit "c": choose from'),
(" abc ", r'^Unexpected time unit " ": choose from'),
],
)
def test_interval_invalid_time_format(invalid_input, error_regex):
with pytest.raises(ArgumentTypeError) as exc:
interval("5")
assert exc.value.args == ('Unexpected time unit "5": choose from y, m, w, d, H, M, S',)
interval(invalid_input)
assert re.search(error_regex, exc.value.args[0])
@pytest.mark.parametrize(
"input, result",
[
("0", 0),
("5", 5),
(" 999 ", 999),
("0S", timedelta(seconds=0)),
("5S", timedelta(seconds=5)),
("1m", timedelta(days=31)),
# already-converted values (jsonargparse idempotency)
(0, 0),
(5, 5),
(timedelta(seconds=5), timedelta(seconds=5)),
(timedelta(days=31), timedelta(days=31)),
],
)
def test_int_or_interval(input, result):
assert int_or_interval(input) == result
@pytest.mark.parametrize(
"invalid_input, error_regex",
[
("H", r"Value is neither an integer nor an interval:"),
("-1d", r"Value is neither an integer nor an interval:"),
("food", r"Value is neither an integer nor an interval:"),
],
)
def test_int_or_interval_time_unit(invalid_input, error_regex):
with pytest.raises(ArgumentTypeError) as exc:
int_or_interval(invalid_input)
assert re.search(error_regex, exc.value.args[0])
def test_parse_timestamp():