From 10b3657ffba364e44e35937d8d379b004d1a3a9a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 14 Apr 2019 20:29:21 +0200 Subject: [PATCH] Rework options/optionhandler to use custom save/load functions --- evennia/accounts/accounts.py | 7 +- evennia/utils/optionclasses.py | 59 +++++++------ evennia/utils/optionhandler.py | 155 +++++++++++++++++++-------------- 3 files changed, 128 insertions(+), 93 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 23a5fe0a1b..4e4d21031c 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -200,7 +200,12 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): @lazy_property def options(self): - return OptionHandler(self, options_dict=settings.OPTIONS_ACCOUNT_DEFAULT, save_category='option') + return OptionHandler(self, + options_dict=settings.OPTIONS_ACCOUNT_DEFAULT, + savefunc=self.attributes.add, + loadfunc=self.attributes.get, + save_kwargs={"category": 'option'}, + load_kwargs={"category": 'option'}) # Do not make this a lazy property; the web UI will not refresh it! @property diff --git a/evennia/utils/optionclasses.py b/evennia/utils/optionclasses.py index 8e24490b4b..82b9c8619e 100644 --- a/evennia/utils/optionclasses.py +++ b/evennia/utils/optionclasses.py @@ -1,6 +1,6 @@ -import datetime as _dt -from evennia import logger as _log -from evennia.utils.ansi import ANSIString as _ANSI +import datetime +from evennia import logger +from evennia.utils.ansi import strip_ansi from evennia.utils.validatorfuncs import _TZ_DICT from evennia.utils.containers import VALIDATOR_FUNCS from evennia.utils.utils import crop @@ -29,7 +29,7 @@ class BaseOption(object): def __repr__(self): return str(self) - def __init__(self, handler, key, description, default, save_data=None): + def __init__(self, handler, key, description, default): """ Args: @@ -38,14 +38,12 @@ class BaseOption(object): Must be unique per OptionHandler. description (str): What this Option's text will show in commands and menus. default: A default value for this Option. - save_data: Whatever was saved to Attributes. This differs by Option. """ self.handler = handler self.key = key self.default_value = default self.description = description - self.save_data = save_data # Value Storage contains None until the Option is loaded. self.value_storage = None @@ -63,7 +61,7 @@ class BaseOption(object): @property def value(self): - if not self.loaded and self.save_data is not None: + if not self.loaded: self.load() if self.loaded: return self.value_storage @@ -98,28 +96,36 @@ class BaseOption(object): Boolean: Whether loading was successful. """ - if self.save_data is not None: - try: - self.value_storage = self.deserialize(self.save_data) - self.loaded = True - return True - except Exception as e: - _log.log_trace(e) - return False + loadfunc = self.handler.loadfunc + load_kwargs = self.handler.load_kwargs + + print("load", self.key, loadfunc, load_kwargs) + try: + self.value_storage = self.deserialize( + loadfunc(self.key, default=self.default_value, **load_kwargs)) + except Exception: + logger.log_trace() + return False + self.loaded = True + return True def save(self, **kwargs): """ - Stores the current value (to an Attribute by default). + Stores the current value using .handler.save_handler(self.key, value, **kwargs) + where kwargs are a combination of those passed into this function and the + ones specified by the OptionHandler. Kwargs: any (any): Not used by default. These are passed in from self.set - and allows the option to let the caller customize saving - if desrired. + and allows the option to let the caller customize saving by + overriding or extend the default save kwargs """ - self.handler.obj.attributes.add(self.key, - category=self.handler.save_category, - value=self.serialize()) + value = self.serialize() + save_kwargs = {**self.handler.save_kwargs, **kwargs} + savefunc = self.handler.savefunc + print("save:", self.key, value, savefunc, save_kwargs) + savefunc(self.key, value=value, **save_kwargs) def deserialize(self, save_data): """ @@ -160,9 +166,10 @@ class BaseOption(object): entries are processed. Returns: - The results of a Validator call. Might be any kind of python object. + any (any): The results of the validation. + """ - return VALIDATOR_FUNCS[self.validator_key](value, thing_name=self.key, **kwargs) + return VALIDATOR_FUNCS.get(self.validator_key)(value, thing_name=self.key, **kwargs) def display(self, **kwargs): """ @@ -227,7 +234,7 @@ class Color(BaseOption): return f'{self.value} - |{self.value}this|n' def deserialize(self, save_data): - if not save_data or len(_ANSI(f'|{save_data}|n')) > 0: + if not save_data or len(strip_ansi(f'|{save_data}|n')) > 0: raise ValueError(f"{self.key} expected Color Code, got '{save_data}'") return save_data @@ -280,7 +287,7 @@ class Duration(BaseOption): def deserialize(self, save_data): if isinstance(save_data, int): - return _dt.timedelta(0, save_data, 0, 0, 0, 0, 0) + return datetime.timedelta(0, save_data, 0, 0, 0, 0, 0) raise ValueError(f"{self.key} expected Timedelta in seconds, got '{save_data}'") def serialize(self): @@ -292,7 +299,7 @@ class Datetime(BaseOption): def deserialize(self, save_data): if isinstance(save_data, int): - return _dt.datetime.utcfromtimestamp(save_data) + return datetime.datetime.utcfromtimestamp(save_data) raise ValueError(f"{self.key} expected UTC Datetime in EPOCH format, got '{save_data}'") def serialize(self): diff --git a/evennia/utils/optionhandler.py b/evennia/utils/optionhandler.py index 8f8380d5ee..ea984e0653 100644 --- a/evennia/utils/optionhandler.py +++ b/evennia/utils/optionhandler.py @@ -2,46 +2,90 @@ from evennia.utils.utils import string_partial_matching from evennia.utils.containers import OPTION_CLASSES +class InMemorySaveHandler(object): + """ + Fallback SaveHandler, implementing a minimum of the required save mechanism + and storing data in memory. + + """ + def __init__(self): + self.storage = {} + + def add(self, key, value=None, **kwargs): + self.storage[key] = value + + def get(self, key, default=None, **kwargs): + return self.storage.get(key, default) + + class OptionHandler(object): """ - This is a generic Option handler meant for Typed Objects - anything that + This is a generic Option handler. It is commonly used implements AttributeHandler. Retrieve options eithers as properties on this handler or by using the .get method. This is used for Account.options but it could be used by Scripts or Objects just as easily. All it needs to be provided is an options_dict. + """ - def __init__(self, obj, options_dict=None, save_category=None): + def __init__(self, obj, options_dict=None, savefunc=None, loadfunc=None, + save_kwargs=None, load_kwargs=None): """ Initialize an OptionHandler. Args: - obj (TypedObject): The Typed Object this sits on. Obj MUST - implement the Evennia AttributeHandler or this will barf. + obj (object): The object this handler sits on. This is usually a TypedObject. options_dict (dict): A dictionary of option keys, where the values are options. The format of those tuples is: ('key', "Description to show", 'option_type', ) - save_category (str): The Options data will be stored to this - Attribute category on obj. + savefunc (callable): A callable for all options to call when saving itself. + It will be called as `savefunc(key, value, **save_kwargs)`. A common one + to pass would be AttributeHandler.add. + loadfunc (callable): A callable for all options to call when loading data into + itself. It will be called as `loadfunc(key, default=default, **load_kwargs)`. + A common one to pass would be AttributeHandler.get. + save_kwargs (any): Optional extra kwargs to pass into `savefunc` above. + load_kwargs (any): Optional extra kwargs to pass into `loadfunc` above. + Notes: + Both loadfunc and savefunc must be specified. If only one is given, the other + will be ignored and in-memory storage will be used. """ - if not options_dict: - options_dict = {} - self.options_dict = options_dict - self.save_category = save_category self.obj = obj + self.options_dict = {} if options_dict is None else options_dict - # This dictionary stores the in-memory Options by their key. Values are the Option objects. + if not savefunc and loadfunc: + self._in_memory_handler = InMemorySaveHandler() + savefunc = InMemorySaveHandler.add + loadfunc = InMemorySaveHandler.get + self.savefunc = savefunc + self.loadfunc = loadfunc + self.save_kwargs = {} if save_kwargs is None else save_kwargs + self.load_kwargs = {} if load_kwargs is None else load_kwargs + + # This dictionary stores the in-memory Options objects by their key for + # quick lookup. self.options = {} - # We use lazy-loading of each Option when it's called for, but it's - # good to have the save data on hand. - self.save_data = {s.key: s.value for s in obj.attributes.get( - category=save_category, return_list=True, return_obj=True) if s} - def __getattr__(self, key): - return self.get(key).value + return self.get(key) + + def _load_option(self, key): + """ + Loads option on-demand if it has not been loaded yet. + + Args: + key (str): The option being loaded. + + Returns: + + """ + desc, clsname, default_val = self.options_dict[key] + loaded_option = OPTION_CLASSES.get(clsname)(self, key, desc, default_val) + # store the value for future easy access + self.options[key] = loaded_option + return loaded_option def get(self, key, return_obj=False): """ @@ -59,13 +103,34 @@ class OptionHandler(object): """ if key not in self.options_dict: raise KeyError("Option not found!") - if key in self.options: - op_found = self.options[key] - else: - op_found = self._load_option(key) - if return_obj: - return op_found - return op_found.value + op_found = self.options.get(key) or self._load_option(key) + return op_found if return_obj else op_found.value + + def set(self, key, value, **kwargs): + """ + Change an individual option. + + Args: + key (str): The key of an option that can be changed. Allows partial matching. + value (str): The value that should be checked, coerced, and stored.: + kwargs (any, optional): These are passed into the Option's validation function, + save function and display function and allows to customize either. + + Returns: + value (any): Value stored in option, after validation. + + """ + if not key: + raise ValueError("Option field blank!") + match = string_partial_matching(list(self.options_dict.keys()), key, ret_index=False) + if not match: + raise ValueError("Option not found!") + if len(match) > 1: + raise ValueError(f"Multiple matches: {', '.join(match)}. Please be more specific.") + match = match[0] + op = self.get(match, return_obj=True) + op.set(value, **kwargs) + return op.value def all(self, return_objs=False): """ @@ -80,45 +145,3 @@ class OptionHandler(object): """ return [self.get(key, return_obj=return_objs) for key in self.options_dict] - - def _load_option(self, key): - """ - Loads option on-demand if it has not been loaded yet. - - Args: - key (str): The option being loaded. - - Returns: - - """ - desc, clsname, default_val = self.options_dict[key] - save_data = self.save_data.get(key, None) - self.obj.msg(save_data) - loaded_option = OPTION_CLASSES.get(clsname)(self, key, desc, default_val, save_data) - self.options[key] = loaded_option - return loaded_option - - def set(self, option, value, **kwargs): - """ - Change an individual option. - - Args: - option (str): The key of an option that can be changed. Allows partial matching. - value (str): The value that should be checked, coerced, and stored. - kwargs (any, optional): These are passed into the Option's validation function, - save function and display function and allows to customize either. - - Returns: - New value - """ - if not option: - raise ValueError("Option field blank!") - found = string_partial_matching(list(self.options_dict.keys()), option, ret_index=False) - if not found: - raise ValueError("Option not found!") - if len(found) > 1: - raise ValueError(f"That matched: {', '.join(found)}. Please be more specific.") - found = found[0] - op = self.get(found, return_obj=True) - op.set(value, **kwargs) - return op.display(**kwargs)