diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf8f173d2..f270ae77f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,9 @@ Web/Django standard initiative (@strikaco) - `evennia.MONITOR_HANDLER.all` now takes keyword argument `obj` to only retrieve monitors from that specific Object (rather than all monitors in the entire handler). - Support adding `\f` in command doc strings to force where EvMore puts page breaks. +- Validation Functions now added with standard API to homogenize user input validation. +- Option Classes added to make storing user-options easier and smoother. +- `evennia.VALIDATOR_CONTAINER` and `evennia.OPTION_CONTAINER` added to load these. ### Contribs diff --git a/evennia/__init__.py b/evennia/__init__.py index 20a7a16a7e..413f7c5dc6 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -110,6 +110,10 @@ TICKER_HANDLER = None MONITOR_HANDLER = None CHANNEL_HANDLER = None +# Containers +VALIDATOR_CONTAINER = None +OPTION_CONTAINER = None + def _create_version(): @@ -154,6 +158,7 @@ def _init(): global create_object, create_script, create_account, create_channel, create_message, create_help_entry global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER, TASK_HANDLER + global VALIDATOR_CONTAINER, OPTION_CONTAINER global EvMenu, EvTable, EvForm, EvMore, EvEditor global ANSIString @@ -215,6 +220,10 @@ def _init(): from .comms.channelhandler import CHANNEL_HANDLER from .scripts.monitorhandler import MONITOR_HANDLER + # containers + from .utils.containers import VALIDATOR_CONTAINER + from .utils.containers import OPTION_CONTAINER + # initialize the doc string global __doc__ __doc__ = ansi.parse_ansi(DOCSTRING) diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 69a69ccb93..5fb952db9d 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -7,11 +7,15 @@ All commands in Evennia inherit from the 'Command' class in this module. from builtins import range import re +import math from django.conf import settings from evennia.locks.lockhandler import LockHandler from evennia.utils.utils import is_iter, fill, lazy_property, make_iter +from evennia.utils.evtable import EvTable +from evennia.utils.ansi import ANSIString + from future.utils import with_metaclass @@ -468,6 +472,98 @@ class Command(with_metaclass(CommandMeta, object)): """ return self.__doc__ + def width(self): + return self.session.protocol_flags['SCREENWIDTH'][0] + + def style_table(self, *args, **kwargs): + border_color = self.account.options.get('border_color') + column_color = self.account.options.get('column_names_color') + + colornames = ['|%s%s|n' % (column_color, col) for col in args] + + h_line_char = kwargs.pop('header_line_char', '~') + header_line_char = ANSIString(f'|{border_color}{h_line_char}|n') + + c_char = kwargs.pop('corner_char', '+') + corner_char = ANSIString(f'|{border_color}{c_char}|n') + + b_left_char = kwargs.pop('border_left_char', '||') + border_left_char = ANSIString(f'|{border_color}{b_left_char}|n') + + b_right_char = kwargs.pop('border_right_char', '||') + border_right_char = ANSIString(f'|{border_color}{b_right_char}|n') + + b_bottom_char = kwargs.pop('border_bottom_char', '-') + border_bottom_char = ANSIString(f'|{border_color}{b_bottom_char}|n') + + b_top_char = kwargs.pop('border_top_char', '-') + border_top_char = ANSIString(f'|{border_color}{b_top_char}|n') + + table = EvTable(*colornames, header_line_char=header_line_char, corner_char=corner_char, + border_left_char=border_left_char, border_right_char=border_right_char, + border_top_char=border_top_char, **kwargs) + return table + + def render_header(self, header_text=None, fill_character=None, edge_character=None, + mode='header', color_header=True): + colors = dict() + colors['border'] = self.account.options.get('border_color') + colors['headertext'] = self.account.options.get('%s_text_color' % mode) + colors['headerstar'] = self.account.options.get('%s_star_color' % mode) + + width = self.width() + if edge_character: + width -= 2 + + if header_text: + if color_header: + header_text = ANSIString(header_text).clean() + header_text = ANSIString('|n|%s%s|n' % (colors['headertext'], header_text)) + if mode == 'header': + begin_center = ANSIString("|n|%s<|%s* |n" % (colors['border'], colors['headerstar'])) + end_center = ANSIString("|n |%s*|%s>|n" % (colors['headerstar'], colors['border'])) + center_string = ANSIString(begin_center + header_text + end_center) + else: + center_string = ANSIString('|n |%s%s |n' % (colors['headertext'], header_text)) + else: + center_string = '' + + fill_character = self.account.options.get('%s_fill' % mode) + + remain_fill = width - len(center_string) + if remain_fill % 2 == 0: + right_width = remain_fill / 2 + left_width = remain_fill / 2 + else: + right_width = math.floor(remain_fill / 2) + left_width = math.ceil(remain_fill / 2) + + right_fill = ANSIString('|n|%s%s|n' % (colors['border'], fill_character * int(right_width))) + left_fill = ANSIString('|n|%s%s|n' % (colors['border'], fill_character * int(left_width))) + + if edge_character: + edge_fill = ANSIString('|n|%s%s|n' % (colors['border'], edge_character)) + main_string = ANSIString(center_string) + final_send = ANSIString(edge_fill) + left_fill + main_string + right_fill + ANSIString(edge_fill) + else: + final_send = left_fill + ANSIString(center_string) + right_fill + return final_send + + def style_header(self, *args, **kwargs): + if 'mode' not in kwargs: + kwargs['mode'] = 'header' + return self.render_header(*args, **kwargs) + + def style_separator(self, *args, **kwargs): + if 'mode' not in kwargs: + kwargs['mode'] = 'separator' + return self.render_header(*args, **kwargs) + + def style_footer(self, *args, **kwargs): + if 'mode' not in kwargs: + kwargs['mode'] = 'footer' + return self.render_header(*args, **kwargs) + class InterruptCommand(Exception): diff --git a/evennia/commands/default/muxcommand.py b/evennia/commands/default/muxcommand.py index 2dc9c18b60..a44ae0c1dd 100644 --- a/evennia/commands/default/muxcommand.py +++ b/evennia/commands/default/muxcommand.py @@ -3,11 +3,8 @@ The command template for the default MUX-style command set. There is also an Account/OOC version that makes sure caller is an Account object. """ -import math from evennia.utils import utils from evennia.commands.command import Command -from evennia.utils.evtable import EvTable -from evennia.utils.ansi import ANSIString # limit symbol import for API __all__ = ("MuxCommand", "MuxAccountCommand") @@ -232,98 +229,6 @@ class MuxCommand(Command): string += "-" * 50 self.caller.msg(string) - def width(self): - return self.session.protocol_flags['SCREENWIDTH'][0] - - def style_table(self, *args, **kwargs): - border_color = self.account.options.get('border_color') - column_color = self.account.options.get('column_names_color') - - colornames = ['|%s%s|n' % (column_color, col) for col in args] - - h_line_char = kwargs.pop('header_line_char', '-') - header_line_char = ANSIString(f'|{border_color}{h_line_char}|n') - - c_char = kwargs.pop('corner_char', '+') - corner_char = ANSIString(f'|{border_color}{c_char}|n') - - b_left_char = kwargs.pop('border_left_char', '||') - border_left_char = ANSIString(f'|{border_color}{b_left_char}|n') - - b_right_char = kwargs.pop('border_right_char', '||') - border_right_char = ANSIString(f'|{border_color}{b_right_char}|n') - - b_bottom_char = kwargs.pop('border_bottom_char', '-') - border_bottom_char = ANSIString(f'|{border_color}{b_bottom_char}|n') - - b_top_char = kwargs.pop('border_top_char', '-') - border_top_char = ANSIString(f'|{border_color}{b_top_char}|n') - - table = EvTable(*colornames, header_line_char=header_line_char, corner_char=corner_char, - border_left_char=border_left_char, border_right_char=border_right_char, - border_bottom_char=border_bottom_char, border_top_char=border_top_char, **kwargs) - return table - - def render_header(self, header_text=None, fill_character=None, edge_character=None, - mode='header', color_header=True): - colors = dict() - colors['border'] = self.account.options.get('border_color') - colors['headertext'] = self.account.options.get('%s_text_color' % mode) - colors['headerstar'] = self.account.options.get('%s_star_color' % mode) - - width = self.width() - if edge_character: - width -= 2 - - if header_text: - if color_header: - header_text = ANSIString(header_text).clean() - header_text = ANSIString('|n|%s%s|n' % (colors['headertext'], header_text)) - if mode == 'header': - begin_center = ANSIString("|n|%s<|%s* |n" % (colors['border'], colors['headerstar'])) - end_center = ANSIString("|n |%s*|%s>|n" % (colors['headerstar'], colors['border'])) - center_string = ANSIString(begin_center + header_text + end_center) - else: - center_string = ANSIString('|n |%s%s |n' % (colors['headertext'], header_text)) - else: - center_string = '' - - fill_character = self.account.options.get('%s_fill' % mode) - - remain_fill = width - len(center_string) - if remain_fill % 2 == 0: - right_width = remain_fill / 2 - left_width = remain_fill / 2 - else: - right_width = math.floor(remain_fill / 2) - left_width = math.ceil(remain_fill / 2) - - right_fill = ANSIString('|n|%s%s|n' % (colors['border'], fill_character * int(right_width))) - left_fill = ANSIString('|n|%s%s|n' % (colors['border'], fill_character * int(left_width))) - - if edge_character: - edge_fill = ANSIString('|n|%s%s|n' % (colors['border'], edge_character)) - main_string = ANSIString(center_string) - final_send = ANSIString(edge_fill) + left_fill + main_string + right_fill + ANSIString(edge_fill) - else: - final_send = left_fill + ANSIString(center_string) + right_fill - return final_send - - def style_header(self, *args, **kwargs): - if 'mode' not in kwargs: - kwargs['mode'] = 'header' - return self.render_header(*args, **kwargs) - - def style_separator(self, *args, **kwargs): - if 'mode' not in kwargs: - kwargs['mode'] = 'separator' - return self.render_header(*args, **kwargs) - - def style_footer(self, *args, **kwargs): - if 'mode' not in kwargs: - kwargs['mode'] = 'footer' - return self.render_header(*args, **kwargs) - class MuxAccountCommand(MuxCommand): """ diff --git a/evennia/settings_default.py b/evennia/settings_default.py index fd4a535c48..c771f7e1cf 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -511,6 +511,7 @@ OPTIONS_ACCOUNT_DEFAULT = { 'footer_fill': ('Fill for Footer Lines.', 'Text', '='), 'help_category_color': ('Help category names.', 'Color', 'g'), 'help_entry_color': ('Help entry names.', 'Color', 'c'), + 'timezone': ('Timezone for dates. @tz for a list.', 'Timezone', 'UTC') } @@ -565,11 +566,11 @@ PROTOTYPEFUNC_MODULES = ["evennia.utils.prototypefuncs", # Module holding validator functions. functions in later modules will # override those in earlier ones. -VALIDATOR_MODULES = ['evennia.utils.validfuncs', ] +VALIDATOR_MODULES = ['evennia.utils.validatorfunctions', ] # Modules holding Option classes. Those in later modules will # override ones in earlier modules. -OPTION_MODULES = ['evennia.utils.opclasses', ] +OPTION_MODULES = ['evennia.utils.optionclasses', ] ###################################################################### # Default Account setup and access diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py index 9d1d0e0deb..332899ad3d 100644 --- a/evennia/utils/containers.py +++ b/evennia/utils/containers.py @@ -2,7 +2,7 @@ from django.conf import settings from evennia.utils.utils import callables_from_module -class ValidContainer(object): +class ValidatorContainer(object): """ Loads and stores the final list of VALIDATOR FUNCTIONS. @@ -22,7 +22,7 @@ class ValidContainer(object): # Ensure that we have a Singleton of ValidHandler that is always loaded... and only needs to be loaded once. -VALID_CONTAINER = ValidContainer() +VALIDATOR_CONTAINER = ValidatorContainer() class OptionContainer(object): diff --git a/evennia/utils/option.py b/evennia/utils/option.py index 099e09be9d..2792fa0238 100644 --- a/evennia/utils/option.py +++ b/evennia/utils/option.py @@ -39,20 +39,49 @@ class OptionHandler(object): return_list=True, return_obj=True) if s} def __getitem__(self, item): + """ + Shortcut to self.get(item) used as a different syntax. This entire object is + essentially a dictionary of option_key -> value. + + Args: + item (str): The Key of the item to get. + + Returns: + The Option's value. + """ return self.get(item).value def get(self, item, return_obj=False): + """ + Retrieves an Option stored in the handler. Will load it if it doesn't exist. + + Args: + item (str): The key to retrieve. + return_obj (bool): If True, returns the actual option object instead of its value. + + Returns: + An option value (varies) or the Option itself. + """ if item not in self.options_dict: raise KeyError("Option not found!") if item in self.options: op_found = self.options[item] else: - op_found = self.load_option(item) + op_found = self._load_option(item) if return_obj: return op_found return op_found.value - def load_option(self, key): + def _load_option(self, key): + """ + Loads option on-demand if it has not been loaded yet. + + Args: + key (str): The option being loaded. + + Returns: + + """ option_def = self.options_dict[key] save_data = self.save_data.get(key, None) self.obj.msg(save_data) @@ -60,7 +89,7 @@ class OptionHandler(object): self.options[key] = loaded_option return loaded_option - def set(self, option, value): + def set(self, option, value, **kwargs): """ Change an individual option. @@ -80,7 +109,7 @@ class OptionHandler(object): raise ValueError(f"That matched: {', '.join(found)}. Please be more specific.") found = found[0] op = self.get(found, return_obj=True) - op.value = value + op.set(value, **kwargs) return op.display() diff --git a/evennia/utils/opclasses.py b/evennia/utils/optionclasses.py similarity index 94% rename from evennia/utils/opclasses.py rename to evennia/utils/optionclasses.py index 0304feeab8..fe864fd14f 100644 --- a/evennia/utils/opclasses.py +++ b/evennia/utils/optionclasses.py @@ -1,8 +1,8 @@ import datetime as _dt from evennia import logger as _log from evennia.utils.ansi import ANSIString as _ANSI -from evennia.utils.validfuncs import _TZ_DICT -from evennia.utils.containers import VALID_CONTAINER as _VAL +from evennia.utils.validatorfunctions import _TZ_DICT +from evennia.utils.containers import VALIDATOR_CONTAINER as _VAL class BaseOption(object): @@ -14,9 +14,8 @@ class BaseOption(object): Designed to be extremely overloadable as some options can be cantankerous. Properties: - expect_type (str): What users will see this as asking for. Example: Color or email. valid: Shortcut to the loaded VALID_HANDLER. - valid_type (str): The key of the Validator this uses. + validator_key (str): The key of the Validator this uses. """ validator_key = '' @@ -55,7 +54,7 @@ class BaseOption(object): """ return self.value - def _load(self): + def load(self): """ Takes the provided save data, validates it, and gets this Option ready to use. @@ -71,7 +70,7 @@ class BaseOption(object): _log.log_trace(e) return False - def _save(self): + def save(self): """ Exports the current value to an Attribute. @@ -115,7 +114,7 @@ class BaseOption(object): @property def value(self): if not self.loaded and self.save_data is not None: - self._load() + self.load() if self.loaded: return self.value_storage else: @@ -123,22 +122,24 @@ class BaseOption(object): @value.setter def value(self, value): + self.set(value) + + def set(self, value, **kwargs): """ Takes user input, presumed to be a string, and changes the value if it is a valid input. Args: - value: - account: + value: The new value of this Option. Returns: None """ - final_value = self.validate(value) + final_value = self.validate(value, **kwargs) self.value_storage = final_value self.loaded = True - self._save() + self.save() - def validate(self, value): + def validate(self, value, **kwargs): """ Validate user input, which is presumed to be a string. @@ -151,7 +152,7 @@ class BaseOption(object): Returns: The results of a Validator call. Might be any kind of python object. """ - return _VAL[self.validator_key](value, thing_name=self.key) + return _VAL[self.validator_key](value, thing_name=self.key, **kwargs) class Text(BaseOption): diff --git a/evennia/utils/validfuncs.py b/evennia/utils/validatorfunctions.py similarity index 96% rename from evennia/utils/validfuncs.py rename to evennia/utils/validatorfunctions.py index 77ad4cf288..41e5a8ffaf 100644 --- a/evennia/utils/validfuncs.py +++ b/evennia/utils/validatorfunctions.py @@ -170,9 +170,11 @@ def timezone(entry, thing_name="Timezone", **kwargs): """ if not entry: raise ValueError(f"No {thing_name} entered!") - found = _partial(list(_TZ_DICT.keys()), entry) + found = _partial(list(_TZ_DICT.keys()), entry, ret_index=False) + if len(found) > 1: + raise ValueError(f"That matched: {', '.join(str(t) for t in found)}. Please be more specific!") if found: - return _TZ_DICT[found] + return _TZ_DICT[found[0]] raise ValueError(f"Could not find timezone '{entry}' for {thing_name}!")