Add working **kwargs support to nodes/callbacks in evmenu
This commit is contained in:
parent
2475d14691
commit
7b295fa98b
1 changed files with 109 additions and 63 deletions
|
|
@ -83,15 +83,11 @@ menu is immediately exited and the default "look" command is called.
|
||||||
Such a callable should return either a str or a (str, dict), where the
|
Such a callable should return either a str or a (str, dict), where the
|
||||||
string is the name of the next node to go to and the dict is the new,
|
string is the name of the next node to go to and the dict is the new,
|
||||||
(possibly modified) kwarg to pass into the next node.
|
(possibly modified) kwarg to pass into the next node.
|
||||||
- `exec` (str, callable or tuple, optional): This specified either the name of
|
- `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
|
||||||
a menu node to execute as a callback or a regular callable. If a tuple, the
|
and runs before it. If given a node name, the node will be executed but will not
|
||||||
first element is either the menu-node name or the callback, while the second
|
be considered the next node. If node/callback returns str or (str, dict), these will
|
||||||
is a dict for the **kwargs to pass into the node/callback. This callback/node
|
replace the `goto` step (`goto` callbacks will not fire), with the string being the
|
||||||
will execute *before* going any `goto` function and before going to the next
|
next node name and the optional dict acting as the kwargs-input for the next node.
|
||||||
node. The callback should look like a node, so `callback(caller[,raw_input][,**kwargs])`.
|
|
||||||
If this callable returns a single string (only) then that will replace the
|
|
||||||
current goto location (if a `goto` callback is set, it will never fire). Returning
|
|
||||||
anything else has no effect.
|
|
||||||
|
|
||||||
If key is not given, the option will automatically be identified by
|
If key is not given, the option will automatically be identified by
|
||||||
its number 1..N.
|
its number 1..N.
|
||||||
|
|
@ -107,7 +103,7 @@ Example:
|
||||||
"This is help text for this node")
|
"This is help text for this node")
|
||||||
options = ({"key": "testing",
|
options = ({"key": "testing",
|
||||||
"desc": "Select this to go to node 2",
|
"desc": "Select this to go to node 2",
|
||||||
"goto": "node2",
|
"goto": ("node2", {"foo": "bar"}),
|
||||||
"exec": "callback1"},
|
"exec": "callback1"},
|
||||||
{"desc": "Go to node 3.",
|
{"desc": "Go to node 3.",
|
||||||
"goto": "node3"})
|
"goto": "node3"})
|
||||||
|
|
@ -120,12 +116,13 @@ Example:
|
||||||
# by the normal 'goto' option key above.
|
# by the normal 'goto' option key above.
|
||||||
caller.msg("Callback called!")
|
caller.msg("Callback called!")
|
||||||
|
|
||||||
def node2(caller):
|
def node2(caller, **kwargs):
|
||||||
text = '''
|
text = '''
|
||||||
This is node 2. It only allows you to go back
|
This is node 2. It only allows you to go back
|
||||||
to the original node1. This extra indent will
|
to the original node1. This extra indent will
|
||||||
be stripped. We don't include a help text.
|
be stripped. We don't include a help text but
|
||||||
'''
|
here are the variables passed to us: {}
|
||||||
|
'''.format(kwargs)
|
||||||
options = {"goto": "node1"}
|
options = {"goto": "node1"}
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
@ -160,6 +157,7 @@ evennia.utils.evmenu`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
import random
|
||||||
from builtins import object, range
|
from builtins import object, range
|
||||||
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
@ -403,6 +401,7 @@ class EvMenu(object):
|
||||||
self._startnode = startnode
|
self._startnode = startnode
|
||||||
self._menutree = self._parse_menudata(menudata)
|
self._menutree = self._parse_menudata(menudata)
|
||||||
self._persistent = persistent
|
self._persistent = persistent
|
||||||
|
self._quitting = False
|
||||||
|
|
||||||
if startnode not in self._menutree:
|
if startnode not in self._menutree:
|
||||||
raise EvMenuError("Start node '%s' not in menu tree!" % startnode)
|
raise EvMenuError("Start node '%s' not in menu tree!" % startnode)
|
||||||
|
|
@ -538,35 +537,34 @@ class EvMenu(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
nspec = getargspec(callback).args
|
|
||||||
kspec = getargspec(callback).defaults
|
|
||||||
try:
|
try:
|
||||||
# this counts both args and kwargs
|
nargs = len(getargspec(callback).args)
|
||||||
nspec = len(nspec)
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
||||||
nkwargs = len(kspec) if kspec else 0
|
supports_kwargs = bool(getargspec(callback).keywords)
|
||||||
nargs = nspec - nkwargs
|
|
||||||
if nargs <= 0:
|
if nargs <= 0:
|
||||||
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
||||||
|
|
||||||
if nkwargs:
|
if supports_kwargs:
|
||||||
if nargs > 1:
|
if nargs > 1:
|
||||||
return callback(self.caller, raw_string, **kwargs)
|
ret = callback(self.caller, raw_string, **kwargs)
|
||||||
# callback accepting raw_string, **kwargs
|
# callback accepting raw_string, **kwargs
|
||||||
else:
|
else:
|
||||||
# callback accepting **kwargs
|
# callback accepting **kwargs
|
||||||
return callback(self.caller, **kwargs)
|
ret = callback(self.caller, **kwargs)
|
||||||
elif nargs > 1:
|
elif nargs > 1:
|
||||||
# callback accepting raw_string
|
# callback accepting raw_string
|
||||||
return callback(self.caller, raw_string)
|
ret = callback(self.caller, raw_string)
|
||||||
else:
|
else:
|
||||||
# normal callback, only the caller as arg
|
# normal callback, only the caller as arg
|
||||||
return callback(self.caller)
|
ret = callback(self.caller)
|
||||||
except Exception:
|
except EvMenuError:
|
||||||
self.caller.msg(_ERR_GENERAL.format(nodename=callback), self._session)
|
errmsg = _ERR_GENERAL.format(nodename=callback)
|
||||||
|
self.caller.msg(errmsg, self._session)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
def _execute_node(self, nodename, raw_string, **kwargs):
|
def _execute_node(self, nodename, raw_string, **kwargs):
|
||||||
"""
|
"""
|
||||||
Execute a node.
|
Execute a node.
|
||||||
|
|
@ -589,7 +587,11 @@ class EvMenu(object):
|
||||||
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:
|
||||||
nodetext, options = self._safe_call(node, raw_string, **kwargs)
|
ret = self._safe_call(node, raw_string, **kwargs)
|
||||||
|
if isinstance(ret, (tuple, list)) and len(ret) > 1:
|
||||||
|
nodetext, options = ret[:2]
|
||||||
|
else:
|
||||||
|
nodetext, options = ret, None
|
||||||
except KeyError:
|
except KeyError:
|
||||||
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
|
||||||
|
|
@ -622,27 +624,31 @@ class EvMenu(object):
|
||||||
relying on this.
|
relying on this.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if callable(nodename):
|
try:
|
||||||
# this is a direct callable - execute it directly
|
if callable(nodename):
|
||||||
ret = self._safe_call(nodename, raw_string, **kwargs)
|
# this is a direct callable - execute it directly
|
||||||
else:
|
ret = self._safe_call(nodename, raw_string, **kwargs)
|
||||||
# nodename is a string; lookup as node and run as node (but don't
|
if isinstance(ret, (tuple, list)):
|
||||||
# care about options)
|
if not len(ret) > 1 or not isinstance(ret[1], dict):
|
||||||
try:
|
raise EvMenuError("exec callable must return either None, str or (str, dict)")
|
||||||
|
ret, kwargs = ret[:2]
|
||||||
|
else:
|
||||||
|
# nodename is a string; lookup as node and run as node in-place (don't goto it)
|
||||||
# execute the node
|
# execute the node
|
||||||
ret = self._execute_node(nodename, raw_string, **kwargs)
|
ret = self._execute_node(nodename, raw_string, **kwargs)
|
||||||
if isinstance(ret, (tuple, list)) and len(ret) == 2:
|
if isinstance(ret, (tuple, list)):
|
||||||
# a (text, options) tuple. We only want the text.
|
if not len(ret) > 1 and ret[1] and not isinstance(ret[1], dict):
|
||||||
ret = ret[0]
|
raise EvMenuError("exec node must return either None, str or (str, dict)")
|
||||||
except EvMenuError as err:
|
ret, kwargs = ret[:2]
|
||||||
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string, err)
|
except EvMenuError as err:
|
||||||
self.caller.msg("|r%s|n" % errmsg)
|
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string.rstrip(), err)
|
||||||
logger.log_trace(errmsg)
|
self.caller.msg("|r%s|n" % errmsg)
|
||||||
return
|
logger.log_trace(errmsg)
|
||||||
|
return
|
||||||
|
|
||||||
if isinstance(ret, basestring):
|
if isinstance(ret, basestring):
|
||||||
# only return a value if a string (a goto target), ignore all other returns
|
# only return a value if a string (a goto target), ignore all other returns
|
||||||
return ret
|
return ret, kwargs
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def goto(self, nodename, raw_string, **kwargs):
|
def goto(self, nodename, raw_string, **kwargs):
|
||||||
|
|
@ -684,10 +690,12 @@ class EvMenu(object):
|
||||||
|
|
||||||
if callable(nodename):
|
if callable(nodename):
|
||||||
# run the "goto" callable, if possible
|
# run the "goto" callable, if possible
|
||||||
|
inp_nodename = nodename
|
||||||
nodename = self._safe_call(nodename, raw_string, **kwargs)
|
nodename = self._safe_call(nodename, raw_string, **kwargs)
|
||||||
if isinstance(nodename, (tuple, list)):
|
if isinstance(nodename, (tuple, list)):
|
||||||
if not len(nodename) > 1 or not isinstance(nodename[1], dict):
|
if not len(nodename) > 1 or not isinstance(nodename[1], dict):
|
||||||
raise EvMenuError("{}: goto callable must return str or (str, dict)")
|
raise EvMenuError(
|
||||||
|
"{}: goto callable must return str or (str, dict)".format(inp_nodename))
|
||||||
nodename, kwargs = nodename[:2]
|
nodename, kwargs = nodename[:2]
|
||||||
try:
|
try:
|
||||||
# execute the found node, make use of the returns.
|
# execute the found node, make use of the returns.
|
||||||
|
|
@ -765,8 +773,10 @@ class EvMenu(object):
|
||||||
"""
|
"""
|
||||||
if runexec:
|
if runexec:
|
||||||
# replace goto only if callback returns
|
# replace goto only if callback returns
|
||||||
goto = self.run_exec(runexec, raw_string,
|
goto, goto_kwargs = (
|
||||||
**(runexec_kwargs if runexec_kwargs else {})) or goto
|
self.run_exec(runexec, raw_string,
|
||||||
|
**(runexec_kwargs if runexec_kwargs else {})) or
|
||||||
|
(goto, goto_kwargs))
|
||||||
if goto:
|
if goto:
|
||||||
self.goto(goto, raw_string, **(goto_kwargs if goto_kwargs else {}))
|
self.goto(goto, raw_string, **(goto_kwargs if goto_kwargs else {}))
|
||||||
|
|
||||||
|
|
@ -774,13 +784,16 @@ class EvMenu(object):
|
||||||
"""
|
"""
|
||||||
Shutdown menu; occurs when reaching the end node or using the quit command.
|
Shutdown menu; occurs when reaching the end node or using the quit command.
|
||||||
"""
|
"""
|
||||||
self.caller.cmdset.remove(EvMenuCmdSet)
|
if not self._quitting:
|
||||||
del self.caller.ndb._menutree
|
# avoid multiple calls from different sources
|
||||||
if self._persistent:
|
self._quitting = True
|
||||||
self.caller.attributes.remove("_menutree_saved")
|
self.caller.cmdset.remove(EvMenuCmdSet)
|
||||||
self.caller.attributes.remove("_menutree_saved_startnode")
|
del self.caller.ndb._menutree
|
||||||
if self.cmd_on_exit is not None:
|
if self._persistent:
|
||||||
self.cmd_on_exit(self.caller, self)
|
self.caller.attributes.remove("_menutree_saved")
|
||||||
|
self.caller.attributes.remove("_menutree_saved_startnode")
|
||||||
|
if self.cmd_on_exit is not None:
|
||||||
|
self.cmd_on_exit(self.caller, self)
|
||||||
|
|
||||||
def parse_input(self, raw_string):
|
def parse_input(self, raw_string):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1064,7 +1077,7 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs):
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
def _generate_goto(caller, **kwargs):
|
def _generate_goto(caller, **kwargs):
|
||||||
return kwargs.get("name", "text_start_node"), {"name": "replaced!"}
|
return kwargs.get("name", "test_dynamic_node"), {"name": "replaced!"}
|
||||||
|
|
||||||
|
|
||||||
def test_start_node(caller):
|
def test_start_node(caller):
|
||||||
|
|
@ -1135,7 +1148,7 @@ def test_set_node(caller):
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
def test_view_node(caller):
|
def test_view_node(caller, **kwargs):
|
||||||
text = """
|
text = """
|
||||||
Your name is |g%s|n!
|
Your name is |g%s|n!
|
||||||
|
|
||||||
|
|
@ -1145,9 +1158,14 @@ def test_view_node(caller):
|
||||||
-always- use numbers (1...N) to refer to listed options also if you
|
-always- use numbers (1...N) to refer to listed options also if you
|
||||||
don't see a string option key (try it!).
|
don't see a string option key (try it!).
|
||||||
""" % caller.key
|
""" % caller.key
|
||||||
options = {"desc": "back to main",
|
if kwargs.get("executed_from_dynamic_node", False):
|
||||||
"goto": "test_start_node"}
|
# we are calling this node as a exec, skip return values
|
||||||
return text, options
|
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):
|
def test_displayinput_node(caller, raw_string):
|
||||||
|
|
@ -1163,20 +1181,48 @@ def test_displayinput_node(caller, raw_string):
|
||||||
makes it hidden from view. It catches all input (except the
|
makes it hidden from view. It catches all input (except the
|
||||||
in-menu help/quit commands) and will, in this case, bring you back
|
in-menu help/quit commands) and will, in this case, bring you back
|
||||||
to the start node.
|
to the start node.
|
||||||
""" % raw_string
|
""" % raw_string.rstrip()
|
||||||
options = {"key": "_default",
|
options = {"key": "_default",
|
||||||
"goto": "test_start_node"}
|
"goto": "test_start_node"}
|
||||||
return text, options
|
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):
|
def test_dynamic_node(caller, **kwargs):
|
||||||
text = """
|
text = """
|
||||||
This is a dynamic node, whose name was
|
This is a dynamic node with input:
|
||||||
generated by the goto function.
|
{}
|
||||||
"""
|
""".format(kwargs)
|
||||||
options = {}
|
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
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
def test_end_node(caller):
|
def test_end_node(caller):
|
||||||
text = """
|
text = """
|
||||||
This is the end of the menu and since it has no options the menu
|
This is the end of the menu and since it has no options the menu
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue