Start add edit_node decorator (untested)

This commit is contained in:
Griatch 2018-04-02 18:46:55 +02:00
parent 02b9654f1c
commit ed3e57edd0
2 changed files with 331 additions and 106 deletions

View file

@ -175,7 +175,7 @@ from evennia import Command, CmdSet
from evennia.utils import logger
from evennia.utils.evtable import EvTable
from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import mod_import, make_iter, pad, m_len
from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter
from evennia.commands import cmdhandler
# read from protocol NAWS later?
@ -683,6 +683,43 @@ class EvMenu(object):
return ret, kwargs
return None
def extract_goto_exec(self, nodename, option_dict):
"""
Helper: Get callables and their eventual kwargs.
Args:
nodename (str): The current node name (used for error reporting).
option_dict (dict): The seleted option's dict.
Returns:
goto (str, callable or None): The goto directive in the option.
goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty.
execute (callable or None): Executable given by the `exec` directive.
exec_kwargs (dict): Kwargs for `execute` if it's callable, otherwise empty.
"""
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:
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
def goto(self, nodename, raw_string, **kwargs):
"""
Run a node by name, optionally dynamically generating that name first.
@ -696,29 +733,6 @@ class EvMenu(object):
argument)
"""
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:
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
@ -764,12 +778,12 @@ class EvMenu(object):
desc = dic.get("desc", dic.get("text", None))
if "_default" in keys:
keys = [key for key in keys if key != "_default"]
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, 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())))
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
if keys:
display_options.append((keys[0], desc))
for key in keys:
@ -975,11 +989,143 @@ class EvMenu(object):
# -----------------------------------------------------------
#
# List node
# Edit node (decorator turning a node into an editing
# point for a given resource
#
# -----------------------------------------------------------
def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
def edit_node(edit_text, add_text, edit_callback, add_callback, get_choices=None):
"""
Decorator for turning an EvMenu node into an editing
page. Will add new options, prepending those options
added in the node.
Args:
edit_text (str or callable): Will be used as text for the edit node. If
callable, it will be called as edittext(selection)
and should return the node text for the edit-node, probably listing
the current value of all editable propnames, if possible.
add_text (str) or callable: Gives text for node in add-mode. If a callable,
called as add_text() and should return the text for the node.
edit_callback (callable): Will be called as edit_callback(editable, raw_string)
and should return a boolean True/False if the setting of the property
succeeded or not. The value will always be a string and should be
converted as needed.
add_callback (callable): Will be called as add_callback(raw_string) and
should return a boolean True/False if the addition succeded.
get_choices (callable): Produce the available editable choices. If this
is not given, the `goto` callable must have been provided with the
kwarg `available_choices` by the decorated node.
"""
def decorator(func):
def _setter_goto(caller, raw_string, **kwargs):
editable = kwargs.get("editable")
mode = kwargs.get("edit_node_mode")
try:
if mode == 'edit':
is_ok = edit_callback(editable, raw_string)
else:
is_ok = add_callback(raw_string)
except Exception:
logger.log_trace()
if not is_ok:
caller.msg("|rValue could not be set.")
return None
def _patch_goto(caller, raw_string, **kwargs):
# parse incoming string to figure out if there is a match to edit/add
match = re.search(r"(^[a-zA-Z]*)\s*([0-9]*)$", raw_string)
cmd, number = match.groups()
edit_mode = None
available_choices = None
selection = None
if get_choices:
available_choices = make_iter(get_choices(caller, raw_string, **kwargs))
if not available_choices:
available_choices = kwargs.get("available_choices", [])
if available_choices and cmd.startswith("e"):
try:
index = int(cmd) - 1
selection = available_choices[index]
edit_mode = 'edit'
except (IndexError, TypeError):
caller.msg("|rNot a valid 'edit' command.")
if cmd.startswith("a") and not number:
# add mode
edit_mode = "add"
if edit_mode:
# replace with edit text/options
text = edit_text(selection) if edit_mode == "edit" else add_text()
options = ({"key": "_default",
"goto": (_setter_goto,
{"selection": selection,
"edit_node_mode": edit_mode})})
return text, options
# no matches - pass through to the original decorated goto instruction
decorated_opt = kwargs.get("decorated_opt")
if decorated_opt:
# use EvMenu's parser to get the goto/goto-kwargs out of
# the decorated option structure
dec_goto, dec_goto_kwargs, _, _ = \
caller.ndb._menutree.extract_goto_exec("edit-node", decorated_opt)
if callable(dec_goto):
try:
return dec_goto(caller, raw_string,
**{dec_goto_kwargs if dec_goto_kwargs else {}})
except Exception:
caller.msg("|rThere was an error in the edit node.")
logger.log_trace()
return None
def _edit_node(caller, raw_string, **kwargs):
text, options = func(caller, raw_string, **kwargs)
if options:
# find eventual _default in options and patch it with a handler for
# catching editing
decorated_opt = None
iopt = 0
for iopt, optdict in enumerate(options):
if optdict.get('key') == "_default":
decorated_opt = optdict
break
if decorated_opt:
# inject our wrapper over the original goto instruction for the
# _default action (save the original)
options[iopt]["goto"] = (_patch_goto,
{"decorated_opt": decorated_opt})
return text, options
return _edit_node
return decorator
# -----------------------------------------------------------
#
# List node (decorator turning a node into a list with
# look/edit/add functionality for the elements)
#
# -----------------------------------------------------------
def list_node(option_generator, select=None, examine=None, edit=None, add=None, pagesize=10):
"""
Decorator for making an EvMenu node into a multi-page list node. Will add new options,
prepending those options added in the node.
@ -987,17 +1133,25 @@ def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
Args:
option_generator (callable or list): A list of strings indicating the options, or a callable
that is called without any arguments to produce such a list.
examine_processor (callable, optional): Will be called with the caller and the chosen option
when examining said option. Should return a text string to display in the node.
goto_processor (callable, optional): Will be called as goto_processor(caller, menuchoice)
select (callable, option): Will be called as select(caller, menuchoice)
where menuchoice is the chosen option as a string. Should return the target node to
goto after this selection.
goto after this selection. Note that if this is not given, the decorated node must itself
provide a way to continue from the node!
examine (callable, optional): If given, allows for examining options in detail. Will
be called with examine(caller, menuchoice) and should return a text string to
display in-place in the node.
edit (callable, optional): If given, this callable will be called as edit(caller, menuchoice).
It should return the node-key to a node decorated with the `edit_node` decorator. The
menuchoice will automatically be stored on the menutree as `list_node_edit`.
add (tuple, optional): If given, this callable will be called as add(caller, menuchoice).
It should return the node-key to a node decorated with the `edit_node` decorator. The
menuchoice will automatically be stored on the menutree as `list_node_add`.
pagesize (int): How many options to show per page.
Example:
@list_node(['foo', 'bar'], examine_processor, goto_processor)
@list_node(['foo', 'bar'], examine, select)
def node_index(caller):
text = "describing the list"
return text, []
@ -1006,27 +1160,63 @@ def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
def decorator(func):
def _input_parser(caller, raw_string, **kwargs):
"Parse which input was given, select from option_list"
def _select_parser(caller, raw_string, **kwargs):
"""
Parse the select action
"""
available_choices = kwargs.get("available_choices", [])
processor = kwargs.get("selection_processor")
try:
match_ind = int(re.search(r"[0-9]+$", raw_string).group()) - 1
selection = available_choices[match_ind]
except (AttributeError, KeyError, IndexError, ValueError):
return None
if processor:
index = int(raw_string.strip()) - 1
selection = available_choices[index]
except Exception:
caller.msg("|rInvalid choice.|n")
else:
try:
return processor(caller, selection)
return select(caller, selection)
except Exception:
logger.log_trace()
return selection
return None
def _input_parser(caller, raw_string, **kwargs):
"""
Parse which input was given, select from option_list.
Understood input is [cmd]<num>, where [cmd] is either empty (`select`)
or one of the supported actions `look`, `edit` or `add` depending on
which processors are available.
"""
available_choices = kwargs.get("available_choices", [])
match = re.search(r"(^[a-zA-Z]*)\s*([0-9]*)$", raw_string)
cmd, number = match.groups()
mode, selection = None, None
if number:
number = int(number) - 1
cmd = cmd.lower().strip()
if cmd.startswith("e") or cmd.startswith("a") and edit:
mode = "edit"
elif examine:
mode = "examine"
try:
selection = available_choices[number]
except IndexError:
caller.msg("|rInvalid index")
mode = None
else:
caller.msg("|rMust supply a number.")
return mode, selection
def _relay_to_edit_or_add(caller, raw_string, **kwargs):
pass
def _list_node(caller, raw_string, **kwargs):
mode = kwargs.get("list_mode", None)
option_list = option_generator() if callable(option_generator) else option_generator
nall_options = len(option_list)
@ -1035,71 +1225,104 @@ def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
page = pages[page_index]
entry = None
extra_text = None
# dynamic, multi-page option list. We use _input_parser as a goto-callable,
# with the `goto_processor` redirecting when we leave the node.
options = [{"desc": opt,
"goto": (_input_parser,
{"available_choices": page,
"selection_processor": goto_processor})} for opt in page]
if mode == "arbitrary":
# freeform input, we must parse it for the allowed commands (look/edit)
mode, entry = _input_parser(caller, raw_string,
**{"available_choices": page})
if npages > 1:
# if the goto callable returns None, the same node is rerun, and
# kwargs not used by the callable are passed on to the node. This
# allows us to call ourselves over and over, using different kwargs.
options.append({"key": ("|Wcurrent|n", "c"),
"desc": "|W({}/{})|n".format(page_index + 1, npages),
"goto": (lambda caller: None,
{"optionpage_index": page_index})})
if page_index > 0:
options.append({"key": ("|wp|Wrevious page|n", "p"),
"goto": (lambda caller: None,
{"optionpage_index": page_index - 1})})
if page_index < npages - 1:
options.append({"key": ("|wn|Wext page|n", "n"),
"goto": (lambda caller: None,
{"optionpage_index": page_index + 1})})
# this catches arbitrary input, notably to examine entries ('look 4' or 'l4' etc)
options.append({"key": "_default",
"goto": (lambda caller: None,
{"show_detail": True, "optionpage_index": page_index})})
# update text with detail, if set. Here we call _input_parser like a normal function
text_detail = None
if raw_string and 'show_detail' in kwargs:
text_detail = _input_parser(
caller, raw_string, **{"available_choices": page,
"selection_processor": examine_processor})
if text_detail is None:
text_detail = "|rThat's not a valid command or option.|n"
# add data from the decorated node
text = ''
extra_options = []
try:
text, extra_options = func(caller, raw_string)
except TypeError:
if examine and mode: # == "look":
# look mode - we are examining a given entry
try:
text, extra_options = func(caller)
text = examine(caller, entry)
except Exception:
raise
except Exception:
logger.log_trace()
print("extra_options:", extra_options)
logger.log_trace()
text = "|rCould not view."
options = [{"key": ("|wb|Wack|n", "b"),
"goto": (lambda caller: None,
{"optionpage_index": page_index})},
{"key": "_default",
"goto": (lambda caller: None,
{"optionpage_index": page_index})}]
return text, options
# if edit and mode == "edit":
# pass
# elif add and mode == "add":
# # add mode - we are adding a new entry
# pass
else:
if isinstance(extra_options, {}):
extra_options = [extra_options]
# normal mode - list
pass
if select:
# We have a processor to handle selecting an entry
# dynamic, multi-page option list. Each selection leads to the `select`
# callback being called with a result from the available choices
options = [{"desc": opt,
"goto": (_select_parser,
{"available_choices": page})} for opt in page]
if add:
# We have a processor to handle adding a new entry. Re-run this node
# in the 'add' mode
options.append({"key": ("|wadd|Wdd new|n", "a"),
"goto": (lambda caller: None,
{"optionpage_index": page_index,
"list_mode": "add"})})
if npages > 1:
# if the goto callable returns None, the same node is rerun, and
# kwargs not used by the callable are passed on to the node. This
# allows us to call ourselves over and over, using different kwargs.
options.append({"key": ("|Wcurrent|n", "c"),
"desc": "|W({}/{})|n".format(page_index + 1, npages),
"goto": (lambda caller: None,
{"optionpage_index": page_index})})
if page_index > 0:
options.append({"key": ("|wp|Wrevious page|n", "p"),
"goto": (lambda caller: None,
{"optionpage_index": page_index - 1})})
if page_index < npages - 1:
options.append({"key": ("|wn|Wext page|n", "n"),
"goto": (lambda caller: None,
{"optionpage_index": page_index + 1})})
# this catches arbitrary input and reruns this node with the 'arbitrary' mode
# this could mean input on the form 'look <num>' or 'edit <num>'
options.append({"key": "_default",
"goto": (lambda caller: None,
{"optionpage_index": page_index,
"available_choices": page,
"list_mode": "arbitrary"})})
# add data from the decorated node
extra_options = []
try:
text, extra_options = func(caller, raw_string)
except TypeError:
try:
text, extra_options = func(caller)
except Exception:
raise
except Exception:
logger.log_trace()
print("extra_options:", extra_options)
else:
extra_options = make_iter(extra_options)
if isinstance(extra_options, {}):
extra_options = [extra_options]
else:
extra_options = make_iter(extra_options)
options.extend(extra_options)
text = text + "\n\n" + text_detail if text_detail else text
text += "\n\n(Make a choice or enter 'look <num>' to examine an option closer)"
options.extend(extra_options)
text = text + "\n\n" + extra_text if extra_text else text
text += "\n\n(Make a choice or enter 'look <num>' to examine an option closer)"
return text, options
return text, options
return _list_node
return decorator

View file

@ -690,9 +690,11 @@ def spawn(*prototypes, **kwargs):
# Helper functions
def _get_menu_metaprot(caller):
metaproto = None
if hasattr(caller.ndb._menutree, "olc_metaprot"):
return caller.ndb._menutree.olc_metaprot
else:
metaproto = caller.ndb._menutree.olc_metaprot
if not metaproto:
metaproto = build_metaproto(None, '', [], [], None)
caller.ndb._menutree.olc_metaprot = metaproto
caller.ndb._menutree.olc_new = True
@ -931,7 +933,7 @@ def _prototype_select(caller, prototype):
return ret
@list_node(_all_prototypes, _prototype_examine, _prototype_select)
@list_node(_all_prototypes, _prototype_select, examine=_prototype_examine)
def node_prototype(caller):
metaprot = _get_menu_metaprot(caller)
prot = metaprot.prototype
@ -979,7 +981,7 @@ def _typeclass_select(caller, typeclass):
return ret
@list_node(_all_typeclasses, _typeclass_examine, _typeclass_select)
@list_node(_all_typeclasses, _typeclass_select, examine=_typeclass_examine)
def node_typeclass(caller):
metaprot = _get_menu_metaprot(caller)
prot = metaprot.prototype