Rework options/optionhandler to use custom save/load functions
This commit is contained in:
parent
f2d9391827
commit
10b3657ffb
3 changed files with 128 additions and 93 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue