mirror of
https://github.com/borgbackup/borg.git
synced 2026-10-12 02:11:36 +00:00
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:
@@ -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]
|
||||
|
||||
@@ -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::
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user