Refactor containers for inheritance and delayed loading

This commit is contained in:
Griatch 2019-04-14 15:37:34 +02:00
parent b024c17f8a
commit b70d8ed70f
8 changed files with 234 additions and 173 deletions

View file

@ -223,9 +223,9 @@ def _init():
from .server.sessionhandler import SESSION_HANDLER
from .comms.channelhandler import CHANNEL_HANDLER
from .scripts.monitorhandler import MONITOR_HANDLER
from .utils.containers import GLOBAL_SCRIPTS
# containers
from .utils.containers import GLOBAL_SCRIPTS
from .utils.containers import VALIDATOR_FUNCS
from .utils.containers import OPTION_CLASSES

View file

@ -20,7 +20,6 @@ from django.utils.module_loading import import_string
from evennia.typeclasses.models import TypeclassBase
from evennia.accounts.manager import AccountManager
from evennia.accounts.models import AccountDB
from evennia.utils.option import OptionHandler
from evennia.objects.models import ObjectDB
from evennia.comms.models import ChannelDB
from evennia.commands import cmdhandler
@ -33,6 +32,7 @@ from evennia.utils.utils import (lazy_property, to_str,
from evennia.typeclasses.attributes import NickHandler
from evennia.scripts.scripthandler import ScriptHandler
from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.utils.optionhandler import OptionHandler
from django.utils.translation import ugettext as _
from future.utils import with_metaclass

View file

@ -168,6 +168,9 @@ class ScriptBase(with_metaclass(TypeclassBase, ScriptDB)):
def __str__(self):
return "<{cls} {key}>".format(cls=self.__class__.__name__, key=self.key)
def __repr__(self):
return str(self)
def _start_task(self):
"""
Start task runner.

View file

@ -493,14 +493,13 @@ TYPECLASS_AGGRESSIVE_CACHE = True
# Options and validators
######################################################################
# Replace or add entries in this dictionary to specify options available
# on accounts. An option goes goteth
# Options available on Accounts. Each such option is described by a
# class available from evennia.OPTION_CLASSES, in turn making use
# of validators from evennia.VALIDATOR_FUNCS to validate input when
# the user changes an option. The options are accessed through the
# `Account.options` handler.
# Evennia uses for commands. Or add more entries! Accounts can change
# their own settings with a command, but this sets down defaults.
# Option tuples are in this format:
# ("Description", 'Option Class', 'Default Value')
# ("Description", 'Option Class name in evennia.OPTIONS_CLASSES', 'Default Value')
OPTIONS_ACCOUNT_DEFAULT = {
'border_color': ('Headers, footers, table borders, etc.', 'Color', 'M'),
@ -518,12 +517,12 @@ OPTIONS_ACCOUNT_DEFAULT = {
# Modules holding Option classes, responsible for serializing the option and
# calling validator functions on it. Same-named functions in modules added
# later in this list will override those added earlier.
OPTION_MODULES = ['evennia.utils.optionclasses', ]
OPTION_CLASS_MODULES = ['evennia.utils.optionclasses', ]
# Module holding validator functions. These are used as a resource for
# validating options, but can also be used as input validators in general.#
# Same-named functions in modules added later in this list will override those
# added earlier.
VALIDATOR_MODULES = ['evennia.utils.validatorfunctions', ]
VALIDATOR_FUNC_MODULES = ['evennia.utils.validatorfuncs', ]
######################################################################
# Batch processors

View file

@ -1,6 +1,14 @@
"""
Containers
Containers are storage classes usually initialized from a setting. They
represent Singletons and acts as a convenient place to find resources (
available as properties on the singleton)
evennia.GLOBAL_SCRIPTS
evennia.VALIDATOR_FUNCS
evennia.OPTION_CLASSES
"""
@ -9,7 +17,82 @@ from evennia.utils.utils import class_from_module, callables_from_module
from evennia.utils import logger
class GlobalScriptContainer(object):
class Container(object):
"""
Base container class. A container is simply a storage object whose
properties can be acquired as a property on it. This is generally
considered a read-only affair.
The container is initialized by a list of modules containing callables.
"""
storage_modules = []
def __init__(self):
"""
Read data from module.
"""
self.loaded_data = None
def _load_data(self):
"""
Delayed import to avoid eventual circular imports from inside
the storage modules.
"""
if self.loaded_data is None:
for module in self.storage_modules:
self.loaded_data.update(callables_from_module(module))
def __getattr__(self, key):
self._load_data()
return self.loaded_data.get(key)
def get(self, key):
"""
Retrive data by key (in case of not knowing it beforehand).
Args:
key (str): The name of the script.
Returns:
any (any): The data loaded on this container.
"""
return self.__getattr__(key)
def all(self):
"""
Get all stored data
Returns:
scripts (list): All global script objects stored on the container.
"""
self._load_data()
return list(self.loaded_data.values())
class ValidatorContainer(Container):
"""
Loads and stores the final list of VALIDATOR FUNCTIONS.
Can access these as properties or dictionary-contents.
"""
storage_modules = settings.VALIDATOR_FUNC_MODULES
class OptionContainer(Container):
"""
Loads and stores the final list of OPTION CLASSES.
Can access these as properties or dictionary-contents.
"""
storage_modules = settings.OPTION_CLASS_MODULES
class GlobalScriptContainer(Container):
"""
Simple Handler object loaded by the Evennia API to contain and manage a
game's Global Scripts. Scripts to start are defined by
@ -19,45 +102,56 @@ class GlobalScriptContainer(object):
import evennia
evennia.GLOBAL_SCRIPTS.scriptname
"""
Note:
This does not use much of the BaseContainer since it's not loading
callables from settings but a custom dict of tuples.
"""
def __init__(self):
"""
Initialize the container by preparing scripts. Lazy-load only when the
script is requested.
Note: We must delay loading of typeclasses since this module may get
initialized before Scripts are actually initialized.
"""
self.script_data = {key: {} if data is None else data
self.loaded_data = {key: {} if data is None else data
for key, data in settings.GLOBAL_SCRIPTS.items()}
self.script_storage = {}
self.typeclass_storage = {}
for key, data in self.script_data.items():
try:
typeclass = data.get('typeclass', settings.BASE_SCRIPT_TYPECLASS)
self.typeclass_storage[key] = class_from_module(typeclass)
except ImportError as err:
logger.log_err(f"GlobalContainer could not start global script {key}: {err}")
def __getitem__(self, key):
if key not in self.typeclass_storage:
# this script is unknown to the container
return None
# (re)create script on-demand
return self.script_storage.get(key) or self._load_script(key)
self.typeclass_storage = None
def __getattr__(self, key):
return self[key]
if key not in self.loaded_data:
return None
return self.script_storage.get(key) or self._load_script(key)
def _load_data(self):
"""
This delayed import avoids trying to load Scripts before they are
initialized.
"""
if self.typeclass_storage is None:
self.typeclass_storage = {}
for key, data in self.loaded_data.items():
try:
typeclass = data.get('typeclass', settings.BASE_SCRIPT_TYPECLASS)
self.typeclass_storage[key] = class_from_module(typeclass)
except ImportError as err:
logger.log_err(
f"GlobalScriptContainer could not start global script {key}: {err}")
def _load_script(self, key):
self._load_data()
typeclass = self.typeclass_storage[key]
found = typeclass.objects.filter(db_key=key).first()
interval = self.script_data[key].get('interval', None)
start_delay = self.script_data[key].get('start_delay', None)
repeats = self.script_data[key].get('repeats', 0)
desc = self.script_data[key].get('desc', '')
interval = self.loaded_data[key].get('interval', None)
start_delay = self.loaded_data[key].get('start_delay', None)
repeats = self.loaded_data[key].get('repeats', 0)
desc = self.loaded_data[key].get('desc', '')
if not found:
new_script, errors = typeclass.create(key=key, persistent=True,
@ -81,20 +175,6 @@ class GlobalScriptContainer(object):
self.script_storage[key] = found
return found
def get(self, key):
"""
Retrive script by key (in case of not knowing it beforehand).
Args:
key (str): The name of the script.
Returns:
script (Script): The named global script.
"""
# note that this will recreate the script if it doesn't exist/was lost
return self[key]
def all(self):
"""
Get all scripts.
@ -103,53 +183,11 @@ class GlobalScriptContainer(object):
scripts (list): All global script objects stored on the container.
"""
return list(self.script_storage.values())
return [self.__getattr__(key) for key in self.loaded_data]
# Create singleton of the GlobalHandler for the API.
# Create all singletons
GLOBAL_SCRIPTS = GlobalScriptContainer()
class ValidatorContainer(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_FUNC_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.
VALIDATOR_FUNCS = ValidatorContainer()
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_CLASS_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 Option classes
OPTION_CLASSES = OptionContainer()

View file

@ -1,21 +1,23 @@
import datetime as _dt
from evennia import logger as _log
from evennia.utils.ansi import ANSIString as _ANSI
from evennia.utils.validatorfunctions import _TZ_DICT
from evennia.utils.containers import VALIDATOR_CONTAINER as _VAL
from evennia.utils.validatorfuncs import _TZ_DICT
from evennia.utils.containers import VALIDATOR_FUNCS
class BaseOption(object):
"""
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
its Handler's save category.
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 its Handler's save
category.
Designed to be extremely overloadable as some options can be cantankerous.
Properties:
valid: Shortcut to the loaded VALID_HANDLER.
validator_key (str): The key of the Validator this uses.
"""
validator_key = ''
@ -27,11 +29,12 @@ class BaseOption(object):
Args:
handler (OptionHandler): The OptionHandler that 'owns' this Option.
key (str): The name this will be used for storage in a dictionary. Must be unique per
OptionHandler.
key (str): The name this will be used for storage in a dictionary.
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
@ -45,64 +48,6 @@ class BaseOption(object):
# And it's not loaded until it's called upon to spit out its contents.
self.loaded = False
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.
Returns:
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
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.serialize())
def deserialize(self, save_data):
"""
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
very useful here.
Args:
save_data: The data to check.
Returns:
Arbitrary: Whatever the Option needs to track, like a string or a datetime. Not the same as what
users are SHOWN.
"""
return save_data
def serialize(self):
"""
Serializes the save data for Attribute storage if it's something complicated.
Returns:
Whatever best handles the Attribute.
"""
return self.value_storage
@property
def changed(self):
return self.value_storage != self.default_value
@ -126,18 +71,77 @@ class BaseOption(object):
def set(self, value, **kwargs):
"""
Takes user input, presumed to be a string, and changes the value if it is a valid input.
Takes user input and stores appropriately. This method allows for
passing extra instructions into the validator.
Args:
value: The new value of this Option.
value (str): The new value of this Option.
kwargs (any): Any kwargs will be passed into
`self.validate(value, **kwargs)` and `self.save(**kwargs)`.
Returns:
None
"""
final_value = self.validate(value, **kwargs)
self.value_storage = final_value
self.loaded = True
self.save()
self.save(**kwargs)
def load(self):
"""
Takes the provided save data, validates it, and gets this Option ready to use.
Returns:
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
def save(self, **kwargs):
"""
Stores the current value (to an Attribute by default).
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.
"""
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 as it is loaded from storage.
This isn't the same as what validator-functions provide (those work on
user input). For example, save data might be a timedelta or a list or
some other object.
Args:
save_data: The data to check.
Returns:
any (any): Whatever the Option needs to track, like a string or a
datetime. The display hook is responsible for what is actually
displayed to user.
"""
return save_data
def serialize(self):
"""
Serializes the save data for Attribute storage.
Returns:
any (any): Whatever is best for storage.
"""
return self.value_storage
def validate(self, value, **kwargs):
"""
@ -145,14 +149,28 @@ class BaseOption(object):
Args:
value (str): User input.
account (AccountDB): The Account that is performing the validation. This is necessary because of
other settings which may affect the check, such as an Account's timezone affecting how their
datetime entries are processed.
account (AccountDB): The Account that is performing the validation.
This is necessary because of other settings which may affect the
check, such as an Account's timezone affecting how their datetime
entries are processed.
Returns:
The results of a Validator call. Might be any kind of python object.
"""
return _VAL[self.validator_key](value, thing_name=self.key, **kwargs)
return VALIDATOR_FUNCS[self.validator_key](value, thing_name=self.key, **kwargs)
def display(self, **kwargs):
"""
Renders the Option's value as something pretty to look at.
Returns:
str: How the stored value should be projected to users (e.g. a raw
timedelta is pretty ugly).
"""
return self.value
class Text(BaseOption):

View file

@ -1,5 +1,5 @@
from evennia.utils.utils import string_partial_matching
from evennia.utils.containers import OPTION_CONTAINER
from evennia.utils.containers import OPTION_CLASSES
class OptionHandler(object):

View file

@ -13,17 +13,20 @@ import pytz as _pytz
import datetime as _dt
from django.core.exceptions import ValidationError as _error
from django.core.validators import validate_email as _val_email
from evennia.utils.ansi import ANSIString as _ansi
from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import string_partial_matching as _partial
_TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones}
def color(entry, thing_name='Color', **kwargs):
"""
The color should be just a color character, so 'r' if red color is desired.
"""
if not entry:
raise ValueError(f"Nothing entered for a {thing_name}!")
test_str = _ansi('|%s|n' % entry)
if len(test_str):
test_str = strip_ansi(f'|{entry}|n')
if test_str:
raise ValueError(f"'{entry}' is not a valid {thing_name}.")
return entry