From ed3e57edd030f2a4d7c00de01b2168fb3aaeb260 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 2 Apr 2018 18:46:55 +0200 Subject: [PATCH] Start add edit_node decorator (untested) --- evennia/utils/evmenu.py | 427 +++++++++++++++++++++++++++++---------- evennia/utils/spawner.py | 10 +- 2 files changed, 331 insertions(+), 106 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index cccda6798f..e9e3f1c8af 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -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], 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 ' or 'edit ' + 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 ' 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 ' to examine an option closer)" - return text, options + return text, options return _list_node return decorator diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index bab8cdc9a1..eaab84b202 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -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