Split up parsing and filtering for --keep-within (#2726)

Split up parsing and filtering for --keep-within

Fixes #2610

Parse --keep-within argument early, via new method within_range passed
to argparse type=, so that better error messages can be given.

Also swallows ValueError stacktrace per the comment in the old code that
including it wasn't desirable.
This commit is contained in:
edgewood 2017-06-25 17:31:04 -04:00 committed by TW
parent 04aa426334
commit 932fb9ec7a
3 changed files with 62 additions and 12 deletions

View File

@ -21,7 +21,7 @@ import collections
from . import __version__ from . import __version__
from .helpers import Error, location_validator, archivename_validator, format_line, format_time, format_file_size, \ from .helpers import Error, location_validator, archivename_validator, format_line, format_time, format_file_size, \
parse_pattern, PathPrefixPattern, to_localtime, timestamp, safe_timestamp, bin_to_hex, \ parse_pattern, PathPrefixPattern, to_localtime, timestamp, safe_timestamp, bin_to_hex, \
get_cache_dir, prune_within, prune_split, check_python, \ get_cache_dir, interval, prune_within, prune_split, check_python, \
Manifest, NoManifestError, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \ Manifest, NoManifestError, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
dir_is_tagged, bigint_to_int, ChunkerParams, CompressionSpec, PrefixSpec, is_slow_msgpack, yes, sysinfo, \ dir_is_tagged, bigint_to_int, ChunkerParams, CompressionSpec, PrefixSpec, is_slow_msgpack, yes, sysinfo, \
EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, log_multi, PatternMatcher, ErrorIgnoringTextIOWrapper, set_ec, \ EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, log_multi, PatternMatcher, ErrorIgnoringTextIOWrapper, set_ec, \
@ -1717,7 +1717,7 @@ class Archiver:
subparser.add_argument('--list', dest='output_list', subparser.add_argument('--list', dest='output_list',
action='store_true', default=False, action='store_true', default=False,
help='output verbose list of archives it keeps/prunes. Requires -v/--verbose.') help='output verbose list of archives it keeps/prunes. Requires -v/--verbose.')
subparser.add_argument('--keep-within', dest='within', type=str, metavar='WITHIN', subparser.add_argument('--keep-within', dest='within', type=interval, metavar='INTERVAL',
help='keep all archives within this time interval') help='keep all archives within this time interval')
subparser.add_argument('-H', '--keep-hourly', dest='hourly', type=int, default=0, subparser.add_argument('-H', '--keep-hourly', dest='hourly', type=int, default=0,
help='number of hourly archives to keep') help='number of hourly archives to keep')

View File

@ -279,15 +279,32 @@ class Manifest:
return archives return archives
def prune_within(archives, within): def interval(s):
"""Convert a string representing a valid interval to a number of hours."""
multiplier = {'H': 1, 'd': 24, 'w': 24 * 7, 'm': 24 * 31, 'y': 24 * 365} multiplier = {'H': 1, 'd': 24, 'w': 24 * 7, 'm': 24 * 31, 'y': 24 * 365}
if s.endswith(tuple(multiplier.keys())):
number = s[:-1]
suffix = s[-1]
else:
# range suffixes in ascending multiplier order
ranges = [k for k, v in sorted(multiplier.items(), key=lambda t: t[1])]
raise argparse.ArgumentTypeError(
'Unexpected interval time unit "%s": expected one of %r' % (s[-1], ranges))
try: try:
hours = int(within[:-1]) * multiplier[within[-1]] hours = int(number) * multiplier[suffix]
except (KeyError, ValueError): except ValueError:
# I don't like how this displays the original exception too: hours = -1
raise argparse.ArgumentTypeError('Unable to parse --keep-within option: "%s"' % within)
if hours <= 0: if hours <= 0:
raise argparse.ArgumentTypeError('Number specified using --keep-within option must be positive') raise argparse.ArgumentTypeError(
'Unexpected interval number "%s": expected an integer greater than 0' % number)
return hours
def prune_within(archives, hours):
target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600) target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600)
return [a for a in archives if a.ts > target] return [a for a in archives if a.ts > target]

View File

@ -1,4 +1,5 @@
import hashlib import hashlib
from argparse import ArgumentTypeError
from time import mktime, strptime from time import mktime, strptime
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from io import StringIO from io import StringIO
@ -11,7 +12,7 @@ import msgpack.fallback
import time import time
from ..helpers import Location, format_file_size, format_timedelta, format_line, PlaceholderError, make_path_safe, \ from ..helpers import Location, format_file_size, format_timedelta, format_line, PlaceholderError, make_path_safe, \
prune_within, prune_split, get_cache_dir, get_keys_dir, get_security_dir, Statistics, is_slow_msgpack, \ interval, prune_within, prune_split, get_cache_dir, get_keys_dir, get_security_dir, Statistics, is_slow_msgpack, \
yes, TRUISH, FALSISH, DEFAULTISH, \ yes, TRUISH, FALSISH, DEFAULTISH, \
StableDict, int_to_bigint, bigint_to_int, parse_timestamp, CompressionSpec, ChunkerParams, \ StableDict, int_to_bigint, bigint_to_int, parse_timestamp, CompressionSpec, ChunkerParams, \
ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \ ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \
@ -664,16 +665,48 @@ class PruneSplitTestCase(BaseTestCase):
dotest(test_archives, 0, [], []) dotest(test_archives, 0, [], [])
class PruneWithinTestCase(BaseTestCase): class IntervalTestCase(BaseTestCase):
def test_interval(self):
self.assert_equal(interval('1H'), 1)
self.assert_equal(interval('1d'), 24)
self.assert_equal(interval('1w'), 168)
self.assert_equal(interval('1m'), 744)
self.assert_equal(interval('1y'), 8760)
def test(self): def test_interval_time_unit(self):
with pytest.raises(ArgumentTypeError) as exc:
interval('H')
self.assert_equal(
exc.value.args,
('Unexpected interval number "": expected an integer greater than 0',))
with pytest.raises(ArgumentTypeError) as exc:
interval('-1d')
self.assert_equal(
exc.value.args,
('Unexpected interval number "-1": expected an integer greater than 0',))
with pytest.raises(ArgumentTypeError) as exc:
interval('food')
self.assert_equal(
exc.value.args,
('Unexpected interval number "foo": expected an integer greater than 0',))
def test_interval_number(self):
with pytest.raises(ArgumentTypeError) as exc:
interval('5')
self.assert_equal(
exc.value.args,
("Unexpected interval time unit \"5\": expected one of ['H', 'd', 'w', 'm', 'y']",))
class PruneWithinTestCase(BaseTestCase):
def test_prune_within(self):
def subset(lst, indices): def subset(lst, indices):
return {lst[i] for i in indices} return {lst[i] for i in indices}
def dotest(test_archives, within, indices): def dotest(test_archives, within, indices):
for ta in test_archives, reversed(test_archives): for ta in test_archives, reversed(test_archives):
self.assert_equal(set(prune_within(ta, within)), self.assert_equal(set(prune_within(ta, interval(within))),
subset(test_archives, indices)) subset(test_archives, indices))
# 1 minute, 1.5 hours, 2.5 hours, 3.5 hours, 25 hours, 49 hours # 1 minute, 1.5 hours, 2.5 hours, 3.5 hours, 25 hours, 49 hours