--- /dev/null
+#!/usr/bin/env python3
+
+from abc import ABC, abstractmethod
+import atexit
+import functools
+import logging
+
+import dill
+
+import file_utils
+
+
+logger = logging.getLogger(__name__)
+
+
+class Persistent(ABC):
+ @abstractmethod
+ def save(self):
+ pass
+
+ @abstractmethod
+ def load(self):
+ pass
+
+
+class persistent_autoload_singleton(Persistent):
+ def __init__(self, filename: str, *, max_age_sec: int = 0):
+ self.filename = filename
+ self.max_age_sec = max_age_sec
+ self.instance = None
+
+ def __call__(self, cls):
+ @functools.wraps(cls)
+ def _load(*args, **kwargs):
+
+ # If class has already been loaded, act like a singleton
+ # and return a reference to the one and only instance in
+ # memory.
+ if self.instance is not None:
+ logger.debug(
+ f'Returning already instantiated singleton instance of {cls.__name__}.'
+ )
+ return self.instance
+
+ if not self.load():
+ assert self.instance is None
+ logger.debug(
+ f'Instantiating {cls.__name__} directly.'
+ )
+ self.instance = cls(*args, **kwargs)
+
+ # On program exit, save state to disk.
+ atexit.register(self.save)
+ assert self.instance is not None
+ return self.instance
+ return _load
+
+ def load(self) -> bool:
+ if (
+ file_utils.does_file_exist(self.filename)
+ and (
+ self.max_age_sec == 0 or
+ file_utils.get_file_mtime_age_seconds(self.filename) <= self.max_age_sec
+ )
+ ):
+ logger.debug(
+ f'Attempting to load from file {self.filename}'
+ )
+ try:
+ with open(self.filename, 'rb') as f:
+ self.instance = dill.load(f)
+ return True
+ except Exception:
+ self.instance = None
+ return False
+ return False
+
+ def save(self) -> bool:
+ if self.instance is not None:
+ logger.debug(
+ f'Attempting to save {type(self.instance).__name__} to file {self.filename}'
+ )
+ try:
+ with open(self.filename, 'wb') as f:
+ dill.dump(self.instance, f, dill.HIGHEST_PROTOCOL)
+ return True
+ except Exception:
+ return False
+ return False