From b70d8ed70f89ed033ee4282e9cae808efea8c3aa Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 14 Apr 2019 15:37:34 +0200 Subject: [PATCH] Refactor containers for inheritance and delayed loading --- evennia/__init__.py | 2 +- evennia/accounts/accounts.py | 2 +- evennia/scripts/scripts.py | 3 + evennia/settings_default.py | 17 +- evennia/utils/containers.py | 206 +++++++++++------- evennia/utils/optionclasses.py | 166 +++++++------- evennia/utils/{option.py => optionhandler.py} | 2 +- ...alidatorfunctions.py => validatorfuncs.py} | 9 +- 8 files changed, 234 insertions(+), 173 deletions(-) rename evennia/utils/{option.py => optionhandler.py} (98%) rename evennia/utils/{validatorfunctions.py => validatorfuncs.py} (97%) diff --git a/evennia/__init__.py b/evennia/__init__.py index c88ef8df18..b903dafc2a 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -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 diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 160e2d41de..23a5fe0a1b 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -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 diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index 3f6e440ce3..5563a9391e 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -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. diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 26f6049805..69edc54f59 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -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 diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py index 7566f7b2cf..59fb2df9f2 100644 --- a/evennia/utils/containers.py +++ b/evennia/utils/containers.py @@ -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() diff --git a/evennia/utils/optionclasses.py b/evennia/utils/optionclasses.py index fe864fd14f..83558f34b6 100644 --- a/evennia/utils/optionclasses.py +++ b/evennia/utils/optionclasses.py @@ -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): diff --git a/evennia/utils/option.py b/evennia/utils/optionhandler.py similarity index 98% rename from evennia/utils/option.py rename to evennia/utils/optionhandler.py index 2792fa0238..7071a2902f 100644 --- a/evennia/utils/option.py +++ b/evennia/utils/optionhandler.py @@ -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): diff --git a/evennia/utils/validatorfunctions.py b/evennia/utils/validatorfuncs.py similarity index 97% rename from evennia/utils/validatorfunctions.py rename to evennia/utils/validatorfuncs.py index 41e5a8ffaf..a070a32445 100644 --- a/evennia/utils/validatorfunctions.py +++ b/evennia/utils/validatorfuncs.py @@ -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