@@ -260,10 +260,98 @@ def rotate_backups(directory, rotation_scheme, **options):
260260 program .rotate_backups (directory )
261261
262262
263+ class Match :
264+ def match_to_datetime (self ) -> datetime .datetime :
265+ pass
266+
267+
268+ class Matcher :
269+ def search (self , location : str , entry : str ) -> Match :
270+ pass
271+
272+
273+ class FilenameMatch (Match ):
274+ match : re .Match = None
275+
276+ def __init__ (self , match : re .Match ):
277+ self .match = match
278+
279+ def match_to_datetime (self ) -> datetime .datetime :
280+ """
281+ Convert the regular expression match to a :class:`~datetime.datetime` value.
282+
283+ :returns: A :class:`~datetime.datetime` value.
284+ :raises: :exc:`exceptions.ValueError` when a required date component is
285+ not captured by the pattern, the captured value is an empty
286+ string or the captured value cannot be interpreted as a
287+ base-10 integer.
288+
289+ .. seealso:: :data:`SUPPORTED_DATE_COMPONENTS`
290+ """
291+ kw = {}
292+ captures = self .match .groupdict ()
293+ for component , required in SUPPORTED_DATE_COMPONENTS :
294+ value = captures .get (component )
295+ if value :
296+ kw [component ] = int (value , 10 )
297+ elif required :
298+ raise ValueError ("Missing required date component! (%s)" % component )
299+ else :
300+ kw [component ] = 0
301+ return datetime .datetime (** kw )
302+
303+
304+ class FilenameMatcher (Matcher ):
305+ timestamp_pattern : re .Pattern = None
306+
307+ def __init__ (self , timestamp_pattern : re .Pattern ):
308+ self .timestamp_pattern = timestamp_pattern
309+
310+ def search (self , location : str , entry : str ) -> FilenameMatch :
311+ if match := self .timestamp_pattern .search (entry ):
312+ return FilenameMatch (match )
313+
314+ @mutable_property
315+ def timestamp_pattern (self ):
316+ """
317+ The pattern used to extract timestamps from filenames (defaults to :data:`TIMESTAMP_PATTERN`).
318+
319+ The value of this property is a compiled regular expression object.
320+ Callers can provide their own compiled regular expression which
321+ makes it possible to customize the compilation flags (see the
322+ :func:`re.compile()` documentation for details).
323+
324+ The regular expression pattern is expected to be a Python compatible
325+ regular expression that defines the named capture groups 'year',
326+ 'month' and 'day' and optionally 'hour', 'minute' and 'second'.
327+
328+ String values are automatically coerced to compiled regular expressions
329+ by calling :func:`~humanfriendly.coerce_pattern()`, in this case only
330+ the :data:`re.VERBOSE` flag is used.
331+
332+ If the caller provides a custom pattern it will be validated
333+ to confirm that the pattern contains named capture groups
334+ corresponding to each of the required date components
335+ defined by :data:`SUPPORTED_DATE_COMPONENTS`.
336+ """
337+ return TIMESTAMP_PATTERN
338+
339+ @timestamp_pattern .setter
340+ def timestamp_pattern (self , value ):
341+ """Coerce the value of :attr:`timestamp_pattern` to a compiled regular expression."""
342+ pattern = coerce_pattern (value , re .VERBOSE )
343+ for component , required in SUPPORTED_DATE_COMPONENTS :
344+ if component not in pattern .groupindex and required :
345+ raise ValueError ("Pattern is missing required capture group! (%s)" % component )
346+ set_property (self , 'timestamp_pattern' , pattern )
347+
348+
263349class RotateBackups (PropertyManager ):
264350
265351 """Python API for the ``rotate-backups`` program."""
266352
353+ _matcher : Matcher = FilenameMatcher (TIMESTAMP_PATTERN )
354+
267355 def __init__ (self , rotation_scheme , ** options ):
268356 """
269357 Initialize a :class:`RotateBackups` object.
@@ -449,37 +537,21 @@ def strict(self):
449537
450538 @mutable_property
451539 def timestamp_pattern (self ):
452- """
453- The pattern used to extract timestamps from filenames (defaults to :data:`TIMESTAMP_PATTERN`).
454-
455- The value of this property is a compiled regular expression object.
456- Callers can provide their own compiled regular expression which
457- makes it possible to customize the compilation flags (see the
458- :func:`re.compile()` documentation for details).
459-
460- The regular expression pattern is expected to be a Python compatible
461- regular expression that defines the named capture groups 'year',
462- 'month' and 'day' and optionally 'hour', 'minute' and 'second'.
463-
464- String values are automatically coerced to compiled regular expressions
465- by calling :func:`~humanfriendly.coerce_pattern()`, in this case only
466- the :data:`re.VERBOSE` flag is used.
467-
468- If the caller provides a custom pattern it will be validated
469- to confirm that the pattern contains named capture groups
470- corresponding to each of the required date components
471- defined by :data:`SUPPORTED_DATE_COMPONENTS`.
472- """
473- return TIMESTAMP_PATTERN
540+ return self .matcher .timestamp_pattern
474541
475542 @timestamp_pattern .setter
476543 def timestamp_pattern (self , value ):
477- """Coerce the value of :attr:`timestamp_pattern` to a compiled regular expression."""
478- pattern = coerce_pattern (value , re .VERBOSE )
479- for component , required in SUPPORTED_DATE_COMPONENTS :
480- if component not in pattern .groupindex and required :
481- raise ValueError ("Pattern is missing required capture group! (%s)" % component )
482- set_property (self , 'timestamp_pattern' , pattern )
544+ self .matcher .timestamp_pattern = value
545+
546+ @mutable_property
547+ def matcher (self ):
548+ return self ._matcher
549+
550+ @matcher .setter
551+ def matcher (self , matcher ):
552+ if not isinstance (matcher , Matcher ):
553+ raise ValueError (f'{ matcher } is not a Matcher' )
554+ set_property (self , '_matcher' , matcher )
483555
484556 def rotate_concurrent (self , * locations , ** kw ):
485557 """
@@ -642,8 +714,9 @@ def collect_backups(self, location):
642714 location = coerce_location (location )
643715 logger .info ("Scanning %s for backups .." , location )
644716 location .ensure_readable (self .force )
717+
645718 for entry in natsort (location .context .list_entries (location .directory )):
646- match = self .timestamp_pattern .search (entry )
719+ match = self .matcher .search (location , entry )
647720 if match :
648721 if self .exclude_list and any (fnmatch .fnmatch (entry , p ) for p in self .exclude_list ):
649722 logger .verbose ("Excluded %s (it matched the exclude list)." , entry )
@@ -653,7 +726,7 @@ def collect_backups(self, location):
653726 try :
654727 backups .append (Backup (
655728 pathname = os .path .join (location .directory , entry ),
656- timestamp = self .match_to_datetime (match ),
729+ timestamp = match .match_to_datetime (),
657730 ))
658731 except ValueError as e :
659732 logger .notice ("Ignoring %s due to invalid date (%s)." , entry , e )
@@ -663,31 +736,6 @@ def collect_backups(self, location):
663736 logger .info ("Found %i timestamped backups in %s." , len (backups ), location )
664737 return sorted (backups )
665738
666- def match_to_datetime (self , match ):
667- """
668- Convert a regular expression match to a :class:`~datetime.datetime` value.
669-
670- :param match: A regular expression match object.
671- :returns: A :class:`~datetime.datetime` value.
672- :raises: :exc:`exceptions.ValueError` when a required date component is
673- not captured by the pattern, the captured value is an empty
674- string or the captured value cannot be interpreted as a
675- base-10 integer.
676-
677- .. seealso:: :data:`SUPPORTED_DATE_COMPONENTS`
678- """
679- kw = {}
680- captures = match .groupdict ()
681- for component , required in SUPPORTED_DATE_COMPONENTS :
682- value = captures .get (component )
683- if value :
684- kw [component ] = int (value , 10 )
685- elif required :
686- raise ValueError ("Missing required date component! (%s)" % component )
687- else :
688- kw [component ] = 0
689- return datetime .datetime (** kw )
690-
691739 def group_backups (self , backups ):
692740 """
693741 Group backups collected by :func:`collect_backups()` by rotation frequencies.
@@ -781,25 +829,25 @@ class Location(PropertyManager):
781829
782830 """:class:`Location` objects represent a root directory containing backups."""
783831
784- @required_property
832+ @ required_property
785833 def context (self ):
786834 """An execution context created using :mod:`executor.contexts`."""
787835
788- @required_property
836+ @ required_property
789837 def directory (self ):
790838 """The pathname of a directory containing backups (a string)."""
791839
792- @lazy_property
840+ @ lazy_property
793841 def have_ionice (self ):
794842 """:data:`True` when ionice_ is available, :data:`False` otherwise."""
795843 return self .context .have_ionice
796844
797- @lazy_property
845+ @ lazy_property
798846 def have_wildcards (self ):
799847 """:data:`True` if :attr:`directory` is a filename pattern, :data:`False` otherwise."""
800848 return '*' in self .directory
801849
802- @lazy_property
850+ @ lazy_property
803851 def mount_point (self ):
804852 """
805853 The pathname of the mount point of :attr:`directory` (a string or :data:`None`).
@@ -814,17 +862,17 @@ def mount_point(self):
814862 except ExternalCommandFailed :
815863 return None
816864
817- @lazy_property
865+ @ lazy_property
818866 def is_remote (self ):
819867 """:data:`True` if the location is remote, :data:`False` otherwise."""
820868 return isinstance (self .context , RemoteContext )
821869
822- @lazy_property
870+ @ lazy_property
823871 def ssh_alias (self ):
824872 """The SSH alias of a remote location (a string or :data:`None`)."""
825873 return self .context .ssh_alias if self .is_remote else None
826874
827- @property
875+ @ property
828876 def key_properties (self ):
829877 """
830878 A list of strings with the names of the :attr:`~custom_property.key` properties.
@@ -975,15 +1023,15 @@ class Backup(PropertyManager):
9751023 :attr:`~property_manager.PropertyManager.key_properties`.
9761024 """
9771025
978- @key_property
1026+ @ key_property
9791027 def pathname (self ):
9801028 """The pathname of the backup (a string)."""
9811029
982- @key_property
1030+ @ key_property
9831031 def timestamp (self ):
9841032 """The date and time when the backup was created (a :class:`~datetime.datetime` object)."""
9851033
986- @property
1034+ @ property
9871035 def week (self ):
9881036 """The ISO week number of :attr:`timestamp` (a number)."""
9891037 return self .timestamp .isocalendar ()[1 ]
0 commit comments