Merge branch 'master' of https://github.com/Henddher/evennia into bug_1663
This commit is contained in:
commit
74b1d2415d
131 changed files with 22227 additions and 3937 deletions
|
|
@ -54,7 +54,8 @@ _GA = object.__getattribute__
|
|||
|
||||
def create_object(typeclass=None, key=None, location=None, home=None,
|
||||
permissions=None, locks=None, aliases=None, tags=None,
|
||||
destination=None, report_to=None, nohome=False):
|
||||
destination=None, report_to=None, nohome=False, attributes=None,
|
||||
nattributes=None):
|
||||
"""
|
||||
|
||||
Create a new in-game object.
|
||||
|
|
@ -68,13 +69,18 @@ def create_object(typeclass=None, key=None, location=None, home=None,
|
|||
permissions (list): A list of permission strings or tuples (permstring, category).
|
||||
locks (str): one or more lockstrings, separated by semicolons.
|
||||
aliases (list): A list of alternative keys or tuples (aliasstring, category).
|
||||
tags (list): List of tag keys or tuples (tagkey, category).
|
||||
tags (list): List of tag keys or tuples (tagkey, category) or (tagkey, category, data).
|
||||
destination (Object or str): Obj or #dbref to use as an Exit's
|
||||
target.
|
||||
report_to (Object): The object to return error messages to.
|
||||
nohome (bool): This allows the creation of objects without a
|
||||
default home location; only used when creating the default
|
||||
location itself or during unittests.
|
||||
attributes (list): Tuples on the form (key, value) or (key, value, category),
|
||||
(key, value, lockstring) or (key, value, lockstring, default_access).
|
||||
to set as Attributes on the new object.
|
||||
nattributes (list): Non-persistent tuples on the form (key, value). Note that
|
||||
adding this rarely makes sense since this data will not survive a reload.
|
||||
|
||||
Returns:
|
||||
object (Object): A newly created object of the given typeclass.
|
||||
|
|
@ -95,6 +101,7 @@ def create_object(typeclass=None, key=None, location=None, home=None,
|
|||
locks = make_iter(locks) if locks is not None else None
|
||||
aliases = make_iter(aliases) if aliases is not None else None
|
||||
tags = make_iter(tags) if tags is not None else None
|
||||
attributes = make_iter(attributes) if attributes is not None else None
|
||||
|
||||
|
||||
if isinstance(typeclass, basestring):
|
||||
|
|
@ -122,7 +129,8 @@ def create_object(typeclass=None, key=None, location=None, home=None,
|
|||
# store the call signature for the signal
|
||||
new_object._createdict = dict(key=key, location=location, destination=destination, home=home,
|
||||
typeclass=typeclass.path, permissions=permissions, locks=locks,
|
||||
aliases=aliases, tags=tags, report_to=report_to, nohome=nohome)
|
||||
aliases=aliases, tags=tags, report_to=report_to, nohome=nohome,
|
||||
attributes=attributes, nattributes=nattributes)
|
||||
# this will trigger the save signal which in turn calls the
|
||||
# at_first_save hook on the typeclass, where the _createdict can be
|
||||
# used.
|
||||
|
|
@ -139,7 +147,8 @@ object = create_object
|
|||
|
||||
def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
||||
interval=None, start_delay=None, repeats=None,
|
||||
persistent=None, autostart=True, report_to=None, desc=None):
|
||||
persistent=None, autostart=True, report_to=None, desc=None,
|
||||
tags=None, attributes=None):
|
||||
"""
|
||||
Create a new script. All scripts are a combination of a database
|
||||
object that communicates with the database, and an typeclass that
|
||||
|
|
@ -169,7 +178,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
|||
created or if the `start` method must be called explicitly.
|
||||
report_to (Object): The object to return error messages to.
|
||||
desc (str): Optional description of script
|
||||
|
||||
tags (list): List of tags or tuples (tag, category).
|
||||
attributes (list): List if tuples (key, value) or (key, value, category)
|
||||
(key, value, lockstring) or (key, value, lockstring, default_access).
|
||||
|
||||
See evennia.scripts.manager for methods to manipulate existing
|
||||
scripts in the database.
|
||||
|
|
@ -190,9 +201,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
|||
if key:
|
||||
kwarg["db_key"] = key
|
||||
if account:
|
||||
kwarg["db_account"] = dbid_to_obj(account, _ScriptDB)
|
||||
kwarg["db_account"] = dbid_to_obj(account, _AccountDB)
|
||||
if obj:
|
||||
kwarg["db_obj"] = dbid_to_obj(obj, _ScriptDB)
|
||||
kwarg["db_obj"] = dbid_to_obj(obj, _ObjectDB)
|
||||
if interval:
|
||||
kwarg["db_interval"] = interval
|
||||
if start_delay:
|
||||
|
|
@ -203,6 +214,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
|||
kwarg["db_persistent"] = persistent
|
||||
if desc:
|
||||
kwarg["db_desc"] = desc
|
||||
tags = make_iter(tags) if tags is not None else None
|
||||
attributes = make_iter(attributes) if attributes is not None else None
|
||||
|
||||
# create new instance
|
||||
new_script = typeclass(**kwarg)
|
||||
|
|
@ -210,7 +223,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
|||
# store the call signature for the signal
|
||||
new_script._createdict = dict(key=key, obj=obj, account=account, locks=locks, interval=interval,
|
||||
start_delay=start_delay, repeats=repeats, persistent=persistent,
|
||||
autostart=autostart, report_to=report_to)
|
||||
autostart=autostart, report_to=report_to, desc=desc,
|
||||
tags=tags, attributes=attributes)
|
||||
# this will trigger the save signal which in turn calls the
|
||||
# at_first_save hook on the typeclass, where the _createdict
|
||||
# can be used.
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ except ImportError:
|
|||
from pickle import dumps, loads
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from evennia.utils.utils import to_str, uses_database
|
||||
from evennia.utils.utils import to_str, uses_database, is_iter
|
||||
from evennia.utils import logger
|
||||
|
||||
__all__ = ("to_pickle", "from_pickle", "do_pickle", "do_unpickle",
|
||||
|
|
@ -364,6 +364,31 @@ class _SaverDeque(_SaverMutable):
|
|||
def rotate(self, *args):
|
||||
self._data.rotate(*args)
|
||||
|
||||
|
||||
_DESERIALIZE_MAPPING = {_SaverList.__name__: list, _SaverDict.__name__: dict,
|
||||
_SaverSet.__name__: set, _SaverOrderedDict.__name__: OrderedDict,
|
||||
_SaverDeque.__name__: deque}
|
||||
|
||||
|
||||
def deserialize(obj):
|
||||
"""
|
||||
Make sure to *fully* decouple a structure from the database, by turning all _Saver*-mutables
|
||||
inside it back into their normal Python forms.
|
||||
|
||||
"""
|
||||
def _iter(obj):
|
||||
typ = type(obj)
|
||||
tname = typ.__name__
|
||||
if tname in ('_SaverDict', 'dict'):
|
||||
return {_iter(key): _iter(val) for key, val in obj.items()}
|
||||
elif tname in _DESERIALIZE_MAPPING:
|
||||
return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj)
|
||||
elif is_iter(obj):
|
||||
return typ(_iter(val) for val in obj)
|
||||
return obj
|
||||
return _iter(obj)
|
||||
|
||||
|
||||
#
|
||||
# serialization helpers
|
||||
|
||||
|
|
|
|||
|
|
@ -153,6 +153,22 @@ INVALID_FORMCHARS = r"\s\/\|\\\*\_\-\#\<\>\~\^\:\;\.\,"
|
|||
_ANSI_ESCAPE = re.compile(r"\|\|")
|
||||
|
||||
|
||||
def _to_rect(lines):
|
||||
"""
|
||||
Forces all lines to be as long as the longest
|
||||
|
||||
Args:
|
||||
lines (list): list of `ANSIString`s
|
||||
|
||||
Returns:
|
||||
(list): list of `ANSIString`s of
|
||||
same length as the longest input line
|
||||
|
||||
"""
|
||||
maxl = max(len(line) for line in lines)
|
||||
return [line + ' ' * (maxl - len(line)) for line in lines]
|
||||
|
||||
|
||||
def _to_ansi(obj, regexable=False):
|
||||
"convert to ANSIString"
|
||||
if isinstance(obj, basestring):
|
||||
|
|
@ -184,7 +200,7 @@ class EvForm(object):
|
|||
filename (str): Path to template file.
|
||||
cells (dict): A dictionary mapping of {id:text}
|
||||
tables (dict): A dictionary mapping of {id:EvTable}.
|
||||
form (dict): A dictionary of {"CELLCHAR":char,
|
||||
form (dict): A dictionary of {"FORMCHAR":char,
|
||||
"TABLECHAR":char,
|
||||
"FORM":templatestring}
|
||||
if this is given, filename is not read.
|
||||
|
|
@ -408,7 +424,9 @@ class EvForm(object):
|
|||
self.tablechar = tablechar[0] if len(tablechar) > 1 else tablechar
|
||||
|
||||
# split into a list of list of lines. Form can be indexed with form[iy][ix]
|
||||
self.raw_form = _to_ansi(to_unicode(datadict.get("FORM", "")).split("\n"))
|
||||
raw_form = _to_ansi(to_unicode(datadict.get("FORM", "")).split("\n"))
|
||||
self.raw_form = _to_rect(raw_form)
|
||||
|
||||
# strip first line
|
||||
self.raw_form = self.raw_form[1:] if self.raw_form else self.raw_form
|
||||
|
||||
|
|
@ -440,7 +458,8 @@ def _test():
|
|||
6: 5,
|
||||
7: 18,
|
||||
8: 10,
|
||||
9: 3})
|
||||
9: 3,
|
||||
"F": "rev 1"})
|
||||
# create the EvTables
|
||||
tableA = EvTable("HP", "MV", "MP",
|
||||
table=[["**"], ["*****"], ["***"]],
|
||||
|
|
|
|||
|
|
@ -43,13 +43,18 @@ command definition too) with function definitions:
|
|||
def node_with_other_name(caller, input_string):
|
||||
# code
|
||||
return text, options
|
||||
|
||||
def another_node(caller, input_string, **kwargs):
|
||||
# code
|
||||
return text, options
|
||||
```
|
||||
|
||||
Where caller is the object using the menu and input_string is the
|
||||
command entered by the user on the *previous* node (the command
|
||||
entered to get to this node). The node function code will only be
|
||||
executed once per node-visit and the system will accept nodes with
|
||||
both one or two arguments interchangeably.
|
||||
both one or two arguments interchangeably. It also accepts nodes
|
||||
that takes **kwargs.
|
||||
|
||||
The menu tree itself is available on the caller as
|
||||
`caller.ndb._menutree`. This makes it a convenient place to store
|
||||
|
|
@ -63,23 +68,33 @@ menu is immediately exited and the default "look" command is called.
|
|||
text (str, tuple or None): Text shown at this node. If a tuple, the
|
||||
second element in the tuple is a help text to display at this
|
||||
node when the user enters the menu help command there.
|
||||
options (tuple, dict or None): (
|
||||
{'key': name, # can also be a list of aliases. A special key is
|
||||
# "_default", which marks this option as the default
|
||||
# fallback when no other option matches the user input.
|
||||
'desc': description, # optional description
|
||||
'goto': nodekey, # node to go to when chosen. This can also be a callable with
|
||||
# caller and/or raw_string args. It must return a string
|
||||
# with the key pointing to the node to go to.
|
||||
'exec': nodekey}, # node or callback to trigger as callback when chosen. This
|
||||
# will execute *before* going to the next node. Both node
|
||||
# and the explicit callback will be called as normal nodes
|
||||
# (with caller and/or raw_string args). If the callable/node
|
||||
# returns a single string (only), this will replace the current
|
||||
# goto location string in-place (if a goto callback, it will never fire).
|
||||
# Note that relying to much on letting exec assign the goto
|
||||
# location can make it hard to debug your menu logic.
|
||||
{...}, ...)
|
||||
options (tuple, dict or None): If `None`, this exits the menu.
|
||||
If a single dict, this is a single-option node. If a tuple,
|
||||
it should be a tuple of option dictionaries. Option dicts have
|
||||
the following keys:
|
||||
- `key` (str or tuple, optional): What to enter to choose this option.
|
||||
If a tuple, it must be a tuple of strings, where the first string is the
|
||||
key which will be shown to the user and the others are aliases.
|
||||
If unset, the options' number will be used. The special key `_default`
|
||||
marks this option as the default fallback when no other option matches
|
||||
the user input. There can only be one `_default` option per node. It
|
||||
will not be displayed in the list.
|
||||
- `desc` (str, optional): This describes what choosing the option will do.
|
||||
- `goto` (str, tuple or callable): If string, should be the name of node to go to
|
||||
when this option is selected. If a callable, it has the signature
|
||||
`callable(caller[,raw_input][,**kwargs]). If a tuple, the first element
|
||||
is the callable and the second is a dict with the **kwargs to pass to
|
||||
the callable. Those kwargs will also be passed into the next node if possible.
|
||||
Such a callable should return either a str or a (str, dict), where the
|
||||
string is the name of the next node to go to and the dict is the new,
|
||||
(possibly modified) kwarg to pass into the next node. If the callable returns
|
||||
None or the empty string, the current node will be revisited.
|
||||
- `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
|
||||
and runs before it. If given a node name, the node will be executed but will not
|
||||
be considered the next node. If node/callback returns str or (str, dict), these will
|
||||
replace the `goto` step (`goto` callbacks will not fire), with the string being the
|
||||
next node name and the optional dict acting as the kwargs-input for the next node.
|
||||
If an exec callable returns the empty string (only), the current node is re-run.
|
||||
|
||||
If key is not given, the option will automatically be identified by
|
||||
its number 1..N.
|
||||
|
|
@ -95,7 +110,7 @@ Example:
|
|||
"This is help text for this node")
|
||||
options = ({"key": "testing",
|
||||
"desc": "Select this to go to node 2",
|
||||
"goto": "node2",
|
||||
"goto": ("node2", {"foo": "bar"}),
|
||||
"exec": "callback1"},
|
||||
{"desc": "Go to node 3.",
|
||||
"goto": "node3"})
|
||||
|
|
@ -108,12 +123,13 @@ Example:
|
|||
# by the normal 'goto' option key above.
|
||||
caller.msg("Callback called!")
|
||||
|
||||
def node2(caller):
|
||||
def node2(caller, **kwargs):
|
||||
text = '''
|
||||
This is node 2. It only allows you to go back
|
||||
to the original node1. This extra indent will
|
||||
be stripped. We don't include a help text.
|
||||
'''
|
||||
be stripped. We don't include a help text but
|
||||
here are the variables passed to us: {}
|
||||
'''.format(kwargs)
|
||||
options = {"goto": "node1"}
|
||||
return text, options
|
||||
|
||||
|
|
@ -148,16 +164,17 @@ evennia.utils.evmenu`.
|
|||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
import random
|
||||
import inspect
|
||||
from builtins import object, range
|
||||
|
||||
from textwrap import dedent
|
||||
from inspect import isfunction, getargspec
|
||||
from django.conf import settings
|
||||
from evennia import Command, CmdSet
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.ansi import strip_ansi
|
||||
from evennia.utils.utils import mod_import, make_iter, pad, m_len
|
||||
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
|
||||
from evennia.commands import cmdhandler
|
||||
|
||||
# read from protocol NAWS later?
|
||||
|
|
@ -172,7 +189,8 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
|||
|
||||
# i18n
|
||||
from django.utils.translation import ugettext as _
|
||||
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is not implemented. Make another choice.")
|
||||
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is either not implemented or "
|
||||
"caused an error. Make another choice.")
|
||||
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
|
||||
_ERR_NO_OPTION_DESC = _("No description.")
|
||||
_HELP_FULL = _("Commands: <menu option>, help, quit")
|
||||
|
|
@ -260,7 +278,7 @@ class CmdEvMenuNode(Command):
|
|||
err = "Menu object not found as %s.ndb._menutree!" % orig_caller
|
||||
orig_caller.msg(err) # don't give the session as a kwarg here, direct to original
|
||||
raise EvMenuError(err)
|
||||
# we must do this after the caller with the menui has been correctly identified since it
|
||||
# we must do this after the caller with the menu has been correctly identified since it
|
||||
# can be either Account, Object or Session (in the latter case this info will be superfluous).
|
||||
caller.ndb._menutree._session = self.session
|
||||
# we have a menu, use it.
|
||||
|
|
@ -305,7 +323,7 @@ class EvMenu(object):
|
|||
auto_quit=True, auto_look=True, auto_help=True,
|
||||
cmd_on_exit="look",
|
||||
persistent=False, startnode_input="", session=None,
|
||||
**kwargs):
|
||||
debug=False, **kwargs):
|
||||
"""
|
||||
Initialize the menu tree and start the caller onto the first node.
|
||||
|
||||
|
|
@ -358,15 +376,21 @@ class EvMenu(object):
|
|||
*pickle*. When the server is reloaded, the latest node shown will be completely
|
||||
re-run with the same input arguments - so be careful if you are counting
|
||||
up some persistent counter or similar - the counter may be run twice if
|
||||
reload happens on the node that does that.
|
||||
startnode_input (str, optional): Send an input text to `startnode` as if
|
||||
a user input text from a fictional previous node. When the server reloads,
|
||||
the latest visited node will be re-run using this kwarg.
|
||||
reload happens on the node that does that. Note that if `debug` is True,
|
||||
this setting is ignored and assumed to be False.
|
||||
startnode_input (str or (str, dict), optional): Send an input text to `startnode` as if
|
||||
a user input text from a fictional previous node. If including the dict, this will
|
||||
be passed as **kwargs to that node. When the server reloads,
|
||||
the latest visited node will be re-run as `node(caller, raw_string, **kwargs)`.
|
||||
session (Session, optional): This is useful when calling EvMenu from an account
|
||||
in multisession mode > 2. Note that this session only really relevant
|
||||
for the very first display of the first node - after that, EvMenu itself
|
||||
will keep the session updated from the command input. So a persistent
|
||||
menu will *not* be using this same session anymore after a reload.
|
||||
debug (bool, optional): If set, the 'menudebug' command will be made available
|
||||
by default in all nodes of the menu. This will print out the current state of
|
||||
the menu. Deactivate for production use! When the debug flag is active, the
|
||||
`persistent` flag is deactivated.
|
||||
|
||||
Kwargs:
|
||||
any (any): All kwargs will become initialization variables on `caller.ndb._menutree`,
|
||||
|
|
@ -390,7 +414,8 @@ class EvMenu(object):
|
|||
"""
|
||||
self._startnode = startnode
|
||||
self._menutree = self._parse_menudata(menudata)
|
||||
self._persistent = persistent
|
||||
self._persistent = persistent if not debug else False
|
||||
self._quitting = False
|
||||
|
||||
if startnode not in self._menutree:
|
||||
raise EvMenuError("Start node '%s' not in menu tree!" % startnode)
|
||||
|
|
@ -403,6 +428,7 @@ class EvMenu(object):
|
|||
self.auto_quit = auto_quit
|
||||
self.auto_look = auto_look
|
||||
self.auto_help = auto_help
|
||||
self.debug_mode = debug
|
||||
self._session = session
|
||||
if isinstance(cmd_on_exit, str):
|
||||
# At this point menu._session will have been replaced by the
|
||||
|
|
@ -417,6 +443,12 @@ class EvMenu(object):
|
|||
self.nodetext = None
|
||||
self.helptext = None
|
||||
self.options = None
|
||||
self.nodename = None
|
||||
self.node_kwargs = {}
|
||||
|
||||
# used for testing
|
||||
self.test_options = {}
|
||||
self.test_nodetext = ""
|
||||
|
||||
# assign kwargs as initialization vars on ourselves.
|
||||
if set(("_startnode", "_menutree", "_session", "_persistent",
|
||||
|
|
@ -463,8 +495,13 @@ class EvMenu(object):
|
|||
menu_cmdset.priority = int(cmdset_priority)
|
||||
self.caller.cmdset.add(menu_cmdset, permanent=persistent)
|
||||
|
||||
startnode_kwargs = {}
|
||||
if isinstance(startnode_input, (tuple, list)) and len(startnode_input) > 1:
|
||||
startnode_input, startnode_kwargs = startnode_input[:2]
|
||||
if not isinstance(startnode_kwargs, dict):
|
||||
raise EvMenuError("startnode_input must be either a str or a tuple (str, dict).")
|
||||
# start the menu
|
||||
self.goto(self._startnode, startnode_input)
|
||||
self.goto(self._startnode, startnode_input, **startnode_kwargs)
|
||||
|
||||
def _parse_menudata(self, menudata):
|
||||
"""
|
||||
|
|
@ -519,7 +556,43 @@ class EvMenu(object):
|
|||
# format the entire node
|
||||
return self.node_formatter(nodetext, optionstext)
|
||||
|
||||
def _execute_node(self, nodename, raw_string):
|
||||
def _safe_call(self, callback, raw_string, **kwargs):
|
||||
"""
|
||||
Call a node-like callable, with a variable number of raw_string, *args, **kwargs, all of
|
||||
which should work also if not present (only `caller` is always required). Return its result.
|
||||
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
nargs = len(getargspec(callback).args)
|
||||
except TypeError:
|
||||
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
||||
supports_kwargs = bool(getargspec(callback).keywords)
|
||||
if nargs <= 0:
|
||||
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
||||
|
||||
if supports_kwargs:
|
||||
if nargs > 1:
|
||||
ret = callback(self.caller, raw_string, **kwargs)
|
||||
# callback accepting raw_string, **kwargs
|
||||
else:
|
||||
# callback accepting **kwargs
|
||||
ret = callback(self.caller, **kwargs)
|
||||
elif nargs > 1:
|
||||
# callback accepting raw_string
|
||||
ret = callback(self.caller, raw_string)
|
||||
else:
|
||||
# normal callback, only the caller as arg
|
||||
ret = callback(self.caller)
|
||||
except EvMenuError:
|
||||
errmsg = _ERR_GENERAL.format(nodename=callback)
|
||||
self.caller.msg(errmsg, self._session)
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
return ret
|
||||
|
||||
def _execute_node(self, nodename, raw_string, **kwargs):
|
||||
"""
|
||||
Execute a node.
|
||||
|
||||
|
|
@ -528,6 +601,7 @@ class EvMenu(object):
|
|||
raw_string (str): The raw default string entered on the
|
||||
previous node (only used if the node accepts it as an
|
||||
argument)
|
||||
kwargs (any, optional): Optional kwargs for the node.
|
||||
|
||||
Returns:
|
||||
nodetext, options (tuple): The node text (a string or a
|
||||
|
|
@ -540,47 +614,27 @@ class EvMenu(object):
|
|||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||
raise EvMenuError
|
||||
try:
|
||||
# the node should return data as (text, options)
|
||||
if len(getargspec(node).args) > 1:
|
||||
# a node accepting raw_string
|
||||
nodetext, options = node(self.caller, raw_string)
|
||||
ret = self._safe_call(node, raw_string, **kwargs)
|
||||
if isinstance(ret, (tuple, list)) and len(ret) > 1:
|
||||
nodetext, options = ret[:2]
|
||||
else:
|
||||
# a normal node, only accepting caller
|
||||
nodetext, options = node(self.caller)
|
||||
nodetext, options = ret, None
|
||||
except KeyError:
|
||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||
logger.log_trace()
|
||||
raise EvMenuError
|
||||
except Exception:
|
||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
# store options to make them easier to test
|
||||
self.test_options = options
|
||||
self.test_nodetext = nodetext
|
||||
|
||||
return nodetext, options
|
||||
|
||||
def display_nodetext(self):
|
||||
self.caller.msg(self.nodetext, session=self._session)
|
||||
|
||||
def display_helptext(self):
|
||||
self.caller.msg(self.helptext, session=self._session)
|
||||
|
||||
def callback_goto(self, callback, goto, raw_string):
|
||||
"""
|
||||
Call callback and goto in sequence.
|
||||
|
||||
Args:
|
||||
callback (callable or str): Callback to run before goto. If
|
||||
the callback returns a string, this is used to replace
|
||||
the `goto` string before going to the next node.
|
||||
goto (str): The target node to go to next (unless replaced
|
||||
by `callable`)..
|
||||
raw_string (str): The original user input.
|
||||
|
||||
"""
|
||||
if callback:
|
||||
# replace goto only if callback returns
|
||||
goto = self.callback(callback, raw_string) or goto
|
||||
if goto:
|
||||
self.goto(goto, raw_string)
|
||||
|
||||
def callback(self, nodename, raw_string):
|
||||
def run_exec(self, nodename, raw_string, **kwargs):
|
||||
"""
|
||||
Run a function or node as a callback (with the 'exec' option key).
|
||||
|
||||
|
|
@ -592,6 +646,8 @@ class EvMenu(object):
|
|||
raw_string (str): The raw default string entered on the
|
||||
previous node (only used if the node accepts it as an
|
||||
argument)
|
||||
kwargs (any): These are optional kwargs passed into goto
|
||||
|
||||
Returns:
|
||||
new_goto (str or None): A replacement goto location string or
|
||||
None (no replacement).
|
||||
|
|
@ -602,64 +658,111 @@ class EvMenu(object):
|
|||
relying on this.
|
||||
|
||||
"""
|
||||
if callable(nodename):
|
||||
# this is a direct callable - execute it directly
|
||||
try:
|
||||
if len(getargspec(nodename).args) > 1:
|
||||
# callable accepting raw_string
|
||||
ret = nodename(self.caller, raw_string)
|
||||
else:
|
||||
# normal callable, only the caller as arg
|
||||
ret = nodename(self.caller)
|
||||
except Exception:
|
||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session)
|
||||
raise
|
||||
else:
|
||||
# nodename is a string; lookup as node
|
||||
try:
|
||||
try:
|
||||
if callable(nodename):
|
||||
# this is a direct callable - execute it directly
|
||||
ret = self._safe_call(nodename, raw_string, **kwargs)
|
||||
if isinstance(ret, (tuple, list)):
|
||||
if not len(ret) > 1 or not isinstance(ret[1], dict):
|
||||
raise EvMenuError("exec callable must return either None, str or (str, dict)")
|
||||
ret, kwargs = ret[:2]
|
||||
else:
|
||||
# nodename is a string; lookup as node and run as node in-place (don't goto it)
|
||||
# execute the node
|
||||
ret = self._execute_node(nodename, raw_string)
|
||||
except EvMenuError as err:
|
||||
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string, err)
|
||||
self.caller.msg("|r%s|n" % errmsg)
|
||||
logger.log_trace(errmsg)
|
||||
return
|
||||
ret = self._execute_node(nodename, raw_string, **kwargs)
|
||||
if isinstance(ret, (tuple, list)):
|
||||
if not len(ret) > 1 and ret[1] and not isinstance(ret[1], dict):
|
||||
raise EvMenuError("exec node must return either None, str or (str, dict)")
|
||||
ret, kwargs = ret[:2]
|
||||
except EvMenuError as err:
|
||||
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string.rstrip(), err)
|
||||
self.caller.msg("|r%s|n" % errmsg)
|
||||
logger.log_trace(errmsg)
|
||||
return
|
||||
|
||||
if isinstance(ret, basestring):
|
||||
# only return a value if a string (a goto target), ignore all other returns
|
||||
return ret
|
||||
if not ret:
|
||||
# an empty string - rerun the same node
|
||||
return self.nodename
|
||||
return ret, kwargs
|
||||
return None
|
||||
|
||||
def goto(self, nodename, raw_string):
|
||||
def extract_goto_exec(self, nodename, option_dict):
|
||||
"""
|
||||
Run a node by name
|
||||
Helper: Get callables and their eventual kwargs.
|
||||
|
||||
Args:
|
||||
nodename (str): The current node name (used for error reporting).
|
||||
option_dict (dict): The seleted option's dict.
|
||||
|
||||
Returns:
|
||||
goto (str, callable or None): The goto directive in the option.
|
||||
goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty.
|
||||
execute (callable or None): Executable given by the `exec` directive.
|
||||
exec_kwargs (dict): Kwargs for `execute` if it's callable, otherwise empty.
|
||||
|
||||
"""
|
||||
goto_kwargs, exec_kwargs = {}, {}
|
||||
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
||||
if goto and isinstance(goto, (tuple, list)):
|
||||
if len(goto) > 1:
|
||||
goto, goto_kwargs = goto[:2] # ignore any extra arguments
|
||||
if not hasattr(goto_kwargs, "__getitem__"):
|
||||
# not a dict-like structure
|
||||
raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format(
|
||||
nodename, goto_kwargs))
|
||||
else:
|
||||
goto = goto[0]
|
||||
if execute and isinstance(execute, (tuple, list)):
|
||||
if len(execute) > 1:
|
||||
execute, exec_kwargs = execute[:2] # ignore any extra arguments
|
||||
if not hasattr(exec_kwargs, "__getitem__"):
|
||||
# not a dict-like structure
|
||||
raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format(
|
||||
nodename, goto_kwargs))
|
||||
else:
|
||||
execute = execute[0]
|
||||
return goto, goto_kwargs, execute, exec_kwargs
|
||||
|
||||
def goto(self, nodename, raw_string, **kwargs):
|
||||
"""
|
||||
Run a node by name, optionally dynamically generating that name first.
|
||||
|
||||
Args:
|
||||
nodename (str or callable): Name of node or a callable
|
||||
to be called as `function(caller, raw_string)` or `function(caller)`
|
||||
to return the actual goto string.
|
||||
to be called as `function(caller, raw_string, **kwargs)` or
|
||||
`function(caller, **kwargs)` to return the actual goto string or
|
||||
a ("nodename", kwargs) tuple.
|
||||
raw_string (str): The raw default string entered on the
|
||||
previous node (only used if the node accepts it as an
|
||||
argument)
|
||||
Kwargs:
|
||||
any: Extra arguments to goto callables.
|
||||
|
||||
"""
|
||||
|
||||
if callable(nodename):
|
||||
try:
|
||||
if len(getargspec(nodename).args) > 1:
|
||||
# callable accepting raw_string
|
||||
nodename = nodename(self.caller, raw_string)
|
||||
else:
|
||||
nodename = nodename(self.caller)
|
||||
except Exception:
|
||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session)
|
||||
raise
|
||||
# run the "goto" callable, if possible
|
||||
inp_nodename = nodename
|
||||
nodename = self._safe_call(nodename, raw_string, **kwargs)
|
||||
if isinstance(nodename, (tuple, list)):
|
||||
if not len(nodename) > 1 or not isinstance(nodename[1], dict):
|
||||
raise EvMenuError(
|
||||
"{}: goto callable must return str or (str, dict)".format(inp_nodename))
|
||||
nodename, kwargs = nodename[:2]
|
||||
if not nodename:
|
||||
# no nodename return. Re-run current node
|
||||
nodename = self.nodename
|
||||
try:
|
||||
# execute the node, make use of the returns.
|
||||
nodetext, options = self._execute_node(nodename, raw_string)
|
||||
# execute the found node, make use of the returns.
|
||||
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
|
||||
except EvMenuError:
|
||||
return
|
||||
|
||||
if self._persistent:
|
||||
self.caller.attributes.add("_menutree_saved_startnode", (nodename, raw_string))
|
||||
self.caller.attributes.add("_menutree_saved_startnode",
|
||||
(nodename, (raw_string, kwargs)))
|
||||
|
||||
# validation of the node return values
|
||||
helptext = ""
|
||||
|
|
@ -680,26 +783,29 @@ class EvMenu(object):
|
|||
for inum, dic in enumerate(options):
|
||||
# fix up the option dicts
|
||||
keys = make_iter(dic.get("key"))
|
||||
desc = dic.get("desc", dic.get("text", None))
|
||||
if "_default" in keys:
|
||||
keys = [key for key in keys if key != "_default"]
|
||||
desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip())
|
||||
goto, execute = dic.get("goto", None), dic.get("exec", None)
|
||||
self.default = (goto, execute)
|
||||
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
|
||||
self.default = (goto, goto_kwargs, execute, exec_kwargs)
|
||||
else:
|
||||
# use the key (only) if set, otherwise use the running number
|
||||
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
|
||||
desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip())
|
||||
goto, execute = dic.get("goto", None), dic.get("exec", None)
|
||||
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
|
||||
if keys:
|
||||
display_options.append((keys[0], desc))
|
||||
for key in keys:
|
||||
if goto or execute:
|
||||
self.options[strip_ansi(key).strip().lower()] = (goto, execute)
|
||||
self.options[strip_ansi(key).strip().lower()] = \
|
||||
(goto, goto_kwargs, execute, exec_kwargs)
|
||||
|
||||
self.nodetext = self._format_node(nodetext, display_options)
|
||||
self.node_kwargs = kwargs
|
||||
self.nodename = nodename
|
||||
|
||||
# handle the helptext
|
||||
if helptext:
|
||||
self.helptext = helptext
|
||||
self.helptext = self.helptext_formatter(helptext)
|
||||
elif options:
|
||||
self.helptext = _HELP_FULL if self.auto_quit else _HELP_NO_QUIT
|
||||
else:
|
||||
|
|
@ -709,17 +815,89 @@ class EvMenu(object):
|
|||
if not options:
|
||||
self.close_menu()
|
||||
|
||||
def run_exec_then_goto(self, runexec, goto, raw_string, runexec_kwargs=None, goto_kwargs=None):
|
||||
"""
|
||||
Call 'exec' callback and goto (which may also be a callable) in sequence.
|
||||
|
||||
Args:
|
||||
runexec (callable or str): Callback to run before goto. If
|
||||
the callback returns a string, this is used to replace
|
||||
the `goto` string/callable before being passed into the goto handler.
|
||||
goto (str): The target node to go to next (may be replaced
|
||||
by `runexec`)..
|
||||
raw_string (str): The original user input.
|
||||
runexec_kwargs (dict, optional): Optional kwargs for runexec.
|
||||
goto_kwargs (dict, optional): Optional kwargs for goto.
|
||||
|
||||
"""
|
||||
if runexec:
|
||||
# replace goto only if callback returns
|
||||
goto, goto_kwargs = (
|
||||
self.run_exec(runexec, raw_string,
|
||||
**(runexec_kwargs if runexec_kwargs else {})) or
|
||||
(goto, goto_kwargs))
|
||||
if goto:
|
||||
self.goto(goto, raw_string, **(goto_kwargs if goto_kwargs else {}))
|
||||
|
||||
def close_menu(self):
|
||||
"""
|
||||
Shutdown menu; occurs when reaching the end node or using the quit command.
|
||||
"""
|
||||
self.caller.cmdset.remove(EvMenuCmdSet)
|
||||
del self.caller.ndb._menutree
|
||||
if self._persistent:
|
||||
self.caller.attributes.remove("_menutree_saved")
|
||||
self.caller.attributes.remove("_menutree_saved_startnode")
|
||||
if self.cmd_on_exit is not None:
|
||||
self.cmd_on_exit(self.caller, self)
|
||||
if not self._quitting:
|
||||
# avoid multiple calls from different sources
|
||||
self._quitting = True
|
||||
self.caller.cmdset.remove(EvMenuCmdSet)
|
||||
del self.caller.ndb._menutree
|
||||
if self._persistent:
|
||||
self.caller.attributes.remove("_menutree_saved")
|
||||
self.caller.attributes.remove("_menutree_saved_startnode")
|
||||
if self.cmd_on_exit is not None:
|
||||
self.cmd_on_exit(self.caller, self)
|
||||
|
||||
def print_debug_info(self, arg):
|
||||
"""
|
||||
Messages the caller with the current menu state, for debug purposes.
|
||||
|
||||
Args:
|
||||
arg (str): Arg to debug instruction, either nothing, 'full' or the name
|
||||
of a property to inspect.
|
||||
|
||||
"""
|
||||
all_props = inspect.getmembers(self)
|
||||
all_methods = [name for name, _ in inspect.getmembers(self, predicate=inspect.ismethod)]
|
||||
all_builtins = [name for name, _ in inspect.getmembers(self, predicate=inspect.isbuiltin)]
|
||||
props = {prop: value for prop, value in all_props if prop not in all_methods and
|
||||
prop not in all_builtins and not prop.endswith("__")}
|
||||
|
||||
local = {key: var for key, var in locals().items()
|
||||
if key not in all_props and not key.endswith("__")}
|
||||
|
||||
if arg:
|
||||
if arg in props:
|
||||
debugtxt = " |y* {}:|n\n{}".format(arg, props[arg])
|
||||
elif arg in local:
|
||||
debugtxt = " |y* {}:|n\n{}".format(arg, local[arg])
|
||||
elif arg == 'full':
|
||||
debugtxt = ("|yMENU DEBUG full ... |n\n" + "\n".join(
|
||||
"|y *|n {}: {}".format(key, val)
|
||||
for key, val in sorted(props.items())) +
|
||||
"\n |yLOCAL VARS:|n\n" + "\n".join(
|
||||
"|y *|n {}: {}".format(key, val)
|
||||
for key, val in sorted(local.items())) +
|
||||
"\n |y... END MENU DEBUG|n")
|
||||
else:
|
||||
debugtxt = "|yUsage: menudebug full|<name of property>|n"
|
||||
else:
|
||||
debugtxt = ("|yMENU DEBUG properties ... |n\n" + "\n".join(
|
||||
"|y *|n {}: {}".format(
|
||||
key, crop(to_str(val, force_string=True), width=50))
|
||||
for key, val in sorted(props.items())) +
|
||||
"\n |yLOCAL VARS:|n\n" + "\n".join(
|
||||
"|y *|n {}: {}".format(
|
||||
key, crop(to_str(val, force_string=True), width=50))
|
||||
for key, val in sorted(local.items())) +
|
||||
"\n |y... END MENU DEBUG|n")
|
||||
self.caller.msg(debugtxt)
|
||||
|
||||
def parse_input(self, raw_string):
|
||||
"""
|
||||
|
|
@ -734,25 +912,33 @@ class EvMenu(object):
|
|||
should also report errors directly to the user.
|
||||
|
||||
"""
|
||||
cmd = raw_string.strip().lower()
|
||||
cmd = strip_ansi(raw_string.strip().lower())
|
||||
|
||||
if cmd in self.options:
|
||||
# this will take precedence over the default commands
|
||||
# below
|
||||
goto, callback = self.options[cmd]
|
||||
self.callback_goto(callback, goto, raw_string)
|
||||
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
|
||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||
elif self.auto_look and cmd in ("look", "l"):
|
||||
self.display_nodetext()
|
||||
elif self.auto_help and cmd in ("help", "h"):
|
||||
self.display_helptext()
|
||||
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
||||
self.close_menu()
|
||||
elif self.debug_mode and cmd.startswith("menudebug"):
|
||||
self.print_debug_info(cmd[9:].strip())
|
||||
elif self.default:
|
||||
goto, callback = self.default
|
||||
self.callback_goto(callback, goto, raw_string)
|
||||
goto, goto_kwargs, execfunc, exec_kwargs = self.default
|
||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||
else:
|
||||
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
|
||||
|
||||
def display_nodetext(self):
|
||||
self.caller.msg(self.nodetext, session=self._session)
|
||||
|
||||
def display_helptext(self):
|
||||
self.caller.msg(self.helptext, session=self._session)
|
||||
|
||||
# formatters - override in a child class
|
||||
|
||||
def nodetext_formatter(self, nodetext):
|
||||
|
|
@ -766,7 +952,20 @@ class EvMenu(object):
|
|||
nodetext (str): The formatted node text.
|
||||
|
||||
"""
|
||||
return dedent(nodetext).strip()
|
||||
return dedent(nodetext.strip('\n'), baseline_index=0).rstrip()
|
||||
|
||||
def helptext_formatter(self, helptext):
|
||||
"""
|
||||
Format the node's help text
|
||||
|
||||
Args:
|
||||
helptext (str): The unformatted help text for the node.
|
||||
|
||||
Returns:
|
||||
helptext (str): The formatted help text.
|
||||
|
||||
"""
|
||||
return dedent(helptext.strip('\n'), baseline_index=0).rstrip()
|
||||
|
||||
def options_formatter(self, optionlist):
|
||||
"""
|
||||
|
|
@ -795,16 +994,17 @@ class EvMenu(object):
|
|||
for key, desc in optionlist:
|
||||
if not (key or desc):
|
||||
continue
|
||||
desc_string = ": %s" % desc if desc else ""
|
||||
table_width_max = max(table_width_max,
|
||||
max(m_len(p) for p in key.split("\n")) +
|
||||
max(m_len(p) for p in desc.split("\n")) + colsep)
|
||||
max(m_len(p) for p in desc_string.split("\n")) + colsep)
|
||||
raw_key = strip_ansi(key)
|
||||
if raw_key != key:
|
||||
# already decorations in key definition
|
||||
table.append(" |lc%s|lt%s|le: %s" % (raw_key, key, desc))
|
||||
table.append(" |lc%s|lt%s|le%s" % (raw_key, key, desc_string))
|
||||
else:
|
||||
# add a default white color to key
|
||||
table.append(" |lc%s|lt|w%s|n|le: %s" % (raw_key, raw_key, desc))
|
||||
table.append(" |lc%s|lt|w%s|n|le%s" % (raw_key, raw_key, desc_string))
|
||||
|
||||
ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols
|
||||
|
||||
|
|
@ -845,14 +1045,188 @@ class EvMenu(object):
|
|||
node (str): The formatted node to display.
|
||||
|
||||
"""
|
||||
if self._session:
|
||||
screen_width = self._session.protocol_flags.get(
|
||||
"SCREENWIDTH", {0: _MAX_TEXT_WIDTH})[0]
|
||||
else:
|
||||
screen_width = _MAX_TEXT_WIDTH
|
||||
|
||||
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
|
||||
options_width_max = max(m_len(line) for line in optionstext.split("\n"))
|
||||
total_width = max(options_width_max, nodetext_width_max)
|
||||
total_width = min(screen_width, max(options_width_max, nodetext_width_max))
|
||||
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
|
||||
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
|
||||
return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext
|
||||
|
||||
|
||||
# -----------------------------------------------------------
|
||||
#
|
||||
# List node (decorator turning a node into a list with
|
||||
# look/edit/add functionality for the elements)
|
||||
#
|
||||
# -----------------------------------------------------------
|
||||
|
||||
def list_node(option_generator, select=None, pagesize=10):
|
||||
"""
|
||||
Decorator for making an EvMenu node into a multi-page list node. Will add new options,
|
||||
prepending those options added in the node.
|
||||
|
||||
Args:
|
||||
option_generator (callable or list): A list of strings indicating the options, or a callable
|
||||
that is called as option_generator(caller) to produce such a list.
|
||||
select (callable or str, optional): Node to redirect a selection to. Its `**kwargs` will
|
||||
contain the `available_choices` list and `selection` will hold one of the elements in
|
||||
that list. If a callable, it will be called as select(caller, menuchoice) where
|
||||
menuchoice is the chosen option as a string. Should return the target node to goto after
|
||||
this selection (or None to repeat the list-node). Note that if this is not given, the
|
||||
decorated node must itself provide a way to continue from the node!
|
||||
pagesize (int): How many options to show per page.
|
||||
|
||||
Example:
|
||||
@list_node(['foo', 'bar'], select)
|
||||
def node_index(caller):
|
||||
text = "describing the list"
|
||||
return text, []
|
||||
|
||||
Notes:
|
||||
All normal `goto` or `exec` callables returned from the decorated nodes will, if they accept
|
||||
**kwargs, get a new kwarg 'available_choices' injected. These are the ordered list of named
|
||||
options (descs) visible on the current node page.
|
||||
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
|
||||
def _select_parser(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Parse the select action
|
||||
"""
|
||||
available_choices = kwargs.get("available_choices", [])
|
||||
|
||||
try:
|
||||
index = int(raw_string.strip()) - 1
|
||||
selection = available_choices[index]
|
||||
except Exception:
|
||||
caller.msg("|rInvalid choice.|n")
|
||||
else:
|
||||
if callable(select):
|
||||
try:
|
||||
if bool(getargspec(select).keywords):
|
||||
return select(caller, selection, available_choices=available_choices)
|
||||
else:
|
||||
return select(caller, selection)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
elif select:
|
||||
# we assume a string was given, we inject the result into the kwargs
|
||||
# to pass on to the next node
|
||||
kwargs['selection'] = selection
|
||||
return str(select)
|
||||
# this means the previous node will be re-run with these same kwargs
|
||||
return None
|
||||
|
||||
def _list_node(caller, raw_string, **kwargs):
|
||||
|
||||
option_list = option_generator(caller) \
|
||||
if callable(option_generator) else option_generator
|
||||
|
||||
npages = 0
|
||||
page_index = 0
|
||||
page = []
|
||||
options = []
|
||||
|
||||
if option_list:
|
||||
nall_options = len(option_list)
|
||||
pages = [option_list[ind:ind + pagesize]
|
||||
for ind in range(0, nall_options, pagesize)]
|
||||
npages = len(pages)
|
||||
|
||||
page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
|
||||
page = pages[page_index]
|
||||
|
||||
text = ""
|
||||
extra_text = None
|
||||
|
||||
# dynamic, multi-page option list. Each selection leads to the `select`
|
||||
# callback being called with a result from the available choices
|
||||
options.extend([{"desc": opt,
|
||||
"goto": (_select_parser,
|
||||
{"available_choices": page})} for opt in page])
|
||||
|
||||
if npages > 1:
|
||||
# if the goto callable returns None, the same node is rerun, and
|
||||
# kwargs not used by the callable are passed on to the node. This
|
||||
# allows us to call ourselves over and over, using different kwargs.
|
||||
options.append({"key": ("|Wcurrent|n", "c"),
|
||||
"desc": "|W({}/{})|n".format(page_index + 1, npages),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index})})
|
||||
if page_index > 0:
|
||||
options.append({"key": ("|wp|Wrevious page|n", "p"),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index - 1})})
|
||||
if page_index < npages - 1:
|
||||
options.append({"key": ("|wn|Wext page|n", "n"),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index + 1})})
|
||||
|
||||
# add data from the decorated node
|
||||
|
||||
decorated_options = []
|
||||
supports_kwargs = bool(getargspec(func).keywords)
|
||||
try:
|
||||
if supports_kwargs:
|
||||
text, decorated_options = func(caller, raw_string, **kwargs)
|
||||
else:
|
||||
text, decorated_options = func(caller, raw_string)
|
||||
except TypeError:
|
||||
try:
|
||||
if supports_kwargs:
|
||||
text, decorated_options = func(caller, **kwargs)
|
||||
else:
|
||||
text, decorated_options = func(caller)
|
||||
except Exception:
|
||||
raise
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
else:
|
||||
if isinstance(decorated_options, dict):
|
||||
decorated_options = [decorated_options]
|
||||
else:
|
||||
decorated_options = make_iter(decorated_options)
|
||||
|
||||
extra_options = []
|
||||
if isinstance(decorated_options, dict):
|
||||
decorated_options = [decorated_options]
|
||||
for eopt in decorated_options:
|
||||
cback = ("goto" in eopt and "goto") or ("exec" in eopt and "exec") or None
|
||||
if cback:
|
||||
signature = eopt[cback]
|
||||
if callable(signature):
|
||||
# callable with no kwargs defined
|
||||
eopt[cback] = (signature, {"available_choices": page})
|
||||
elif is_iter(signature):
|
||||
if len(signature) > 1 and isinstance(signature[1], dict):
|
||||
signature[1]["available_choices"] = page
|
||||
eopt[cback] = signature
|
||||
elif signature:
|
||||
# a callable alone in a tuple (i.e. no previous kwargs)
|
||||
eopt[cback] = (signature[0], {"available_choices": page})
|
||||
else:
|
||||
# malformed input.
|
||||
logger.log_err("EvMenu @list_node decorator found "
|
||||
"malformed option to decorate: {}".format(eopt))
|
||||
extra_options.append(eopt)
|
||||
|
||||
options.extend(extra_options)
|
||||
text = text + "\n\n" + extra_text if extra_text else text
|
||||
|
||||
return text, options
|
||||
|
||||
return _list_node
|
||||
return decorator
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Simple input shortcuts
|
||||
|
|
@ -992,6 +1366,10 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs):
|
|||
#
|
||||
# -------------------------------------------------------------
|
||||
|
||||
def _generate_goto(caller, **kwargs):
|
||||
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
|
||||
|
||||
|
||||
def test_start_node(caller):
|
||||
menu = caller.ndb._menutree
|
||||
text = """
|
||||
|
|
@ -1016,6 +1394,9 @@ def test_start_node(caller):
|
|||
{"key": ("|yV|niew", "v"),
|
||||
"desc": "View your own name",
|
||||
"goto": "test_view_node"},
|
||||
{"key": ("|yD|nynamic", "d"),
|
||||
"desc": "Dynamic node",
|
||||
"goto": (_generate_goto, {"name": "test_dynamic_node"})},
|
||||
{"key": ("|yQ|nuit", "quit", "q", "Q"),
|
||||
"desc": "Quit this menu example.",
|
||||
"goto": "test_end_node"},
|
||||
|
|
@ -1025,7 +1406,7 @@ def test_start_node(caller):
|
|||
|
||||
|
||||
def test_look_node(caller):
|
||||
text = ""
|
||||
text = "This is a custom look location!"
|
||||
options = {"key": ("|yL|nook", "l"),
|
||||
"desc": "Go back to the previous menu.",
|
||||
"goto": "test_start_node"}
|
||||
|
|
@ -1052,12 +1433,11 @@ def test_set_node(caller):
|
|||
""")
|
||||
|
||||
options = {"key": ("back (default)", "_default"),
|
||||
"desc": "back to main",
|
||||
"goto": "test_start_node"}
|
||||
return text, options
|
||||
|
||||
|
||||
def test_view_node(caller):
|
||||
def test_view_node(caller, **kwargs):
|
||||
text = """
|
||||
Your name is |g%s|n!
|
||||
|
||||
|
|
@ -1067,9 +1447,14 @@ def test_view_node(caller):
|
|||
-always- use numbers (1...N) to refer to listed options also if you
|
||||
don't see a string option key (try it!).
|
||||
""" % caller.key
|
||||
options = {"desc": "back to main",
|
||||
"goto": "test_start_node"}
|
||||
return text, options
|
||||
if kwargs.get("executed_from_dynamic_node", False):
|
||||
# we are calling this node as a exec, skip return values
|
||||
caller.msg("|gCalled from dynamic node:|n \n {}".format(text))
|
||||
return
|
||||
else:
|
||||
options = {"desc": "back to main",
|
||||
"goto": "test_start_node"}
|
||||
return text, options
|
||||
|
||||
|
||||
def test_displayinput_node(caller, raw_string):
|
||||
|
|
@ -1085,12 +1470,48 @@ def test_displayinput_node(caller, raw_string):
|
|||
makes it hidden from view. It catches all input (except the
|
||||
in-menu help/quit commands) and will, in this case, bring you back
|
||||
to the start node.
|
||||
""" % raw_string
|
||||
""" % raw_string.rstrip()
|
||||
options = {"key": "_default",
|
||||
"goto": "test_start_node"}
|
||||
return text, options
|
||||
|
||||
|
||||
def _test_call(caller, raw_input, **kwargs):
|
||||
mode = kwargs.get("mode", "exec")
|
||||
|
||||
caller.msg("\n|y'{}' |n_test_call|y function called with\n "
|
||||
"caller: |n{}\n |yraw_input: \"|n{}|y\" \n kwargs: |n{}\n".format(
|
||||
mode, caller, raw_input.rstrip(), kwargs))
|
||||
|
||||
if mode == "exec":
|
||||
kwargs = {"random": random.random()}
|
||||
caller.msg("function modify kwargs to {}".format(kwargs))
|
||||
else:
|
||||
caller.msg("|ypassing function kwargs without modification.|n")
|
||||
|
||||
return "test_dynamic_node", kwargs
|
||||
|
||||
|
||||
def test_dynamic_node(caller, **kwargs):
|
||||
text = """
|
||||
This is a dynamic node with input:
|
||||
{}
|
||||
""".format(kwargs)
|
||||
options = ({"desc": "pass a new random number to this node",
|
||||
"goto": ("test_dynamic_node", {"random": random.random()})},
|
||||
{"desc": "execute a func with kwargs",
|
||||
"exec": (_test_call, {"mode": "exec", "test_random": random.random()})},
|
||||
{"desc": "dynamic_goto",
|
||||
"goto": (_test_call, {"mode": "goto", "goto_input": "test"})},
|
||||
{"desc": "exec test_view_node with kwargs",
|
||||
"exec": ("test_view_node", {"executed_from_dynamic_node": True}),
|
||||
"goto": "test_dynamic_node"},
|
||||
{"desc": "back to main",
|
||||
"goto": "test_start_node"})
|
||||
|
||||
return text, options
|
||||
|
||||
|
||||
def test_end_node(caller):
|
||||
text = """
|
||||
This is the end of the menu and since it has no options the menu
|
||||
|
|
|
|||
|
|
@ -122,7 +122,8 @@ class EvMore(object):
|
|||
"""
|
||||
|
||||
def __init__(self, caller, text, always_page=False, session=None,
|
||||
justify_kwargs=None, exit_on_lastpage=False, **kwargs):
|
||||
justify_kwargs=None, exit_on_lastpage=False,
|
||||
exit_cmd=None, **kwargs):
|
||||
"""
|
||||
Initialization of the text handler.
|
||||
|
||||
|
|
@ -141,6 +142,10 @@ class EvMore(object):
|
|||
page being completely filled, exit pager immediately. If unset,
|
||||
another move forward is required to exit. If set, the pager
|
||||
exit message will not be shown.
|
||||
exit_cmd (str, optional): If given, this command-string will be executed on
|
||||
the caller when the more page exits. Note that this will be using whatever
|
||||
cmdset the user had *before* the evmore pager was activated (so none of
|
||||
the evmore commands will be available when this is run).
|
||||
kwargs (any, optional): These will be passed on
|
||||
to the `caller.msg` method.
|
||||
|
||||
|
|
@ -151,6 +156,7 @@ class EvMore(object):
|
|||
self._npages = []
|
||||
self._npos = []
|
||||
self.exit_on_lastpage = exit_on_lastpage
|
||||
self.exit_cmd = exit_cmd
|
||||
self._exit_msg = "Exited |wmore|n pager."
|
||||
if not session:
|
||||
# if not supplied, use the first session to
|
||||
|
|
@ -202,15 +208,18 @@ class EvMore(object):
|
|||
# goto top of the text
|
||||
self.page_top()
|
||||
|
||||
def display(self):
|
||||
def display(self, show_footer=True):
|
||||
"""
|
||||
Pretty-print the page.
|
||||
"""
|
||||
pos = self._pos
|
||||
text = self._pages[pos]
|
||||
page = _DISPLAY.format(text=text,
|
||||
pageno=pos + 1,
|
||||
pagemax=self._npages)
|
||||
if show_footer:
|
||||
page = _DISPLAY.format(text=text,
|
||||
pageno=pos + 1,
|
||||
pagemax=self._npages)
|
||||
else:
|
||||
page = text
|
||||
# check to make sure our session is still valid
|
||||
sessions = self._caller.sessions.get()
|
||||
if not sessions:
|
||||
|
|
@ -245,9 +254,11 @@ class EvMore(object):
|
|||
self.page_quit()
|
||||
else:
|
||||
self._pos += 1
|
||||
self.display()
|
||||
if self.exit_on_lastpage and self._pos >= self._npages - 1:
|
||||
self.page_quit()
|
||||
if self.exit_on_lastpage and self._pos >= (self._npages - 1):
|
||||
self.display(show_footer=False)
|
||||
self.page_quit(quiet=True)
|
||||
else:
|
||||
self.display()
|
||||
|
||||
def page_back(self):
|
||||
"""
|
||||
|
|
@ -256,16 +267,20 @@ class EvMore(object):
|
|||
self._pos = max(0, self._pos - 1)
|
||||
self.display()
|
||||
|
||||
def page_quit(self):
|
||||
def page_quit(self, quiet=False):
|
||||
"""
|
||||
Quit the pager
|
||||
"""
|
||||
del self._caller.ndb._more
|
||||
self._caller.msg(text=self._exit_msg, **self._kwargs)
|
||||
if not quiet:
|
||||
self._caller.msg(text=self._exit_msg, **self._kwargs)
|
||||
self._caller.cmdset.remove(CmdSetMore)
|
||||
if self.exit_cmd:
|
||||
self._caller.execute_cmd(self.exit_cmd, session=self._session)
|
||||
|
||||
|
||||
def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, **kwargs):
|
||||
def msg(caller, text="", always_page=False, session=None,
|
||||
justify_kwargs=None, exit_on_lastpage=True, **kwargs):
|
||||
"""
|
||||
More-supported version of msg, mimicking the normal msg method.
|
||||
|
||||
|
|
@ -280,9 +295,10 @@ def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, *
|
|||
justify_kwargs (dict, bool or None, optional): If given, this should
|
||||
be valid keyword arguments to the utils.justify() function. If False,
|
||||
no justification will be done.
|
||||
exit_on_lastpage (bool, optional): Immediately exit pager when reaching the last page.
|
||||
kwargs (any, optional): These will be passed on
|
||||
to the `caller.msg` method.
|
||||
|
||||
"""
|
||||
EvMore(caller, text, always_page=always_page, session=session,
|
||||
justify_kwargs=justify_kwargs, **kwargs)
|
||||
justify_kwargs=justify_kwargs, exit_on_lastpage=exit_on_lastpage, **kwargs)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ from evennia.utils.create import create_script
|
|||
# to real time.
|
||||
|
||||
TIMEFACTOR = settings.TIME_FACTOR
|
||||
IGNORE_DOWNTIMES = settings.TIME_IGNORE_DOWNTIMES
|
||||
|
||||
|
||||
# Only set if gametime_reset was called at some point.
|
||||
GAME_TIME_OFFSET = ServerConfig.objects.conf("gametime_offset", default=0)
|
||||
|
|
@ -133,7 +135,10 @@ def gametime(absolute=False):
|
|||
|
||||
"""
|
||||
epoch = game_epoch() if absolute else 0
|
||||
gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR
|
||||
if IGNORE_DOWNTIMES:
|
||||
gtime = epoch + (time.time() - server_epoch()) * TIMEFACTOR
|
||||
else:
|
||||
gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR
|
||||
return gtime
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,6 @@ from django.db.models.manager import Manager
|
|||
|
||||
|
||||
class SharedMemoryManager(Manager):
|
||||
# CL: this ensures our manager is used when accessing instances via
|
||||
# ForeignKey etc. (see docs)
|
||||
use_for_related_fields = True
|
||||
|
||||
# TODO: improve on this implementation
|
||||
# We need a way to handle reverse lookups so that this model can
|
||||
# still use the singleton cache, but the active model isn't required
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ class RegularCategory(models.Model):
|
|||
|
||||
class Article(SharedMemoryModel):
|
||||
name = models.CharField(max_length=32)
|
||||
category = models.ForeignKey(Category)
|
||||
category2 = models.ForeignKey(RegularCategory)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
||||
category2 = models.ForeignKey(RegularCategory, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class RegularArticle(models.Model):
|
||||
name = models.CharField(max_length=32)
|
||||
category = models.ForeignKey(Category)
|
||||
category2 = models.ForeignKey(RegularCategory)
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
||||
category2 = models.ForeignKey(RegularCategory, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class SharedMemorysTest(TestCase):
|
||||
|
|
|
|||
|
|
@ -1,467 +1,528 @@
|
|||
"""
|
||||
Inline functions (nested form).
|
||||
|
||||
This parser accepts nested inlinefunctions on the form
|
||||
|
||||
```
|
||||
$funcname(arg, arg, ...)
|
||||
```
|
||||
|
||||
embedded in any text where any arg can be another $funcname{} call.
|
||||
This functionality is turned off by default - to activate,
|
||||
`settings.INLINEFUNC_ENABLED` must be set to `True`.
|
||||
|
||||
Each token starts with "$funcname(" where there must be no space
|
||||
between the $funcname and (. It ends with a matched ending parentesis.
|
||||
")".
|
||||
|
||||
Inside the inlinefunc definition, one can use `\` to escape. This is
|
||||
mainly needed for escaping commas in flowing text (which would
|
||||
otherwise be interpreted as an argument separator), or to escape `}`
|
||||
when not intended to close the function block. Enclosing text in
|
||||
matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will
|
||||
also escape *everything* within without needing to escape individual
|
||||
characters.
|
||||
|
||||
The available inlinefuncs are defined as global-level functions in
|
||||
modules defined by `settings.INLINEFUNC_MODULES`. They are identified
|
||||
by their function name (and ignored if this name starts with `_`). They
|
||||
should be on the following form:
|
||||
|
||||
```python
|
||||
def funcname (*args, **kwargs):
|
||||
# ...
|
||||
```
|
||||
|
||||
Here, the arguments given to `$funcname(arg1,arg2)` will appear as the
|
||||
`*args` tuple. This will be populated by the arguments given to the
|
||||
inlinefunc in-game - the only part that will be available from
|
||||
in-game. `**kwargs` are not supported from in-game but are only used
|
||||
internally by Evennia to make details about the caller available to
|
||||
the function. The kwarg passed to all functions is `session`, the
|
||||
Sessionobject for the object seeing the string. This may be `None` if
|
||||
the string is sent to a non-puppetable object. The inlinefunc should
|
||||
never raise an exception.
|
||||
|
||||
There are two reserved function names:
|
||||
- "nomatch": This is called if the user uses a functionname that is
|
||||
not registered. The nomatch function will get the name of the
|
||||
not-found function as its first argument followed by the normal
|
||||
arguments to the given function. If not defined the default effect is
|
||||
to print `<UNKNOWN>` to replace the unknown function.
|
||||
- "stackfull": This is called when the maximum nested function stack is reached.
|
||||
When this happens, the original parsed string is returned and the result of
|
||||
the `stackfull` inlinefunc is appended to the end. By default this is an
|
||||
error message.
|
||||
|
||||
Error handling:
|
||||
Syntax errors, notably not completely closing all inlinefunc
|
||||
blocks, will lead to the entire string remaining unparsed.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from django.conf import settings
|
||||
from evennia.utils import utils
|
||||
|
||||
|
||||
# example/testing inline functions
|
||||
|
||||
def pad(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Pads text to given width.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to pad.
|
||||
width (str, optional): Will be converted to integer. Width
|
||||
of padding.
|
||||
align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
|
||||
fillchar (str, optional): Character used for padding. Defaults to a space.
|
||||
|
||||
Kwargs:
|
||||
session (Session): Session performing the pad.
|
||||
|
||||
Example:
|
||||
`$pad(text, width, align, fillchar)`
|
||||
|
||||
"""
|
||||
text, width, align, fillchar = "", 78, 'c', ' '
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
align = args[2] if args[2] in ('c', 'l', 'r') else 'c'
|
||||
if nargs > 3:
|
||||
fillchar = args[3]
|
||||
return utils.pad(text, width=width, align=align, fillchar=fillchar)
|
||||
|
||||
|
||||
def crop(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Crops ingoing text to given widths.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to crop.
|
||||
width (str, optional): Will be converted to an integer. Width of
|
||||
crop in characters.
|
||||
suffix (str, optional): End string to mark the fact that a part
|
||||
of the string was cropped. Defaults to `[...]`.
|
||||
Kwargs:
|
||||
session (Session): Session performing the crop.
|
||||
|
||||
Example:
|
||||
`$crop(text, width=78, suffix='[...]')`
|
||||
|
||||
"""
|
||||
text, width, suffix = "", 78, "[...]"
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
suffix = args[2]
|
||||
return utils.crop(text, width=width, suffix=suffix)
|
||||
|
||||
|
||||
def clr(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Colorizes nested text.
|
||||
|
||||
Args:
|
||||
startclr (str, optional): An ANSI color abbreviation without the
|
||||
prefix `|`, such as `r` (red foreground) or `[r` (red background).
|
||||
text (str, optional): Text
|
||||
endclr (str, optional): The color to use at the end of the string. Defaults
|
||||
to `|n` (reset-color).
|
||||
Kwargs:
|
||||
session (Session): Session object triggering inlinefunc.
|
||||
|
||||
Example:
|
||||
`$clr(startclr, text, endclr)`
|
||||
|
||||
"""
|
||||
text = ""
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
color = args[0].strip()
|
||||
if nargs > 1:
|
||||
text = args[1]
|
||||
text = "|" + color + text
|
||||
if nargs > 2:
|
||||
text += "|" + args[2].strip()
|
||||
else:
|
||||
text += "|n"
|
||||
return text
|
||||
|
||||
|
||||
# we specify a default nomatch function to use if no matching func was
|
||||
# found. This will be overloaded by any nomatch function defined in
|
||||
# the imported modules.
|
||||
_INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "<UKNOWN>",
|
||||
"stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"}
|
||||
|
||||
|
||||
# load custom inline func modules.
|
||||
for module in utils.make_iter(settings.INLINEFUNC_MODULES):
|
||||
try:
|
||||
_INLINE_FUNCS.update(utils.callables_from_module(module))
|
||||
except ImportError as err:
|
||||
if module == "server.conf.inlinefuncs":
|
||||
# a temporary warning since the default module changed name
|
||||
raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
|
||||
"be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
# remove the core function if we include examples in this module itself
|
||||
#_INLINE_FUNCS.pop("inline_func_parse", None)
|
||||
|
||||
|
||||
# The stack size is a security measure. Set to <=0 to disable.
|
||||
try:
|
||||
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
|
||||
except AttributeError:
|
||||
_STACK_MAXSIZE = 20
|
||||
|
||||
# regex definitions
|
||||
|
||||
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname{ (start of function call)
|
||||
|
||||
# note: this regex can be experimented with at https://regex101.com/r/kGR3vE/1
|
||||
_RE_TOKEN = re.compile(r"""
|
||||
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
|
||||
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
|
||||
(?P<comma>(?<!\\)\,)| # , (argument sep)
|
||||
(?P<end>(?<!\\)\))| # ) (end of func call)
|
||||
(?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
|
||||
(?P<escaped> # escaped tokens to re-insert sans backslash
|
||||
\\\'|\\\"|\\\)|\\\$\w+\()|
|
||||
(?P<rest> # everything else to re-insert verbatim
|
||||
\$(?!\w+\()|\'{1}|\"{1}|\\{1}|[^),$\'\"\\]+)""",
|
||||
re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL)
|
||||
|
||||
|
||||
# Cache for function lookups.
|
||||
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
|
||||
|
||||
|
||||
class ParseStack(list):
|
||||
"""
|
||||
Custom stack that always concatenates strings together when the
|
||||
strings are added next to one another. Tuples are stored
|
||||
separately and None is used to mark that a string should be broken
|
||||
up into a new chunk. Below is the resulting stack after separately
|
||||
appending 3 strings, None, 2 strings, a tuple and finally 2
|
||||
strings:
|
||||
|
||||
[string + string + string,
|
||||
None
|
||||
string + string,
|
||||
tuple,
|
||||
string + string]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ParseStack, self).__init__(*args, **kwargs)
|
||||
# always start stack with the empty string
|
||||
list.append(self, "")
|
||||
# indicates if the top of the stack is a string or not
|
||||
self._string_last = True
|
||||
|
||||
def __eq__(self, other):
|
||||
return (super(ParseStack).__eq__(other) and
|
||||
hasattr(other, "_string_last") and self._string_last == other._string_last)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def append(self, item):
|
||||
"""
|
||||
The stack will merge strings, add other things as normal
|
||||
"""
|
||||
if isinstance(item, basestring):
|
||||
if self._string_last:
|
||||
self[-1] += item
|
||||
else:
|
||||
list.append(self, item)
|
||||
self._string_last = True
|
||||
else:
|
||||
# everything else is added as normal
|
||||
list.append(self, item)
|
||||
self._string_last = False
|
||||
|
||||
|
||||
class InlinefuncError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_inlinefunc(string, strip=False, **kwargs):
|
||||
"""
|
||||
Parse the incoming string.
|
||||
|
||||
Args:
|
||||
string (str): The incoming string to parse.
|
||||
strip (bool, optional): Whether to strip function calls rather than
|
||||
execute them.
|
||||
Kwargs:
|
||||
session (Session): This is sent to this function by Evennia when triggering
|
||||
it. It is passed to the inlinefunc.
|
||||
kwargs (any): All other kwargs are also passed on to the inlinefunc.
|
||||
|
||||
|
||||
"""
|
||||
global _PARSING_CACHE
|
||||
if string in _PARSING_CACHE:
|
||||
# stack is already cached
|
||||
stack = _PARSING_CACHE[string]
|
||||
elif not _RE_STARTTOKEN.search(string):
|
||||
# if there are no unescaped start tokens at all, return immediately.
|
||||
return string
|
||||
else:
|
||||
# no cached stack; build a new stack and continue
|
||||
stack = ParseStack()
|
||||
|
||||
# process string on stack
|
||||
ncallable = 0
|
||||
for match in _RE_TOKEN.finditer(string):
|
||||
gdict = match.groupdict()
|
||||
if gdict["singlequote"]:
|
||||
stack.append(gdict["singlequote"])
|
||||
elif gdict["doublequote"]:
|
||||
stack.append(gdict["doublequote"])
|
||||
elif gdict["end"]:
|
||||
if ncallable <= 0:
|
||||
stack.append(")")
|
||||
continue
|
||||
args = []
|
||||
while stack:
|
||||
operation = stack.pop()
|
||||
if callable(operation):
|
||||
if not strip:
|
||||
stack.append((operation, [arg for arg in reversed(args)]))
|
||||
ncallable -= 1
|
||||
break
|
||||
else:
|
||||
args.append(operation)
|
||||
elif gdict["start"]:
|
||||
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
|
||||
try:
|
||||
# try to fetch the matching inlinefunc from storage
|
||||
stack.append(_INLINE_FUNCS[funcname])
|
||||
except KeyError:
|
||||
stack.append(_INLINE_FUNCS["nomatch"])
|
||||
stack.append(funcname)
|
||||
ncallable += 1
|
||||
elif gdict["escaped"]:
|
||||
# escaped tokens
|
||||
token = gdict["escaped"].lstrip("\\")
|
||||
stack.append(token)
|
||||
elif gdict["comma"]:
|
||||
if ncallable > 0:
|
||||
# commas outside strings and inside a callable are
|
||||
# used to mark argument separation - we use None
|
||||
# in the stack to indicate such a separation.
|
||||
stack.append(None)
|
||||
else:
|
||||
# no callable active - just a string
|
||||
stack.append(",")
|
||||
else:
|
||||
# the rest
|
||||
stack.append(gdict["rest"])
|
||||
|
||||
if ncallable > 0:
|
||||
# this means not all inlinefuncs were complete
|
||||
return string
|
||||
|
||||
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack):
|
||||
# if stack is larger than limit, throw away parsing
|
||||
return string + gdict["stackfull"](*args, **kwargs)
|
||||
else:
|
||||
# cache the stack
|
||||
_PARSING_CACHE[string] = stack
|
||||
|
||||
# run the stack recursively
|
||||
def _run_stack(item, depth=0):
|
||||
retval = item
|
||||
if isinstance(item, tuple):
|
||||
if strip:
|
||||
return ""
|
||||
else:
|
||||
func, arglist = item
|
||||
args = [""]
|
||||
for arg in arglist:
|
||||
if arg is None:
|
||||
# an argument-separating comma - start a new arg
|
||||
args.append("")
|
||||
else:
|
||||
# all other args should merge into one string
|
||||
args[-1] += _run_stack(arg, depth=depth + 1)
|
||||
# execute the inlinefunc at this point or strip it.
|
||||
kwargs["inlinefunc_stack_depth"] = depth
|
||||
retval = "" if strip else func(*args, **kwargs)
|
||||
return utils.to_str(retval, force_string=True)
|
||||
|
||||
# execute the stack from the cache
|
||||
return "".join(_run_stack(item) for item in _PARSING_CACHE[string])
|
||||
|
||||
#
|
||||
# Nick templating
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
This supports the use of replacement templates in nicks:
|
||||
|
||||
This happens in two steps:
|
||||
|
||||
1) The user supplies a template that is converted to a regex according
|
||||
to the unix-like templating language.
|
||||
2) This regex is tested against nicks depending on which nick replacement
|
||||
strategy is considered (most commonly inputline).
|
||||
3) If there is a template match and there are templating markers,
|
||||
these are replaced with the arguments actually given.
|
||||
|
||||
@desc $1 $2 $3
|
||||
|
||||
This will be converted to the following regex:
|
||||
|
||||
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
|
||||
|
||||
Supported template markers (through fnmatch)
|
||||
* matches anything (non-greedy) -> .*?
|
||||
? matches any single character ->
|
||||
[seq] matches any entry in sequence
|
||||
[!seq] matches entries not in sequence
|
||||
Custom arg markers
|
||||
$N argument position (1-99)
|
||||
|
||||
"""
|
||||
import fnmatch
|
||||
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_SPACE = re.compile(r"\\ ")
|
||||
|
||||
|
||||
class NickTemplateInvalid(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def initialize_nick_templates(in_template, out_template):
|
||||
"""
|
||||
Initialize the nick templates for matching and remapping a string.
|
||||
|
||||
Args:
|
||||
in_template (str): The template to be used for nick recognition.
|
||||
out_template (str): The template to be used to replace the string
|
||||
matched by the in_template.
|
||||
|
||||
Returns:
|
||||
regex (regex): Regex to match against strings
|
||||
template (str): Template with markers {arg1}, {arg2}, etc for
|
||||
replacement using the standard .format method.
|
||||
|
||||
Raises:
|
||||
NickTemplateInvalid: If the in/out template does not have a matching
|
||||
number of $args.
|
||||
|
||||
"""
|
||||
# create the regex for in_template
|
||||
regex_string = fnmatch.translate(in_template)
|
||||
n_inargs = len(_RE_NICK_ARG.findall(regex_string))
|
||||
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
|
||||
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
|
||||
|
||||
# create the out_template
|
||||
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
|
||||
|
||||
# validate the tempaltes - they should at least have the same number of args
|
||||
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
|
||||
if n_inargs != n_outargs:
|
||||
print n_inargs, n_outargs
|
||||
raise NickTemplateInvalid
|
||||
|
||||
return re.compile(regex_string), template_string
|
||||
|
||||
|
||||
def parse_nick_template(string, template_regex, outtemplate):
|
||||
"""
|
||||
Parse a text using a template and map it to another template
|
||||
|
||||
Args:
|
||||
string (str): The input string to processj
|
||||
template_regex (regex): A template regex created with
|
||||
initialize_nick_template.
|
||||
outtemplate (str): The template to which to map the matches
|
||||
produced by the template_regex. This should have $1, $2,
|
||||
etc to match the regex.
|
||||
|
||||
"""
|
||||
match = template_regex.match(string)
|
||||
if match:
|
||||
return outtemplate.format(**match.groupdict())
|
||||
return string
|
||||
"""
|
||||
Inline functions (nested form).
|
||||
|
||||
This parser accepts nested inlinefunctions on the form
|
||||
|
||||
```
|
||||
$funcname(arg, arg, ...)
|
||||
```
|
||||
|
||||
embedded in any text where any arg can be another $funcname{} call.
|
||||
This functionality is turned off by default - to activate,
|
||||
`settings.INLINEFUNC_ENABLED` must be set to `True`.
|
||||
|
||||
Each token starts with "$funcname(" where there must be no space
|
||||
between the $funcname and (. It ends with a matched ending parentesis.
|
||||
")".
|
||||
|
||||
Inside the inlinefunc definition, one can use `\` to escape. This is
|
||||
mainly needed for escaping commas in flowing text (which would
|
||||
otherwise be interpreted as an argument separator), or to escape `}`
|
||||
when not intended to close the function block. Enclosing text in
|
||||
matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will
|
||||
also escape *everything* within without needing to escape individual
|
||||
characters.
|
||||
|
||||
The available inlinefuncs are defined as global-level functions in
|
||||
modules defined by `settings.INLINEFUNC_MODULES`. They are identified
|
||||
by their function name (and ignored if this name starts with `_`). They
|
||||
should be on the following form:
|
||||
|
||||
```python
|
||||
def funcname (*args, **kwargs):
|
||||
# ...
|
||||
```
|
||||
|
||||
Here, the arguments given to `$funcname(arg1,arg2)` will appear as the
|
||||
`*args` tuple. This will be populated by the arguments given to the
|
||||
inlinefunc in-game - the only part that will be available from
|
||||
in-game. `**kwargs` are not supported from in-game but are only used
|
||||
internally by Evennia to make details about the caller available to
|
||||
the function. The kwarg passed to all functions is `session`, the
|
||||
Sessionobject for the object seeing the string. This may be `None` if
|
||||
the string is sent to a non-puppetable object. The inlinefunc should
|
||||
never raise an exception.
|
||||
|
||||
There are two reserved function names:
|
||||
- "nomatch": This is called if the user uses a functionname that is
|
||||
not registered. The nomatch function will get the name of the
|
||||
not-found function as its first argument followed by the normal
|
||||
arguments to the given function. If not defined the default effect is
|
||||
to print `<UNKNOWN>` to replace the unknown function.
|
||||
- "stackfull": This is called when the maximum nested function stack is reached.
|
||||
When this happens, the original parsed string is returned and the result of
|
||||
the `stackfull` inlinefunc is appended to the end. By default this is an
|
||||
error message.
|
||||
|
||||
Error handling:
|
||||
Syntax errors, notably not completely closing all inlinefunc
|
||||
blocks, will lead to the entire string remaining unparsed.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import fnmatch
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.utils import utils, logger
|
||||
|
||||
|
||||
# example/testing inline functions
|
||||
|
||||
def pad(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Pads text to given width.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to pad.
|
||||
width (str, optional): Will be converted to integer. Width
|
||||
of padding.
|
||||
align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
|
||||
fillchar (str, optional): Character used for padding. Defaults to a space.
|
||||
|
||||
Kwargs:
|
||||
session (Session): Session performing the pad.
|
||||
|
||||
Example:
|
||||
`$pad(text, width, align, fillchar)`
|
||||
|
||||
"""
|
||||
text, width, align, fillchar = "", 78, 'c', ' '
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
align = args[2] if args[2] in ('c', 'l', 'r') else 'c'
|
||||
if nargs > 3:
|
||||
fillchar = args[3]
|
||||
return utils.pad(text, width=width, align=align, fillchar=fillchar)
|
||||
|
||||
|
||||
def crop(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Crops ingoing text to given widths.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to crop.
|
||||
width (str, optional): Will be converted to an integer. Width of
|
||||
crop in characters.
|
||||
suffix (str, optional): End string to mark the fact that a part
|
||||
of the string was cropped. Defaults to `[...]`.
|
||||
Kwargs:
|
||||
session (Session): Session performing the crop.
|
||||
|
||||
Example:
|
||||
`$crop(text, width=78, suffix='[...]')`
|
||||
|
||||
"""
|
||||
text, width, suffix = "", 78, "[...]"
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
suffix = args[2]
|
||||
return utils.crop(text, width=width, suffix=suffix)
|
||||
|
||||
|
||||
def clr(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Colorizes nested text.
|
||||
|
||||
Args:
|
||||
startclr (str, optional): An ANSI color abbreviation without the
|
||||
prefix `|`, such as `r` (red foreground) or `[r` (red background).
|
||||
text (str, optional): Text
|
||||
endclr (str, optional): The color to use at the end of the string. Defaults
|
||||
to `|n` (reset-color).
|
||||
Kwargs:
|
||||
session (Session): Session object triggering inlinefunc.
|
||||
|
||||
Example:
|
||||
`$clr(startclr, text, endclr)`
|
||||
|
||||
"""
|
||||
text = ""
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
color = args[0].strip()
|
||||
if nargs > 1:
|
||||
text = args[1]
|
||||
text = "|" + color + text
|
||||
if nargs > 2:
|
||||
text += "|" + args[2].strip()
|
||||
else:
|
||||
text += "|n"
|
||||
return text
|
||||
|
||||
|
||||
def null(*args, **kwargs):
|
||||
return args[0] if args else ''
|
||||
|
||||
|
||||
def nomatch(name, *args, **kwargs):
|
||||
"""
|
||||
Default implementation of nomatch returns the function as-is as a string.
|
||||
|
||||
"""
|
||||
kwargs.pop("inlinefunc_stack_depth", None)
|
||||
kwargs.pop("session")
|
||||
|
||||
return "${name}({args}{kwargs})".format(
|
||||
name=name,
|
||||
args=",".join(args),
|
||||
kwargs=",".join("{}={}".format(key, val) for key, val in kwargs.items()))
|
||||
|
||||
_INLINE_FUNCS = {}
|
||||
|
||||
# we specify a default nomatch function to use if no matching func was
|
||||
# found. This will be overloaded by any nomatch function defined in
|
||||
# the imported modules.
|
||||
_DEFAULT_FUNCS = {"nomatch": lambda *args, **kwargs: "<UNKNOWN>",
|
||||
"stackfull": lambda *args, **kwargs: "\n (not parsed: "}
|
||||
|
||||
_INLINE_FUNCS.update(_DEFAULT_FUNCS)
|
||||
|
||||
# load custom inline func modules.
|
||||
for module in utils.make_iter(settings.INLINEFUNC_MODULES):
|
||||
try:
|
||||
_INLINE_FUNCS.update(utils.callables_from_module(module))
|
||||
except ImportError as err:
|
||||
if module == "server.conf.inlinefuncs":
|
||||
# a temporary warning since the default module changed name
|
||||
raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
|
||||
"be renamed to mygame/server/conf/inlinefuncs.py (note "
|
||||
"the S at the end)." % err)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
# The stack size is a security measure. Set to <=0 to disable.
|
||||
try:
|
||||
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
|
||||
except AttributeError:
|
||||
_STACK_MAXSIZE = 20
|
||||
|
||||
# regex definitions
|
||||
|
||||
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname( (start of function call)
|
||||
|
||||
# note: this regex can be experimented with at https://regex101.com/r/kGR3vE/2
|
||||
_RE_TOKEN = re.compile(r"""
|
||||
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
|
||||
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
|
||||
(?P<comma>(?<!\\)\,)| # , (argument sep)
|
||||
(?P<end>(?<!\\)\))| # ) (possible end of func call)
|
||||
(?P<leftparens>(?<!\\)\()| # ( (lone left-parens)
|
||||
(?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
|
||||
(?P<escaped> # escaped tokens to re-insert sans backslash
|
||||
\\\'|\\\"|\\\)|\\\$\w+\(|\\\()|
|
||||
(?P<rest> # everything else to re-insert verbatim
|
||||
\$(?!\w+\()|\'|\"|\\|[^),$\'\"\\\(]+)""",
|
||||
re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL)
|
||||
|
||||
# Cache for function lookups.
|
||||
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
|
||||
|
||||
|
||||
class ParseStack(list):
|
||||
"""
|
||||
Custom stack that always concatenates strings together when the
|
||||
strings are added next to one another. Tuples are stored
|
||||
separately and None is used to mark that a string should be broken
|
||||
up into a new chunk. Below is the resulting stack after separately
|
||||
appending 3 strings, None, 2 strings, a tuple and finally 2
|
||||
strings:
|
||||
|
||||
[string + string + string,
|
||||
None
|
||||
string + string,
|
||||
tuple,
|
||||
string + string]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ParseStack, self).__init__(*args, **kwargs)
|
||||
# always start stack with the empty string
|
||||
list.append(self, "")
|
||||
# indicates if the top of the stack is a string or not
|
||||
self._string_last = True
|
||||
|
||||
def __eq__(self, other):
|
||||
return (super(ParseStack).__eq__(other) and
|
||||
hasattr(other, "_string_last") and self._string_last == other._string_last)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def append(self, item):
|
||||
"""
|
||||
The stack will merge strings, add other things as normal
|
||||
"""
|
||||
if isinstance(item, basestring):
|
||||
if self._string_last:
|
||||
self[-1] += item
|
||||
else:
|
||||
list.append(self, item)
|
||||
self._string_last = True
|
||||
else:
|
||||
# everything else is added as normal
|
||||
list.append(self, item)
|
||||
self._string_last = False
|
||||
|
||||
|
||||
class InlinefuncError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False, **kwargs):
|
||||
"""
|
||||
Parse the incoming string.
|
||||
|
||||
Args:
|
||||
string (str): The incoming string to parse.
|
||||
strip (bool, optional): Whether to strip function calls rather than
|
||||
execute them.
|
||||
available_funcs (dict, optional): Define an alternative source of functions to parse for.
|
||||
If unset, use the functions found through `settings.INLINEFUNC_MODULES`.
|
||||
stacktrace (bool, optional): If set, print the stacktrace to log.
|
||||
Kwargs:
|
||||
session (Session): This is sent to this function by Evennia when triggering
|
||||
it. It is passed to the inlinefunc.
|
||||
kwargs (any): All other kwargs are also passed on to the inlinefunc.
|
||||
|
||||
|
||||
"""
|
||||
global _PARSING_CACHE
|
||||
usecache = False
|
||||
if not available_funcs:
|
||||
available_funcs = _INLINE_FUNCS
|
||||
usecache = True
|
||||
else:
|
||||
# make sure the default keys are available, but also allow overriding
|
||||
tmp = _DEFAULT_FUNCS.copy()
|
||||
tmp.update(available_funcs)
|
||||
available_funcs = tmp
|
||||
|
||||
if usecache and string in _PARSING_CACHE:
|
||||
# stack is already cached
|
||||
stack = _PARSING_CACHE[string]
|
||||
elif not _RE_STARTTOKEN.search(string):
|
||||
# if there are no unescaped start tokens at all, return immediately.
|
||||
return string
|
||||
else:
|
||||
# no cached stack; build a new stack and continue
|
||||
stack = ParseStack()
|
||||
|
||||
# process string on stack
|
||||
ncallable = 0
|
||||
nlparens = 0
|
||||
nvalid = 0
|
||||
|
||||
if stacktrace:
|
||||
out = "STRING: {} =>".format(string)
|
||||
print(out)
|
||||
logger.log_info(out)
|
||||
|
||||
for match in _RE_TOKEN.finditer(string):
|
||||
gdict = match.groupdict()
|
||||
|
||||
if stacktrace:
|
||||
out = " MATCH: {}".format({key: val for key, val in gdict.items() if val})
|
||||
print(out)
|
||||
logger.log_info(out)
|
||||
|
||||
if gdict["singlequote"]:
|
||||
stack.append(gdict["singlequote"])
|
||||
elif gdict["doublequote"]:
|
||||
stack.append(gdict["doublequote"])
|
||||
elif gdict["leftparens"]:
|
||||
# we have a left-parens inside a callable
|
||||
if ncallable:
|
||||
nlparens += 1
|
||||
stack.append("(")
|
||||
elif gdict["end"]:
|
||||
if nlparens > 0:
|
||||
nlparens -= 1
|
||||
stack.append(")")
|
||||
continue
|
||||
if ncallable <= 0:
|
||||
stack.append(")")
|
||||
continue
|
||||
args = []
|
||||
while stack:
|
||||
operation = stack.pop()
|
||||
if callable(operation):
|
||||
if not strip:
|
||||
stack.append((operation, [arg for arg in reversed(args)]))
|
||||
ncallable -= 1
|
||||
break
|
||||
else:
|
||||
args.append(operation)
|
||||
elif gdict["start"]:
|
||||
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
|
||||
try:
|
||||
# try to fetch the matching inlinefunc from storage
|
||||
stack.append(available_funcs[funcname])
|
||||
nvalid += 1
|
||||
except KeyError:
|
||||
stack.append(available_funcs["nomatch"])
|
||||
stack.append(funcname)
|
||||
stack.append(None)
|
||||
ncallable += 1
|
||||
elif gdict["escaped"]:
|
||||
# escaped tokens
|
||||
token = gdict["escaped"].lstrip("\\")
|
||||
stack.append(token)
|
||||
elif gdict["comma"]:
|
||||
if ncallable > 0:
|
||||
# commas outside strings and inside a callable are
|
||||
# used to mark argument separation - we use None
|
||||
# in the stack to indicate such a separation.
|
||||
stack.append(None)
|
||||
else:
|
||||
# no callable active - just a string
|
||||
stack.append(",")
|
||||
else:
|
||||
# the rest
|
||||
stack.append(gdict["rest"])
|
||||
|
||||
if ncallable > 0:
|
||||
# this means not all inlinefuncs were complete
|
||||
return string
|
||||
|
||||
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid:
|
||||
# if stack is larger than limit, throw away parsing
|
||||
return string + available_funcs["stackfull"](*args, **kwargs)
|
||||
elif usecache:
|
||||
# cache the stack - we do this also if we don't check the cache above
|
||||
_PARSING_CACHE[string] = stack
|
||||
|
||||
# run the stack recursively
|
||||
def _run_stack(item, depth=0):
|
||||
retval = item
|
||||
if isinstance(item, tuple):
|
||||
if strip:
|
||||
return ""
|
||||
else:
|
||||
func, arglist = item
|
||||
args = [""]
|
||||
for arg in arglist:
|
||||
if arg is None:
|
||||
# an argument-separating comma - start a new arg
|
||||
args.append("")
|
||||
else:
|
||||
# all other args should merge into one string
|
||||
args[-1] += _run_stack(arg, depth=depth + 1)
|
||||
# execute the inlinefunc at this point or strip it.
|
||||
kwargs["inlinefunc_stack_depth"] = depth
|
||||
retval = "" if strip else func(*args, **kwargs)
|
||||
return utils.to_str(retval, force_string=True)
|
||||
retval = "".join(_run_stack(item) for item in stack)
|
||||
if stacktrace:
|
||||
out = "STACK: \n{} => {}\n".format(stack, retval)
|
||||
print(out)
|
||||
logger.log_info(out)
|
||||
|
||||
# execute the stack
|
||||
return retval
|
||||
|
||||
#
|
||||
# Nick templating
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
This supports the use of replacement templates in nicks:
|
||||
|
||||
This happens in two steps:
|
||||
|
||||
1) The user supplies a template that is converted to a regex according
|
||||
to the unix-like templating language.
|
||||
2) This regex is tested against nicks depending on which nick replacement
|
||||
strategy is considered (most commonly inputline).
|
||||
3) If there is a template match and there are templating markers,
|
||||
these are replaced with the arguments actually given.
|
||||
|
||||
@desc $1 $2 $3
|
||||
|
||||
This will be converted to the following regex:
|
||||
|
||||
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
|
||||
|
||||
Supported template markers (through fnmatch)
|
||||
* matches anything (non-greedy) -> .*?
|
||||
? matches any single character ->
|
||||
[seq] matches any entry in sequence
|
||||
[!seq] matches entries not in sequence
|
||||
Custom arg markers
|
||||
$N argument position (1-99)
|
||||
|
||||
"""
|
||||
|
||||
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_SPACE = re.compile(r"\\ ")
|
||||
|
||||
|
||||
class NickTemplateInvalid(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def initialize_nick_templates(in_template, out_template):
|
||||
"""
|
||||
Initialize the nick templates for matching and remapping a string.
|
||||
|
||||
Args:
|
||||
in_template (str): The template to be used for nick recognition.
|
||||
out_template (str): The template to be used to replace the string
|
||||
matched by the in_template.
|
||||
|
||||
Returns:
|
||||
regex (regex): Regex to match against strings
|
||||
template (str): Template with markers {arg1}, {arg2}, etc for
|
||||
replacement using the standard .format method.
|
||||
|
||||
Raises:
|
||||
NickTemplateInvalid: If the in/out template does not have a matching
|
||||
number of $args.
|
||||
|
||||
"""
|
||||
# create the regex for in_template
|
||||
regex_string = fnmatch.translate(in_template)
|
||||
n_inargs = len(_RE_NICK_ARG.findall(regex_string))
|
||||
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
|
||||
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
|
||||
|
||||
# create the out_template
|
||||
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
|
||||
|
||||
# validate the tempaltes - they should at least have the same number of args
|
||||
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
|
||||
if n_inargs != n_outargs:
|
||||
raise NickTemplateInvalid
|
||||
|
||||
return re.compile(regex_string), template_string
|
||||
|
||||
|
||||
def parse_nick_template(string, template_regex, outtemplate):
|
||||
"""
|
||||
Parse a text using a template and map it to another template
|
||||
|
||||
Args:
|
||||
string (str): The input string to processj
|
||||
template_regex (regex): A template regex created with
|
||||
initialize_nick_template.
|
||||
outtemplate (str): The template to which to map the matches
|
||||
produced by the template_regex. This should have $1, $2,
|
||||
etc to match the regex.
|
||||
|
||||
"""
|
||||
match = template_regex.match(string)
|
||||
if match:
|
||||
return outtemplate.format(**match.groupdict())
|
||||
return string
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import time
|
|||
from datetime import datetime
|
||||
from traceback import format_exc
|
||||
from twisted.python import log, logfile
|
||||
from twisted.python import util as twisted_util
|
||||
from twisted.internet.threads import deferToThread
|
||||
|
||||
|
||||
|
|
@ -29,10 +30,15 @@ _TIMEZONE = None
|
|||
_CHANNEL_LOG_NUM_TAIL_LINES = None
|
||||
|
||||
|
||||
# logging overrides
|
||||
|
||||
|
||||
def timeformat(when=None):
|
||||
"""
|
||||
This helper function will format the current time in the same
|
||||
way as twisted's logger does, including time zone info.
|
||||
way as the twisted logger does, including time zone info. Only
|
||||
difference from official logger is that we only use two digits
|
||||
for the year and don't show timezone for CET times.
|
||||
|
||||
Args:
|
||||
when (int, optional): This is a time in POSIX seconds on the form
|
||||
|
|
@ -49,14 +55,70 @@ def timeformat(when=None):
|
|||
tz_offset = tz_offset.days * 86400 + tz_offset.seconds
|
||||
# correct given time to utc
|
||||
when = datetime.utcfromtimestamp(when - tz_offset)
|
||||
tz_hour = abs(int(tz_offset // 3600))
|
||||
tz_mins = abs(int(tz_offset // 60 % 60))
|
||||
tz_sign = "-" if tz_offset >= 0 else "+"
|
||||
|
||||
return '%d-%02d-%02d %02d:%02d:%02d%s%02d%02d' % (
|
||||
when.year, when.month, when.day,
|
||||
when.hour, when.minute, when.second,
|
||||
tz_sign, tz_hour, tz_mins)
|
||||
if tz_offset == 0:
|
||||
tz = ""
|
||||
else:
|
||||
tz_hour = abs(int(tz_offset // 3600))
|
||||
tz_mins = abs(int(tz_offset // 60 % 60))
|
||||
tz_sign = "-" if tz_offset >= 0 else "+"
|
||||
tz = "%s%02d%s" % (tz_sign, tz_hour,
|
||||
(":%02d" % tz_mins if tz_mins else ""))
|
||||
|
||||
return '%d-%02d-%02d %02d:%02d:%02d%s' % (
|
||||
when.year - 2000, when.month, when.day,
|
||||
when.hour, when.minute, when.second, tz)
|
||||
|
||||
|
||||
class WeeklyLogFile(logfile.DailyLogFile):
|
||||
"""
|
||||
Log file that rotates once per week
|
||||
|
||||
"""
|
||||
day_rotation = 7
|
||||
|
||||
def shouldRotate(self):
|
||||
"""Rotate when the date has changed since last write"""
|
||||
# all dates here are tuples (year, month, day)
|
||||
now = self.toDate()
|
||||
then = self.lastDate
|
||||
return now[0] > then[0] or now[1] > then[1] or now[2] > (then[2] + self.day_rotation)
|
||||
|
||||
def write(self, data):
|
||||
"Write data to log file"
|
||||
logfile.BaseLogFile.write(self, data)
|
||||
self.lastDate = max(self.lastDate, self.toDate())
|
||||
|
||||
|
||||
class PortalLogObserver(log.FileLogObserver):
|
||||
"""
|
||||
Reformat logging
|
||||
"""
|
||||
timeFormat = None
|
||||
prefix = " |Portal| "
|
||||
|
||||
def emit(self, eventDict):
|
||||
"""
|
||||
Copied from Twisted parent, to change logging output
|
||||
|
||||
"""
|
||||
text = log.textFromEventDict(eventDict)
|
||||
if text is None:
|
||||
return
|
||||
|
||||
# timeStr = self.formatTime(eventDict["time"])
|
||||
timeStr = timeformat(eventDict["time"])
|
||||
fmtDict = {
|
||||
"text": text.replace("\n", "\n\t")}
|
||||
|
||||
msgStr = log._safeFormat("%(text)s\n", fmtDict)
|
||||
|
||||
twisted_util.untilConcludes(self.write, timeStr + "%s" % self.prefix + msgStr)
|
||||
twisted_util.untilConcludes(self.flush)
|
||||
|
||||
|
||||
class ServerLogObserver(PortalLogObserver):
|
||||
prefix = " "
|
||||
|
||||
|
||||
def log_msg(msg):
|
||||
|
|
@ -124,6 +186,20 @@ def log_err(errmsg):
|
|||
log_errmsg = log_err
|
||||
|
||||
|
||||
def log_server(servermsg):
|
||||
"""
|
||||
This is for the Portal to log captured Server stdout messages (it's
|
||||
usually only used during startup, before Server log is open)
|
||||
|
||||
"""
|
||||
try:
|
||||
servermsg = str(servermsg)
|
||||
except Exception as e:
|
||||
servermsg = str(e)
|
||||
for line in servermsg.splitlines():
|
||||
log_msg('[Server] %s' % line)
|
||||
|
||||
|
||||
def log_warn(warnmsg):
|
||||
"""
|
||||
Prints/logs any warnings that aren't critical but should be noted.
|
||||
|
|
@ -178,6 +254,23 @@ def log_dep(depmsg):
|
|||
|
||||
log_depmsg = log_dep
|
||||
|
||||
def log_sec(secmsg):
|
||||
"""
|
||||
Prints a security-related message.
|
||||
|
||||
Args:
|
||||
secmsg (str): The security message to log.
|
||||
"""
|
||||
try:
|
||||
secmsg = str(secmsg)
|
||||
except Exception as e:
|
||||
secmsg = str(e)
|
||||
for line in secmsg.splitlines():
|
||||
log_msg('[SS] %s' % line)
|
||||
|
||||
|
||||
log_secmsg = log_sec
|
||||
|
||||
|
||||
# Arbitrary file logger
|
||||
|
||||
|
|
|
|||
|
|
@ -1,342 +0,0 @@
|
|||
"""
|
||||
Spawner
|
||||
|
||||
The spawner takes input files containing object definitions in
|
||||
dictionary forms. These use a prototype architecture to define
|
||||
unique objects without having to make a Typeclass for each.
|
||||
|
||||
The main function is `spawn(*prototype)`, where the `prototype`
|
||||
is a dictionary like this:
|
||||
|
||||
```python
|
||||
GOBLIN = {
|
||||
"typeclass": "types.objects.Monster",
|
||||
"key": "goblin grunt",
|
||||
"health": lambda: randint(20,30),
|
||||
"resists": ["cold", "poison"],
|
||||
"attacks": ["fists"],
|
||||
"weaknesses": ["fire", "light"]
|
||||
"tags": ["mob", "evil", ('greenskin','mob')]
|
||||
"args": [("weapon", "sword")]
|
||||
}
|
||||
```
|
||||
|
||||
Possible keywords are:
|
||||
prototype - string parent prototype
|
||||
key - string, the main object identifier
|
||||
typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`
|
||||
location - this should be a valid object or #dbref
|
||||
home - valid object or #dbref
|
||||
destination - only valid for exits (object or dbref)
|
||||
|
||||
permissions - string or list of permission strings
|
||||
locks - a lock-string
|
||||
aliases - string or list of strings
|
||||
exec - this is a string of python code to execute or a list of such codes.
|
||||
This can be used e.g. to trigger custom handlers on the object. The
|
||||
execution namespace contains 'evennia' for the library and 'obj'
|
||||
tags - string or list of strings or tuples `(tagstr, category)`. Plain
|
||||
strings will be result in tags with no category (default tags).
|
||||
attrs - tuple or list of tuples of Attributes to add. This form allows
|
||||
more complex Attributes to be set. Tuples at least specify `(key, value)`
|
||||
but can also specify up to `(key, value, category, lockstring)`. If
|
||||
you want to specify a lockstring but not a category, set the category
|
||||
to `None`.
|
||||
ndb_<name> - value of a nattribute (ndb_ is stripped)
|
||||
other - any other name is interpreted as the key of an Attribute with
|
||||
its value. Such Attributes have no categories.
|
||||
|
||||
Each value can also be a callable that takes no arguments. It should
|
||||
return the value to enter into the field and will be called every time
|
||||
the prototype is used to spawn an object. Note, if you want to store
|
||||
a callable in an Attribute, embed it in a tuple to the `args` keyword.
|
||||
|
||||
By specifying the "prototype" key, the prototype becomes a child of
|
||||
that prototype, inheritng all prototype slots it does not explicitly
|
||||
define itself, while overloading those that it does specify.
|
||||
|
||||
```python
|
||||
GOBLIN_WIZARD = {
|
||||
"prototype": GOBLIN,
|
||||
"key": "goblin wizard",
|
||||
"spells": ["fire ball", "lighting bolt"]
|
||||
}
|
||||
|
||||
GOBLIN_ARCHER = {
|
||||
"prototype": GOBLIN,
|
||||
"key": "goblin archer",
|
||||
"attacks": ["short bow"]
|
||||
}
|
||||
```
|
||||
|
||||
One can also have multiple prototypes. These are inherited from the
|
||||
left, with the ones further to the right taking precedence.
|
||||
|
||||
```python
|
||||
ARCHWIZARD = {
|
||||
"attack": ["archwizard staff", "eye of doom"]
|
||||
|
||||
GOBLIN_ARCHWIZARD = {
|
||||
"key" : "goblin archwizard"
|
||||
"prototype": (GOBLIN_WIZARD, ARCHWIZARD),
|
||||
}
|
||||
```
|
||||
|
||||
The *goblin archwizard* will have some different attacks, but will
|
||||
otherwise have the same spells as a *goblin wizard* who in turn shares
|
||||
many traits with a normal *goblin*.
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import copy
|
||||
from django.conf import settings
|
||||
from random import randint
|
||||
import evennia
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj
|
||||
|
||||
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
|
||||
|
||||
|
||||
def _handle_dbref(inp):
|
||||
return dbid_to_obj(inp, ObjectDB)
|
||||
|
||||
|
||||
def _validate_prototype(key, prototype, protparents, visited):
|
||||
"""
|
||||
Run validation on a prototype, checking for inifinite regress.
|
||||
|
||||
"""
|
||||
assert isinstance(prototype, dict)
|
||||
if id(prototype) in visited:
|
||||
raise RuntimeError("%s has infinite nesting of prototypes." % key or prototype)
|
||||
visited.append(id(prototype))
|
||||
protstrings = prototype.get("prototype")
|
||||
if protstrings:
|
||||
for protstring in make_iter(protstrings):
|
||||
if key is not None and protstring == key:
|
||||
raise RuntimeError("%s tries to prototype itself." % key or prototype)
|
||||
protparent = protparents.get(protstring)
|
||||
if not protparent:
|
||||
raise RuntimeError("%s's prototype '%s' was not found." % (key or prototype, protstring))
|
||||
_validate_prototype(protstring, protparent, protparents, visited)
|
||||
|
||||
|
||||
def _get_prototype(dic, prot, protparents):
|
||||
"""
|
||||
Recursively traverse a prototype dictionary, including multiple
|
||||
inheritance. Use _validate_prototype before this, we don't check
|
||||
for infinite recursion here.
|
||||
|
||||
"""
|
||||
if "prototype" in dic:
|
||||
# move backwards through the inheritance
|
||||
for prototype in make_iter(dic["prototype"]):
|
||||
# Build the prot dictionary in reverse order, overloading
|
||||
new_prot = _get_prototype(protparents.get(prototype, {}), prot, protparents)
|
||||
prot.update(new_prot)
|
||||
prot.update(dic)
|
||||
prot.pop("prototype", None) # we don't need this anymore
|
||||
return prot
|
||||
|
||||
|
||||
def _batch_create_object(*objparams):
|
||||
"""
|
||||
This is a cut-down version of the create_object() function,
|
||||
optimized for speed. It does NOT check and convert various input
|
||||
so make sure the spawned Typeclass works before using this!
|
||||
|
||||
Args:
|
||||
objsparams (tuple): Parameters for the respective creation/add
|
||||
handlers in the following order:
|
||||
- `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`.
|
||||
- `permissions` (str): Permission string used with `new_obj.batch_add(permission)`.
|
||||
- `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`.
|
||||
- `aliases` (list): A list of alias strings for
|
||||
adding with `new_object.aliases.batch_add(*aliases)`.
|
||||
- `nattributes` (list): list of tuples `(key, value)` to be loop-added to
|
||||
add with `new_obj.nattributes.add(*tuple)`.
|
||||
- `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for
|
||||
adding with `new_obj.attributes.batch_add(*attributes)`.
|
||||
- `tags` (list): list of tuples `(key, category)` for adding
|
||||
with `new_obj.tags.batch_add(*tags)`.
|
||||
- `execs` (list): Code strings to execute together with the creation
|
||||
of each object. They will be executed with `evennia` and `obj`
|
||||
(the newly created object) available in the namespace. Execution
|
||||
will happend after all other properties have been assigned and
|
||||
is intended for calling custom handlers etc.
|
||||
for the respective creation/add handlers in the following
|
||||
order: (create_kwargs, permissions, locks, aliases, nattributes,
|
||||
attributes, tags, execs)
|
||||
|
||||
Returns:
|
||||
objects (list): A list of created objects
|
||||
|
||||
Notes:
|
||||
The `exec` list will execute arbitrary python code so don't allow this to be availble to
|
||||
unprivileged users!
|
||||
|
||||
"""
|
||||
|
||||
# bulk create all objects in one go
|
||||
|
||||
# unfortunately this doesn't work since bulk_create doesn't creates pks;
|
||||
# the result would be duplicate objects at the next stage, so we comment
|
||||
# it out for now:
|
||||
# dbobjs = _ObjectDB.objects.bulk_create(dbobjs)
|
||||
|
||||
dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams]
|
||||
objs = []
|
||||
for iobj, obj in enumerate(dbobjs):
|
||||
# call all setup hooks on each object
|
||||
objparam = objparams[iobj]
|
||||
# setup
|
||||
obj._createdict = {"permissions": make_iter(objparam[1]),
|
||||
"locks": objparam[2],
|
||||
"aliases": make_iter(objparam[3]),
|
||||
"nattributes": objparam[4],
|
||||
"attributes": objparam[5],
|
||||
"tags": make_iter(objparam[6])}
|
||||
# this triggers all hooks
|
||||
obj.save()
|
||||
# run eventual extra code
|
||||
for code in objparam[7]:
|
||||
if code:
|
||||
exec(code, {}, {"evennia": evennia, "obj": obj})
|
||||
objs.append(obj)
|
||||
return objs
|
||||
|
||||
|
||||
def spawn(*prototypes, **kwargs):
|
||||
"""
|
||||
Spawn a number of prototyped objects.
|
||||
|
||||
Args:
|
||||
prototypes (dict): Each argument should be a prototype
|
||||
dictionary.
|
||||
Kwargs:
|
||||
prototype_modules (str or list): A python-path to a prototype
|
||||
module, or a list of such paths. These will be used to build
|
||||
the global protparents dictionary accessible by the input
|
||||
prototypes. If not given, it will instead look for modules
|
||||
defined by settings.PROTOTYPE_MODULES.
|
||||
prototype_parents (dict): A dictionary holding a custom
|
||||
prototype-parent dictionary. Will overload same-named
|
||||
prototypes from prototype_modules.
|
||||
return_prototypes (bool): Only return a list of the
|
||||
prototype-parents (no object creation happens)
|
||||
|
||||
"""
|
||||
|
||||
protparents = {}
|
||||
protmodules = make_iter(kwargs.get("prototype_modules", []))
|
||||
if not protmodules and hasattr(settings, "PROTOTYPE_MODULES"):
|
||||
protmodules = make_iter(settings.PROTOTYPE_MODULES)
|
||||
for prototype_module in protmodules:
|
||||
protparents.update(dict((key, val) for key, val in
|
||||
all_from_module(prototype_module).items() if isinstance(val, dict)))
|
||||
# overload module's protparents with specifically given protparents
|
||||
protparents.update(kwargs.get("prototype_parents", {}))
|
||||
for key, prototype in protparents.items():
|
||||
_validate_prototype(key, prototype, protparents, [])
|
||||
|
||||
if "return_prototypes" in kwargs:
|
||||
# only return the parents
|
||||
return copy.deepcopy(protparents)
|
||||
|
||||
objsparams = []
|
||||
for prototype in prototypes:
|
||||
|
||||
_validate_prototype(None, prototype, protparents, [])
|
||||
prot = _get_prototype(prototype, {}, protparents)
|
||||
if not prot:
|
||||
continue
|
||||
|
||||
# extract the keyword args we need to create the object itself. If we get a callable,
|
||||
# call that to get the value (don't catch errors)
|
||||
create_kwargs = {}
|
||||
keyval = prot.pop("key", "Spawned Object %06i" % randint(1, 100000))
|
||||
create_kwargs["db_key"] = keyval() if callable(keyval) else keyval
|
||||
|
||||
locval = prot.pop("location", None)
|
||||
create_kwargs["db_location"] = locval() if callable(locval) else _handle_dbref(locval)
|
||||
|
||||
homval = prot.pop("home", settings.DEFAULT_HOME)
|
||||
create_kwargs["db_home"] = homval() if callable(homval) else _handle_dbref(homval)
|
||||
|
||||
destval = prot.pop("destination", None)
|
||||
create_kwargs["db_destination"] = destval() if callable(destval) else _handle_dbref(destval)
|
||||
|
||||
typval = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
|
||||
create_kwargs["db_typeclass_path"] = typval() if callable(typval) else typval
|
||||
|
||||
# extract calls to handlers
|
||||
permval = prot.pop("permissions", [])
|
||||
permission_string = permval() if callable(permval) else permval
|
||||
lockval = prot.pop("locks", "")
|
||||
lock_string = lockval() if callable(lockval) else lockval
|
||||
aliasval = prot.pop("aliases", "")
|
||||
alias_string = aliasval() if callable(aliasval) else aliasval
|
||||
tagval = prot.pop("tags", [])
|
||||
tags = tagval() if callable(tagval) else tagval
|
||||
attrval = prot.pop("attrs", [])
|
||||
attributes = attrval() if callable(tagval) else attrval
|
||||
|
||||
exval = prot.pop("exec", "")
|
||||
execs = make_iter(exval() if callable(exval) else exval)
|
||||
|
||||
# extract ndb assignments
|
||||
nattributes = dict((key.split("_", 1)[1], value() if callable(value) else value)
|
||||
for key, value in prot.items() if key.startswith("ndb_"))
|
||||
|
||||
# the rest are attributes
|
||||
simple_attributes = [(key, value()) if callable(value) else (key, value)
|
||||
for key, value in prot.items() if not key.startswith("ndb_")]
|
||||
attributes = attributes + simple_attributes
|
||||
attributes = [tup for tup in attributes if not tup[0] in _CREATE_OBJECT_KWARGS]
|
||||
|
||||
# pack for call into _batch_create_object
|
||||
objsparams.append((create_kwargs, permission_string, lock_string,
|
||||
alias_string, nattributes, attributes, tags, execs))
|
||||
|
||||
return _batch_create_object(*objsparams)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# testing
|
||||
|
||||
protparents = {
|
||||
"NOBODY": {},
|
||||
# "INFINITE" : {
|
||||
# "prototype":"INFINITE"
|
||||
# },
|
||||
"GOBLIN": {
|
||||
"key": "goblin grunt",
|
||||
"health": lambda: randint(20, 30),
|
||||
"resists": ["cold", "poison"],
|
||||
"attacks": ["fists"],
|
||||
"weaknesses": ["fire", "light"]
|
||||
},
|
||||
"GOBLIN_WIZARD": {
|
||||
"prototype": "GOBLIN",
|
||||
"key": "goblin wizard",
|
||||
"spells": ["fire ball", "lighting bolt"]
|
||||
},
|
||||
"GOBLIN_ARCHER": {
|
||||
"prototype": "GOBLIN",
|
||||
"key": "goblin archer",
|
||||
"attacks": ["short bow"]
|
||||
},
|
||||
"ARCHWIZARD": {
|
||||
"attacks": ["archwizard staff"],
|
||||
},
|
||||
"GOBLIN_ARCHWIZARD": {
|
||||
"key": "goblin archwizard",
|
||||
"prototype": ("GOBLIN_WIZARD", "ARCHWIZARD")
|
||||
}
|
||||
}
|
||||
# test
|
||||
print([o.key for o in spawn(protparents["GOBLIN"],
|
||||
protparents["GOBLIN_ARCHWIZARD"],
|
||||
prototype_parents=protparents)])
|
||||
|
|
@ -6,6 +6,7 @@ Test form
|
|||
FORMCHAR = "x"
|
||||
TABLECHAR = "c"
|
||||
|
||||
|
||||
FORM = """
|
||||
.------------------------------------------------.
|
||||
| |
|
||||
|
|
@ -27,4 +28,6 @@ FORM = """
|
|||
| ccccccccc | ccccccccccccccccBccccccccccccccccc |
|
||||
| | |
|
||||
-----------`-------------------------------------
|
||||
Footer: xxxFxxx
|
||||
info
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class TestEvForm(TestCase):
|
|||
u'|\n'
|
||||
u'| \x1b[0m\x1b[1m\x1b[32mBouncer\x1b[0m \x1b[0m |\n'
|
||||
u'| |\n'
|
||||
u' >----------------------------------------------<\n'
|
||||
u' >----------------------------------------------< \n'
|
||||
u'| |\n'
|
||||
u'| Desc: \x1b[0mA sturdy \x1b[0m \x1b[0m'
|
||||
u' STR: \x1b[0m12 \x1b[0m\x1b[0m\x1b[0m\x1b[0m'
|
||||
|
|
@ -31,7 +31,7 @@ class TestEvForm(TestCase):
|
|||
u' LUC: \x1b[0m10 \x1b[0m\x1b[0m\x1b[0m'
|
||||
u' MAG: \x1b[0m3 \x1b[0m\x1b[0m\x1b[0m |\n'
|
||||
u'| |\n'
|
||||
u' >----------.-----------------------------------<\n'
|
||||
u' >----------.-----------------------------------< \n'
|
||||
u'| | |\n'
|
||||
u'| \x1b[0mHP\x1b[0m|\x1b[0mMV \x1b[0m|\x1b[0mMP\x1b[0m '
|
||||
u'| \x1b[0mSkill \x1b[0m|\x1b[0mValue \x1b[0m'
|
||||
|
|
@ -47,7 +47,10 @@ class TestEvForm(TestCase):
|
|||
u'| \x1b[0mSmithing \x1b[0m|\x1b[0m9 \x1b[0m'
|
||||
u'|\x1b[0m205/900 \x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m\x1b[0m |\n'
|
||||
u'| | |\n'
|
||||
u' -----------`-------------------------------------\n')
|
||||
u' -----------`-------------------------------------\n'
|
||||
u' Footer: \x1b[0mrev 1 \x1b[0m \n'
|
||||
u' info \n'
|
||||
u' ')
|
||||
|
||||
def test_ansi_escape(self):
|
||||
# note that in a msg() call, the result would be the correct |-----,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,230 @@
|
|||
"""
|
||||
Unit tests for the EvMenu system
|
||||
|
||||
TODO: This need expansion.
|
||||
This sets up a testing parent for testing EvMenu trees. It is configured by subclassing the
|
||||
`TestEvMenu` class from this module and setting the class variables to point to the menu that should
|
||||
be tested and how it should be called.
|
||||
|
||||
Without adding any further test methods, the tester will process all nodes of the menu, depth first,
|
||||
by stepping through all options for every node. Optionally, it can check that all nodes are visited.
|
||||
It will create a hierarchical list of node names that describes the tree structure. This can then be
|
||||
compared against a template to make sure the menu structure is sound. Easiest way to use this is to
|
||||
run the test once to see how the structure looks.
|
||||
|
||||
The system also allows for testing the returns of each node as part of the parsing.
|
||||
|
||||
To help debug the menu, turn on `debug_output`, which will print the traversal process in detail.
|
||||
|
||||
"""
|
||||
|
||||
import copy
|
||||
from django.test import TestCase
|
||||
from evennia.utils import evmenu
|
||||
from mock import Mock
|
||||
from evennia.utils import ansi
|
||||
from mock import MagicMock
|
||||
|
||||
|
||||
class TestEvMenu(TestCase):
|
||||
"Run the EvMenu testing."
|
||||
menutree = {} # can also be the path to the menu tree
|
||||
startnode = "start"
|
||||
cmdset_mergetype = "Replace"
|
||||
cmdset_priority = 1
|
||||
auto_quit = True
|
||||
auto_look = True
|
||||
auto_help = True
|
||||
cmd_on_exit = "look"
|
||||
persistent = False
|
||||
startnode_input = ""
|
||||
kwargs = {}
|
||||
|
||||
# if all nodes must be visited for the test to pass. This is not on
|
||||
# by default since there may be exec-nodes that are made to not be
|
||||
# visited.
|
||||
expect_all_nodes = False
|
||||
|
||||
# this is compared against the full tree structure generated
|
||||
expected_tree = []
|
||||
# this allows for verifying that a given node returns a given text. The
|
||||
# text is compared with .startswith, so the entire text need not be matched.
|
||||
expected_node_texts = {}
|
||||
# just check the number of options from each node
|
||||
expected_node_options_count = {}
|
||||
# check the actual options
|
||||
expected_node_options = {}
|
||||
|
||||
# set this to print the traversal as it happens (debugging)
|
||||
debug_output = False
|
||||
|
||||
def _debug_output(self, indent, msg):
|
||||
if self.debug_output:
|
||||
print(" " * indent + ansi.strip_ansi(msg))
|
||||
|
||||
def _test_menutree(self, menu):
|
||||
"""
|
||||
This is a automatic tester of the menu tree by recursively progressing through the
|
||||
structure.
|
||||
"""
|
||||
|
||||
def _depth_first(menu, tree, visited, indent):
|
||||
|
||||
# we are in a given node here
|
||||
nodename = menu.nodename
|
||||
options = menu.test_options
|
||||
if isinstance(options, dict):
|
||||
options = (options, )
|
||||
|
||||
# run validation tests for this node
|
||||
compare_text = self.expected_node_texts.get(nodename, None)
|
||||
if compare_text is not None:
|
||||
compare_text = ansi.strip_ansi(compare_text.strip())
|
||||
node_text = menu.test_nodetext
|
||||
self.assertIsNotNone(
|
||||
bool(node_text),
|
||||
"node: {}: node-text is None, which was not expected.".format(nodename))
|
||||
if isinstance(node_text, tuple):
|
||||
node_text, helptext = node_text
|
||||
node_text = ansi.strip_ansi(node_text.strip())
|
||||
self.assertTrue(
|
||||
node_text.startswith(compare_text),
|
||||
"\nnode \"{}\':\nOutput:\n{}\n\nExpected (startswith):\n{}".format(
|
||||
nodename, node_text, compare_text))
|
||||
compare_options_count = self.expected_node_options_count.get(nodename, None)
|
||||
if compare_options_count is not None:
|
||||
self.assertEqual(
|
||||
len(options), compare_options_count,
|
||||
"Not the right number of options returned from node {}.".format(nodename))
|
||||
compare_options = self.expected_node_options.get(nodename, None)
|
||||
if compare_options:
|
||||
self.assertEqual(
|
||||
options, compare_options,
|
||||
"Options returned from node {} does not match.".format(nodename))
|
||||
|
||||
self._debug_output(indent, "*{}".format(nodename))
|
||||
subtree = []
|
||||
|
||||
if not options:
|
||||
# an end node
|
||||
if nodename not in visited:
|
||||
visited.append(nodename)
|
||||
subtree = nodename
|
||||
else:
|
||||
for inum, optdict in enumerate(options):
|
||||
|
||||
key, desc, execute, goto = optdict.get("key", ""), optdict.get("desc", None),\
|
||||
optdict.get("exec", None), optdict.get("goto", None)
|
||||
|
||||
# prepare the key to pass to the menu
|
||||
if isinstance(key, (tuple, list)) and len(key) > 1:
|
||||
key = key[0]
|
||||
if key == "_default":
|
||||
key = "test raw input"
|
||||
if not key:
|
||||
key = str(inum + 1)
|
||||
|
||||
backup_menu = copy.copy(menu)
|
||||
|
||||
# step the menu
|
||||
menu.parse_input(key)
|
||||
|
||||
# from here on we are likely in a different node
|
||||
nodename = menu.nodename
|
||||
|
||||
if menu.close_menu.called:
|
||||
# this was an end node
|
||||
self._debug_output(indent, " .. menu exited! Back to previous node.")
|
||||
menu = backup_menu
|
||||
menu.close_menu = MagicMock()
|
||||
visited.append(nodename)
|
||||
subtree.append(nodename)
|
||||
elif nodename not in visited:
|
||||
visited.append(nodename)
|
||||
subtree.append(nodename)
|
||||
_depth_first(menu, subtree, visited, indent + 2)
|
||||
#self._debug_output(indent, " -> arrived at {}".format(nodename))
|
||||
else:
|
||||
subtree.append(nodename)
|
||||
#self._debug_output( indent, " -> arrived at {} (circular call)".format(nodename))
|
||||
self._debug_output(indent, "-- {} ({}) -> {}".format(key, desc, goto))
|
||||
|
||||
if subtree:
|
||||
tree.append(subtree)
|
||||
|
||||
# the start node has already fired at this point
|
||||
visited_nodes = [menu.nodename]
|
||||
traversal_tree = [menu.nodename]
|
||||
_depth_first(menu, traversal_tree, visited_nodes, 1)
|
||||
|
||||
if self.expect_all_nodes:
|
||||
self.assertGreaterEqual(len(menu._menutree), len(visited_nodes))
|
||||
self.assertEqual(traversal_tree, self.expected_tree)
|
||||
|
||||
def setUp(self):
|
||||
self.caller = Mock()
|
||||
self.caller.msg = Mock()
|
||||
self.menu = evmenu.EvMenu(self.caller, "evennia.utils.evmenu", startnode="test_start_node",
|
||||
persistent=True, cmdset_mergetype="Replace", testval="val",
|
||||
testval2="val2")
|
||||
self.menu = None
|
||||
if self.menutree:
|
||||
self.caller = MagicMock()
|
||||
self.caller.key = "Test"
|
||||
self.caller2 = MagicMock()
|
||||
self.caller2.key = "Test"
|
||||
self.caller.msg = MagicMock()
|
||||
self.caller2.msg = MagicMock()
|
||||
self.session = MagicMock()
|
||||
self.session2 = MagicMock()
|
||||
|
||||
self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode,
|
||||
cmdset_mergetype=self.cmdset_mergetype,
|
||||
cmdset_priority=self.cmdset_priority,
|
||||
auto_quit=self.auto_quit, auto_look=self.auto_look,
|
||||
auto_help=self.auto_help,
|
||||
cmd_on_exit=self.cmd_on_exit, persistent=False,
|
||||
startnode_input=self.startnode_input, session=self.session,
|
||||
**self.kwargs)
|
||||
# persistent version
|
||||
self.pmenu = evmenu.EvMenu(self.caller2, self.menutree, startnode=self.startnode,
|
||||
cmdset_mergetype=self.cmdset_mergetype,
|
||||
cmdset_priority=self.cmdset_priority,
|
||||
auto_quit=self.auto_quit, auto_look=self.auto_look,
|
||||
auto_help=self.auto_help,
|
||||
cmd_on_exit=self.cmd_on_exit, persistent=True,
|
||||
startnode_input=self.startnode_input, session=self.session2,
|
||||
**self.kwargs)
|
||||
|
||||
self.menu.close_menu = MagicMock()
|
||||
self.pmenu.close_menu = MagicMock()
|
||||
|
||||
def test_menu_structure(self):
|
||||
if self.menu:
|
||||
self._test_menutree(self.menu)
|
||||
self._test_menutree(self.pmenu)
|
||||
|
||||
|
||||
class TestEvMenuExample(TestEvMenu):
|
||||
|
||||
menutree = "evennia.utils.evmenu"
|
||||
startnode = "test_start_node"
|
||||
kwargs = {"testval": "val", "testval2": "val2"}
|
||||
debug_output = False
|
||||
|
||||
expected_node_texts = {
|
||||
"test_view_node": "Your name is"}
|
||||
|
||||
expected_tree = \
|
||||
['test_start_node',
|
||||
['test_set_node',
|
||||
['test_start_node'],
|
||||
'test_look_node',
|
||||
['test_start_node'],
|
||||
'test_view_node',
|
||||
['test_start_node'],
|
||||
'test_dynamic_node',
|
||||
['test_dynamic_node',
|
||||
'test_dynamic_node',
|
||||
'test_dynamic_node',
|
||||
'test_dynamic_node',
|
||||
'test_start_node'],
|
||||
'test_end_node',
|
||||
'test_displayinput_node',
|
||||
['test_start_node']]]
|
||||
|
||||
def test_kwargsave(self):
|
||||
self.assertTrue(hasattr(self.menu, "testval"))
|
||||
|
|
|
|||
|
|
@ -20,18 +20,20 @@ import textwrap
|
|||
import random
|
||||
from os.path import join as osjoin
|
||||
from importlib import import_module
|
||||
from inspect import ismodule, trace, getmembers, getmodule
|
||||
from inspect import ismodule, trace, getmembers, getmodule, getmro
|
||||
from collections import defaultdict, OrderedDict
|
||||
from twisted.internet import threads, reactor, task
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.apps import apps
|
||||
from evennia.utils import logger
|
||||
|
||||
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
|
||||
_EVENNIA_DIR = settings.EVENNIA_DIR
|
||||
_GAME_DIR = settings.GAME_DIR
|
||||
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
|
|
@ -42,8 +44,6 @@ _GA = object.__getattribute__
|
|||
_SA = object.__setattr__
|
||||
_DA = object.__delattr__
|
||||
|
||||
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
|
||||
def is_iter(iterable):
|
||||
"""
|
||||
|
|
@ -79,7 +79,7 @@ def make_iter(obj):
|
|||
return not hasattr(obj, '__iter__') and [obj] or obj
|
||||
|
||||
|
||||
def wrap(text, width=_DEFAULT_WIDTH, indent=0):
|
||||
def wrap(text, width=None, indent=0):
|
||||
"""
|
||||
Safely wrap text to a certain number of characters.
|
||||
|
||||
|
|
@ -92,6 +92,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0):
|
|||
text (str): Properly wrapped text.
|
||||
|
||||
"""
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
if not text:
|
||||
return ""
|
||||
text = to_unicode(text)
|
||||
|
|
@ -103,7 +104,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0):
|
|||
fill = wrap
|
||||
|
||||
|
||||
def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
|
||||
def pad(text, width=None, align="c", fillchar=" "):
|
||||
"""
|
||||
Pads to a given width.
|
||||
|
||||
|
|
@ -118,6 +119,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
|
|||
text (str): The padded text.
|
||||
|
||||
"""
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
align = align if align in ('c', 'l', 'r') else 'c'
|
||||
fillchar = fillchar[0] if fillchar else " "
|
||||
if align == 'l':
|
||||
|
|
@ -128,7 +130,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
|
|||
return text.center(width, fillchar)
|
||||
|
||||
|
||||
def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
|
||||
def crop(text, width=None, suffix="[...]"):
|
||||
"""
|
||||
Crop text to a certain width, throwing away text from too-long
|
||||
lines.
|
||||
|
|
@ -146,7 +148,7 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
|
|||
text (str): The cropped text.
|
||||
|
||||
"""
|
||||
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
utext = to_unicode(text)
|
||||
ltext = len(utext)
|
||||
if ltext <= width:
|
||||
|
|
@ -157,12 +159,16 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
|
|||
return to_str(utext)
|
||||
|
||||
|
||||
def dedent(text):
|
||||
def dedent(text, baseline_index=None):
|
||||
"""
|
||||
Safely clean all whitespace at the left of a paragraph.
|
||||
|
||||
Args:
|
||||
text (str): The text to dedent.
|
||||
baseline_index (int or None, optional): Which row to use as a 'base'
|
||||
for the indentation. Lines will be dedented to this level but
|
||||
no further. If None, indent so as to completely deindent the
|
||||
least indented text.
|
||||
|
||||
Returns:
|
||||
text (str): Dedented string.
|
||||
|
|
@ -175,10 +181,17 @@ def dedent(text):
|
|||
"""
|
||||
if not text:
|
||||
return ""
|
||||
return textwrap.dedent(text)
|
||||
if baseline_index is None:
|
||||
return textwrap.dedent(text)
|
||||
else:
|
||||
lines = text.split('\n')
|
||||
baseline = lines[baseline_index]
|
||||
spaceremove = len(baseline) - len(baseline.lstrip(' '))
|
||||
return "\n".join(line[min(spaceremove, len(line) - len(line.lstrip(' '))):]
|
||||
for line in lines)
|
||||
|
||||
|
||||
def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
|
||||
def justify(text, width=None, align="f", indent=0):
|
||||
"""
|
||||
Fully justify a text so that it fits inside `width`. When using
|
||||
full justification (default) this will be done by padding between
|
||||
|
|
@ -197,6 +210,7 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
|
|||
justified (str): The justified and indented block of text.
|
||||
|
||||
"""
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
def _process_line(line):
|
||||
"""
|
||||
|
|
@ -208,18 +222,27 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
|
|||
gap = " " # minimum gap between words
|
||||
if line_rest > 0:
|
||||
if align == 'l':
|
||||
line[-1] += " " * line_rest
|
||||
if line[-1] == "\n\n":
|
||||
line[-1] = " " * (line_rest-1) + "\n" + " " * width + "\n" + " " * width
|
||||
else:
|
||||
line[-1] += " " * line_rest
|
||||
elif align == 'r':
|
||||
line[0] = " " * line_rest + line[0]
|
||||
elif align == 'c':
|
||||
pad = " " * (line_rest // 2)
|
||||
line[0] = pad + line[0]
|
||||
line[-1] = line[-1] + pad + " " * (line_rest % 2)
|
||||
if line[-1] == "\n\n":
|
||||
line[-1] += pad + " " * (line_rest % 2 - 1) + \
|
||||
"\n" + " " * width + "\n" + " " * width
|
||||
else:
|
||||
line[-1] = line[-1] + pad + " " * (line_rest % 2)
|
||||
else: # align 'f'
|
||||
gap += " " * (line_rest // max(1, ngaps))
|
||||
rest_gap = line_rest % max(1, ngaps)
|
||||
for i in range(rest_gap):
|
||||
line[i] += " "
|
||||
elif not any(line):
|
||||
return [" " * width]
|
||||
return gap.join(line)
|
||||
|
||||
# split into paragraphs and words
|
||||
|
|
@ -260,6 +283,62 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
|
|||
return "\n".join([indentstring + line for line in lines])
|
||||
|
||||
|
||||
def columnize(string, columns=2, spacing=4, align='l', width=None):
|
||||
"""
|
||||
Break a string into a number of columns, using as little
|
||||
vertical space as possible.
|
||||
|
||||
Args:
|
||||
string (str): The string to columnize.
|
||||
columns (int, optional): The number of columns to use.
|
||||
spacing (int, optional): How much space to have between columns.
|
||||
width (int, optional): The max width of the columns.
|
||||
Defaults to client's default width.
|
||||
|
||||
Returns:
|
||||
columns (str): Text divided into columns.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If given invalid values.
|
||||
|
||||
"""
|
||||
columns = max(1, columns)
|
||||
spacing = max(1, spacing)
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
w_spaces = (columns - 1) * spacing
|
||||
w_txt = max(1, width - w_spaces)
|
||||
|
||||
if w_spaces + columns > width: # require at least 1 char per column
|
||||
raise RuntimeError("Width too small to fit columns")
|
||||
|
||||
colwidth = int(w_txt / (1.0 * columns))
|
||||
|
||||
# first make a single column which we then split
|
||||
onecol = justify(string, width=colwidth, align=align)
|
||||
onecol = onecol.split("\n")
|
||||
|
||||
nrows, dangling = divmod(len(onecol), columns)
|
||||
nrows = [nrows + 1 if i < dangling else nrows for i in range(columns)]
|
||||
|
||||
height = max(nrows)
|
||||
cols = []
|
||||
istart = 0
|
||||
for irows in nrows:
|
||||
cols.append(onecol[istart:istart+irows])
|
||||
istart = istart + irows
|
||||
for col in cols:
|
||||
if len(col) < height:
|
||||
col.append(" " * colwidth)
|
||||
|
||||
sep = " " * spacing
|
||||
rows = []
|
||||
for irow in range(height):
|
||||
rows.append(sep.join(col[irow] for col in cols))
|
||||
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def list_to_string(inlist, endsep="and", addquote=False):
|
||||
"""
|
||||
This pretty-formats a list as string output, adding an optional
|
||||
|
|
@ -931,17 +1010,17 @@ def delay(timedelay, callback, *args, **kwargs):
|
|||
Delay the return of a value.
|
||||
|
||||
Args:
|
||||
timedelay (int or float): The delay in seconds
|
||||
callback (callable): Will be called with optional
|
||||
arguments after `timedelay` seconds.
|
||||
args (any, optional): Will be used as arguments to callback
|
||||
timedelay (int or float): The delay in seconds
|
||||
callback (callable): Will be called as `callback(*args, **kwargs)`
|
||||
after `timedelay` seconds.
|
||||
args (any, optional): Will be used as arguments to callback
|
||||
Kwargs:
|
||||
persistent (bool, optional): should make the delay persistent
|
||||
over a reboot or reload
|
||||
any (any): Will be used to call the callback.
|
||||
persistent (bool, optional): should make the delay persistent
|
||||
over a reboot or reload
|
||||
any (any): Will be used as keyword arguments to callback.
|
||||
|
||||
Returns:
|
||||
deferred (deferred): Will fire fire with callback after
|
||||
deferred (deferred): Will fire with callback after
|
||||
`timedelay` seconds. Note that if `timedelay()` is used in the
|
||||
commandhandler callback chain, the callback chain can be
|
||||
defined directly in the command body and don't need to be
|
||||
|
|
@ -1546,6 +1625,7 @@ def format_table(table, extra_space=1):
|
|||
Examples:
|
||||
|
||||
```python
|
||||
ftable = format_table([[...], [...], ...])
|
||||
for ir, row in enumarate(ftable):
|
||||
if ir == 0:
|
||||
# make first row white
|
||||
|
|
@ -1786,8 +1866,12 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
|
|||
error = kwargs.get("nofound_string") or _("Could not find '%s'." % query)
|
||||
matches = None
|
||||
elif len(matches) > 1:
|
||||
error = kwargs.get("multimatch_string") or \
|
||||
_("More than one match for '%s' (please narrow target):\n" % query)
|
||||
multimatch_string = kwargs.get("multimatch_string")
|
||||
if multimatch_string:
|
||||
error = "%s\n" % multimatch_string
|
||||
else:
|
||||
error = _("More than one match for '%s' (please narrow target):\n" % query)
|
||||
|
||||
for num, result in enumerate(matches):
|
||||
# we need to consider Commands, where .aliases is a list
|
||||
aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases
|
||||
|
|
@ -1875,3 +1959,29 @@ def get_game_dir_path():
|
|||
else:
|
||||
os.chdir(os.pardir)
|
||||
raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.")
|
||||
|
||||
|
||||
def get_all_typeclasses(parent=None):
|
||||
"""
|
||||
List available typeclasses from all available modules.
|
||||
|
||||
Args:
|
||||
parent (str, optional): If given, only return typeclasses inheriting (at any distance)
|
||||
from this parent.
|
||||
|
||||
Returns:
|
||||
typeclasses (dict): On the form {"typeclass.path": typeclass, ...}
|
||||
|
||||
Notes:
|
||||
This will dynamicall retrieve all abstract django models inheriting at any distance
|
||||
from the TypedObject base (aka a Typeclass) so it will work fine with any custom
|
||||
classes being added.
|
||||
|
||||
"""
|
||||
from evennia.typeclasses.models import TypedObject
|
||||
typeclasses = {"{}.{}".format(model.__module__, model.__name__): model
|
||||
for model in apps.get_models() if TypedObject in getmro(model)}
|
||||
if parent:
|
||||
typeclasses = {name: typeclass for name, typeclass in typeclasses.items()
|
||||
if inherits_from(typeclass, parent)}
|
||||
return typeclasses
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue