Resolve merge conflicts, some cleanup

This commit is contained in:
Griatch 2019-04-14 12:18:15 +02:00
commit b024c17f8a
15 changed files with 854 additions and 29 deletions

View file

@ -126,6 +126,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

View file

@ -111,6 +111,10 @@ MONITOR_HANDLER = None
CHANNEL_HANDLER = None
GLOBAL_SCRIPTS = None
# Containers
VALIDATOR_FUNCTIONS = None
OPTION_CLASSES = None
def _create_version():
"""
@ -140,6 +144,7 @@ def _create_version():
__version__ = _create_version()
del _create_version
def _init():
"""
This function is called automatically by the launcher only after
@ -150,12 +155,16 @@ def _init():
global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript
global ObjectDB, AccountDB, ScriptDB, ChannelDB, Msg
global Command, CmdSet, default_cmds, syscmdkeys, InterruptCommand
global search_object, search_script, search_account, search_channel, search_help, search_tag, search_message
global create_object, create_script, create_account, create_channel, create_message, create_help_entry
global search_object, search_script, search_account, search_channel
global search_help, search_tag, search_message
global create_object, create_script, create_account, create_channel
global 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_SCRIPTS
global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER
global CHANNEL_HANDLER, TASK_HANDLER, GLOBAL_SCRIPTS
global VALIDATOR_FUNCS, OPTION_CLASSES
global EvMenu, EvTable, EvForm, EvMore, EvEditor
global ANSIString
global ANSIString
from .accounts.accounts import DefaultAccount
from .accounts.accounts import DefaultGuest
@ -216,6 +225,10 @@ def _init():
from .scripts.monitorhandler import MONITOR_HANDLER
from .utils.containers import GLOBAL_SCRIPTS
# containers
from .utils.containers import VALIDATOR_FUNCS
from .utils.containers import OPTION_CLASSES
# initialize the doc string
global __doc__
__doc__ = ansi.parse_ansi(DOCSTRING)

View file

@ -20,6 +20,7 @@ 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
@ -197,6 +198,10 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
def sessions(self):
return AccountSessionHandler(self)
@lazy_property
def options(self):
return OptionHandler(self, options_dict=settings.OPTIONS_ACCOUNT_DEFAULT, save_category='option')
# Do not make this a lazy property; the web UI will not refresh it!
@property
def characters(self):

View file

@ -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):

View file

@ -367,7 +367,7 @@ class CmdSessions(COMMAND_DEFAULT_CLASS):
"""Implement function"""
account = self.account
sessions = account.sessions.all()
table = evtable.EvTable("|wsessid",
table = self.style_table("|wsessid",
"|wprotocol",
"|whost",
"|wpuppet/character",
@ -418,7 +418,7 @@ class CmdWho(COMMAND_DEFAULT_CLASS):
naccounts = (SESSIONS.account_count())
if show_session_data:
# privileged info
table = evtable.EvTable("|wAccount Name",
table = self.style_table("|wAccount Name",
"|wOn for",
"|wIdle",
"|wPuppeting",
@ -444,7 +444,7 @@ class CmdWho(COMMAND_DEFAULT_CLASS):
isinstance(session.address, tuple) and session.address[0] or session.address)
else:
# unprivileged
table = evtable.EvTable("|wAccount name", "|wOn for", "|wIdle")
table = self.style_table("|wAccount name", "|wOn for", "|wIdle")
for session in session_list:
if not session.logged_in:
continue
@ -524,7 +524,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
options.pop("TTYPE", None)
header = ("Name", "Value", "Saved") if saved_options else ("Name", "Value")
table = evtable.EvTable(*header)
table = self.style_table(*header)
for key in sorted(options):
row = [key, options[key]]
if saved_options:
@ -870,3 +870,30 @@ class CmdQuell(COMMAND_DEFAULT_CLASS):
else:
self.msg("Quelling Account permissions%s. Use @unquell to get them back." % permstr)
self._recache_locks(account)
class CmdStyle(COMMAND_DEFAULT_CLASS):
key = "@style"
switch_options = ['clear']
def func(self):
if not self.args:
self.list_styles()
return
self.set()
def list_styles(self):
styles_table = self.style_table('Option', 'Description', 'Type', 'Value', width=78)
for op_key in self.account.options.options_dict.keys():
op_found = self.account.options.get(op_key, return_obj=True)
styles_table.add_row(op_key, op_found.description, op_found.__class__.__name__, op_found.display())
self.msg(str(styles_table))
def set(self):
try:
result = self.account.options.set(self.lhs, self.rhs)
except ValueError as e:
self.msg(str(e))
return
self.msg('Success! The new value is: %s' % result)

View file

@ -112,7 +112,7 @@ def list_bans(banlist):
if not banlist:
return "No active bans were found."
table = evtable.EvTable("|wid", "|wname/ip", "|wdate", "|wreason")
table = self.style_table("|wid", "|wname/ip", "|wdate", "|wreason")
for inum, ban in enumerate(banlist):
table.add_row(str(inum + 1),
ban[0] and ban[0] or ban[1],

View file

@ -38,6 +38,7 @@ class AccountCmdSet(CmdSet):
self.add(account.CmdPassword())
self.add(account.CmdColorTest())
self.add(account.CmdQuell())
self.add(account.CmdStyle())
# nicks
self.add(general.CmdNick())

View file

@ -292,7 +292,7 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
if self.cmdstring == "comlist":
# just display the subscribed channels with no extra info
comtable = evtable.EvTable("|wchannel|n", "|wmy aliases|n",
comtable = self.style_table("|wchannel|n", "|wmy aliases|n",
"|wdescription|n", align="l", maxwidth=_DEFAULT_WIDTH)
for chan in subs:
clower = chan.key.lower()
@ -306,7 +306,7 @@ class CmdChannels(COMMAND_DEFAULT_CLASS):
" |waddcom|n/|wdelcom|n to sub/unsub):|n\n%s" % comtable)
else:
# full listing (of channels caller is able to listen to)
comtable = evtable.EvTable("|wsub|n", "|wchannel|n", "|wmy aliases|n",
comtable = self.style_table("|wsub|n", "|wchannel|n", "|wmy aliases|n",
"|wlocks|n", "|wdescription|n", maxwidth=_DEFAULT_WIDTH)
for chan in channels:
clower = chan.key.lower()
@ -815,7 +815,7 @@ def _list_bots():
ircbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")]
if ircbots:
from evennia.utils.evtable import EvTable
table = EvTable("|w#dbref|n", "|wbotname|n", "|wev-channel|n",
table = self.style_table("|w#dbref|n", "|wbotname|n", "|wev-channel|n",
"|wirc-channel|n", "|wSSL|n", maxwidth=_DEFAULT_WIDTH)
for ircbot in ircbots:
ircinfo = "%s (%s:%s)" % (ircbot.db.irc_channel, ircbot.db.irc_network, ircbot.db.irc_port)
@ -1051,7 +1051,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
rssbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")]
if rssbots:
from evennia.utils.evtable import EvTable
table = EvTable("|wdbid|n", "|wupdate rate|n", "|wev-channel",
table = self.style_table("|wdbid|n", "|wupdate rate|n", "|wev-channel",
"|wRSS feed URL|n", border="cells", maxwidth=_DEFAULT_WIDTH)
for rssbot in rssbots:
table.add_row(rssbot.id, rssbot.db.rss_rate, rssbot.db.ev_channel, rssbot.db.rss_url)

View file

@ -157,7 +157,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
if not nicklist:
string = "|wNo nicks defined.|n"
else:
table = evtable.EvTable("#", "Type", "Nick match", "Replacement")
table = self.style_table("#", "Type", "Nick match", "Replacement")
for inum, nickobj in enumerate(nicklist):
_, _, nickvalue, replacement = nickobj.value
table.add_row(str(inum + 1), nickobj.db_category, _cy(nickvalue), _cy(replacement))
@ -338,7 +338,7 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
if not items:
string = "You are not carrying anything."
else:
table = evtable.EvTable(border="header")
table = self.style_table(border="header")
for item in items:
table.add_row("|C%s|n" % item.name, item.db.desc or "")
string = "|wYou are carrying:\n%s" % table

View file

@ -445,7 +445,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
nobjs = nobjs or 1 # fix zero-div error with empty database
# total object sum table
totaltable = EvTable("|wtype|n", "|wcomment|n", "|wcount|n", "|w%%|n", border="table", align="l")
totaltable = self.style_table("|wtype|n", "|wcomment|n", "|wcount|n", "|w%%|n", border="table", align="l")
totaltable.align = 'l'
totaltable.add_row("Characters", "(BASE_CHARACTER_TYPECLASS)", nchars, "%.2f" % ((float(nchars) / nobjs) * 100))
totaltable.add_row("Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms) / nobjs) * 100))
@ -453,7 +453,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
totaltable.add_row("Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100))
# typeclass table
typetable = EvTable("|wtypeclass|n", "|wcount|n", "|w%%|n", border="table", align="l")
typetable = self.style_table("|wtypeclass|n", "|wcount|n", "|w%%|n", border="table", align="l")
typetable.align = 'l'
dbtotals = ObjectDB.objects.object_totals()
for path, count in dbtotals.items():
@ -461,7 +461,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
# last N table
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):]
latesttable = EvTable("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table")
latesttable = self.style_table("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table")
latesttable.align = 'l'
for obj in objs:
latesttable.add_row(utils.datetime_format(obj.date_created),
@ -557,12 +557,12 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
# typeclass table
dbtotals = AccountDB.objects.object_totals()
typetable = EvTable("|wtypeclass|n", "|wcount|n", "|w%%|n", border="cells", align="l")
typetable = self.style_table("|wtypeclass|n", "|wcount|n", "|w%%|n", border="cells", align="l")
for path, count in dbtotals.items():
typetable.add_row(path, count, "%.2f" % ((float(count) / naccounts) * 100))
# last N table
plyrs = AccountDB.objects.all().order_by("db_date_created")[max(0, naccounts - nlim):]
latesttable = EvTable("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", border="cells", align="l")
latesttable = self.style_table("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", border="cells", align="l")
for ply in plyrs:
latesttable.add_row(utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.path)
@ -613,7 +613,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
if not switches or switches[0] == "list":
# Just display the list of installed services and their
# status, then exit.
table = EvTable("|wService|n (use @services/start|stop|delete)", "|wstatus", align="l")
table = self.style_table("|wService|n (use @services/start|stop|delete)", "|wstatus", align="l")
for service in service_collection.services:
table.add_row(service.name, service.running and "|gRunning" or "|rNot Running")
caller.msg(str(table))
@ -723,14 +723,14 @@ class CmdTime(COMMAND_DEFAULT_CLASS):
def func(self):
"""Show server time data in a table."""
table1 = EvTable("|wServer time", "", align="l", width=78)
table1 = self.style_table("|wServer time", "", align="l", width=78)
table1.add_row("Current uptime", utils.time_format(gametime.uptime(), 3))
table1.add_row("Portal uptime", utils.time_format(gametime.portal_uptime(), 3))
table1.add_row("Total runtime", utils.time_format(gametime.runtime(), 2))
table1.add_row("First start", datetime.datetime.fromtimestamp(gametime.server_epoch()))
table1.add_row("Current time", datetime.datetime.now())
table1.reformat_column(0, width=30)
table2 = EvTable("|wIn-Game time", "|wReal time x %g" % gametime.TIMEFACTOR, align="l", width=77, border_top=0)
table2 = self.style_table("|wIn-Game time", "|wReal time x %g" % gametime.TIMEFACTOR, align="l", width=77, border_top=0)
epochtxt = "Epoch (%s)" % ("from settings" if settings.TIME_GAME_EPOCH else "server start")
table2.add_row(epochtxt, datetime.datetime.fromtimestamp(gametime.game_epoch()))
table2.add_row("Total time passed:", utils.time_format(gametime.gametime(), 2))
@ -824,7 +824,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
self.caller.msg(string % (rmem, pmem))
return
# Display table
loadtable = EvTable("property", "statistic", align="l")
loadtable = self.style_table("property", "statistic", align="l")
loadtable.add_row("Total CPU load", "%g %%" % loadavg)
loadtable.add_row("Total computer memory usage", "%g MB (%g%%)" % (rmem, pmem))
loadtable.add_row("Process ID", "%g" % pid),
@ -850,7 +850,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
self.caller.msg(string % (rmem, pmem, vmem))
return
loadtable = EvTable("property", "statistic", align="l")
loadtable = self.style_table("property", "statistic", align="l")
loadtable.add_row("Server load (1 min)", "%g" % loadavg)
loadtable.add_row("Process ID", "%g" % pid),
loadtable.add_row("Memory usage", "%g MB (%g%%)" % (rmem, pmem))
@ -875,7 +875,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
total_num, cachedict = _IDMAPPER.cache_size()
sorted_cache = sorted([(key, num) for key, num in cachedict.items() if num > 0],
key=lambda tup: tup[1], reverse=True)
memtable = EvTable("entity name", "number", "idmapper %", align="l")
memtable = self.style_table("entity name", "number", "idmapper %", align="l")
for tup in sorted_cache:
memtable.add_row(tup[0], "%i" % tup[1], "%.2f" % (float(tup[1]) / total_num * 100))
@ -907,7 +907,7 @@ class CmdTickers(COMMAND_DEFAULT_CLASS):
if not all_subs:
self.caller.msg("No tickers are currently active.")
return
table = EvTable("interval (s)", "object", "path/methodname", "idstring", "db")
table = self.style_table("interval (s)", "object", "path/methodname", "idstring", "db")
for sub in all_subs:
table.add_row(sub[3],
"%s%s" % (sub[0] or "[None]",

View file

@ -489,6 +489,42 @@ START_LOCATION = "#2"
# issues.
TYPECLASS_AGGRESSIVE_CACHE = True
######################################################################
# Options and validators
######################################################################
# Replace or add entries in this dictionary to specify options available
# on accounts. An option goes goteth
# 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')
OPTIONS_ACCOUNT_DEFAULT = {
'border_color': ('Headers, footers, table borders, etc.', 'Color', 'M'),
'header_star_color': ('* inside Header lines.', 'Color', 'm'),
'header_text_color': ('Text inside Header lines.', 'Color', 'w'),
'footer_text_color': ('Text inside Footer Lines.', 'Color', 'w'),
'column_names_color': ('Table column header text.', 'Color', 'G'),
'header_fill': ('Fill for Header lines.', 'Text', '='),
'separator_fill': ('Fill for Separator Lines.', 'Text', '-'),
'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')
}
# 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', ]
# 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', ]
######################################################################
# Batch processors
######################################################################
@ -521,7 +557,7 @@ TIME_GAME_EPOCH = None
TIME_IGNORE_DOWNTIMES = False
######################################################################
# Inlinefunc & PrototypeFuncs
# Inlinefunc, PrototypeFuncs
######################################################################
# Evennia supports inline function preprocessing. This allows users
# to supply inline calls on the form $func(arg, arg, ...) to do

View file

@ -5,7 +5,7 @@ Containers
from django.conf import settings
from evennia.utils.utils import class_from_module
from evennia.utils.utils import class_from_module, callables_from_module
from evennia.utils import logger
@ -108,3 +108,48 @@ class GlobalScriptContainer(object):
# Create singleton of the GlobalHandler for the API.
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()

116
evennia/utils/option.py Normal file
View file

@ -0,0 +1,116 @@
from evennia.utils.utils import string_partial_matching
from evennia.utils.containers import OPTION_CONTAINER
class OptionHandler(object):
"""
This is a generic Option handler meant for Typed Objects - anything that implements AttributeHandler.
It uses a dictionary to store-and-cache frequently used settings such as colors for borders or an
account's timezone.
This is used for Account.options but it could be used by Scripts or Objects just as easily. All
it needs to be provided is an options_dict.
"""
def __init__(self, obj, options_dict=None, save_category=None):
"""
Initialize an OptionHandler.
Args:
obj (TypedObject): The Typed Object this sits on. Obj MUST implement the Evennia AttributeHandler
or this will barf.
options_dict (dict): A dictionary of option keys, where the values are options. The format of those
tuples is: ('key', "Description to show", 'option_type', <default value>)
save_category (str): The Options data will be stored to this Attribute category on obj.
"""
if not options_dict:
options_dict = dict()
self.options_dict = options_dict
self.save_category = save_category
self.obj = obj
# This dictionary stores the in-memory Options by their key. Values are the Option objects.
self.options = dict()
# We use lazy-loading of each Option when it's called for, but it's good to have the save data
# on hand.
self.save_data = {s.key: s.value for s in obj.attributes.get(category=save_category,
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)
if return_obj:
return op_found
return op_found.value
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)
loaded_option = OPTION_CONTAINER[option_def[1]](self, key, option_def[0], option_def[2], save_data)
self.options[key] = loaded_option
return loaded_option
def set(self, option, value, **kwargs):
"""
Change an individual option.
Args:
option (str): The key of an option that can be changed. Allows partial matching.
value (str): The value that should be checked, coerced, and stored.
Returns:
New value
"""
if not option:
raise ValueError("Option field blank!")
found = string_partial_matching(list(self.options_dict.keys()), option, ret_index=False)
if not found:
raise ValueError("Option not found!")
if len(found) > 1:
raise ValueError(f"That matched: {', '.join(found)}. Please be more specific.")
found = found[0]
op = self.get(found, return_obj=True)
op.set(value, **kwargs)
return op.display()

View file

@ -0,0 +1,279 @@
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
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.
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 = ''
def __str__(self):
return self.key
def __init__(self, handler, key, description, default, save_data=None):
"""
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.
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
self.default_value = default
self.description = description
self.save_data = save_data
# Value Storage contains None until the Option is loaded.
self.value_storage = None
# 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
@property
def default(self):
return self.default_value
@property
def value(self):
if not self.loaded and self.save_data is not None:
self.load()
if self.loaded:
return self.value_storage
else:
return self.default
@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: The new value of this Option.
Returns:
None
"""
final_value = self.validate(value, **kwargs)
self.value_storage = final_value
self.loaded = True
self.save()
def validate(self, value, **kwargs):
"""
Validate user input, which is presumed to be a string.
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.
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)
class Text(BaseOption):
validator_key = 'text'
def deserialize(self, save_data):
got_data = str(save_data)
if not got_data:
raise ValueError(f"{self.key} expected Text data, got '{save_data}'")
return got_data
class Email(BaseOption):
validator_key = 'email'
def deserialize(self, save_data):
got_data = str(save_data)
if not got_data:
raise ValueError(f"{self.key} expected String data, got '{save_data}'")
return got_data
class Boolean(BaseOption):
validator_key = 'boolean'
def display(self, **kwargs):
if self.value:
return '1 - On/True'
return '0 - Off/False'
def serialize(self):
return self.value
def deserialize(self, save_data):
if not isinstance(save_data, bool):
raise ValueError(f"{self.key} expected Boolean, got '{save_data}'")
return save_data
class Color(BaseOption):
validator_key = 'color'
def display(self, **kwargs):
return f'{self.value} - |{self.value}this|n'
def deserialize(self, save_data):
if not save_data or len(_ANSI(f'|{save_data}|n')) > 0:
raise ValueError(f"{self.key} expected Color Code, got '{save_data}'")
return save_data
class Timezone(BaseOption):
validator_key = 'timezone'
@property
def default(self):
return _TZ_DICT[self.default_value]
def deserialize(self, save_data):
if save_data not in _TZ_DICT:
raise ValueError(f"{self.key} expected Timezone Data, got '{save_data}'")
return _TZ_DICT[save_data]
def serialize(self):
return str(self.value_storage)
class UnsignedInteger(BaseOption):
validator_key = 'unsigned_integer'
def deserialize(self, save_data):
if isinstance(save_data, int) and save_data >= 0:
return save_data
raise ValueError(f"{self.key} expected Whole Number 0+, got '{save_data}'")
class SignedInteger(BaseOption):
validator_key = 'signed_integer'
def deserialize(self, save_data):
if isinstance(save_data, int):
return save_data
raise ValueError(f"{self.key} expected Whole Number, got '{save_data}'")
class PositiveInteger(BaseOption):
validator_key = 'positive_integer'
def deserialize(self, save_data):
if isinstance(save_data, int) and save_data > 0:
return save_data
raise ValueError(f"{self.key} expected Whole Number 1+, got '{save_data}'")
class Duration(BaseOption):
validator_key = 'duration'
def deserialize(self, save_data):
if isinstance(save_data, int):
return _dt.timedelta(0, save_data, 0, 0, 0, 0, 0)
raise ValueError(f"{self.key} expected Timedelta in seconds, got '{save_data}'")
def serialize(self):
return self.value_storage.seconds
class Datetime(BaseOption):
validator_key = 'datetime'
def deserialize(self, save_data):
if isinstance(save_data, int):
return _dt.datetime.utcfromtimestamp(save_data)
raise ValueError(f"{self.key} expected UTC Datetime in EPOCH format, got '{save_data}'")
def serialize(self):
return int(self.value_storage.strftime('%s'))
class Future(Datetime):
validator_key = 'future'
class Lock(Text):
validator_key = 'lock'

View file

@ -0,0 +1,204 @@
"""
Contains all the validation functions.
All validation functions must have a checker (probably a session) and entry arg.
They can employ more paramters at your leisure.
"""
import re as _re
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.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):
if not entry:
raise ValueError(f"Nothing entered for a {thing_name}!")
test_str = _ansi('|%s|n' % entry)
if len(test_str):
raise ValueError(f"'{entry}' is not a valid {thing_name}.")
return entry
def datetime(entry, thing_name='Datetime', account=None, from_tz=None, **kwargs):
"""
Process a datetime string in standard forms while accounting for the inputter's timezone.
Args:
entry (str): A date string from a user.
thing_name (str): Name to display this datetime as.
account (AccountDB): The Account performing this lookup. Unless from_tz is provided,
account's timezone will be used (if found) for local time and convert the results
to UTC.
from_tz (pytz): An instance of pytz from the user. If not provided, defaults to whatever
the Account uses. If neither one is provided, defaults to UTC.
Returns:
datetime in utc.
"""
if not entry:
raise ValueError(f"No {thing_name} entered!")
if not from_tz:
from_tz = _pytz.UTC
utc = _pytz.UTC
now = _dt.datetime.utcnow().replace(tzinfo=utc)
cur_year = now.strftime('%Y')
split_time = entry.split(' ')
if len(split_time) == 3:
entry = f"{split_time[0]} {split_time[1]} {split_time[2]} {cur_year}"
elif len(split_time) == 4:
entry = f"{split_time[0]} {split_time[1]} {split_time[2]} {split_time[3]}"
else:
raise ValueError(f"{thing_name} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%H')}")
try:
local = _dt.datetime.strptime(input, '%b %d %H:%M %Y')
except ValueError:
raise ValueError(f"{thing_name} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%H')}")
local_tz = from_tz.localize(local)
return local_tz.astimezone(utc)
def duration(entry, thing_name='Duration', **kwargs):
"""
Take a string and derive a datetime timedelta from it.
Args:
entry (string): This is a string from user-input. The intended format is, for example: "5d 2w 90s" for
'five days, two weeks, and ninety seconds.' Invalid sections are ignored.
thing_name (str): Name to display this query as.
Returns:
timedelta
"""
time_string = entry.split(" ")
seconds = 0
minutes = 0
hours = 0
days = 0
weeks = 0
for interval in time_string:
if _re.match(r'^[\d]+s$', interval.lower()):
seconds =+ int(interval.lower().rstrip("s"))
elif _re.match(r'^[\d]+m$', interval):
minutes =+ int(interval.lower().rstrip("m"))
elif _re.match(r'^[\d]+h$', interval):
hours =+ int(interval.lower().rstrip("h"))
elif _re.match(r'^[\d]+d$', interval):
days =+ int(interval.lower().rstrip("d"))
elif _re.match(r'^[\d]+w$', interval):
weeks =+ int(interval.lower().rstrip("w"))
elif _re.match(r'^[\d]+y$', interval):
days =+ int(interval.lower().rstrip("y")) * 365
else:
raise ValueError(f"Could not convert section '{interval}' to a {thing_name}.")
return _dt.timedelta(days, seconds, 0, 0, minutes, hours, weeks)
def future(entry, thing_name="Future Datetime", from_tz=None, **kwargs):
time = datetime(entry, thing_name)
if time < _dt.datetime.utcnow():
raise ValueError(f"That {thing_name} is in the past! Must give a Future datetime!")
return time
def signed_integer(entry, thing_name="Signed Integer", **kwargs):
if not entry:
raise ValueError(f"Must enter a whole number for {thing_name}!")
try:
num = int(entry)
except ValueError:
raise ValueError(f"Could not convert '{entry}' to a whole number for {thing_name}!")
return num
def positive_integer(entry, thing_name="Positive Integer", **kwargs):
num = signed_integer(entry, thing_name)
if not num >= 1:
raise ValueError(f"Must enter a whole number greater than 0 for {thing_name}!")
return num
def unsigned_integer(entry, thing_name="Unsigned Integer", **kwargs):
num = signed_integer(entry, thing_name)
if not num >= 0:
raise ValueError(f"{thing_name} must be a whole number greater than or equal to 0!")
return num
def boolean(entry, thing_name="True/False", **kwargs):
"""
Simplest check in computer logic, right? This will take user input to flick the switch on or off
Args:
entry (str): A value such as True, On, Enabled, Disabled, False, 0, or 1.
thing_name (str): What kind of Boolean we are setting. What Option is this for?
Returns:
Boolean
"""
entry = entry.upper()
error = f"Must enter 0 (false) or 1 (true) for {thing_name}. Also accepts True, False, On, Off, Yes, No, Enabled, and Disabled"
if not entry:
raise ValueError(error)
if entry in ('1', 'TRUE', 'ON', 'ENABLED', 'ENABLE', 'YES'):
return True
if entry in ('0', 'FALSE', 'OFF', 'DISABLED', 'DISABLE', 'NO'):
return False
raise ValueError(error)
def timezone(entry, thing_name="Timezone", **kwargs):
"""
Takes user input as string, and partial matches a Timezone.
Args:
entry (str): The name of the Timezone.
thing_name (str): What this Timezone is used for.
Returns:
A PYTZ timezone.
"""
if not entry:
raise ValueError(f"No {thing_name} entered!")
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[0]]
raise ValueError(f"Could not find timezone '{entry}' for {thing_name}!")
def email(entry, thing_name="Email Address", **kwargs):
if not entry:
raise ValueError("Email address field empty!")
try:
_val_email(entry) # offloading the hard work to Django!
except _error:
raise ValueError(f"That isn't a valid {thing_name}!")
return entry
def lock(entry, thing_name='locks', access_options=None, **kwargs):
entry = entry.strip()
if not entry:
raise ValueError(f"No {thing_name} entered to set!")
for locksetting in entry.split(';'):
access_type, lockfunc = locksetting.split(':', 1)
if not access_type:
raise ValueError("Must enter an access type!")
if access_options:
if access_type not in access_options:
raise ValueError(f"Access type must be one of: {', '.join(access_options)}")
if not lockfunc:
raise ValueError("Lock func not entered.")
return entry