Rework options/optionhandler to use custom save/load functions

This commit is contained in:
Griatch 2019-04-14 20:29:21 +02:00
parent f2d9391827
commit 10b3657ffb
3 changed files with 128 additions and 93 deletions

View file

@ -200,7 +200,12 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
@lazy_property @lazy_property
def options(self): 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! # Do not make this a lazy property; the web UI will not refresh it!
@property @property

View file

@ -1,6 +1,6 @@
import datetime as _dt import datetime
from evennia import logger as _log from evennia import logger
from evennia.utils.ansi import ANSIString as _ANSI from evennia.utils.ansi import strip_ansi
from evennia.utils.validatorfuncs import _TZ_DICT from evennia.utils.validatorfuncs import _TZ_DICT
from evennia.utils.containers import VALIDATOR_FUNCS from evennia.utils.containers import VALIDATOR_FUNCS
from evennia.utils.utils import crop from evennia.utils.utils import crop
@ -29,7 +29,7 @@ class BaseOption(object):
def __repr__(self): def __repr__(self):
return str(self) return str(self)
def __init__(self, handler, key, description, default, save_data=None): def __init__(self, handler, key, description, default):
""" """
Args: Args:
@ -38,14 +38,12 @@ class BaseOption(object):
Must be unique per OptionHandler. Must be unique per OptionHandler.
description (str): What this Option's text will show in commands and menus. description (str): What this Option's text will show in commands and menus.
default: A default value for this Option. default: A default value for this Option.
save_data: Whatever was saved to Attributes. This differs by Option.
""" """
self.handler = handler self.handler = handler
self.key = key self.key = key
self.default_value = default self.default_value = default
self.description = description self.description = description
self.save_data = save_data
# Value Storage contains None until the Option is loaded. # Value Storage contains None until the Option is loaded.
self.value_storage = None self.value_storage = None
@ -63,7 +61,7 @@ class BaseOption(object):
@property @property
def value(self): def value(self):
if not self.loaded and self.save_data is not None: if not self.loaded:
self.load() self.load()
if self.loaded: if self.loaded:
return self.value_storage return self.value_storage
@ -98,28 +96,36 @@ class BaseOption(object):
Boolean: Whether loading was successful. Boolean: Whether loading was successful.
""" """
if self.save_data is not None: loadfunc = self.handler.loadfunc
try: load_kwargs = self.handler.load_kwargs
self.value_storage = self.deserialize(self.save_data)
self.loaded = True print("load", self.key, loadfunc, load_kwargs)
return True try:
except Exception as e: self.value_storage = self.deserialize(
_log.log_trace(e) loadfunc(self.key, default=self.default_value, **load_kwargs))
return False except Exception:
logger.log_trace()
return False
self.loaded = True
return True
def save(self, **kwargs): 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: Kwargs:
any (any): Not used by default. These are passed in from self.set any (any): Not used by default. These are passed in from self.set
and allows the option to let the caller customize saving and allows the option to let the caller customize saving by
if desrired. overriding or extend the default save kwargs
""" """
self.handler.obj.attributes.add(self.key, value = self.serialize()
category=self.handler.save_category, save_kwargs = {**self.handler.save_kwargs, **kwargs}
value=self.serialize()) savefunc = self.handler.savefunc
print("save:", self.key, value, savefunc, save_kwargs)
savefunc(self.key, value=value, **save_kwargs)
def deserialize(self, save_data): def deserialize(self, save_data):
""" """
@ -160,9 +166,10 @@ class BaseOption(object):
entries are processed. entries are processed.
Returns: 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): def display(self, **kwargs):
""" """
@ -227,7 +234,7 @@ class Color(BaseOption):
return f'{self.value} - |{self.value}this|n' return f'{self.value} - |{self.value}this|n'
def deserialize(self, save_data): 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}'") raise ValueError(f"{self.key} expected Color Code, got '{save_data}'")
return save_data return save_data
@ -280,7 +287,7 @@ class Duration(BaseOption):
def deserialize(self, save_data): def deserialize(self, save_data):
if isinstance(save_data, int): 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}'") raise ValueError(f"{self.key} expected Timedelta in seconds, got '{save_data}'")
def serialize(self): def serialize(self):
@ -292,7 +299,7 @@ class Datetime(BaseOption):
def deserialize(self, save_data): def deserialize(self, save_data):
if isinstance(save_data, int): 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}'") raise ValueError(f"{self.key} expected UTC Datetime in EPOCH format, got '{save_data}'")
def serialize(self): def serialize(self):

View file

@ -2,46 +2,90 @@ from evennia.utils.utils import string_partial_matching
from evennia.utils.containers import OPTION_CLASSES 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): 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 implements AttributeHandler. Retrieve options eithers as properties on
this handler or by using the .get method. this handler or by using the .get method.
This is used for Account.options but it could be used by Scripts or Objects 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. 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. Initialize an OptionHandler.
Args: Args:
obj (TypedObject): The Typed Object this sits on. Obj MUST obj (object): The object this handler sits on. This is usually a TypedObject.
implement the Evennia AttributeHandler or this will barf.
options_dict (dict): A dictionary of option keys, where the values options_dict (dict): A dictionary of option keys, where the values
are options. The format of those tuples is: ('key', "Description to are options. The format of those tuples is: ('key', "Description to
show", 'option_type', <default value>) show", 'option_type', <default value>)
save_category (str): The Options data will be stored to this savefunc (callable): A callable for all options to call when saving itself.
Attribute category on obj. 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.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 = {} 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): 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): def get(self, key, return_obj=False):
""" """
@ -59,13 +103,34 @@ class OptionHandler(object):
""" """
if key not in self.options_dict: if key not in self.options_dict:
raise KeyError("Option not found!") raise KeyError("Option not found!")
if key in self.options: op_found = self.options.get(key) or self._load_option(key)
op_found = self.options[key] return op_found if return_obj else op_found.value
else:
op_found = self._load_option(key) def set(self, key, value, **kwargs):
if return_obj: """
return op_found Change an individual option.
return op_found.value
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): 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] 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)