diff --git a/evennia/contrib/gametutorial.py b/evennia/contrib/gametutorial.py index ede1e10523..e2221aaad3 100644 --- a/evennia/contrib/gametutorial.py +++ b/evennia/contrib/gametutorial.py @@ -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\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\S+?)(?:\((?P[\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__": diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index b312637ecf..9a9610acac 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -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! + 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 ` 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\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\S+?)(?:\((?P[\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 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 - - 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, + ) diff --git a/evennia/utils/tests/data/evmenu_example.py b/evennia/utils/tests/data/evmenu_example.py new file mode 100644 index 0000000000..4424e82c5b --- /dev/null +++ b/evennia/utils/tests/data/evmenu_example.py @@ -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 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 +# +# 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", +# ) +# diff --git a/evennia/utils/tests/test_evmenu.py b/evennia/utils/tests/test_evmenu.py index 128e857e9f..718ccacbab 100644 --- a/evennia/utils/tests/test_evmenu.py +++ b/evennia/utils/tests/test_evmenu.py @@ -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)