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
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

View file

@ -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):

View file

@ -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', <default value>)
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)