mirror of
https://github.com/evennia/evennia.git
synced 2026-03-27 10:16:32 +01:00
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
|
||||
from ast import literal_eval
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ from fnmatch import fnmatch
|
|||
from django.utils.translation import gettext as _
|
||||
|
||||
_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"(?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}
|
||||
|
||||
|
||||
def _generated_node(caller, raw_string, generated_nodename="", **kwargs):
|
||||
text, options = caller.db._generated_menu_contents[generated_nodename]
|
||||
def _generated_node(caller, raw_string, **kwargs):
|
||||
text, options = caller.db._generated_menu_contents[kwargs["_current_nodename"]]
|
||||
return text, options
|
||||
|
||||
|
||||
|
|
@ -249,7 +250,6 @@ def template2menu(
|
|||
menu_template,
|
||||
goto_callables=None,
|
||||
startnode="start",
|
||||
startnode_input=None,
|
||||
persistent=False,
|
||||
**kwargs,
|
||||
):
|
||||
|
|
@ -266,30 +266,17 @@ def template2menu(
|
|||
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'.
|
||||
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.
|
||||
**kwargs: Other kwargs will be passed to EvMenu.
|
||||
**kwargs: All kwargs will be passed into EvMenu.
|
||||
|
||||
|
||||
"""
|
||||
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)
|
||||
EvMenu(
|
||||
caller,
|
||||
menu_tree,
|
||||
startnode_input=(startnode_raw, startnode_kwargs),
|
||||
persistent=True,
|
||||
persistent=persistent,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
|
@ -309,6 +296,17 @@ def bar(caller, raw_string, **kwargs):
|
|||
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):
|
||||
|
||||
MENU_TEMPLATE = """
|
||||
|
|
@ -344,6 +342,7 @@ def test_generator(caller):
|
|||
back: start
|
||||
to node 2: node2
|
||||
run foo (rerun node): foo()
|
||||
customnode: Go to custom node -> customnode
|
||||
>: return to go back -> start
|
||||
|
||||
# node node2
|
||||
|
|
@ -371,7 +370,13 @@ def test_generator(caller):
|
|||
"""
|
||||
|
||||
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__":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
||||
from ast import literal_eval
|
||||
from fnmatch import fnmatch
|
||||
|
||||
from inspect import isfunction, getargspec
|
||||
from django.conf import settings
|
||||
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.commands import cmdhandler
|
||||
|
||||
# i18n
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
# read from protocol NAWS later?
|
||||
_MAX_TEXT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
|
|
@ -186,8 +292,6 @@ _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. "
|
||||
|
|
@ -668,6 +772,7 @@ class EvMenu:
|
|||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||
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]
|
||||
|
|
@ -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):
|
||||
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.
|
||||
|
||||
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.
|
||||
""" % (
|
||||
menu.testval,
|
||||
menu.testval2,
|
||||
)
|
||||
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}
|
||||
caller.msg(_HELP_NO_OPTION_MATCH)
|
||||
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_input_goto_func(caller, raw_string, **kwargs):
|
||||
gotomap = kwargs["gotomap"]
|
||||
goto_callables = kwargs["goto_callables"]
|
||||
current_nodename = kwargs["current_nodename"]
|
||||
|
||||
# start with glob patterns
|
||||
for pattern, goto in gotomap.items():
|
||||
if fnmatch(raw_string.lower(), pattern):
|
||||
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)]
|
||||
try:
|
||||
key = literal_eval(key)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
value = literal_eval(value)
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|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
|
||||
Parse option section into option dict.
|
||||
"""
|
||||
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 = []
|
||||
optiontxt = optiontxt[0].strip() if optiontxt else ""
|
||||
optionlist = [optline.strip() for optline in optiontxt.split("\n")]
|
||||
inputparsemap = {}
|
||||
|
||||
options = {"key": ("back (default)", "_default"), "goto": "test_start_node"}
|
||||
return text, options
|
||||
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
|
||||
|
||||
key, goto = [part.strip() for part in optline.split(_OPTION_SEP_MARKER, 1)]
|
||||
|
||||
def test_view_node(caller, **kwargs):
|
||||
text = (
|
||||
# desc -> goto
|
||||
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!
|
||||
|
||||
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 the menu string format into a node tree.
|
||||
"""
|
||||
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
|
||||
was not just an example.
|
||||
return nodetree
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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,66 @@ 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue