Add working **kwargs support to nodes/callbacks in evmenu

This commit is contained in:
Griatch 2017-10-28 12:05:32 +02:00
parent 2475d14691
commit 7b295fa98b

View file

@ -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