Refactor containers for inheritance and delayed loading

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

View file

@ -223,9 +223,9 @@ def _init():
from .server.sessionhandler import SESSION_HANDLER from .server.sessionhandler import SESSION_HANDLER
from .comms.channelhandler import CHANNEL_HANDLER from .comms.channelhandler import CHANNEL_HANDLER
from .scripts.monitorhandler import MONITOR_HANDLER from .scripts.monitorhandler import MONITOR_HANDLER
from .utils.containers import GLOBAL_SCRIPTS
# containers # containers
from .utils.containers import GLOBAL_SCRIPTS
from .utils.containers import VALIDATOR_FUNCS from .utils.containers import VALIDATOR_FUNCS
from .utils.containers import OPTION_CLASSES 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.typeclasses.models import TypeclassBase
from evennia.accounts.manager import AccountManager from evennia.accounts.manager import AccountManager
from evennia.accounts.models import AccountDB from evennia.accounts.models import AccountDB
from evennia.utils.option import OptionHandler
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.comms.models import ChannelDB from evennia.comms.models import ChannelDB
from evennia.commands import cmdhandler 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.typeclasses.attributes import NickHandler
from evennia.scripts.scripthandler import ScriptHandler from evennia.scripts.scripthandler import ScriptHandler
from evennia.commands.cmdsethandler import CmdSetHandler from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.utils.optionhandler import OptionHandler
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from future.utils import with_metaclass from future.utils import with_metaclass

View file

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

View file

@ -493,14 +493,13 @@ TYPECLASS_AGGRESSIVE_CACHE = True
# Options and validators # Options and validators
###################################################################### ######################################################################
# Replace or add entries in this dictionary to specify options available # Options available on Accounts. Each such option is described by a
# on accounts. An option goes goteth # 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 # ("Description", 'Option Class name in evennia.OPTIONS_CLASSES', 'Default Value')
# their own settings with a command, but this sets down defaults.
# Option tuples are in this format:
# ("Description", 'Option Class', 'Default Value')
OPTIONS_ACCOUNT_DEFAULT = { OPTIONS_ACCOUNT_DEFAULT = {
'border_color': ('Headers, footers, table borders, etc.', 'Color', 'M'), '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 # Modules holding Option classes, responsible for serializing the option and
# calling validator functions on it. Same-named functions in modules added # calling validator functions on it. Same-named functions in modules added
# later in this list will override those added earlier. # 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 # Module holding validator functions. These are used as a resource for
# validating options, but can also be used as input validators in general.# # 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 # Same-named functions in modules added later in this list will override those
# added earlier. # added earlier.
VALIDATOR_MODULES = ['evennia.utils.validatorfunctions', ] VALIDATOR_FUNC_MODULES = ['evennia.utils.validatorfuncs', ]
###################################################################### ######################################################################
# Batch processors # Batch processors

View file

@ -1,6 +1,14 @@
""" """
Containers 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 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 Simple Handler object loaded by the Evennia API to contain and manage a
game's Global Scripts. Scripts to start are defined by game's Global Scripts. Scripts to start are defined by
@ -19,45 +102,56 @@ class GlobalScriptContainer(object):
import evennia import evennia
evennia.GLOBAL_SCRIPTS.scriptname 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): def __init__(self):
""" """
Initialize the container by preparing scripts. Lazy-load only when the Initialize the container by preparing scripts. Lazy-load only when the
script is requested. 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()} for key, data in settings.GLOBAL_SCRIPTS.items()}
self.script_storage = {} self.script_storage = {}
self.typeclass_storage = {} self.typeclass_storage = None
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)
def __getattr__(self, key): 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): def _load_script(self, key):
self._load_data()
typeclass = self.typeclass_storage[key] typeclass = self.typeclass_storage[key]
found = typeclass.objects.filter(db_key=key).first() found = typeclass.objects.filter(db_key=key).first()
interval = self.script_data[key].get('interval', None) interval = self.loaded_data[key].get('interval', None)
start_delay = self.script_data[key].get('start_delay', None) start_delay = self.loaded_data[key].get('start_delay', None)
repeats = self.script_data[key].get('repeats', 0) repeats = self.loaded_data[key].get('repeats', 0)
desc = self.script_data[key].get('desc', '') desc = self.loaded_data[key].get('desc', '')
if not found: if not found:
new_script, errors = typeclass.create(key=key, persistent=True, new_script, errors = typeclass.create(key=key, persistent=True,
@ -81,20 +175,6 @@ class GlobalScriptContainer(object):
self.script_storage[key] = found self.script_storage[key] = found
return 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): def all(self):
""" """
Get all scripts. Get all scripts.
@ -103,53 +183,11 @@ class GlobalScriptContainer(object):
scripts (list): All global script objects stored on the container. 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() 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() 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() OPTION_CLASSES = OptionContainer()

View file

@ -1,21 +1,23 @@
import datetime as _dt import datetime as _dt
from evennia import logger as _log 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.validatorfunctions import _TZ_DICT from evennia.utils.validatorfuncs import _TZ_DICT
from evennia.utils.containers import VALIDATOR_CONTAINER as _VAL from evennia.utils.containers import VALIDATOR_FUNCS
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
to display in relevant commands and menus, and a default value. It saves to the owner's Attributes using a name/key, a description to display in relevant commands and menus, and a
its Handler's save category. 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. Designed to be extremely overloadable as some options can be cantankerous.
Properties: Properties:
valid: Shortcut to the loaded VALID_HANDLER. valid: Shortcut to the loaded VALID_HANDLER.
validator_key (str): The key of the Validator this uses. validator_key (str): The key of the Validator this uses.
""" """
validator_key = '' validator_key = ''
@ -27,11 +29,12 @@ class BaseOption(object):
Args: Args:
handler (OptionHandler): The OptionHandler that 'owns' this Option. 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 key (str): The name this will be used for storage in a dictionary.
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. save_data: Whatever was saved to Attributes. This differs by Option.
""" """
self.handler = handler self.handler = handler
self.key = key 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. # And it's not loaded until it's called upon to spit out its contents.
self.loaded = False 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 @property
def changed(self): def changed(self):
return self.value_storage != self.default_value return self.value_storage != self.default_value
@ -126,18 +71,77 @@ class BaseOption(object):
def set(self, value, **kwargs): 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: 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) final_value = self.validate(value, **kwargs)
self.value_storage = final_value self.value_storage = final_value
self.loaded = True 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): def validate(self, value, **kwargs):
""" """
@ -145,14 +149,28 @@ class BaseOption(object):
Args: Args:
value (str): User input. value (str): User input.
account (AccountDB): The Account that is performing the validation. This is necessary because of account (AccountDB): The Account that is performing the validation.
other settings which may affect the check, such as an Account's timezone affecting how their This is necessary because of other settings which may affect the
datetime entries are processed. check, such as an Account's timezone affecting how their datetime
entries are processed.
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 _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): class Text(BaseOption):

View file

@ -1,5 +1,5 @@
from evennia.utils.utils import string_partial_matching 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): class OptionHandler(object):

View file

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