From ccfcf37e33c69aa3f69475ab1144277d3ead7b25 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 17 Feb 2017 21:18:15 +0100 Subject: [PATCH] Convert EvMenu to use overridable methods instead of plugin functions. Implements #1205. --- evennia/utils/evmenu.py | 356 +++++++++++++++++++--------------------- evennia/utils/tests.py | 14 ++ 2 files changed, 179 insertions(+), 191 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 987a8ba184..b96a6b9e9e 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -227,16 +227,13 @@ class CmdEvMenuNode(Command): # this will re-start a completely new evmenu call. saved_options = caller.attributes.get("_menutree_saved") if saved_options: - startnode_tuple = caller.attributes.get("_menutree_saved_startnode") - try: - startnode, startnode_input = startnode_tuple - except ValueError: # old form of startnode stor - startnode, startnode_input = startnode_tuple, "" + startnode, startnode_input = caller.attributes.get("_menutree_saved_startnode") if startnode: - saved_options[1]["startnode"] = startnode - saved_options[1]["startnode_input"] = startnode_input + saved_options[2]["startnode"] = startnode + saved_options[2]["startnode_input"] = startnode_input + MenuClass = saved_options[0] # this will create a completely new menu call - EvMenu(caller, *saved_options[0], **saved_options[1]) + MenuClass(caller, *saved_options[1], **saved_options[2]) return True return None @@ -264,7 +261,7 @@ class CmdEvMenuNode(Command): # can be either Player, Object or Session (in the latter case this info will be superfluous). caller.ndb._menutree._session = self.session # we have a menu, use it. - menu._input_parser(menu, self.raw_string, caller) + menu.parse_input(self.raw_string) class EvMenuCmdSet(CmdSet): @@ -286,130 +283,6 @@ class EvMenuCmdSet(CmdSet): self.add(CmdEvMenuNode()) -# These are default node formatters -def dedent_strip_nodetext_formatter(nodetext, has_options, caller=None): - """ - Simple dedent formatter that also strips text - """ - return dedent(nodetext).strip() - - -def dedent_nodetext_formatter(nodetext, has_options, caller=None): - """ - Just dedent text. - """ - return dedent(nodetext) - - -def evtable_options_formatter(optionlist, caller=None): - """ - Formats the option list display. - """ - if not optionlist: - return "" - - # column separation distance - colsep = 4 - - nlist = len(optionlist) - - # get the widest option line in the table. - table_width_max = -1 - table = [] - for key, desc in optionlist: - if not (key or desc): - continue - table_width_max = max(table_width_max, - max(m_len(p) for p in key.split("\n")) + - max(m_len(p) for p in desc.split("\n")) + colsep) - raw_key = strip_ansi(key) - if raw_key != key: - # already decorations in key definition - table.append(" |lc%s|lt%s|le: %s" % (raw_key, key, desc)) - else: - # add a default white color to key - table.append(" |lc%s|lt|w%s|n|le: %s" % (raw_key, raw_key, desc)) - - ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols - - # get the amount of rows needed (start with 4 rows) - nrows = 4 - while nrows * ncols < nlist: - nrows += 1 - ncols = nlist // nrows # number of full columns - nlastcol = nlist % nrows # number of elements in last column - - # get the final column count - ncols = ncols + 1 if nlastcol > 0 else ncols - if ncols > 1: - # only extend if longer than one column - table.extend([" " for i in range(nrows - nlastcol)]) - - # build the actual table grid - table = [table[icol * nrows : (icol * nrows) + nrows] for icol in range(0, ncols)] - - # adjust the width of each column - for icol in range(len(table)): - col_width = max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep - table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]] - - # format the table into columns - return unicode(EvTable(table=table, border="none")) - - -def underline_node_formatter(nodetext, optionstext, caller=None): - """ - Draws a node with underlines '_____' around it. - """ - nodetext_width_max = max(m_len(line) for line in nodetext.split("\n")) - options_width_max = max(m_len(line) for line in optionstext.split("\n")) - total_width = max(options_width_max, nodetext_width_max) - separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" - separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" - return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext - - -def null_node_formatter(nodetext, optionstext, caller=None): - """ - A minimalistic node formatter, no lines or frames. - """ - return nodetext + "\n\n" + optionstext - - -def evtable_parse_input(menuobject, raw_string, caller): - """ - Processes the user's node inputs. - - Args: - menuobject (EvMenu): The EvMenu instance - raw_string (str): The incoming raw_string from the menu - command. - caller (Object, Player or Session): The entity using - the menu. - """ - cmd = raw_string.strip().lower() - - if cmd in menuobject.options: - # this will take precedence over the default commands - # below - goto, callback = menuobject.options[cmd] - menuobject.callback_goto(callback, goto, raw_string) - elif menuobject.auto_look and cmd in ("look", "l"): - menuobject.display_nodetext() - elif menuobject.auto_help and cmd in ("help", "h"): - menuobject.display_helptext() - elif menuobject.auto_quit and cmd in ("quit", "q", "exit"): - menuobject.close_menu() - elif menuobject.default: - goto, callback = menuobject.default - menuobject.callback_goto(callback, goto, raw_string) - else: - caller.msg(_HELP_NO_OPTION_MATCH, session=menuobject._session) - - if not (menuobject.options or menuobject.default): - # no options - we are at the end of the menu. - menuobject.close_menu() - #------------------------------------------------------------ # # Menu main class @@ -426,10 +299,6 @@ class EvMenu(object): cmdset_mergetype="Replace", cmdset_priority=1, auto_quit=True, auto_look=True, auto_help=True, cmd_on_exit="look", - nodetext_formatter=dedent_strip_nodetext_formatter, - options_formatter=evtable_options_formatter, - node_formatter=underline_node_formatter, - input_parser=evtable_parse_input, persistent=False, startnode_input="", session=None, **kwargs): """ @@ -476,38 +345,6 @@ class EvMenu(object): The callback function takes two parameters, the caller then the EvMenu object. This is called after cleanup is complete. Set to None to not call any command. - nodetext_formatter (callable, optional): This callable should be on - the form `function(nodetext, has_options, caller=None)`, where `nodetext` is the - node text string and `has_options` a boolean specifying if there - are options associated with this node. It must return a formatted - string. `caller` is optionally a reference to the user of the menu. - `caller` is optionally a reference to the user of the menu. - options_formatter (callable, optional): This callable should be on - the form `function(optionlist, caller=None)`, where ` optionlist is a list - of option dictionaries, like - [{"key":..., "desc",..., "goto": ..., "exec",...}, ...] - Each dictionary describes each possible option. Note that this - will also be called if there are no options, and so should be - able to handle an empty list. This should - be formatted into an options list and returned as a string, - including the required separator to use between the node text - and the options. If not given the default EvMenu style will be used. - `caller` is optionally a reference to the user of the menu. - node_formatter (callable, optional): This callable should be on the - form `func(nodetext, optionstext, caller=None)` where the arguments are strings - representing the node text and options respectively (possibly prepared - by `nodetext_formatter`/`options_formatter` or by the default styles). - It should return a string representing the final look of the node. This - can e.g. be used to create line separators that take into account the - dynamic width of the parts. `caller` is optionally a reference to the - user of the menu. - input_parser (callable, optional): This callable is responsible for parsing the - options dict from a node and has the form `func(menuobject, raw_string, caller)`, - where menuobject is the active `EvMenu` instance, `input_string` is the - incoming text from the caller and `caller` is the user of the menu. - It should use the helper method of the menuobject to goto new nodes, show - help texts etc. See the default `evtable_parse_input` function for help - with parsing. persistent (bool, optional): Make the Menu persistent (i.e. it will survive a reload. This will make the Menu cmdset persistent. Use with caution - if your menu is buggy you may end up in a state @@ -548,11 +385,6 @@ class EvMenu(object): """ self._startnode = startnode self._menutree = self._parse_menudata(menudata) - - self._nodetext_formatter = nodetext_formatter - self._options_formatter = options_formatter - self._node_formatter = node_formatter - self._input_parser = input_parser self._persistent = persistent if startnode not in self._menutree: @@ -561,6 +393,8 @@ class EvMenu(object): # public variables made available to the command self.caller = caller + + # track EvMenu kwargs self.auto_quit = auto_quit self.auto_look = auto_look self.auto_help = auto_help @@ -573,15 +407,16 @@ class EvMenu(object): self.cmd_on_exit = cmd_on_exit else: self.cmd_on_exit = None + # current menu state self.default = None self.nodetext = None self.helptext = None self.options = None # assign kwargs as initialization vars on ourselves. - if set(("_startnode", "_menutree", "_nodetext_formatter", "_options_formatter", - "node_formatter", "_input_parser", "_peristent", "cmd_on_exit", "default", - "nodetext", "helptext", "options")).intersection(set(kwargs.keys())): + if set(("_startnode", "_menutree", "_session", "_persistent", + "cmd_on_exit", "default", "nodetext", "helptext", + "options", "cmdset_mergetype", "auto_quit")).intersection(set(kwargs.keys())): raise RuntimeError("One or more of the EvMenu `**kwargs` is reserved by EvMenu for internal use.") for key, val in kwargs.iteritems(): setattr(self, key, val) @@ -591,17 +426,17 @@ class EvMenu(object): if persistent: # save the menu to the database + calldict = {"startnode": startnode, + "cmdset_mergetype": cmdset_mergetype, + "cmdset_priority": cmdset_priority, + "auto_quit": auto_quit, + "auto_look": auto_look, + "auto_help": auto_help, + "cmd_on_exit": cmd_on_exit, + "persistent": persistent} + calldict.update(kwargs) try: - caller.attributes.add("_menutree_saved", - ((menudata, ), - {"startnode": startnode, - "cmdset_mergetype": cmdset_mergetype, - "cmdset_priority": cmdset_priority, - "auto_quit": auto_quit, "auto_look": auto_look, "auto_help": auto_help, - "cmd_on_exit": cmd_on_exit, - "nodetext_formatter": nodetext_formatter, "options_formatter": options_formatter, - "node_formatter": node_formatter, "input_parser": input_parser, - "persistent": persistent,})) + caller.attributes.add("_menutree_saved", ( self.__class__, (menudata, ), calldict )) caller.attributes.add("_menutree_saved_startnode", (startnode, startnode_input)) except Exception as err: caller.msg(_ERROR_PERSISTENT_SAVING.format(error=err), session=self._session) @@ -662,13 +497,13 @@ class EvMenu(object): """ # handle the node text - nodetext = self._nodetext_formatter(nodetext, len(optionlist), self.caller) + nodetext = self.nodetext_formatter(nodetext) # handle the options - optionstext = self._options_formatter(optionlist, self.caller) + optionstext = self.options_formatter(optionlist) # format the entire node - return self._node_formatter(nodetext, optionstext, self.caller) + return self.node_formatter(nodetext, optionstext) def _execute_node(self, nodename, raw_string): @@ -774,7 +609,10 @@ class EvMenu(object): try: # execute the node ret = self._execute_node(nodename, raw_string) - except EvMenuError: + 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 @@ -871,6 +709,142 @@ class EvMenu(object): if self.cmd_on_exit is not None: self.cmd_on_exit(self.caller, self) + def parse_input(self, raw_string): + """ + Parses the incoming string from the menu user. + + Args: + raw_string (str): The incoming, unmodified string + from the user. + Notes: + This method is expected to parse input and use the result + to relay execution to the relevant methods of the menu. It + should also report errors directly to the user. + + """ + cmd = raw_string.strip().lower() + + 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) + elif self.auto_look and cmd in ("look", "l"): + self.display_nodetext() + elif self.auto_help and cmd in ("help", "h"): + self.display_helptext() + 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) + else: + self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session) + + if not (self.options or self.default): + # no options - we are at the end of the menu. + self.close_menu() + + # formatters - override in a child class + + def nodetext_formatter(self, nodetext): + """ + Format the node text itself. + + Args: + nodetext (str): The full node text (the text describing the node). + + Returns: + nodetext (str): The formatted node text. + + """ + return dedent(nodetext).strip() + + def options_formatter(self, optionlist): + """ + Formats the option block. + + Args: + optionlist (list): List of (key, description) tuples for every + option related to this node. + caller (Object, Player or None, optional): The caller of the node. + + Returns: + options (str): The formatted option display. + + """ + if not optionlist: + return "" + + # column separation distance + colsep = 4 + + nlist = len(optionlist) + + # get the widest option line in the table. + table_width_max = -1 + table = [] + for key, desc in optionlist: + if not (key or desc): + continue + table_width_max = max(table_width_max, + max(m_len(p) for p in key.split("\n")) + + max(m_len(p) for p in desc.split("\n")) + colsep) + raw_key = strip_ansi(key) + if raw_key != key: + # already decorations in key definition + table.append(" |lc%s|lt%s|le: %s" % (raw_key, key, desc)) + else: + # add a default white color to key + table.append(" |lc%s|lt|w%s|n|le: %s" % (raw_key, raw_key, desc)) + + ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols + + # get the amount of rows needed (start with 4 rows) + nrows = 4 + while nrows * ncols < nlist: + nrows += 1 + ncols = nlist // nrows # number of full columns + nlastcol = nlist % nrows # number of elements in last column + + # get the final column count + ncols = ncols + 1 if nlastcol > 0 else ncols + if ncols > 1: + # only extend if longer than one column + table.extend([" " for i in range(nrows - nlastcol)]) + + # build the actual table grid + table = [table[icol * nrows : (icol * nrows) + nrows] for icol in range(0, ncols)] + + # adjust the width of each column + for icol in range(len(table)): + col_width = max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep + table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]] + + # format the table into columns + return unicode(EvTable(table=table, border="none")) + + def node_formatter(self, nodetext, optionstext): + """ + Formats the entirety of the node. + + Args: + nodetext (str): The node text as returned by `self.nodetext_formatter`. + optionstext (str): The options display as returned by `self.options_formatter`. + caller (Object, Player or None, optional): The caller of the node. + + Returns: + node (str): The formatted node to display. + + """ + nodetext_width_max = max(m_len(line) for line in nodetext.split("\n")) + options_width_max = max(m_len(line) for line in optionstext.split("\n")) + total_width = max(options_width_max, nodetext_width_max) + separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" + separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" + return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext + + # ------------------------------------------------------------------------------------------------- # diff --git a/evennia/utils/tests.py b/evennia/utils/tests.py index 247c8ad0b3..f7fd203e2c 100644 --- a/evennia/utils/tests.py +++ b/evennia/utils/tests.py @@ -321,6 +321,20 @@ class TestTextToHTMLparser(TestCase): self.assertEqual(self.parser.convert_urls('http://example.com/'), 'http://example.com/') +from evennia.utils import evmenu +from mock import Mock +class TestEvMenu(TestCase): + "Run the EvMenu test." + def setUp(self): + self.caller = Mock() + self.caller.msg = Mock() + self.menu = evmenu.EvMenu(self.caller, "evennia.utils.evmenu", startnode="test_start_node", + persistent=True, cmdset_mergetype="Replace", testval="val", testval2="val2") + + def test_kwargsave(self): + self.assertTrue(hasattr(self.menu, "testval")) + self.assertTrue(hasattr(self.menu, "testval2")) + from evennia.utils import inlinefuncs