date: address review feedback

- fix a TZ leak that broke later tests. The local-timezone test set
TZ + tzset() but restored only the env var, corrupting the runner's local time. Restore through a set_timezone fixture that re-runs tzset() on teardown.
- In parsing, reject surrounding whitespace and trailing newlines. Use \Z instead
of $ and drop the redundant strip() so 'date:2025\n' no longer parses.
- add tzdata dependency on Windows so [Region/City] zones resolve there.
- allow a space in place of T as the date-time separator and rewrite the date:
helptext to be clearer, separating exact from interval forms and adding a note on DST/UTC.
This commit is contained in:
Charlie Herz
2026-07-13 20:16:54 -04:00
parent 91bf03e39f
commit dcfa8df552
4 changed files with 68 additions and 21 deletions
+1
View File
@@ -42,6 +42,7 @@ dependencies = [
"jsonargparse>=4.47.0",
"PyYAML>=6.0.2", # we need to register our types with yaml, jsonargparse uses yaml for config files
"blake3>=1.0.0,!=1.0.9", # 1.0.9 does not build on windows/msys2/mingw
"tzdata; sys_platform == 'win32'", # zoneinfo has no system tz database on Windows (date: named zones)
]
[project.optional-dependencies]
+30 -15
View File
@@ -330,26 +330,41 @@ class HelpMixIn:
This is very powerful, but can also get rather complicated.
Date patterns, selector ``date:``
Match archives by creation timestamp. Supported forms are:
Match archives by creation timestamp. You can either match a single archive by
passing its exact creation time, or all archives created within a given time
interval.
- ``YYYY``: 1 year
- ``YYYY-MM``: 1 month
- ``YYYY-MM-DD``: 1 day
- ``YYYY-MM-DDTHH``: 1 hour
- ``YYYY-MM-DDTHH:MM``: 1 minute
- ``YYYY-MM-DDTHH:MM:SS``: 1 second
- ``@1735732800``: 1 second
- ``YYYY-MM-DDTHH:MM:SS.ffffff``: exact timestamp
- ``@1735732800.123456``: exact timestamp
To match a single archive by its exact creation time, use the forms:
- ``YYYY-MM-DDTHH:MM:SS.ffffff``: ISO-8601-like date-time string
- ``@1735732800.123456``: UNIX timestamp
To match a single archive, the pattern must specify the archive's complete
creation timestamp, including any fractional seconds. Fractional-second
patterns accept 1 to 6 digits.
To match all archives created within a given time interval, use the forms:
- ``YYYY``: match all archives created within the given year
- ``YYYY-MM``: within the given month
- ``YYYY-MM-DD``: on the given day
- ``YYYY-MM-DDTHH``: in the given hour
- ``YYYY-MM-DDTHH:MM``: in the given minute
- ``YYYY-MM-DDTHH:MM:SS``: in the given second
- ``@1735732800``: within the 1 second interval from the given UNIX timestamp
The ``T`` date-time separator may also be written as a space, e.g.
``date:2025-01-01 14:30``.
Date and time patterns match the interval implied by their precision, including
the start and excluding the end. Fractional-second patterns accept 1 to 6
digits and match exactly.
the start and excluding the end. For example, ``date:2026-06`` matches archives
created on or after ``2026-06-01T00:00:00`` and before ``2026-07-01T00:00:00``.
Date and time patterns may include a timezone suffix: ``Z``, ``+HH:MM``,
Date and time patterns may include a timezone suffix: ``Z`` (UTC), ``+HH:MM``,
``-HH:MM``, or ``[Region/City]``. Patterns without a timezone are interpreted
in the local timezone. Unix timestamp patterns are UTC and do not accept a
timezone suffix.
in the local timezone. Be wary of Daylight Saving Time (DST) transitions, as
they can make time intervals ambiguous or nonexistent. Use UTC to avoid such
issues. Unix timestamps are always UTC and do not accept a timezone suffix.
Examples::
+2 -3
View File
@@ -255,7 +255,7 @@ DATE_PATTERN_RE = r"""
(?:
-(?P<day>\d{2})
(?:
T(?P<hour>\d{2})
[ T](?P<hour>\d{2})
(?:
:(?P<minute>\d{2})
(?:
@@ -267,7 +267,7 @@ DATE_PATTERN_RE = r"""
)?
)
(?P<tz>Z|[+\-]\d\d:\d\d|\[[^]]+\])?
$
\Z
"""
@@ -341,7 +341,6 @@ def compile_date_pattern(expr: str):
Supported expressions are static calendar timestamps from year to fractional-second precision,
optional timezone suffixes (Z, +/-HH:MM, or [Region/City]), and Unix epoch timestamps prefixed with @.
"""
expr = expr.strip()
start, end = parse_date_pattern_interval(expr)
if start == end:
return date_match_exact(start)
@@ -1,4 +1,5 @@
import calendar
import os
import time
from datetime import datetime, timezone
@@ -12,6 +13,27 @@ from . import cmd, create_src_archive, generate_archiver_tests, RK_ENCRYPTION
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA
@pytest.fixture
def set_timezone(request):
"""Set the process-local timezone for the duration of a test, restoring it afterwards."""
def _set(tz):
old_tz = os.environ.get("TZ")
def restore():
if old_tz is None:
os.environ.pop("TZ", None)
else:
os.environ["TZ"] = old_tz
time.tzset()
request.addfinalizer(restore)
os.environ["TZ"] = tz
time.tzset()
return _set
# (archive_name, timestamp)
YEAR_ARCHIVES = [
("archive-year-start", "2025-01-01T00:00:00+00:00"),
@@ -261,11 +283,10 @@ LOCAL_TZ_ARCHIVES = [
@pytest.mark.skipif(is_win32, reason="time.tzset() is not available on Windows")
def test_match_bare_pattern_uses_local_timezone(archivers, request, monkeypatch):
def test_match_bare_pattern_uses_local_timezone(archivers, request, set_timezone):
"""A pattern without a timezone suffix is interpreted in the local timezone."""
set_timezone("America/New_York")
archiver = request.getfixturevalue(archivers)
monkeypatch.setenv("TZ", "America/New_York")
time.tzset()
cmd(archiver, "repo-create", RK_ENCRYPTION)
for name, ts in LOCAL_TZ_ARCHIVES:
create_src_archive(archiver, name, ts=ts)
@@ -347,3 +368,14 @@ def test_fractional_precision_rejected(archivers, request, invalid_expr):
msg = str(excinfo.value)
assert "Invalid date pattern" in msg
assert invalid_expr in msg
@pytest.mark.parametrize("invalid_expr", ["2025\n", " 2025", "2025 "])
def test_surrounding_whitespace_rejected(archivers, request, invalid_expr):
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", RK_ENCRYPTION)
with pytest.raises(CommandError) as excinfo:
cmd(archiver, "repo-list", "-v", f"--match-archives=date:{invalid_expr}")
assert "Invalid date pattern" in str(excinfo.value)