mirror of
https://github.com/evennia/evennia.git
synced 2026-03-25 09:16:32 +01:00
Refactor containers for inheritance and delayed loading
This commit is contained in:
parent
b024c17f8a
commit
b70d8ed70f
8 changed files with 234 additions and 173 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -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
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue