Start adding unit tests for menu templating
This commit is contained in:
parent
7e58fee171
commit
300429a03f
4 changed files with 619 additions and 210 deletions
|
|
@ -42,6 +42,7 @@ not move on until that command has been tried).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
import re
|
import re
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
|
|
||||||
|
|
@ -52,7 +53,7 @@ from fnmatch import fnmatch
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
_RE_NODE = re.compile(r"#\s*?NODE\s+?(?P<nodename>\S+?)$", re.I + re.M)
|
_RE_NODE = re.compile(r"#\s*?NODE\s+?(?P<nodename>\S+?)$", re.I + re.M)
|
||||||
_RE_OPTIONS_SEP = re.compile(r"##\s*?OPTIONS\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"\S+?\(\)", re.I + re.M)
|
||||||
_RE_CALLABLE = re.compile(
|
_RE_CALLABLE = re.compile(
|
||||||
r"(?P<funcname>\S+?)(?:\((?P<kwargs>[\S\s]+?=[\S\s]+?)\)|\(\))", re.I + re.M
|
r"(?P<funcname>\S+?)(?:\((?P<kwargs>[\S\s]+?=[\S\s]+?)\)|\(\))", re.I + re.M
|
||||||
|
|
@ -133,8 +134,8 @@ def _generated_input_goto_func(caller, raw_string, **kwargs):
|
||||||
return None, {"generated_nodename": current_nodename}
|
return None, {"generated_nodename": current_nodename}
|
||||||
|
|
||||||
|
|
||||||
def _generated_node(caller, raw_string, generated_nodename="", **kwargs):
|
def _generated_node(caller, raw_string, **kwargs):
|
||||||
text, options = caller.db._generated_menu_contents[generated_nodename]
|
text, options = caller.db._generated_menu_contents[kwargs["_current_nodename"]]
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -249,7 +250,6 @@ def template2menu(
|
||||||
menu_template,
|
menu_template,
|
||||||
goto_callables=None,
|
goto_callables=None,
|
||||||
startnode="start",
|
startnode="start",
|
||||||
startnode_input=None,
|
|
||||||
persistent=False,
|
persistent=False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
|
@ -266,30 +266,17 @@ def template2menu(
|
||||||
module-global objects to reference by name in the menu-template.
|
module-global objects to reference by name in the menu-template.
|
||||||
Must be on the form `callable(caller, raw_string, **kwargs)`.
|
Must be on the form `callable(caller, raw_string, **kwargs)`.
|
||||||
startnode (str, optional): The name of the startnode, if not 'start'.
|
startnode (str, optional): The name of the startnode, if not 'start'.
|
||||||
startnode_input (str or tuple, optional): If a string, the `raw_string`
|
|
||||||
arg to pass into the starting node. Otherwise should be on form
|
|
||||||
`(raw_string, {kwargs})`, where `raw_string` and `**kwargs` will be
|
|
||||||
passed into the start node.
|
|
||||||
persistent (bool, optional): If the generated menu should be persistent.
|
persistent (bool, optional): If the generated menu should be persistent.
|
||||||
**kwargs: Other kwargs will be passed to EvMenu.
|
**kwargs: All kwargs will be passed into EvMenu.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
goto_callables = goto_callables or {}
|
goto_callables = goto_callables or {}
|
||||||
startnode_raw = ""
|
|
||||||
startnode_kwargs = {"generated_nodename": startnode}
|
|
||||||
if isinstance(startnode_input, str):
|
|
||||||
startnode_raw = startnode_input
|
|
||||||
elif isinstance(startnode_input, (tuple, list)):
|
|
||||||
startnode_raw = startnode_input[0]
|
|
||||||
startnode_kwargs.update(startnode_input[1])
|
|
||||||
|
|
||||||
menu_tree = parse_menu_template(caller, menu_template, goto_callables)
|
menu_tree = parse_menu_template(caller, menu_template, goto_callables)
|
||||||
EvMenu(
|
EvMenu(
|
||||||
caller,
|
caller,
|
||||||
menu_tree,
|
menu_tree,
|
||||||
startnode_input=(startnode_raw, startnode_kwargs),
|
persistent=persistent,
|
||||||
persistent=True,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -309,6 +296,17 @@ def bar(caller, raw_string, **kwargs):
|
||||||
return "bar"
|
return "bar"
|
||||||
|
|
||||||
|
|
||||||
|
def customcall(caller, raw_string, **kwargs):
|
||||||
|
return "start"
|
||||||
|
|
||||||
|
def customnode(caller, raw_string, **kwargs):
|
||||||
|
text = "This is a custom node!"
|
||||||
|
options = {
|
||||||
|
"desc": "Go back",
|
||||||
|
"goto": customcall
|
||||||
|
}
|
||||||
|
return text, options
|
||||||
|
|
||||||
def test_generator(caller):
|
def test_generator(caller):
|
||||||
|
|
||||||
MENU_TEMPLATE = """
|
MENU_TEMPLATE = """
|
||||||
|
|
@ -344,6 +342,7 @@ def test_generator(caller):
|
||||||
back: start
|
back: start
|
||||||
to node 2: node2
|
to node 2: node2
|
||||||
run foo (rerun node): foo()
|
run foo (rerun node): foo()
|
||||||
|
customnode: Go to custom node -> customnode
|
||||||
>: return to go back -> start
|
>: return to go back -> start
|
||||||
|
|
||||||
# node node2
|
# node node2
|
||||||
|
|
@ -371,7 +370,13 @@ def test_generator(caller):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
callables = {"gotonode3": gotonode3, "foo": foo, "bar": bar}
|
callables = {"gotonode3": gotonode3, "foo": foo, "bar": bar}
|
||||||
template2menu(caller, MENU_TEMPLATE, callables)
|
dct = parse_menu_template(caller, MENU_TEMPLATE, callables)
|
||||||
|
dct["customnode"] = customnode
|
||||||
|
|
||||||
|
EvMenu(caller, dct)
|
||||||
|
|
||||||
|
|
||||||
|
# template2menu(caller, MENU_TEMPLATE, callables)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -162,11 +162,114 @@ For a menu demo, import CmdTestMenu from this module and add it to
|
||||||
your default cmdset. Run it with this module, like `testmenu
|
your default cmdset. Run it with this module, like `testmenu
|
||||||
evennia.utils.evmenu`.
|
evennia.utils.evmenu`.
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
import inspect
|
||||||
|
|
||||||
|
from ast import literal_eval
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
|
||||||
from inspect import isfunction, getargspec
|
from inspect import isfunction, getargspec
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia import Command, CmdSet
|
from evennia import Command, CmdSet
|
||||||
|
|
@ -176,6 +279,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.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
|
|
||||||
|
# i18n
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
# read from protocol NAWS later?
|
# read from protocol NAWS later?
|
||||||
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||||
|
|
||||||
|
|
@ -186,8 +292,6 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
||||||
|
|
||||||
# Return messages
|
# Return messages
|
||||||
|
|
||||||
# i18n
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
_ERR_NOT_IMPLEMENTED = _(
|
_ERR_NOT_IMPLEMENTED = _(
|
||||||
"Menu node '{nodename}' is either not implemented or caused an error. "
|
"Menu node '{nodename}' is either not implemented or caused an error. "
|
||||||
|
|
@ -668,6 +772,7 @@ class EvMenu:
|
||||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||||
raise EvMenuError
|
raise EvMenuError
|
||||||
try:
|
try:
|
||||||
|
kwargs["_current_nodename"] = nodename
|
||||||
ret = self._safe_call(node, raw_string, **kwargs)
|
ret = self._safe_call(node, raw_string, **kwargs)
|
||||||
if isinstance(ret, (tuple, list)) and len(ret) > 1:
|
if isinstance(ret, (tuple, list)) and len(ret) > 1:
|
||||||
nodetext, options = ret[:2]
|
nodetext, options = ret[:2]
|
||||||
|
|
@ -1475,219 +1580,232 @@ 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+?)$", 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]+?=[\S\s]+?)\)|\(\))", re.I + re.M
|
||||||
|
)
|
||||||
|
|
||||||
def _generate_goto(caller, **kwargs):
|
_HELP_NO_OPTION_MATCH = _("Choose an option or try 'help'.")
|
||||||
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
|
|
||||||
|
_OPTION_INPUT_MARKER = ">"
|
||||||
|
_OPTION_ALIAS_MARKER = ";"
|
||||||
|
_OPTION_SEP_MARKER = ":"
|
||||||
|
_OPTION_CALL_MARKER = "->"
|
||||||
|
_OPTION_COMMENT_START = "#"
|
||||||
|
|
||||||
|
|
||||||
def test_start_node(caller):
|
# Input/option/goto handler functions that allows for dynamically generated
|
||||||
menu = caller.ndb._menutree
|
# nodes read from the menu template.
|
||||||
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.
|
def _generated_goto_func(caller, raw_string, **kwargs):
|
||||||
|
goto = kwargs["goto"]
|
||||||
|
goto_callables = kwargs["goto_callables"]
|
||||||
|
current_nodename = kwargs["current_nodename"]
|
||||||
|
|
||||||
The menu was initialized with two variables: %s and %s.
|
if _RE_CALLABLE.match(goto):
|
||||||
""" % (
|
gotofunc = goto.strip()[:-2]
|
||||||
menu.testval,
|
if gotofunc in goto_callables:
|
||||||
menu.testval2,
|
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
|
||||||
)
|
if goto is None:
|
||||||
|
return goto, {"generated_nodename": current_nodename}
|
||||||
|
caller.msg(_HELP_NO_OPTION_MATCH)
|
||||||
|
return goto, {"generated_nodename": goto}
|
||||||
|
|
||||||
options = (
|
|
||||||
{
|
def _generated_input_goto_func(caller, raw_string, **kwargs):
|
||||||
"key": ("|yS|net", "s"),
|
gotomap = kwargs["gotomap"]
|
||||||
"desc": "Set an attribute on yourself.",
|
goto_callables = kwargs["goto_callables"]
|
||||||
"exec": lambda caller: caller.attributes.add("menuattrtest", "Test value"),
|
current_nodename = kwargs["current_nodename"]
|
||||||
"goto": "test_set_node",
|
|
||||||
},
|
# start with glob patterns
|
||||||
{
|
for pattern, goto in gotomap.items():
|
||||||
"key": ("|yL|nook", "l"),
|
if fnmatch(raw_string.lower(), pattern):
|
||||||
"desc": "Look and see a custom message.",
|
match = _RE_CALLABLE.match(goto)
|
||||||
"goto": "test_look_node",
|
if match:
|
||||||
},
|
gotofunc = match.group("funcname")
|
||||||
{"key": ("|yV|niew", "v"), "desc": "View your own name", "goto": "test_view_node"},
|
gotokwargs = match.group("kwargs") or ""
|
||||||
{
|
if gotofunc in goto_callables:
|
||||||
"key": ("|yD|nynamic", "d"),
|
for kwarg in gotokwargs.split(","):
|
||||||
"desc": "Dynamic node",
|
if kwarg and "=" in kwarg:
|
||||||
"goto": (_generate_goto, {"name": "test_dynamic_node"}),
|
key, value = [part.strip() for part in kwarg.split("=", 1)]
|
||||||
},
|
try:
|
||||||
{
|
key = literal_eval(key)
|
||||||
"key": ("|yQ|nuit", "quit", "q", "Q"),
|
except ValueError:
|
||||||
"desc": "Quit this menu example.",
|
pass
|
||||||
"goto": "test_end_node",
|
try:
|
||||||
},
|
value = literal_eval(value)
|
||||||
{"key": "_default", "goto": "test_displayinput_node"},
|
except ValueError:
|
||||||
)
|
pass
|
||||||
|
kwargs[key] = value
|
||||||
|
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
|
||||||
|
if goto is None:
|
||||||
|
return goto, {"generated_nodename": current_nodename}
|
||||||
|
return goto, {"generated_nodename": goto}
|
||||||
|
# no glob pattern match; try regex
|
||||||
|
for pattern, goto in gotomap.items():
|
||||||
|
if re.match(pattern, raw_string.lower(), flags=re.I + re.M):
|
||||||
|
if _RE_CALLABLE.match(goto):
|
||||||
|
gotofunc = goto.strip()[:-2]
|
||||||
|
if gotofunc in goto_callables:
|
||||||
|
goto = goto_callables[gotofunc](caller, raw_string, **kwargs)
|
||||||
|
if goto is None:
|
||||||
|
return goto, {"generated_nodename": current_nodename}
|
||||||
|
return goto, {"generated_nodename": goto}
|
||||||
|
# no match, rerun current node
|
||||||
|
caller.msg(_HELP_NO_OPTION_MATCH)
|
||||||
|
return None, {"generated_nodename": current_nodename}
|
||||||
|
|
||||||
|
|
||||||
|
def _generated_node(caller, raw_string, **kwargs):
|
||||||
|
text, options = caller.db._generated_menu_contents[kwargs["_current_nodename"]]
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
def test_look_node(caller):
|
def parse_menu_template(caller, menu_template, goto_callables=None):
|
||||||
text = "This is a custom look location!"
|
"""
|
||||||
options = {
|
Parse menu-template string
|
||||||
"key": ("|yL|nook", "l"),
|
|
||||||
"desc": "Go back to the previous menu.",
|
|
||||||
"goto": "test_start_node",
|
|
||||||
}
|
|
||||||
return text, options
|
|
||||||
|
|
||||||
|
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 = (
|
|
||||||
|
def _parse_options(nodename, optiontxt, goto_callables):
|
||||||
"""
|
"""
|
||||||
The attribute 'menuattrtest' was set to
|
Parse option section into option dict.
|
||||||
|
|
||||||
|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
|
options = []
|
||||||
the node text as a tuple - the second string in that tuple will be
|
optiontxt = optiontxt[0].strip() if optiontxt else ""
|
||||||
used as the help text.
|
optionlist = [optline.strip() for optline in optiontxt.split("\n")]
|
||||||
""",
|
inputparsemap = {}
|
||||||
)
|
|
||||||
|
|
||||||
options = {"key": ("back (default)", "_default"), "goto": "test_start_node"}
|
for inum, optline in enumerate(optionlist):
|
||||||
return text, options
|
if optline.startswith(_OPTION_COMMENT_START) or _OPTION_SEP_MARKER not in optline:
|
||||||
|
# skip comments or invalid syntax
|
||||||
|
continue
|
||||||
|
key = ""
|
||||||
|
desc = ""
|
||||||
|
pattern = None
|
||||||
|
|
||||||
|
key, goto = [part.strip() for part in optline.split(_OPTION_SEP_MARKER, 1)]
|
||||||
|
|
||||||
def test_view_node(caller, **kwargs):
|
# desc -> goto
|
||||||
text = (
|
if _OPTION_CALL_MARKER in goto:
|
||||||
|
desc, goto = [part.strip() for part in goto.split(_OPTION_CALL_MARKER, 1)]
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
{
|
||||||
|
"goto": goto,
|
||||||
|
"current_nodename": nodename,
|
||||||
|
"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,
|
||||||
|
{
|
||||||
|
"gotomap": inputparsemap,
|
||||||
|
"current_nodename": nodename,
|
||||||
|
"goto_callables": goto_callables,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
def _parse(caller, menu_template, goto_callables):
|
||||||
"""
|
"""
|
||||||
Your name is |g%s|n!
|
Parse the menu string format into a node tree.
|
||||||
|
|
||||||
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:
|
nodetree = {}
|
||||||
|
splits = _RE_NODE.split(menu_template)
|
||||||
|
splits = splits[1:] if splits else []
|
||||||
|
|
||||||
"|w%s|n"
|
# 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._generated_menu_contents = content_map
|
||||||
|
|
||||||
... which could now be handled or stored here in some way if this
|
return nodetree
|
||||||
was not just an example.
|
|
||||||
|
|
||||||
This node has an option with a single alias "_default", which
|
return _parse(caller, menu_template, goto_callables)
|
||||||
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.
|
def template2menu(
|
||||||
|
caller,
|
||||||
|
menu_template,
|
||||||
|
goto_callables=None,
|
||||||
|
startnode="start",
|
||||||
|
persistent=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
% raw_string.rstrip()
|
Helper function to generate and start an EvMenu based on a menu template
|
||||||
)
|
string.
|
||||||
options = {"key": "_default", "goto": "test_start_node"}
|
|
||||||
return text, options
|
|
||||||
|
|
||||||
|
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):
|
Returns:
|
||||||
mode = kwargs.get("mode", "exec")
|
EvMenu: The generated EvMenu.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
goto_callables = goto_callables or {}
|
||||||
key = "testmenu"
|
menu_tree = parse_menu_template(caller, menu_template, goto_callables)
|
||||||
|
return EvMenu(
|
||||||
def func(self):
|
caller,
|
||||||
|
menu_tree,
|
||||||
if not self.args:
|
persistent=persistent,
|
||||||
self.caller.msg("Usage: testmenu menumodule")
|
**kwargs,
|
||||||
return
|
)
|
||||||
# start menu
|
|
||||||
EvMenu(
|
|
||||||
self.caller,
|
|
||||||
self.args.strip(),
|
|
||||||
startnode="test_start_node",
|
|
||||||
persistent=True,
|
|
||||||
cmdset_mergetype="Replace",
|
|
||||||
testval="val",
|
|
||||||
testval2="val2",
|
|
||||||
)
|
|
||||||
|
|
|
||||||
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
|
import copy
|
||||||
|
from anything import Anything
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
from evennia.utils import evmenu
|
from evennia.utils import evmenu
|
||||||
from evennia.utils import ansi
|
from evennia.utils import ansi
|
||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
|
|
@ -229,7 +231,7 @@ class TestEvMenu(TestCase):
|
||||||
|
|
||||||
class TestEvMenuExample(TestEvMenu):
|
class TestEvMenuExample(TestEvMenu):
|
||||||
|
|
||||||
menutree = "evennia.utils.evmenu"
|
menutree = "evennia.utils.tests.data.evmenu_example"
|
||||||
startnode = "test_start_node"
|
startnode = "test_start_node"
|
||||||
kwargs = {"testval": "val", "testval2": "val2"}
|
kwargs = {"testval": "val", "testval2": "val2"}
|
||||||
debug_output = False
|
debug_output = False
|
||||||
|
|
@ -262,3 +264,66 @@ class TestEvMenuExample(TestEvMenu):
|
||||||
def test_kwargsave(self):
|
def test_kwargsave(self):
|
||||||
self.assertTrue(hasattr(self.menu, "testval"))
|
self.assertTrue(hasattr(self.menu, "testval"))
|
||||||
self.assertTrue(hasattr(self.menu, "testval2"))
|
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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue