Almost finished with kwargs-support for evmenu
This commit is contained in:
parent
2c1ebf68e3
commit
2475d14691
1 changed files with 166 additions and 84 deletions
|
|
@ -63,23 +63,35 @@ menu is immediately exited and the default "look" command is called.
|
||||||
text (str, tuple or None): Text shown at this node. If a tuple, the
|
text (str, tuple or None): Text shown at this node. If a tuple, the
|
||||||
second element in the tuple is a help text to display at this
|
second element in the tuple is a help text to display at this
|
||||||
node when the user enters the menu help command there.
|
node when the user enters the menu help command there.
|
||||||
options (tuple, dict or None): (
|
options (tuple, dict or None): If `None`, this exits the menu.
|
||||||
{'key': name, # can also be a list of aliases. A special key is
|
If a single dict, this is a single-option node. If a tuple,
|
||||||
# "_default", which marks this option as the default
|
it should be a tuple of option dictionaries. Option dicts have
|
||||||
# fallback when no other option matches the user input.
|
the following keys:
|
||||||
'desc': description, # optional description
|
- `key` (str or tuple, optional): What to enter to choose this option.
|
||||||
'goto': nodekey, # node to go to when chosen. This can also be a callable with
|
If a tuple, it must be a tuple of strings, where the first string is the
|
||||||
# caller and/or raw_string args. It must return a string
|
key which will be shown to the user and the others are aliases.
|
||||||
# with the key pointing to the node to go to.
|
If unset, the options' number will be used. The special key `_default`
|
||||||
'exec': nodekey}, # node or callback to trigger as callback when chosen. This
|
marks this option as the default fallback when no other option matches
|
||||||
# will execute *before* going to the next node. Both node
|
the user input. There can only be one `_default` option per node. It
|
||||||
# and the explicit callback will be called as normal nodes
|
will not be displayed in the list.
|
||||||
# (with caller and/or raw_string args). If the callable/node
|
- `desc` (str, optional): This describes what choosing the option will do.
|
||||||
# returns a single string (only), this will replace the current
|
- `goto` (str, tuple or callable): If string, should be the name of node to go to
|
||||||
# goto location string in-place (if a goto callback, it will never fire).
|
when this option is selected. If a callable, it has the signature
|
||||||
# Note that relying to much on letting exec assign the goto
|
`callable(caller[,raw_input][,**kwargs]). If a tuple, the first element
|
||||||
# location can make it hard to debug your menu logic.
|
is the callable and the second is a dict with the **kwargs to pass to
|
||||||
{...}, ...)
|
the callable. Those kwargs will also be passed into the next node if possible.
|
||||||
|
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,
|
||||||
|
(possibly modified) kwarg to pass into the next node.
|
||||||
|
- `exec` (str, callable or tuple, optional): This specified either the name of
|
||||||
|
a menu node to execute as a callback or a regular callable. If a tuple, the
|
||||||
|
first element is either the menu-node name or the callback, while the second
|
||||||
|
is a dict for the **kwargs to pass into the node/callback. This callback/node
|
||||||
|
will execute *before* going any `goto` function and before going to the next
|
||||||
|
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.
|
||||||
|
|
@ -519,7 +531,43 @@ class EvMenu(object):
|
||||||
# format the entire node
|
# format the entire node
|
||||||
return self.node_formatter(nodetext, optionstext)
|
return self.node_formatter(nodetext, optionstext)
|
||||||
|
|
||||||
def _execute_node(self, nodename, raw_string):
|
def _safe_call(self, callback, raw_string, **kwargs):
|
||||||
|
"""
|
||||||
|
Call a node-like callable, with a variable number of raw_string, *args, **kwargs, all of
|
||||||
|
which should work also if not present (only `caller` is always required). Return its result.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
nspec = getargspec(callback).args
|
||||||
|
kspec = getargspec(callback).defaults
|
||||||
|
try:
|
||||||
|
# this counts both args and kwargs
|
||||||
|
nspec = len(nspec)
|
||||||
|
except TypeError:
|
||||||
|
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
||||||
|
nkwargs = len(kspec) if kspec else 0
|
||||||
|
nargs = nspec - nkwargs
|
||||||
|
if nargs <= 0:
|
||||||
|
raise EvMenuError("Callable {} doesn't accept any arguments!".format(callback))
|
||||||
|
|
||||||
|
if nkwargs:
|
||||||
|
if nargs > 1:
|
||||||
|
return callback(self.caller, raw_string, **kwargs)
|
||||||
|
# callback accepting raw_string, **kwargs
|
||||||
|
else:
|
||||||
|
# callback accepting **kwargs
|
||||||
|
return callback(self.caller, **kwargs)
|
||||||
|
elif nargs > 1:
|
||||||
|
# callback accepting raw_string
|
||||||
|
return callback(self.caller, raw_string)
|
||||||
|
else:
|
||||||
|
# normal callback, only the caller as arg
|
||||||
|
return callback(self.caller)
|
||||||
|
except Exception:
|
||||||
|
self.caller.msg(_ERR_GENERAL.format(nodename=callback), self._session)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _execute_node(self, nodename, raw_string, **kwargs):
|
||||||
"""
|
"""
|
||||||
Execute a node.
|
Execute a node.
|
||||||
|
|
||||||
|
|
@ -528,6 +576,7 @@ class EvMenu(object):
|
||||||
raw_string (str): The raw default string entered on the
|
raw_string (str): The raw default string entered on the
|
||||||
previous node (only used if the node accepts it as an
|
previous node (only used if the node accepts it as an
|
||||||
argument)
|
argument)
|
||||||
|
kwargs (any, optional): Optional kwargs for the node.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
nodetext, options (tuple): The node text (a string or a
|
nodetext, options (tuple): The node text (a string or a
|
||||||
|
|
@ -540,13 +589,7 @@ 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:
|
||||||
# the node should return data as (text, options)
|
nodetext, options = self._safe_call(node, raw_string, **kwargs)
|
||||||
if len(getargspec(node).args) > 1:
|
|
||||||
# a node accepting raw_string
|
|
||||||
nodetext, options = node(self.caller, raw_string)
|
|
||||||
else:
|
|
||||||
# a normal node, only accepting caller
|
|
||||||
nodetext, options = node(self.caller)
|
|
||||||
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
|
||||||
|
|
@ -555,32 +598,7 @@ class EvMenu(object):
|
||||||
raise
|
raise
|
||||||
return nodetext, options
|
return nodetext, options
|
||||||
|
|
||||||
def display_nodetext(self):
|
def run_exec(self, nodename, raw_string, **kwargs):
|
||||||
self.caller.msg(self.nodetext, session=self._session)
|
|
||||||
|
|
||||||
def display_helptext(self):
|
|
||||||
self.caller.msg(self.helptext, session=self._session)
|
|
||||||
|
|
||||||
def callback_goto(self, callback, goto, raw_string):
|
|
||||||
"""
|
|
||||||
Call callback and goto in sequence.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
callback (callable or str): Callback to run before goto. If
|
|
||||||
the callback returns a string, this is used to replace
|
|
||||||
the `goto` string before going to the next node.
|
|
||||||
goto (str): The target node to go to next (unless replaced
|
|
||||||
by `callable`)..
|
|
||||||
raw_string (str): The original user input.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if callback:
|
|
||||||
# replace goto only if callback returns
|
|
||||||
goto = self.callback(callback, raw_string) or goto
|
|
||||||
if goto:
|
|
||||||
self.goto(goto, raw_string)
|
|
||||||
|
|
||||||
def callback(self, nodename, raw_string):
|
|
||||||
"""
|
"""
|
||||||
Run a function or node as a callback (with the 'exec' option key).
|
Run a function or node as a callback (with the 'exec' option key).
|
||||||
|
|
||||||
|
|
@ -592,6 +610,8 @@ class EvMenu(object):
|
||||||
raw_string (str): The raw default string entered on the
|
raw_string (str): The raw default string entered on the
|
||||||
previous node (only used if the node accepts it as an
|
previous node (only used if the node accepts it as an
|
||||||
argument)
|
argument)
|
||||||
|
kwargs (any): These are optional kwargs passed into goto
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
new_goto (str or None): A replacement goto location string or
|
new_goto (str or None): A replacement goto location string or
|
||||||
None (no replacement).
|
None (no replacement).
|
||||||
|
|
@ -604,34 +624,30 @@ class EvMenu(object):
|
||||||
"""
|
"""
|
||||||
if callable(nodename):
|
if callable(nodename):
|
||||||
# this is a direct callable - execute it directly
|
# this is a direct callable - execute it directly
|
||||||
try:
|
ret = self._safe_call(nodename, raw_string, **kwargs)
|
||||||
if len(getargspec(nodename).args) > 1:
|
|
||||||
# callable accepting raw_string
|
|
||||||
ret = nodename(self.caller, raw_string)
|
|
||||||
else:
|
else:
|
||||||
# normal callable, only the caller as arg
|
# nodename is a string; lookup as node and run as node (but don't
|
||||||
ret = nodename(self.caller)
|
# care about options)
|
||||||
except Exception:
|
|
||||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
# nodename is a string; lookup as node
|
|
||||||
try:
|
try:
|
||||||
# execute the node
|
# execute the node
|
||||||
ret = self._execute_node(nodename, raw_string)
|
ret = self._execute_node(nodename, raw_string, **kwargs)
|
||||||
|
if isinstance(ret, (tuple, list)) and len(ret) == 2:
|
||||||
|
# a (text, options) tuple. We only want the text.
|
||||||
|
ret = ret[0]
|
||||||
except EvMenuError as err:
|
except EvMenuError as err:
|
||||||
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string, err)
|
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string, err)
|
||||||
self.caller.msg("|r%s|n" % errmsg)
|
self.caller.msg("|r%s|n" % errmsg)
|
||||||
logger.log_trace(errmsg)
|
logger.log_trace(errmsg)
|
||||||
return
|
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
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def goto(self, nodename, raw_string):
|
def goto(self, nodename, raw_string, **kwargs):
|
||||||
"""
|
"""
|
||||||
Run a node by name
|
Run a node by name, optionally dynamically generating that name first.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nodename (str or callable): Name of node or a callable
|
nodename (str or callable): Name of node or a callable
|
||||||
|
|
@ -642,19 +658,40 @@ class EvMenu(object):
|
||||||
argument)
|
argument)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if callable(nodename):
|
def _extract_goto_exec(option_dict):
|
||||||
try:
|
"Helper: Get callables and their eventual kwargs"
|
||||||
if len(getargspec(nodename).args) > 1:
|
goto_kwargs, exec_kwargs = {}, {}
|
||||||
# callable accepting raw_string
|
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
||||||
nodename = nodename(self.caller, raw_string)
|
if goto and isinstance(goto, (tuple, list)):
|
||||||
|
if len(goto) > 1:
|
||||||
|
goto, goto_kwargs = goto[:2] # ignore any extra arguments
|
||||||
|
if not hasattr(goto_kwargs, "__getitem__"):
|
||||||
|
# not a dict-like structure
|
||||||
|
raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format(
|
||||||
|
nodename, goto_kwargs))
|
||||||
else:
|
else:
|
||||||
nodename = nodename(self.caller)
|
goto = goto[0]
|
||||||
except Exception:
|
if execute and isinstance(execute, (tuple, list)):
|
||||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session)
|
if len(execute) > 1:
|
||||||
raise
|
execute, exec_kwargs = execute[:2] # ignore any extra arguments
|
||||||
|
if not hasattr(exec_kwargs, "__getitem__"):
|
||||||
|
# not a dict-like structure
|
||||||
|
raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format(
|
||||||
|
nodename, goto_kwargs))
|
||||||
|
else:
|
||||||
|
execute = execute[0]
|
||||||
|
return goto, goto_kwargs, execute, exec_kwargs
|
||||||
|
|
||||||
|
if callable(nodename):
|
||||||
|
# run the "goto" callable, if possible
|
||||||
|
nodename = self._safe_call(nodename, raw_string, **kwargs)
|
||||||
|
if isinstance(nodename, (tuple, list)):
|
||||||
|
if not len(nodename) > 1 or not isinstance(nodename[1], dict):
|
||||||
|
raise EvMenuError("{}: goto callable must return str or (str, dict)")
|
||||||
|
nodename, kwargs = nodename[:2]
|
||||||
try:
|
try:
|
||||||
# execute the node, make use of the returns.
|
# execute the found node, make use of the returns.
|
||||||
nodetext, options = self._execute_node(nodename, raw_string)
|
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
|
||||||
except EvMenuError:
|
except EvMenuError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -683,17 +720,19 @@ class EvMenu(object):
|
||||||
if "_default" in keys:
|
if "_default" in keys:
|
||||||
keys = [key for key in keys if key != "_default"]
|
keys = [key for key in keys if key != "_default"]
|
||||||
desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip())
|
desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip())
|
||||||
goto, execute = dic.get("goto", None), dic.get("exec", None)
|
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
|
||||||
self.default = (goto, execute)
|
self.default = (goto, goto_kwargs, execute, exec_kwargs)
|
||||||
else:
|
else:
|
||||||
|
# use the key (only) if set, otherwise use the running number
|
||||||
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
|
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
|
||||||
desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip())
|
desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip())
|
||||||
goto, execute = dic.get("goto", None), dic.get("exec", None)
|
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
|
||||||
if keys:
|
if keys:
|
||||||
display_options.append((keys[0], desc))
|
display_options.append((keys[0], desc))
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if goto or execute:
|
if goto or execute:
|
||||||
self.options[strip_ansi(key).strip().lower()] = (goto, execute)
|
self.options[strip_ansi(key).strip().lower()] = \
|
||||||
|
(goto, goto_kwargs, execute, exec_kwargs)
|
||||||
|
|
||||||
self.nodetext = self._format_node(nodetext, display_options)
|
self.nodetext = self._format_node(nodetext, display_options)
|
||||||
|
|
||||||
|
|
@ -709,6 +748,28 @@ class EvMenu(object):
|
||||||
if not options:
|
if not options:
|
||||||
self.close_menu()
|
self.close_menu()
|
||||||
|
|
||||||
|
def run_exec_then_goto(self, runexec, goto, raw_string, runexec_kwargs=None, goto_kwargs=None):
|
||||||
|
"""
|
||||||
|
Call 'exec' callback and goto (which may also be a callable) in sequence.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
runexec (callable or str): Callback to run before goto. If
|
||||||
|
the callback returns a string, this is used to replace
|
||||||
|
the `goto` string/callable before being passed into the goto handler.
|
||||||
|
goto (str): The target node to go to next (may be replaced
|
||||||
|
by `runexec`)..
|
||||||
|
raw_string (str): The original user input.
|
||||||
|
runexec_kwargs (dict, optional): Optional kwargs for runexec.
|
||||||
|
goto_kwargs (dict, optional): Optional kwargs for goto.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if runexec:
|
||||||
|
# replace goto only if callback returns
|
||||||
|
goto = self.run_exec(runexec, raw_string,
|
||||||
|
**(runexec_kwargs if runexec_kwargs else {})) or goto
|
||||||
|
if goto:
|
||||||
|
self.goto(goto, raw_string, **(goto_kwargs if goto_kwargs else {}))
|
||||||
|
|
||||||
def close_menu(self):
|
def close_menu(self):
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
|
|
@ -739,8 +800,8 @@ class EvMenu(object):
|
||||||
if cmd in self.options:
|
if cmd in self.options:
|
||||||
# this will take precedence over the default commands
|
# this will take precedence over the default commands
|
||||||
# below
|
# below
|
||||||
goto, callback = self.options[cmd]
|
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
|
||||||
self.callback_goto(callback, goto, raw_string)
|
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||||
elif self.auto_look and cmd in ("look", "l"):
|
elif self.auto_look and cmd in ("look", "l"):
|
||||||
self.display_nodetext()
|
self.display_nodetext()
|
||||||
elif self.auto_help and cmd in ("help", "h"):
|
elif self.auto_help and cmd in ("help", "h"):
|
||||||
|
|
@ -748,8 +809,8 @@ class EvMenu(object):
|
||||||
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
||||||
self.close_menu()
|
self.close_menu()
|
||||||
elif self.default:
|
elif self.default:
|
||||||
goto, callback = self.default
|
goto, goto_kwargs, execfunc, exec_kwargs = self.default
|
||||||
self.callback_goto(callback, goto, raw_string)
|
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||||
else:
|
else:
|
||||||
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
|
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
|
||||||
|
|
||||||
|
|
@ -757,6 +818,12 @@ class EvMenu(object):
|
||||||
# no options - we are at the end of the menu.
|
# no options - we are at the end of the menu.
|
||||||
self.close_menu()
|
self.close_menu()
|
||||||
|
|
||||||
|
def display_nodetext(self):
|
||||||
|
self.caller.msg(self.nodetext, session=self._session)
|
||||||
|
|
||||||
|
def display_helptext(self):
|
||||||
|
self.caller.msg(self.helptext, session=self._session)
|
||||||
|
|
||||||
# formatters - override in a child class
|
# formatters - override in a child class
|
||||||
|
|
||||||
def nodetext_formatter(self, nodetext):
|
def nodetext_formatter(self, nodetext):
|
||||||
|
|
@ -996,6 +1063,10 @@ def get_input(caller, prompt, callback, session=None, *args, **kwargs):
|
||||||
#
|
#
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
def _generate_goto(caller, **kwargs):
|
||||||
|
return kwargs.get("name", "text_start_node"), {"name": "replaced!"}
|
||||||
|
|
||||||
|
|
||||||
def test_start_node(caller):
|
def test_start_node(caller):
|
||||||
menu = caller.ndb._menutree
|
menu = caller.ndb._menutree
|
||||||
text = """
|
text = """
|
||||||
|
|
@ -1020,6 +1091,9 @@ def test_start_node(caller):
|
||||||
{"key": ("|yV|niew", "v"),
|
{"key": ("|yV|niew", "v"),
|
||||||
"desc": "View your own name",
|
"desc": "View your own name",
|
||||||
"goto": "test_view_node"},
|
"goto": "test_view_node"},
|
||||||
|
{"key": ("|yD|nynamic", "d"),
|
||||||
|
"desc": "Dynamic node",
|
||||||
|
"goto": (_generate_goto, {"name": "test_dynamic_node"})},
|
||||||
{"key": ("|yQ|nuit", "quit", "q", "Q"),
|
{"key": ("|yQ|nuit", "quit", "q", "Q"),
|
||||||
"desc": "Quit this menu example.",
|
"desc": "Quit this menu example.",
|
||||||
"goto": "test_end_node"},
|
"goto": "test_end_node"},
|
||||||
|
|
@ -1095,6 +1169,14 @@ def test_displayinput_node(caller, raw_string):
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_node(caller, **kwargs):
|
||||||
|
text = """
|
||||||
|
This is a dynamic node, whose name was
|
||||||
|
generated by the goto function.
|
||||||
|
"""
|
||||||
|
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