X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=src%2Fpyutils%2Fpersistent.py;h=eb237b7634717ce930b4739980a07ed76171d551;hb=16f960286dd78e5b7b70d15fe0a5a4da64db759a;hp=2b03ea6f4e29e496f6dfd9a75d92bd33047cd6c3;hpb=69566c003b4f1c3a4905f37d3735d7921502d14a;p=pyutils.git diff --git a/src/pyutils/persistent.py b/src/pyutils/persistent.py index 2b03ea6..eb237b7 100644 --- a/src/pyutils/persistent.py +++ b/src/pyutils/persistent.py @@ -2,9 +2,30 @@ # © Copyright 2021-2022, Scott Gasch -"""A :class:`Persistent` is just a class with a load and save method. This -module defines the :class:`Persistent` base and a decorator that can be used to -create a persistent singleton that autoloads and autosaves.""" +""" +This module defines a class hierarchy (base class :class:`Persistent`) and +a decorator (`@persistent_autoloaded_singleton`) that can be used to create +objects that load and save their state from some external storage location +automatically, optionally and conditionally. + +A :class:`Persistent` is just a class with a :meth:`Persistent.load` and +:meth:`Persistent.save` method. Various subclasses such as +:class:`JsonFileBasedPersistent` and :class:`PicklingFileBasedPersistent` +define these methods to, save data in a particular format. The details +of where and whether to save are left to your code to decide by implementing +interface methods like :meth:`Persistent.get_filename` and +:meth:`Persistent.should_we_load_data`. + +This module inculdes some helpers to make deciding whether to load persisted +state easier such as :meth:`was_file_written_today` and +:meth:`was_file_written_within_n_seconds`. + +:class:`Persistent` classes are good for things backed by persisted +state that is loaded all or most of the time. For example, the high +score list of a game, the configuration settings of a tool, +etc... Really anything that wants to save/load state from storage and +not bother with the plumbing to do so. +""" import atexit import datetime @@ -63,31 +84,86 @@ class Persistent(ABC): class FileBasedPersistent(Persistent): - """A Persistent that uses a file to save/load data and knows the conditions - under which the state should be saved/loaded.""" + """A :class:`Persistent` subclass that uses a file to save/load + data and knows the conditions under which the state should be + saved/loaded. + """ @staticmethod @abstractmethod def get_filename() -> str: - """Since this class saves/loads to/from a file, what's its full path?""" + """ + Returns: + The full path of the file in which we are saving/loading data. + """ pass @staticmethod @abstractmethod def should_we_save_data(filename: str) -> bool: + """ + Returns: + True if we should save our state now or False otherwise. + """ pass @staticmethod @abstractmethod def should_we_load_data(filename: str) -> bool: + """ + Returns: + True if we should load persisted state now or False otherwise. + """ pass @abstractmethod def get_persistent_data(self) -> Any: + """ + Returns: + The raw state data read from the filesystem. Can be any format. + """ pass class PicklingFileBasedPersistent(FileBasedPersistent): + """ + A class that stores its state in a file as pickled Python objects. + + Example usage:: + + import persistent + + @persistent.persistent_autoloaded_singleton() + class MyClass(persistent.PicklingFileBasedPersistent): + def __init__(self, data: Optional[Whatever]): + if data: + # initialize state from data + else: + # if desired, initialize an "empty" object with new state. + + @staticmethod + @overrides + def get_filename() -> str: + return "/path/to/where/you/want/to/save/data.bin" + + @staticmethod + @overrides + def should_we_save_data(filename: str) -> bool: + return true_if_we_should_save_the_data_this_time() + + @staticmethod + @overrides + def should_we_load_data(filename: str) -> bool: + return persistent.was_file_written_within_n_seconds(whatever) + + # Persistent will handle the plumbing to instantiate your class from its + # persisted state iff the :meth:`should_we_load_data` says it's ok to. It + # will also persist the current in-memory state to disk at program exit iff + # the :meth:`should_we_save_data` methods says to. + c = MyClass() + + """ + @classmethod @overrides def load(cls) -> Optional[Any]: @@ -124,6 +200,47 @@ class PicklingFileBasedPersistent(FileBasedPersistent): class JsonFileBasedPersistent(FileBasedPersistent): + """A class that stores its state in a JSON format file. + + Example usage:: + + import persistent + + @persistent.persistent_autoloaded_singleton() + class MyClass(persistent.JsonFileBasedPersistent): + def __init__(self, data: Optional[dict[str, Any]]): + # load already deserialized the JSON data for you; it's + # a "cooked" JSON dict of string -> values, lists, dicts, + # etc... + if data: + #initialize youself from data... + else: + # if desired, initialize an empty state object + # when json_data isn't provided. + + @staticmethod + @overrides + def get_filename() -> str: + return "/path/to/where/you/want/to/save/data.json" + + @staticmethod + @overrides + def should_we_save_data(filename: str) -> bool: + return true_if_we_should_save_the_data_this_time() + + @staticmethod + @overrides + def should_we_load_data(filename: str) -> bool: + return persistent.was_file_written_within_n_seconds(whatever) + + # Persistent will handle the plumbing to instantiate your + # class from its persisted state iff the + # :meth:`should_we_load_data` says it's ok to. It will also + # persist the current in memory state to disk at program exit + # iff the :meth:`should_we_save_data methods` says to. + c = MyClass() + """ + @classmethod @overrides def load(cls) -> Any: @@ -144,7 +261,6 @@ class JsonFileBasedPersistent(FileBasedPersistent): for line in lines: line = re.sub(r'#.*$', '', line) buf += line - json_dict = json.loads(buf) return cls(json_dict) @@ -174,7 +290,7 @@ def was_file_written_today(filename: str) -> bool: """Convenience wrapper around :meth:`was_file_written_within_n_seconds`. Args: - filename: filename to check + filename: path / filename to check Returns: True if filename was written today. @@ -194,7 +310,6 @@ def was_file_written_today(filename: str) -> bool: >>> was_file_written_today(filename) False """ - if not file_utils.does_file_exist(filename): return False @@ -269,7 +384,7 @@ class persistent_autoloaded_singleton(object): chance to read state from somewhere persistent (disk, db, whatever). Subsequent calls to construt instances of the wrapped class will return a single, global instance (i.e. the wrapped - class is a singleton). + class is must be a singleton). If :meth:`load` fails (returns None), the c'tor is invoked with the original args as a fallback.