mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
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
|
||||
second element in the tuple is a help text to display at this
|
||||
node when the user enters the menu help command there.
|
||||
options (tuple, dict or None): (
|
||||
{'key': name, # can also be a list of aliases. A special key is
|
||||
# "_default", which marks this option as the default
|
||||
# fallback when no other option matches the user input.
|
||||
'desc': description, # optional description
|
||||
'goto': nodekey, # node to go to when chosen. This can also be a callable with
|
||||
# caller and/or raw_string args. It must return a string
|
||||
# with the key pointing to the node to go to.
|
||||
'exec': nodekey}, # node or callback to trigger as callback when chosen. This
|
||||
# will execute *before* going to the next node. Both node
|
||||
# and the explicit callback will be called as normal nodes
|
||||
# (with caller and/or raw_string args). If the callable/node
|
||||
# returns a single string (only), this will replace the current
|
||||
# goto location string in-place (if a goto callback, it will never fire).
|
||||
# Note that relying to much on letting exec assign the goto
|
||||
# location can make it hard to debug your menu logic.
|
||||
{...}, ...)
|
||||
options (tuple, dict or None): If `None`, this exits the menu.
|
||||
If a single dict, this is a single-option node. If a tuple,
|
||||
it should be a tuple of option dictionaries. Option dicts have
|
||||
the following keys:
|
||||
- `key` (str or tuple, optional): What to enter to choose this option.
|
||||
If a tuple, it must be a tuple of strings, where the first string is the
|
||||
key which will be shown to the user and the others are aliases.
|
||||
If unset, the options' number will be used. The special key `_default`
|
||||
marks this option as the default fallback when no other option matches
|
||||
the user input. There can only be one `_default` option per node. It
|
||||
will not be displayed in the list.
|
||||
- `desc` (str, optional): This describes what choosing the option will do.
|
||||
- `goto` (str, tuple or callable): If string, should be the name of node to go to
|
||||
when this option is selected. If a callable, it has the signature
|
||||
`callable(caller[,raw_input][,**kwargs]). If a tuple, the first element
|
||||
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
|
||||
its number 1..N.
|
||||
|
|
@ -519,7 +531,43 @@ class EvMenu(object):
|
|||
# format the entire node
|
||||
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.
|
||||
|
||||
|
|
@ -528,6 +576,7 @@ class EvMenu(object):
|
|||
raw_string (str): The raw default string entered on the
|
||||
previous node (only used if the node accepts it as an
|
||||
argument)
|
||||
kwargs (any, optional): Optional kwargs for the node.
|
||||
|
||||
Returns:
|
||||
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)
|
||||
raise EvMenuError
|
||||
try:
|
||||
# the node should return data as (text, options)
|
||||
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)
|
||||
nodetext, options = self._safe_call(node, raw_string, **kwargs)
|
||||
except KeyError:
|
||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||
raise EvMenuError
|
||||
|
|
@ -555,32 +598,7 @@ class EvMenu(object):
|
|||
raise
|
||||
return nodetext, options
|
||||
|
||||
def display_nodetext(self):
|
||||
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):
|
||||
def run_exec(self, nodename, raw_string, **kwargs):
|
||||
"""
|
||||
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
|
||||
previous node (only used if the node accepts it as an
|
||||
argument)
|
||||
kwargs (any): These are optional kwargs passed into goto
|
||||
|
||||
Returns:
|
||||
new_goto (str or None): A replacement goto location string or
|
||||
None (no replacement).
|
||||
|
|
@ -604,34 +624,30 @@ class EvMenu(object):
|
|||
"""
|
||||
if callable(nodename):
|
||||
# this is a direct callable - execute it directly
|
||||
try:
|
||||
if len(getargspec(nodename).args) > 1:
|
||||
# callable accepting raw_string
|
||||
ret = nodename(self.caller, raw_string)
|
||||
else:
|
||||
# normal callable, only the caller as arg
|
||||
ret = nodename(self.caller)
|
||||
except Exception:
|
||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session)
|
||||
raise
|
||||
ret = self._safe_call(nodename, raw_string, **kwargs)
|
||||
else:
|
||||
# nodename is a string; lookup as node
|
||||
# nodename is a string; lookup as node and run as node (but don't
|
||||
# care about options)
|
||||
try:
|
||||
# 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:
|
||||
errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string, err)
|
||||
self.caller.msg("|r%s|n" % errmsg)
|
||||
logger.log_trace(errmsg)
|
||||
return
|
||||
|
||||
if isinstance(ret, basestring):
|
||||
# only return a value if a string (a goto target), ignore all other returns
|
||||
return ret
|
||||
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:
|
||||
nodename (str or callable): Name of node or a callable
|
||||
|
|
@ -642,19 +658,40 @@ class EvMenu(object):
|
|||
argument)
|
||||
|
||||
"""
|
||||
if callable(nodename):
|
||||
try:
|
||||
if len(getargspec(nodename).args) > 1:
|
||||
# callable accepting raw_string
|
||||
nodename = nodename(self.caller, raw_string)
|
||||
def _extract_goto_exec(option_dict):
|
||||
"Helper: Get callables and their eventual kwargs"
|
||||
goto_kwargs, exec_kwargs = {}, {}
|
||||
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
||||
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:
|
||||
nodename = nodename(self.caller)
|
||||
except Exception:
|
||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), self._session)
|
||||
raise
|
||||
goto = goto[0]
|
||||
if execute and isinstance(execute, (tuple, list)):
|
||||
if len(execute) > 1:
|
||||
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:
|
||||
# execute the node, make use of the returns.
|
||||
nodetext, options = self._execute_node(nodename, raw_string)
|
||||
# execute the found node, make use of the returns.
|
||||
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
|
||||
except EvMenuError:
|
||||
return
|
||||
|
||||
|
|
@ -683,17 +720,19 @@ class EvMenu(object):
|
|||
if "_default" in keys:
|
||||
keys = [key for key in keys if key != "_default"]
|
||||
desc = dic.get("desc", dic.get("text", _ERR_NO_OPTION_DESC).strip())
|
||||
goto, execute = dic.get("goto", None), dic.get("exec", None)
|
||||
self.default = (goto, execute)
|
||||
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
|
||||
self.default = (goto, goto_kwargs, execute, exec_kwargs)
|
||||
else:
|
||||
# use the key (only) if set, otherwise use the running number
|
||||
keys = list(make_iter(dic.get("key", str(inum + 1).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:
|
||||
display_options.append((keys[0], desc))
|
||||
for key in keys:
|
||||
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)
|
||||
|
||||
|
|
@ -709,6 +748,28 @@ class EvMenu(object):
|
|||
if not options:
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
# this will take precedence over the default commands
|
||||
# below
|
||||
goto, callback = self.options[cmd]
|
||||
self.callback_goto(callback, goto, raw_string)
|
||||
goto, goto_kwargs, execfunc, exec_kwargs = self.options[cmd]
|
||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||
elif self.auto_look and cmd in ("look", "l"):
|
||||
self.display_nodetext()
|
||||
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"):
|
||||
self.close_menu()
|
||||
elif self.default:
|
||||
goto, callback = self.default
|
||||
self.callback_goto(callback, goto, raw_string)
|
||||
goto, goto_kwargs, execfunc, exec_kwargs = self.default
|
||||
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
|
||||
else:
|
||||
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.
|
||||
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
|
||||
|
||||
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):
|
||||
menu = caller.ndb._menutree
|
||||
text = """
|
||||
|
|
@ -1020,6 +1091,9 @@ def test_start_node(caller):
|
|||
{"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"},
|
||||
|
|
@ -1095,6 +1169,14 @@ def test_displayinput_node(caller, raw_string):
|
|||
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):
|
||||
text = """
|
||||
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