Resolve merge conflicts
This commit is contained in:
commit
90a1a0cba8
35 changed files with 2766 additions and 973 deletions
|
|
@ -57,7 +57,7 @@ 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
|
||||
`caller.ndb._evmenu`. This makes it a convenient place to store
|
||||
temporary state variables between nodes, since this NAttribute is
|
||||
deleted when the menu is exited.
|
||||
|
||||
|
|
@ -165,11 +165,114 @@ your default cmdset. Run it with this module, like `testmenu evennia.utils.evmen
|
|||
|
||||
----
|
||||
|
||||
|
||||
## Menu generation from template string
|
||||
|
||||
In evmenu.py is a helper function `parse_menu_template` that parses a
|
||||
template-string and outputs a menu-tree dictionary suitable to pass into
|
||||
EvMenu:
|
||||
::
|
||||
|
||||
menutree = evmenu.parse_menu_template(caller, menu_template, goto_callables)
|
||||
EvMenu(caller, menutree)
|
||||
|
||||
For maximum flexibility you can inject normally-created nodes in the menu tree
|
||||
before passing it to EvMenu. If that's not needed, you can also create a menu
|
||||
in one step with:
|
||||
::
|
||||
|
||||
evmenu.template2menu(caller, menu_template, goto_callables)
|
||||
|
||||
The `goto_callables` is a mapping `{"funcname": callable, ...}`, where each
|
||||
callable must be a module-global function on the form
|
||||
`funcname(caller, raw_string, **kwargs)` (like any goto-callable). The
|
||||
`menu_template` is a multi-line string on the following form:
|
||||
::
|
||||
|
||||
## node start
|
||||
|
||||
This is the text of the start node.
|
||||
The text area can have multiple lines, line breaks etc.
|
||||
|
||||
Each option below is one of these forms
|
||||
key: desc -> gotostr_or_func
|
||||
key: gotostr_or_func
|
||||
>: gotostr_or_func
|
||||
> glob/regex: gotostr_or_func
|
||||
|
||||
## options
|
||||
|
||||
# comments are only allowed from beginning of line.
|
||||
# Indenting is not necessary, but good for readability
|
||||
|
||||
1: Option number 1 -> node1
|
||||
2: Option number 2 -> node2
|
||||
next: This steps next -> go_back()
|
||||
# the -> can be ignored if there is no desc
|
||||
back: go_back(from_node=start)
|
||||
abort: abort
|
||||
|
||||
## node node1
|
||||
|
||||
Text for Node1. Enter a message!
|
||||
<return> to go back.
|
||||
|
||||
## options
|
||||
|
||||
# Starting the option-line with >
|
||||
# allows to perform different actions depending on
|
||||
# what is inserted.
|
||||
|
||||
# this catches everything starting with foo
|
||||
> foo*: handle_foo_message()
|
||||
|
||||
# regex are also allowed (this catches number inputs)
|
||||
> [0-9]+?: handle_numbers()
|
||||
|
||||
# this catches the empty return
|
||||
>: start
|
||||
|
||||
# this catches everything else
|
||||
> *: handle_message(from_node=node1)
|
||||
|
||||
## node node2
|
||||
|
||||
Text for Node2. Just go back.
|
||||
|
||||
## options
|
||||
|
||||
>: start
|
||||
|
||||
# node abort
|
||||
|
||||
This exits the menu since there is no `## options` section.
|
||||
|
||||
Each menu node is defined by a `# node <name>` containing the text of the node,
|
||||
followed by `## options` Also `## NODE` and `## OPTIONS` work. No python code
|
||||
logics is allowed in the template, this code is not evaluated but parsed. More
|
||||
advanced dynamic usage requires a full node-function (which can be added to the
|
||||
generated dict, as said).
|
||||
|
||||
Adding `(..)` to a goto treats it as a callable and it must then be included in
|
||||
the `goto_callable` mapping. Only named keywords (or no args at all) are
|
||||
allowed, these will be added to the `**kwargs` going into the callable. Quoting
|
||||
strings is only needed if wanting to pass strippable spaces, otherwise the
|
||||
key:values will be converted to strings/numbers with literal_eval before passed
|
||||
into the callable.
|
||||
|
||||
The `> ` option takes a glob or regex to perform different actions depending on user
|
||||
input. Make sure to sort these in increasing order of generality since they
|
||||
will be tested in sequence.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
import inspect
|
||||
|
||||
from ast import literal_eval
|
||||
from fnmatch import fnmatch
|
||||
|
||||
from inspect import isfunction, getargspec
|
||||
from django.conf import settings
|
||||
from evennia import Command, CmdSet
|
||||
|
|
@ -179,6 +282,9 @@ from evennia.utils.ansi import strip_ansi
|
|||
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
|
||||
from evennia.commands import cmdhandler
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
# read from protocol NAWS later?
|
||||
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
|
|
@ -189,11 +295,10 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
|||
|
||||
# Return messages
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
_ERR_NOT_IMPLEMENTED = _(
|
||||
"Menu node '{nodename}' is either not implemented or " "caused an error. Make another choice."
|
||||
"Menu node '{nodename}' is either not implemented or caused an error. "
|
||||
"Make another choice or try 'q' to abort."
|
||||
)
|
||||
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
|
||||
_ERR_NO_OPTION_DESC = _("No description.")
|
||||
|
|
@ -227,6 +332,19 @@ class EvMenuError(RuntimeError):
|
|||
pass
|
||||
|
||||
|
||||
class EvMenuGotoAbortMessage(RuntimeError):
|
||||
"""
|
||||
This can be raised by a goto-callable to abort the goto flow. The message
|
||||
stored with the executable will be sent to the caller who will remain on
|
||||
the current node. This can be used to pass single-line returns without
|
||||
re-running the entire node with text and options.
|
||||
|
||||
Example:
|
||||
raise EvMenuGotoMessage("That makes no sense.")
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
#
|
||||
# Menu command and command set
|
||||
|
|
@ -243,6 +361,10 @@ class CmdEvMenuNode(Command):
|
|||
aliases = [_CMD_NOMATCH]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Menu"
|
||||
auto_help_display_key = "<menu commands>"
|
||||
|
||||
def get_help(self):
|
||||
return "Menu commands are explained within the menu."
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
|
|
@ -271,28 +393,28 @@ class CmdEvMenuNode(Command):
|
|||
caller = self.caller
|
||||
# we store Session on the menu since this can be hard to
|
||||
# get in multisession environemtns if caller is an Account.
|
||||
menu = caller.ndb._menutree
|
||||
menu = caller.ndb._evmenu
|
||||
if not menu:
|
||||
if _restore(caller):
|
||||
return
|
||||
orig_caller = caller
|
||||
caller = caller.account if hasattr(caller, "account") else None
|
||||
menu = caller.ndb._menutree if caller else None
|
||||
menu = caller.ndb._evmenu if caller else None
|
||||
if not menu:
|
||||
if caller and _restore(caller):
|
||||
return
|
||||
caller = self.session
|
||||
menu = caller.ndb._menutree
|
||||
menu = caller.ndb._evmenu
|
||||
if not menu:
|
||||
# can't restore from a session
|
||||
err = "Menu object not found as %s.ndb._menutree!" % orig_caller
|
||||
err = "Menu object not found as %s.ndb._evmenu!" % 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 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
|
||||
caller.ndb._evmenu._session = self.session
|
||||
# we have a menu, use it.
|
||||
menu.parse_input(self.raw_string)
|
||||
|
||||
|
|
@ -324,7 +446,7 @@ class EvMenuCmdSet(CmdSet):
|
|||
# -------------------------------------------------------------
|
||||
|
||||
|
||||
class EvMenu(object):
|
||||
class EvMenu:
|
||||
"""
|
||||
This object represents an operational menu. It is initialized from
|
||||
a menufile.py instruction.
|
||||
|
|
@ -425,9 +547,9 @@ class EvMenu(object):
|
|||
EvMenuError: If the start/end node is not found in menu tree.
|
||||
|
||||
Notes:
|
||||
While running, the menu is stored on the caller as `caller.ndb._menutree`. Also
|
||||
While running, the menu is stored on the caller as `caller.ndb._evmenu`. Also
|
||||
the current Session (from the Command, so this is still valid in multisession
|
||||
environments) is available through `caller.ndb._menutree._session`. The `_menutree`
|
||||
environments) is available through `caller.ndb._evmenu._session`. The `_evmenu`
|
||||
property is a good one for storing intermediary data on between nodes since it
|
||||
will be automatically deleted when the menu closes.
|
||||
|
||||
|
|
@ -478,7 +600,7 @@ class EvMenu(object):
|
|||
self.test_nodetext = ""
|
||||
|
||||
# assign kwargs as initialization vars on ourselves.
|
||||
if set(
|
||||
reserved_clash = set(
|
||||
(
|
||||
"_startnode",
|
||||
"_menutree",
|
||||
|
|
@ -492,22 +614,26 @@ class EvMenu(object):
|
|||
"cmdset_mergetype",
|
||||
"auto_quit",
|
||||
)
|
||||
).intersection(set(kwargs.keys())):
|
||||
).intersection(set(kwargs.keys()))
|
||||
if reserved_clash:
|
||||
raise RuntimeError(
|
||||
"One or more of the EvMenu `**kwargs` is reserved by EvMenu for internal use."
|
||||
f"One or more of the EvMenu `**kwargs` ({list(reserved_clash)}) is reserved by EvMenu for internal use."
|
||||
)
|
||||
for key, val in kwargs.items():
|
||||
setattr(self, key, val)
|
||||
|
||||
if self.caller.ndb._menutree:
|
||||
if self.caller.ndb._evmenu:
|
||||
# an evmenu already exists - we try to close it cleanly. Note that this will
|
||||
# not fire the previous menu's end node.
|
||||
try:
|
||||
self.caller.ndb._menutree.close_menu()
|
||||
self.caller.ndb._evmenu.close_menu()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# store ourself on the object
|
||||
self.caller.ndb._evmenu = self
|
||||
|
||||
# DEPRECATED - for backwards-compatibility
|
||||
self.caller.ndb._menutree = self
|
||||
|
||||
if persistent:
|
||||
|
|
@ -527,7 +653,7 @@ class EvMenu(object):
|
|||
caller.attributes.add("_menutree_saved", (self.__class__, (menudata,), calldict))
|
||||
caller.attributes.add("_menutree_saved_startnode", (startnode, startnode_input))
|
||||
except Exception as err:
|
||||
caller.msg(_ERROR_PERSISTENT_SAVING.format(error=err), session=self._session)
|
||||
self.msg(_ERROR_PERSISTENT_SAVING.format(error=err))
|
||||
logger.log_trace(_TRACE_PERSISTENT_SAVING)
|
||||
persistent = False
|
||||
|
||||
|
|
@ -537,11 +663,19 @@ class EvMenu(object):
|
|||
menu_cmdset.priority = int(cmdset_priority)
|
||||
self.caller.cmdset.add(menu_cmdset, permanent=persistent)
|
||||
|
||||
reserved_startnode_kwargs = set(("nodename", "raw_string"))
|
||||
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).")
|
||||
clashing_kwargs = reserved_startnode_kwargs.intersection(set(startnode_kwargs.keys()))
|
||||
if clashing_kwargs:
|
||||
raise RuntimeError(
|
||||
f"Evmenu startnode_inputs includes kwargs {tuple(clashing_kwargs)} that "
|
||||
"clashes with EvMenu's internal usage."
|
||||
)
|
||||
|
||||
# start the menu
|
||||
self.goto(self._startnode, startnode_input, **startnode_kwargs)
|
||||
|
||||
|
|
@ -631,7 +765,7 @@ class EvMenu(object):
|
|||
ret = callback(self.caller)
|
||||
except EvMenuError:
|
||||
errmsg = _ERR_GENERAL.format(nodename=callback)
|
||||
self.caller.msg(errmsg, self._session)
|
||||
self.msg(errmsg)
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
|
|
@ -656,20 +790,21 @@ class EvMenu(object):
|
|||
try:
|
||||
node = self._menutree[nodename]
|
||||
except KeyError:
|
||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||
self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
|
||||
raise EvMenuError
|
||||
try:
|
||||
kwargs["_current_nodename"] = nodename
|
||||
ret = self._safe_call(node, raw_string, **kwargs)
|
||||
if isinstance(ret, (tuple, list)) and len(ret) > 1:
|
||||
nodetext, options = ret[:2]
|
||||
else:
|
||||
nodetext, options = ret, None
|
||||
except KeyError:
|
||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||
self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
|
||||
logger.log_trace()
|
||||
raise EvMenuError
|
||||
except Exception:
|
||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
||||
self.msg(_ERR_GENERAL.format(nodename=nodename))
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
|
|
@ -679,8 +814,27 @@ class EvMenu(object):
|
|||
|
||||
return nodetext, options
|
||||
|
||||
def msg(self, txt):
|
||||
"""
|
||||
This is a central point for sending return texts to the caller. It
|
||||
allows for a central point to add custom messaging when creating custom
|
||||
EvMenu overrides.
|
||||
|
||||
Args:
|
||||
txt (str): The text to send.
|
||||
|
||||
Notes:
|
||||
By default this will send to the same session provided to EvMenu
|
||||
(if `session` kwarg was provided to `EvMenu.__init__`). It will
|
||||
also send it with a `type=menu` for the benefit of OOB/webclient.
|
||||
|
||||
"""
|
||||
self.caller.msg(text=(txt, {"type": "menu"}), session=self._session)
|
||||
|
||||
def run_exec(self, nodename, raw_string, **kwargs):
|
||||
"""
|
||||
NOTE: This is deprecated. Use `goto` directly instead.
|
||||
|
||||
Run a function or node as a callback (with the 'exec' option key).
|
||||
|
||||
Args:
|
||||
|
|
@ -723,7 +877,7 @@ class EvMenu(object):
|
|||
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)
|
||||
self.msg("|r%s|n" % errmsg)
|
||||
logger.log_trace(errmsg)
|
||||
return
|
||||
|
||||
|
|
@ -904,12 +1058,14 @@ class EvMenu(object):
|
|||
# avoid multiple calls from different sources
|
||||
self._quitting = True
|
||||
self.caller.cmdset.remove(EvMenuCmdSet)
|
||||
del self.caller.ndb._menutree
|
||||
del self.caller.ndb._evmenu
|
||||
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)
|
||||
# special for template-generated menues
|
||||
del self.caller.db._evmenu_template_contents
|
||||
|
||||
def print_debug_info(self, arg):
|
||||
"""
|
||||
|
|
@ -968,7 +1124,7 @@ class EvMenu(object):
|
|||
)
|
||||
+ "\n |y... END MENU DEBUG|n"
|
||||
)
|
||||
self.caller.msg(debugtxt)
|
||||
self.msg(debugtxt)
|
||||
|
||||
def parse_input(self, raw_string):
|
||||
"""
|
||||
|
|
@ -985,30 +1141,35 @@ class EvMenu(object):
|
|||
"""
|
||||
cmd = strip_ansi(raw_string.strip().lower())
|
||||
|
||||
if cmd in self.options:
|
||||
# this will take precedence over the default commands
|
||||
# below
|
||||
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, 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)
|
||||
try:
|
||||
if self.options and cmd in self.options:
|
||||
# this will take precedence over the default commands
|
||||
# below
|
||||
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, goto_kwargs, execfunc, exec_kwargs = self.default
|
||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||
else:
|
||||
self.msg(_HELP_NO_OPTION_MATCH)
|
||||
except EvMenuGotoAbortMessage as err:
|
||||
# custom interrupt from inside a goto callable - print the message and
|
||||
# stay on the current node.
|
||||
self.msg(str(err))
|
||||
|
||||
def display_nodetext(self):
|
||||
self.caller.msg(self.nodetext, session=self._session)
|
||||
self.msg(self.nodetext)
|
||||
|
||||
def display_helptext(self):
|
||||
self.caller.msg(self.helptext, session=self._session)
|
||||
self.msg(self.helptext)
|
||||
|
||||
# formatters - override in a child class
|
||||
|
||||
|
|
@ -1460,219 +1621,288 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs):
|
|||
|
||||
# -------------------------------------------------------------
|
||||
#
|
||||
# test menu strucure and testing command
|
||||
# Menu generation from menu template string
|
||||
#
|
||||
# -------------------------------------------------------------
|
||||
|
||||
_RE_NODE = re.compile(r"##\s*?NODE\s+?(?P<nodename>\S[\S\s]*?)$", re.I + re.M)
|
||||
_RE_OPTIONS_SEP = re.compile(r"##\s*?OPTIONS\s*?$", re.I + re.M)
|
||||
_RE_CALLABLE = re.compile(r"\S+?\(\)", re.I + re.M)
|
||||
_RE_CALLABLE = re.compile(
|
||||
r"(?P<funcname>\S+?)(?:\((?P<kwargs>[\S\s]+?)\)|\(\))", re.I + re.M
|
||||
)
|
||||
|
||||
def _generate_goto(caller, **kwargs):
|
||||
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
|
||||
_HELP_NO_OPTION_MATCH = _("Choose an option or try 'help'.")
|
||||
|
||||
_OPTION_INPUT_MARKER = ">"
|
||||
_OPTION_ALIAS_MARKER = ";"
|
||||
_OPTION_SEP_MARKER = ":"
|
||||
_OPTION_CALL_MARKER = "->"
|
||||
_OPTION_COMMENT_START = "#"
|
||||
|
||||
|
||||
def test_start_node(caller):
|
||||
menu = caller.ndb._menutree
|
||||
text = """
|
||||
This is an example menu.
|
||||
# Input/option/goto handler functions that allows for dynamically generated
|
||||
# nodes read from the menu template.
|
||||
|
||||
If you enter anything except the valid options, your input will be
|
||||
recorded and you will be brought to a menu entry showing your
|
||||
input.
|
||||
def _process_callable(caller, goto, goto_callables, raw_string,
|
||||
current_nodename, kwargs):
|
||||
"""
|
||||
Central helper for parsing a goto-callable (`funcname(**kwargs)`) out of
|
||||
the right-hand-side of the template options and map this to an actual
|
||||
callable registered with the template generator. This involves parsing the
|
||||
func-name and running literal-eval on its kwargs.
|
||||
|
||||
Select options or use 'quit' to exit the menu.
|
||||
"""
|
||||
match = _RE_CALLABLE.match(goto)
|
||||
if match:
|
||||
gotofunc = match.group("funcname")
|
||||
gotokwargs = match.group("kwargs") or ""
|
||||
if gotofunc in goto_callables:
|
||||
for kwarg in gotokwargs.split(","):
|
||||
if kwarg and "=" in kwarg:
|
||||
key, value = [part.strip() for part in kwarg.split("=", 1)]
|
||||
if key in ("evmenu_goto", "evmenu_gotomap", "_current_nodename",
|
||||
"evmenu_current_nodename", "evmenu_goto_callables"):
|
||||
raise RuntimeError(
|
||||
f"EvMenu template error: goto-callable '{goto}' uses a "
|
||||
f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
|
||||
"system. Rename the kwarg.")
|
||||
try:
|
||||
key = literal_eval(key)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
value = literal_eval(value)
|
||||
except ValueError:
|
||||
pass
|
||||
kwargs[key] = value
|
||||
|
||||
The menu was initialized with two variables: %s and %s.
|
||||
""" % (
|
||||
menu.testval,
|
||||
menu.testval2,
|
||||
)
|
||||
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
|
||||
if goto is None:
|
||||
return goto, {"generated_nodename": current_nodename}
|
||||
return goto, {"generated_nodename": goto}
|
||||
|
||||
options = (
|
||||
{
|
||||
"key": ("|yS|net", "s"),
|
||||
"desc": "Set an attribute on yourself.",
|
||||
"exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"),
|
||||
"goto": "test_set_node",
|
||||
},
|
||||
{
|
||||
"key": ("|yL|nook", "l"),
|
||||
"desc": "Look and see a custom message.",
|
||||
"goto": "test_look_node",
|
||||
},
|
||||
{"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",
|
||||
},
|
||||
{"key": "_default", "goto": "test_displayinput_node"},
|
||||
)
|
||||
|
||||
def _generated_goto_func(caller, raw_string, **kwargs):
|
||||
"""
|
||||
This rerouter handles normal direct goto func call matches.
|
||||
|
||||
key : ... -> goto_callable(**kwargs)
|
||||
|
||||
"""
|
||||
goto = kwargs["evmenu_goto"]
|
||||
goto_callables = kwargs["evmenu_goto_callables"]
|
||||
current_nodename = kwargs["evmenu_current_nodename"]
|
||||
return _process_callable(caller, goto, goto_callables, raw_string,
|
||||
current_nodename, kwargs)
|
||||
|
||||
|
||||
def _generated_input_goto_func(caller, raw_string, **kwargs):
|
||||
"""
|
||||
This goto-func acts as a rerouter for >-type line parsing (by acting as the
|
||||
_default option). The patterns discovered in the menu maps to different
|
||||
*actual* goto-funcs. We map to those here.
|
||||
|
||||
>pattern: ... -> goto_callable
|
||||
|
||||
"""
|
||||
gotomap = kwargs["evmenu_gotomap"]
|
||||
goto_callables = kwargs["evmenu_goto_callables"]
|
||||
current_nodename = kwargs["evmenu_current_nodename"]
|
||||
raw_string = raw_string.strip("\n") # strip is necessary to catch empty return
|
||||
|
||||
# start with glob patterns
|
||||
for pattern, goto in gotomap.items():
|
||||
if fnmatch(raw_string.lower(), pattern):
|
||||
return _process_callable(caller, goto, goto_callables, raw_string,
|
||||
current_nodename, kwargs)
|
||||
# no glob pattern match; try regex
|
||||
for pattern, goto in gotomap.items():
|
||||
if pattern and re.match(pattern, raw_string.lower(), flags=re.I + re.M):
|
||||
return _process_callable(caller, goto, goto_callables, raw_string,
|
||||
current_nodename, kwargs)
|
||||
# no match, show error
|
||||
raise EvMenuGotoAbortMessage(_HELP_NO_OPTION_MATCH)
|
||||
|
||||
|
||||
def _generated_node(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Every node in the templated menu will be this node, but with dynamically
|
||||
changing text/options. It must be a global function like this because
|
||||
otherwise we could not make the templated-menu persistent.
|
||||
|
||||
"""
|
||||
text, options = caller.db._evmenu_template_contents[kwargs["_current_nodename"]]
|
||||
return text, options
|
||||
|
||||
|
||||
def test_look_node(caller):
|
||||
text = "This is a custom look location!"
|
||||
options = {
|
||||
"key": ("|yL|nook", "l"),
|
||||
"desc": "Go back to the previous menu.",
|
||||
"goto": "test_start_node",
|
||||
}
|
||||
return text, options
|
||||
def parse_menu_template(caller, menu_template, goto_callables=None):
|
||||
"""
|
||||
Parse menu-template string. The main function of the EvMenu templating system.
|
||||
|
||||
Args:
|
||||
caller (Object or Account): Entity using the menu.
|
||||
menu_template (str): Menu described using the templating format.
|
||||
goto_callables (dict, optional): Mapping between call-names and callables
|
||||
on the form `callable(caller, raw_string, **kwargs)`. These are what is
|
||||
available to use in the `menu_template` string.
|
||||
|
||||
def test_set_node(caller):
|
||||
text = (
|
||||
Returns:
|
||||
dict: A `{"node": nodefunc}` menutree suitable to pass into EvMenu.
|
||||
|
||||
"""
|
||||
def _validate_kwarg(goto, kwarg):
|
||||
"""
|
||||
The attribute 'menuattrtest' was set to
|
||||
|
||||
|w%s|n
|
||||
|
||||
(check it with examine after quitting the menu).
|
||||
|
||||
This node's has only one option, and one of its key aliases is the
|
||||
string "_default", meaning it will catch any input, in this case
|
||||
to return to the main menu. So you can e.g. press <return> to go
|
||||
back now.
|
||||
"""
|
||||
% caller.db.menuattrtest, # optional help text for this node
|
||||
Validate goto-callable kwarg is on correct form.
|
||||
"""
|
||||
This is the help entry for this node. It is created by returning
|
||||
the node text as a tuple - the second string in that tuple will be
|
||||
used as the help text.
|
||||
""",
|
||||
)
|
||||
if not "=" in kwarg:
|
||||
raise RuntimeError(
|
||||
f"EvMenu template error: goto-callable '{goto}' has a "
|
||||
f"non-kwarg argument ({kwarg}). All callables in the "
|
||||
"template must have only keyword-arguments, or no "
|
||||
"args at all.")
|
||||
key, _ = [part.strip() for part in kwarg.split("=", 1)]
|
||||
if key in ("evmenu_goto", "evmenu_gotomap", "_current_nodename",
|
||||
"evmenu_current_nodename", "evmenu_goto_callables"):
|
||||
raise RuntimeError(
|
||||
f"EvMenu template error: goto-callable '{goto}' uses a "
|
||||
f"kwarg ({kwarg}) that is reserved for the EvMenu templating "
|
||||
"system. Rename the kwarg.")
|
||||
|
||||
options = {"key": ("back (default)", "_default"), "goto": "test_start_node"}
|
||||
return text, options
|
||||
|
||||
|
||||
def test_view_node(caller, **kwargs):
|
||||
text = (
|
||||
def _parse_options(nodename, optiontxt, goto_callables):
|
||||
"""
|
||||
Your name is |g%s|n!
|
||||
|
||||
click |lclook|lthere|le to trigger a look command under MXP.
|
||||
This node's option has no explicit key (nor the "_default" key
|
||||
set), and so gets assigned a number automatically. You can infact
|
||||
-always- use numbers (1...N) to refer to listed options also if you
|
||||
don't see a string option key (try it!).
|
||||
"""
|
||||
% caller.key
|
||||
)
|
||||
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):
|
||||
text = (
|
||||
Parse option section into option dict.
|
||||
"""
|
||||
You entered the text:
|
||||
options = []
|
||||
optiontxt = optiontxt[0].strip() if optiontxt else ""
|
||||
optionlist = [optline.strip() for optline in optiontxt.split("\n")]
|
||||
inputparsemap = {}
|
||||
|
||||
"|w%s|n"
|
||||
for inum, optline in enumerate(optionlist):
|
||||
if optline.startswith(_OPTION_COMMENT_START) or _OPTION_SEP_MARKER not in optline:
|
||||
# skip comments or invalid syntax
|
||||
continue
|
||||
key = ""
|
||||
desc = ""
|
||||
pattern = None
|
||||
|
||||
... which could now be handled or stored here in some way if this
|
||||
was not just an example.
|
||||
key, goto = [part.strip() for part in optline.split(_OPTION_SEP_MARKER, 1)]
|
||||
|
||||
This node has an option with a single alias "_default", which
|
||||
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.
|
||||
# desc -> goto
|
||||
if _OPTION_CALL_MARKER in goto:
|
||||
desc, goto = [part.strip() for part in goto.split(_OPTION_CALL_MARKER, 1)]
|
||||
|
||||
# validate callable
|
||||
match = _RE_CALLABLE.match(goto)
|
||||
if match:
|
||||
kwargs = match.group("kwargs")
|
||||
if kwargs:
|
||||
for kwarg in kwargs.split(','):
|
||||
_validate_kwarg(goto, kwarg)
|
||||
|
||||
# parse key [;aliases|pattern]
|
||||
key = [part.strip() for part in key.split(_OPTION_ALIAS_MARKER)]
|
||||
if not key:
|
||||
# fall back to this being the Nth option
|
||||
key = [f"{inum + 1}"]
|
||||
main_key = key[0]
|
||||
|
||||
if main_key.startswith(_OPTION_INPUT_MARKER):
|
||||
# if we have a pattern, build the arguments for _default later
|
||||
pattern = main_key[len(_OPTION_INPUT_MARKER):].strip()
|
||||
inputparsemap[pattern] = goto
|
||||
else:
|
||||
# a regular goto string/callable target
|
||||
option = {
|
||||
"key": key,
|
||||
"goto": (
|
||||
_generated_goto_func,
|
||||
{
|
||||
"evmenu_goto": goto,
|
||||
"evmenu_current_nodename": nodename,
|
||||
"evmenu_goto_callables": goto_callables,
|
||||
},
|
||||
),
|
||||
}
|
||||
if desc:
|
||||
option["desc"] = desc
|
||||
options.append(option)
|
||||
|
||||
if inputparsemap:
|
||||
# if this exists we must create a _default entry too
|
||||
options.append(
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": (
|
||||
_generated_input_goto_func,
|
||||
{
|
||||
"evmenu_gotomap": inputparsemap,
|
||||
"evmenu_current_nodename": nodename,
|
||||
"evmenu_goto_callables": goto_callables,
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
def _parse(caller, menu_template, goto_callables):
|
||||
"""
|
||||
Parse the menu string format into a node tree.
|
||||
"""
|
||||
nodetree = {}
|
||||
splits = _RE_NODE.split(menu_template)
|
||||
splits = splits[1:] if splits else []
|
||||
|
||||
# from evennia import set_trace;set_trace(term_size=(140,120))
|
||||
content_map = {}
|
||||
for node_ind in range(0, len(splits), 2):
|
||||
nodename, nodetxt = splits[node_ind], splits[node_ind + 1]
|
||||
text, *optiontxt = _RE_OPTIONS_SEP.split(nodetxt, maxsplit=2)
|
||||
options = _parse_options(nodename, optiontxt, goto_callables)
|
||||
content_map[nodename] = (text, options)
|
||||
nodetree[nodename] = _generated_node
|
||||
caller.db._evmenu_template_contents = content_map
|
||||
|
||||
return nodetree
|
||||
|
||||
return _parse(caller, menu_template, goto_callables)
|
||||
|
||||
|
||||
def template2menu(
|
||||
caller,
|
||||
menu_template,
|
||||
goto_callables=None,
|
||||
startnode="start",
|
||||
persistent=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
% raw_string.rstrip()
|
||||
)
|
||||
options = {"key": "_default", "goto": "test_start_node"}
|
||||
return text, options
|
||||
Helper function to generate and start an EvMenu based on a menu template
|
||||
string. This will internall call `parse_menu_template` and run a default
|
||||
EvMenu with its results.
|
||||
|
||||
Args:
|
||||
caller (Object or Account): The entity using the menu.
|
||||
menu_template (str): The menu-template string describing the content
|
||||
and structure of the menu. It can also be the python-path to, or a module
|
||||
containing a `MENU_TEMPLATE` global variable with the template.
|
||||
goto_callables (dict, optional): Mapping of callable-names to
|
||||
module-global objects to reference by name in the menu-template.
|
||||
Must be on the form `callable(caller, raw_string, **kwargs)`.
|
||||
startnode (str, optional): The name of the startnode, if not 'start'.
|
||||
persistent (bool, optional): If the generated menu should be persistent.
|
||||
**kwargs: All kwargs will be passed into EvMenu.
|
||||
|
||||
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
|
||||
will exit here, followed by a call of the "look" command.
|
||||
"""
|
||||
return text, None
|
||||
|
||||
|
||||
class CmdTestMenu(Command):
|
||||
"""
|
||||
Test menu
|
||||
|
||||
Usage:
|
||||
testmenu <menumodule>
|
||||
|
||||
Starts a demo menu from a menu node definition module.
|
||||
Returns:
|
||||
EvMenu: The generated EvMenu.
|
||||
|
||||
"""
|
||||
|
||||
key = "testmenu"
|
||||
|
||||
def func(self):
|
||||
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: testmenu menumodule")
|
||||
return
|
||||
# start menu
|
||||
EvMenu(
|
||||
self.caller,
|
||||
self.args.strip(),
|
||||
startnode="test_start_node",
|
||||
persistent=True,
|
||||
cmdset_mergetype="Replace",
|
||||
testval="val",
|
||||
testval2="val2",
|
||||
)
|
||||
goto_callables = goto_callables or {}
|
||||
menu_tree = parse_menu_template(caller, menu_template, goto_callables)
|
||||
return EvMenu(
|
||||
caller,
|
||||
menu_tree,
|
||||
persistent=persistent,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
|
|||
221
evennia/utils/tests/data/evmenu_example.py
Normal file
221
evennia/utils/tests/data/evmenu_example.py
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# -------------------------------------------------------------
|
||||
#
|
||||
# test menu strucure and testing command
|
||||
#
|
||||
# -------------------------------------------------------------
|
||||
|
||||
import random
|
||||
|
||||
|
||||
def _generate_goto(caller, **kwargs):
|
||||
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
|
||||
|
||||
|
||||
def test_start_node(caller):
|
||||
menu = caller.ndb._menutree
|
||||
text = """
|
||||
This is an example menu.
|
||||
|
||||
If you enter anything except the valid options, your input will be
|
||||
recorded and you will be brought to a menu entry showing your
|
||||
input.
|
||||
|
||||
Select options or use 'quit' to exit the menu.
|
||||
|
||||
The menu was initialized with two variables: %s and %s.
|
||||
""" % (
|
||||
menu.testval,
|
||||
menu.testval2,
|
||||
)
|
||||
|
||||
options = (
|
||||
{
|
||||
"key": ("|yS|net", "s"),
|
||||
"desc": "Set an attribute on yourself.",
|
||||
"exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"),
|
||||
"goto": "test_set_node",
|
||||
},
|
||||
{
|
||||
"key": ("|yL|nook", "l"),
|
||||
"desc": "Look and see a custom message.",
|
||||
"goto": "test_look_node",
|
||||
},
|
||||
{"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",
|
||||
},
|
||||
{"key": "_default", "goto": "test_displayinput_node"},
|
||||
)
|
||||
return text, options
|
||||
|
||||
|
||||
def test_look_node(caller):
|
||||
text = "This is a custom look location!"
|
||||
options = {
|
||||
"key": ("|yL|nook", "l"),
|
||||
"desc": "Go back to the previous menu.",
|
||||
"goto": "test_start_node",
|
||||
}
|
||||
return text, options
|
||||
|
||||
|
||||
def test_set_node(caller):
|
||||
text = (
|
||||
"""
|
||||
The attribute 'menuattrtest' was set to
|
||||
|
||||
|w%s|n
|
||||
|
||||
(check it with examine after quitting the menu).
|
||||
|
||||
This node's has only one option, and one of its key aliases is the
|
||||
string "_default", meaning it will catch any input, in this case
|
||||
to return to the main menu. So you can e.g. press <return> to go
|
||||
back now.
|
||||
"""
|
||||
% caller.db.menuattrtest, # optional help text for this node
|
||||
"""
|
||||
This is the help entry for this node. It is created by returning
|
||||
the node text as a tuple - the second string in that tuple will be
|
||||
used as the help text.
|
||||
""",
|
||||
)
|
||||
|
||||
options = {"key": ("back (default)", "_default"), "goto": "test_start_node"}
|
||||
return text, options
|
||||
|
||||
|
||||
def test_view_node(caller, **kwargs):
|
||||
text = (
|
||||
"""
|
||||
Your name is |g%s|n!
|
||||
|
||||
click |lclook|lthere|le to trigger a look command under MXP.
|
||||
This node's option has no explicit key (nor the "_default" key
|
||||
set), and so gets assigned a number automatically. You can infact
|
||||
-always- use numbers (1...N) to refer to listed options also if you
|
||||
don't see a string option key (try it!).
|
||||
"""
|
||||
% caller.key
|
||||
)
|
||||
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):
|
||||
text = (
|
||||
"""
|
||||
You entered the text:
|
||||
|
||||
"|w%s|n"
|
||||
|
||||
... which could now be handled or stored here in some way if this
|
||||
was not just an example.
|
||||
|
||||
This node has an option with a single alias "_default", which
|
||||
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.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
|
||||
will exit here, followed by a call of the "look" command.
|
||||
"""
|
||||
return text, None
|
||||
|
||||
|
||||
# class CmdTestMenu(Command):
|
||||
# """
|
||||
# Test menu
|
||||
#
|
||||
# Usage:
|
||||
# testmenu <menumodule>
|
||||
#
|
||||
# Starts a demo menu from a menu node definition module.
|
||||
#
|
||||
# """
|
||||
#
|
||||
# key = "testmenu"
|
||||
#
|
||||
# def func(self):
|
||||
#
|
||||
# if not self.args:
|
||||
# self.caller.msg("Usage: testmenu menumodule")
|
||||
# return
|
||||
# # start menu
|
||||
# EvMenu(
|
||||
# self.caller,
|
||||
# self.args.strip(),
|
||||
# startnode="test_start_node",
|
||||
# persistent=True,
|
||||
# cmdset_mergetype="Replace",
|
||||
# testval="val",
|
||||
# testval2="val2",
|
||||
# )
|
||||
#
|
||||
|
|
@ -18,7 +18,9 @@ To help debug the menu, turn on `debug_output`, which will print the traversal p
|
|||
"""
|
||||
|
||||
import copy
|
||||
from anything import Anything
|
||||
from django.test import TestCase
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.utils import evmenu
|
||||
from evennia.utils import ansi
|
||||
from mock import MagicMock
|
||||
|
|
@ -229,7 +231,7 @@ class TestEvMenu(TestCase):
|
|||
|
||||
class TestEvMenuExample(TestEvMenu):
|
||||
|
||||
menutree = "evennia.utils.evmenu"
|
||||
menutree = "evennia.utils.tests.data.evmenu_example"
|
||||
startnode = "test_start_node"
|
||||
kwargs = {"testval": "val", "testval2": "val2"}
|
||||
debug_output = False
|
||||
|
|
@ -262,3 +264,79 @@ class TestEvMenuExample(TestEvMenu):
|
|||
def test_kwargsave(self):
|
||||
self.assertTrue(hasattr(self.menu, "testval"))
|
||||
self.assertTrue(hasattr(self.menu, "testval2"))
|
||||
|
||||
|
||||
def _callnode1(caller, raw_string, **kwargs):
|
||||
return "node1"
|
||||
|
||||
|
||||
def _callnode2(caller, raw_string, **kwargs):
|
||||
return "node2"
|
||||
|
||||
|
||||
class TestMenuTemplateParse(EvenniaTest):
|
||||
"""Test menu templating helpers"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.menu_template = """
|
||||
## node start
|
||||
|
||||
Neque ea alias perferendis molestiae eligendi. Debitis exercitationem
|
||||
exercitationem quas blanditiis quisquam officia ut. Fugit aut fugit enim quia
|
||||
non. Earum et excepturi animi ex esse accusantium et. Id adipisci eos enim
|
||||
ratione.
|
||||
|
||||
## options
|
||||
|
||||
1: first option -> node1
|
||||
2: second option -> node2
|
||||
next: node1
|
||||
|
||||
## node node1
|
||||
|
||||
Node 1
|
||||
|
||||
## options
|
||||
|
||||
fwd: node2
|
||||
call1: callnode1()
|
||||
call2: callnode2(foo=bar, bar=22, goo="another test")
|
||||
>: start
|
||||
|
||||
## node node2
|
||||
|
||||
Text of node 2
|
||||
|
||||
## options
|
||||
|
||||
> foo*: node1
|
||||
> [0-9]+?: node2
|
||||
> back: start
|
||||
|
||||
"""
|
||||
self.goto_callables = {"callnode1": _callnode1, "callnode2": _callnode2}
|
||||
|
||||
def test_parse_menu_template(self):
|
||||
"""EvMenu template testing"""
|
||||
|
||||
menutree = evmenu.parse_menu_template(self.char1, self.menu_template,
|
||||
self.goto_callables)
|
||||
self.assertEqual(menutree, {"start": Anything, "node1": Anything, "node2": Anything})
|
||||
|
||||
def test_template2menu(self):
|
||||
evmenu.template2menu(self.char1, self.menu_template, self.goto_callables)
|
||||
|
||||
def test_parse_menu_fail(self):
|
||||
template = """
|
||||
## NODE
|
||||
|
||||
Text
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next: callnode2(invalid)
|
||||
"""
|
||||
with self.assertRaises(RuntimeError):
|
||||
evmenu.parse_menu_template(self.char1, template, self.goto_callables)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue