Cleanups. Containers created and BaseOption done better.

This commit is contained in:
Andrew Bastien 2019-04-11 21:06:15 -04:00
parent 5bc9a42bb5
commit d96cf3b809
9 changed files with 159 additions and 234 deletions

View file

@ -198,6 +198,10 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
def sessions(self): def sessions(self):
return AccountSessionHandler(self) return AccountSessionHandler(self)
@lazy_property
def options(self):
return OptionHandler(self, options_dict=settings.OPTIONS_ACCOUNT_DEFAULT, save_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
def characters(self): def characters(self):
@ -1384,10 +1388,6 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
look_string = ("-" * 68) + "\n" + "".join(result) + "\n" + ("-" * 68) look_string = ("-" * 68) + "\n" + "".join(result) + "\n" + ("-" * 68)
return look_string return look_string
@lazy_property
def options(self):
return OptionHandler(self, options_dict=settings.ACCOUNT_OPTIONS, save_category='option')
class DefaultGuest(DefaultAccount): class DefaultGuest(DefaultAccount):
""" """

View file

@ -1,36 +0,0 @@
"""
Styles (playing off CSS) are a way to change the colors and symbols used for standardized
displays used in Evennia. Accounts all have a StyleHandler accessible via .style which
retrieves per-Account settings, falling back to the global settings contained in settings.py.
"""
from django.conf import settings
class StyleHandler(object):
category = 'style'
def __init__(self, acc):
self.acc = acc
def set(self, option, value):
pass
def get(self, option):
"""
Get the stored Style information from this Account's Attributes if possible.
If not, fallback to the Global.
Args:
option (str): The key of the Style to retrieve.
Returns:
String or None
"""
stored = self.acc.attributes.get(option, category=self.category)
if stored:
return stored
default = settings.DEFAULT_STYLES.get(option, None)
if default:
return default[2]
return None

View file

@ -886,12 +886,12 @@ class CmdStyle(COMMAND_DEFAULT_CLASS):
styles_table = self.style_table('Option', 'Description', 'Type', 'Value', width=78) styles_table = self.style_table('Option', 'Description', 'Type', 'Value', width=78)
for op_key in self.account.options.options_dict.keys(): for op_key in self.account.options.options_dict.keys():
op_found = self.account.options.get(op_key, return_obj=True) op_found = self.account.options.get(op_key, return_obj=True)
styles_table.add_row(op_key, op_found.description, op_found.expect_type, op_found.display()) styles_table.add_row(op_key, op_found.description, op_found.__class__.__name__, op_found.display())
self.msg(str(styles_table)) self.msg(str(styles_table))
def set(self): def set(self):
try: try:
result = self.account.options.set(self.lhs, self.rhs, account=self.account) result = self.account.options.set(self.lhs, self.rhs)
except ValueError as e: except ValueError as e:
self.msg(str(e)) self.msg(str(e))
return return

View file

@ -500,7 +500,7 @@ TYPECLASS_AGGRESSIVE_CACHE = True
# Option tuples are in this format: # Option tuples are in this format:
# ("Description", 'Option Class', 'Default Value') # ("Description", 'Option Class', 'Default Value')
ACCOUNT_OPTIONS = { OPTIONS_ACCOUNT_DEFAULT = {
'border_color': ('Headers, footers, table borders, etc.', 'Color', 'M'), 'border_color': ('Headers, footers, table borders, etc.', 'Color', 'M'),
'header_star_color': ('* inside Header lines.', 'Color', 'm'), 'header_star_color': ('* inside Header lines.', 'Color', 'm'),
'header_text_color': ('Text inside Header lines.', 'Color', 'w'), 'header_text_color': ('Text inside Header lines.', 'Color', 'w'),
@ -565,11 +565,11 @@ PROTOTYPEFUNC_MODULES = ["evennia.utils.prototypefuncs",
# Module holding validator functions. functions in later modules will # Module holding validator functions. functions in later modules will
# override those in earlier ones. # override those in earlier ones.
VALIDFUNC_MODULES = ['evennia.utils.validfuncs', ] VALIDATOR_MODULES = ['evennia.utils.validfuncs', ]
# Modules holding Option classes. Those in later modules will # Modules holding Option classes. Those in later modules will
# override ones in earlier modules. # override ones in earlier modules.
OPTIONCLASS_MODULES = ['evennia.utils.opclasses', ] OPTION_MODULES = ['evennia.utils.opclasses', ]
###################################################################### ######################################################################
# Default Account setup and access # Default Account setup and access

View file

@ -0,0 +1,47 @@
from django.conf import settings
from evennia.utils.utils import callables_from_module
class ValidContainer(object):
"""
Loads and stores the final list of VALIDATOR FUNCTIONS.
Can access these as properties or dictionary-contents.
"""
def __init__(self):
self.valid_storage = {}
for module in settings.VALIDATOR_MODULES:
self.valid_storage.update(callables_from_module(module))
def __getitem__(self, item):
return self.valid_storage.get(item, None)
def __getattr__(self, item):
return self[item]
# Ensure that we have a Singleton of ValidHandler that is always loaded... and only needs to be loaded once.
VALID_CONTAINER = ValidContainer()
class OptionContainer(object):
"""
Loads and stores the final list of OPTION CLASSES.
Can access these as properties or dictionary-contents.
"""
def __init__(self):
self.option_storage = {}
for module in settings.OPTION_MODULES:
self.option_storage.update(callables_from_module(module))
def __getitem__(self, item):
return self.option_storage.get(item, None)
def __getattr__(self, item):
return self[item]
# Ensure that we have a Singleton that keeps all loaded Options.
OPTION_CONTAINER = OptionContainer()

View file

@ -1,10 +1,11 @@
import datetime as _dt import datetime as _dt
from evennia import logger as _log
from evennia.utils.ansi import ANSIString as _ANSI from evennia.utils.ansi import ANSIString as _ANSI
from evennia.utils.validfuncs import _TZ_DICT from evennia.utils.validfuncs import _TZ_DICT
from evennia.utils.valid import VALID_HANDLER as _VAL from evennia.utils.containers import VALID_CONTAINER as _VAL
class _BaseOption(object): class BaseOption(object):
""" """
Abstract Class to deal with encapsulating individual Options. An Option has a name/key, a description Abstract Class to deal with encapsulating individual Options. An Option has a name/key, a description
to display in relevant commands and menus, and a default value. It saves to the owner's Attributes using to display in relevant commands and menus, and a default value. It saves to the owner's Attributes using
@ -17,9 +18,7 @@ class _BaseOption(object):
valid: Shortcut to the loaded VALID_HANDLER. valid: Shortcut to the loaded VALID_HANDLER.
valid_type (str): The key of the Validator this uses. valid_type (str): The key of the Validator this uses.
""" """
expect_type = '' validator_key = ''
valid = _VAL
valid_type = ''
def __str__(self): def __str__(self):
return self.key return self.key
@ -47,7 +46,16 @@ class _BaseOption(object):
# And it's not loaded until it's called upon to spit out its contents. # And it's not loaded until it's called upon to spit out its contents.
self.loaded = False self.loaded = False
def load(self): def display(self, **kwargs):
"""
Renders the Option's value as something pretty to look at.
Returns:
How the stored value should be projected to users. a raw timedelta is pretty ugly, y'know?
"""
return self.value
def _load(self):
""" """
Takes the provided save data, validates it, and gets this Option ready to use. Takes the provided save data, validates it, and gets this Option ready to use.
@ -56,17 +64,23 @@ class _BaseOption(object):
""" """
if self.save_data is not None: if self.save_data is not None:
try: try:
self.value_storage = self.valid_save(self.save_data) self.value_storage = self.deserialize(self.save_data)
self.loaded = True self.loaded = True
return True return True
except Exception as e: except Exception as e:
print(e) # need some kind of error message here! _log.log_trace(e)
return False return False
def customized(self): def _save(self):
return self.value_storage != self.default_value """
Exports the current value to an Attribute.
def valid_save(self, save_data): Returns:
None
"""
self.handler.obj.attributes.add(self.key, category=self.handler.save_category, value=self.serialize())
def deserialize(self, save_data):
""" """
Perform sanity-checking on the save data. This isn't the same as Validators, as Validators deal with Perform sanity-checking on the save data. This isn't the same as Validators, as Validators deal with
user input. save data might be a timedelta or a list or some other object. isinstance() is probably user input. save data might be a timedelta or a list or some other object. isinstance() is probably
@ -81,16 +95,18 @@ class _BaseOption(object):
""" """
return save_data return save_data
def clear(self): def serialize(self):
""" """
Resets this Option to default settings. Serializes the save data for Attribute storage if it's something complicated.
Returns: Returns:
self. Why? Whatever best handles the Attribute.
""" """
self.value_storage = None return self.value_storage
self.loaded = False
return self @property
def changed(self):
return self.value_storage != self.default_value
@property @property
def default(self): def default(self):
@ -99,13 +115,30 @@ 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 and self.save_data is not None:
self.load() self._load()
if self.loaded: if self.loaded:
return self.value_storage return self.value_storage
else: else:
return self.default return self.default
def validate(self, value, account): @value.setter
def value(self, value):
"""
Takes user input, presumed to be a string, and changes the value if it is a valid input.
Args:
value:
account:
Returns:
None
"""
final_value = self.validate(value)
self.value_storage = final_value
self.loaded = True
self._save()
def validate(self, value):
""" """
Validate user input, which is presumed to be a string. Validate user input, which is presumed to be a string.
@ -118,202 +151,128 @@ class _BaseOption(object):
Returns: Returns:
The results of a Validator call. Might be any kind of python object. The results of a Validator call. Might be any kind of python object.
""" """
return self.do_validate(value, account) return _VAL[self.validator_key](value, thing_name=self.key)
def do_validate(self, value, account):
"""
Second layer of abstraction on validation due to design choices.
Args:
value:
account:
Returns:
"""
return self.valid[self.valid_type](value, thing_name=self.key, account=account)
def set(self, value, account):
"""
Takes user input, presumed to be a string, and changes the value if it is a valid input.
Args:
value:
account:
Returns:
"""
final_value = self.validate(value, account)
self.value_storage = final_value
self.loaded = True
self.save()
return self.display()
def display(self):
"""
Renders the Option's value as something pretty to look at.
Returns:
How the stored value should be projected to users. a raw timedelta is pretty ugly, y'know?
"""
return self.value
def export(self):
"""
Serializes the save data for Attribute storage if it's something complicated.
Returns:
Whatever best handles the Attribute.
"""
return self.value_storage
def save(self):
"""
Exports the current value to an Attribute.
Returns:
None
"""
self.handler.obj.attributes.add(self.key, category=self.handler.save_category, value=self.export())
class Text(_BaseOption): class Text(BaseOption):
expect_type = 'Text' validator_key = 'text'
valid_type = 'text'
def do_validate(self, value, account): def deserialize(self, save_data):
if not str(value):
raise ValueError("Must enter some text!")
return str(value)
def valid_save(self, save_data):
got_data = str(save_data) got_data = str(save_data)
if not got_data: if not got_data:
raise ValueError(f"{self.key} expected Text data, got '{save_data}'") raise ValueError(f"{self.key} expected Text data, got '{save_data}'")
return got_data return got_data
class Email(_BaseOption): class Email(BaseOption):
expect_type = 'Email' validator_key = 'email'
valid_type = 'email'
def valid_save(self, save_data): def deserialize(self, save_data):
got_data = str(save_data) got_data = str(save_data)
if not got_data: if not got_data:
raise ValueError(f"{self.key} expected String data, got '{save_data}'") raise ValueError(f"{self.key} expected String data, got '{save_data}'")
return got_data return got_data
class Boolean(_BaseOption): class Boolean(BaseOption):
expect_type = 'Boolean' validator_key = 'boolean'
valid_type = 'boolean'
def display(self): def display(self, **kwargs):
if self.value: if self.value:
return '1 - On/True' return '1 - On/True'
return '0 - Off/False' return '0 - Off/False'
def export(self): def serialize(self):
return self.value return self.value
def valid_save(self, save_data): def deserialize(self, save_data):
if not isinstance(save_data, bool): if not isinstance(save_data, bool):
raise ValueError(f"{self.key} expected Boolean, got '{save_data}'") raise ValueError(f"{self.key} expected Boolean, got '{save_data}'")
return save_data return save_data
class Color(_BaseOption): class Color(BaseOption):
expect_type = 'Color' validator_key = 'color'
valid_type = 'color'
def display(self): def display(self, **kwargs):
return f'{self.value} - |{self.value}this|n' return f'{self.value} - |{self.value}this|n'
def valid_save(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(_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
class Timezone(_BaseOption): class Timezone(BaseOption):
expect_type = 'Timezone' validator_key = 'timezone'
valid_type = 'timezone'
@property @property
def default(self): def default(self):
return _TZ_DICT[self.default_value] return _TZ_DICT[self.default_value]
def valid_save(self, save_data): def deserialize(self, save_data):
if save_data not in _TZ_DICT: if save_data not in _TZ_DICT:
raise ValueError(f"{self.key} expected Timezone Data, got '{save_data}'") raise ValueError(f"{self.key} expected Timezone Data, got '{save_data}'")
return _TZ_DICT[save_data] return _TZ_DICT[save_data]
def export(self): def serialize(self):
return str(self.value_storage) return str(self.value_storage)
class UnsignedInteger(_BaseOption): class UnsignedInteger(BaseOption):
expect_type = 'Whole Number 0+' validator_key = 'unsigned_integer'
valid_type = 'unsigned_integer'
def valid_save(self, save_data): def deserialize(self, save_data):
if isinstance(save_data, int) and save_data >= 0: if isinstance(save_data, int) and save_data >= 0:
return save_data return save_data
raise ValueError(f"{self.key} expected Whole Number 0+, got '{save_data}'") raise ValueError(f"{self.key} expected Whole Number 0+, got '{save_data}'")
class SignedInteger(_BaseOption): class SignedInteger(BaseOption):
expect_type = 'Whole Number' validator_key = 'signed_integer'
valid_type = 'signed_integer'
def valid_save(self, save_data): def deserialize(self, save_data):
if isinstance(save_data, int): if isinstance(save_data, int):
return save_data return save_data
raise ValueError(f"{self.key} expected Whole Number, got '{save_data}'") raise ValueError(f"{self.key} expected Whole Number, got '{save_data}'")
class PositiveInteger(_BaseOption): class PositiveInteger(BaseOption):
expect_type = 'Whole Number 1+' validator_key = 'positive_integer'
valid_type = 'positive_integer'
def valid_save(self, save_data): def deserialize(self, save_data):
if isinstance(save_data, int) and save_data > 0: if isinstance(save_data, int) and save_data > 0:
return save_data return save_data
raise ValueError(f"{self.key} expected Whole Number 1+, got '{save_data}'") raise ValueError(f"{self.key} expected Whole Number 1+, got '{save_data}'")
class Duration(_BaseOption): class Duration(BaseOption):
expect_type = 'Duration' validator_key = 'duration'
valid_type = 'duration'
def valid_save(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 _dt.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 export(self): def serialize(self):
return self.value_storage.seconds return self.value_storage.seconds
class Datetime(_BaseOption): class Datetime(BaseOption):
expect_type = 'Datetime' validator_key = 'datetime'
valid_type = 'datetime'
def valid_save(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 _dt.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 export(self): def serialize(self):
return int(self.value_storage.strftime('%s')) return int(self.value_storage.strftime('%s'))
class Future(Datetime): class Future(Datetime):
expect_type = 'Future Datetime' validator_key = 'future'
valid_type = 'future'
class Lock(Text): class Lock(Text):
expect_type = 'Lock String' validator_key = 'lock'
valid_type = 'lock'

View file

@ -1,27 +1,5 @@
from django.conf import settings from evennia.utils.utils import string_partial_matching
from evennia.utils.utils import string_partial_matching, callables_from_module from evennia.utils.containers import OPTION_CONTAINER
class OptionManager(object):
"""
Loads and stores the final list of OPTION CLASSES.
Can access these as properties or dictionary-contents.
"""
def __init__(self):
self.option_storage = {}
for module in settings.OPTIONCLASS_MODULES:
self.option_storage.update(callables_from_module(module))
def __getitem__(self, item):
return self.option_storage.get(item, None)
def __getattr__(self, item):
return self[item]
# Ensure that we have a Singleton that keeps all loaded Options.
OPTION_MANAGER = OptionManager()
class OptionHandler(object): class OptionHandler(object):
@ -58,7 +36,7 @@ class OptionHandler(object):
# We use lazy-loading of each Option when it's called for, but it's good to have the save data # We use lazy-loading of each Option when it's called for, but it's good to have the save data
# on hand. # on hand.
self.save_data = {s.key: s.value for s in obj.attributes.get(category=save_category, self.save_data = {s.key: s.value for s in obj.attributes.get(category=save_category,
return_list=True, return_obj=True) if s} return_list=True, return_obj=True) if s}
def __getitem__(self, item): def __getitem__(self, item):
return self.get(item).value return self.get(item).value
@ -78,19 +56,17 @@ class OptionHandler(object):
option_def = self.options_dict[key] option_def = self.options_dict[key]
save_data = self.save_data.get(key, None) save_data = self.save_data.get(key, None)
self.obj.msg(save_data) self.obj.msg(save_data)
loaded_option = OPTION_MANAGER[option_def[1]](self, key, option_def[0], option_def[2], save_data) loaded_option = OPTION_CONTAINER[option_def[1]](self, key, option_def[0], option_def[2], save_data)
self.options[key] = loaded_option self.options[key] = loaded_option
return loaded_option return loaded_option
def set(self, option, value, account): def set(self, option, value):
""" """
Change an individual option. Change an individual option.
Args: Args:
option (str): The key of an option that can be changed. Allows partial matching. 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. value (str): The value that should be checked, coerced, and stored.
account (AccountDB): The Account performing the setting. Necessary due to other
option lookups like timezone.
Returns: Returns:
New value New value
@ -104,4 +80,8 @@ class OptionHandler(object):
raise ValueError(f"That matched: {', '.join(found)}. Please be more specific.") raise ValueError(f"That matched: {', '.join(found)}. Please be more specific.")
found = found[0] found = found[0]
op = self.get(found, return_obj=True) op = self.get(found, return_obj=True)
return op.set(value, account) op.value = value
return op.display()

View file

@ -1,25 +0,0 @@
from django.conf import settings
from evennia.utils.utils import callables_from_module
class ValidHandler(object):
"""
Loads and stores the final list of VALIDATOR FUNCTIONS.
Can access these as properties or dictionary-contents.
"""
def __init__(self):
self.valid_storage = {}
for module in settings.VALIDFUNC_MODULES:
self.valid_storage.update(callables_from_module(module))
def __getitem__(self, item):
return self.valid_storage.get(item, None)
def __getattr__(self, item):
return self[item]
# Ensure that we have a Singleton of ValidHandler that is always loaded... and only needs to be loaded once.
VALID_HANDLER = ValidHandler()

View file

@ -47,8 +47,8 @@ def datetime(entry, thing_name='Datetime', account=None, from_tz=None, **kwargs)
if not entry: if not entry:
raise ValueError(f"No {thing_name} entered!") raise ValueError(f"No {thing_name} entered!")
if not from_tz: if not from_tz:
from_tz = _pytz['UTC'] from_tz = _pytz.UTC
utc = _pytz['UTC'] utc = _pytz.UTC
now = _dt.datetime.utcnow().replace(tzinfo=utc) now = _dt.datetime.utcnow().replace(tzinfo=utc)
cur_year = now.strftime('%Y') cur_year = now.strftime('%Y')
split_time = entry.split(' ') split_time = entry.split(' ')