diff --git a/CHANGELOG.md b/CHANGELOG.md index a6665a90f4..a691a5c64f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - new `drop:holds()` lock default to limit dropping nonsensical things. Access check defaults to True for backwards-compatibility in 0.9, will be False in 1.0 -### Already in master +### Evennia 0.95 (master) - `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False - `py` command now reroutes stdout to output results in-game client. `py` without arguments starts a full interactive Python console. @@ -74,7 +74,12 @@ without arguments starts a full interactive Python console. pagination (e.g. to create EvTables for every page instead of splittine one table) - Using `EvMore pagination`, dramatically improves performance of `spawn/list` and `scripts` listings (100x speed increase for displaying 1000+ prototypes/scripts). - +- `EvMenu` now uses the more logically named `.ndb._evmenu` instead of `.ndb._menutree` to store itself. + Both still work for backward compatibility, but `_menutree` is deprecated. +- `EvMenu.msg(txt)` added as a central place to send text to the user, makes it easier to override. + Default `EvMenu.msg` sends with OOB type="menu" for use with OOB and webclient pane-redirects. +- New EvMenu templating system for quickly building simpler EvMenus without as much code. + ## Evennia 0.9 (2018-2019) diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index b22a80ed19..a8839f2ad0 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -494,6 +494,11 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string) cmdset = None for cset in (cset for cset in local_obj_cmdsets if cset): cset.duplicates = cset.old_duplicates + # important - this syncs the CmdSetHandler's .current field with the + # true current cmdset! + if cmdset: + caller.cmdset.current = cmdset + returnValue(cmdset) except ErrorReported: raise diff --git a/evennia/commands/cmdset.py b/evennia/commands/cmdset.py index f296982905..4437ac7a6c 100644 --- a/evennia/commands/cmdset.py +++ b/evennia/commands/cmdset.py @@ -106,9 +106,9 @@ class CmdSet(object, metaclass=_CmdSetMeta): commands preference. duplicates - determines what happens when two sets of equal - priority merge. Default has the first of them in the + priority merge (only). Defaults to None and has the first of them in the merger (i.e. A above) automatically taking - precedence. But if allow_duplicates is true, the + precedence. But if `duplicates` is true, the result will be a merger with more than one of each name match. This will usually lead to the account receiving a multiple-match error higher up the road, @@ -119,6 +119,16 @@ class CmdSet(object, metaclass=_CmdSetMeta): select which ball to kick ... Allowing duplicates only makes sense for Union and Intersect, the setting is ignored for the other mergetypes. + Note that the `duplicates` flag is *not* propagated in + a cmdset merger. So `A + B = C` will result in + a cmdset with duplicate commands, but C.duplicates will + be `None`. For duplication to apply to a whole cmdset + stack merge, _all_ cmdsets in the stack must have + `.duplicates=True` set. + Finally, if a final cmdset has `.duplicates=None` (the normal + unless created alone with another value), the cmdhandler + will assume True for object-based cmdsets and False for + all other. This is usually the most intuitive outcome. key_mergetype (dict) - allows the cmdset to define a unique mergetype for particular cmdsets. Format is @@ -144,14 +154,27 @@ class CmdSet(object, metaclass=_CmdSetMeta): mergetype = "Union" priority = 0 - # These flags, if set to None, will allow "pass-through" of lower-prio settings - # of True/False. If set to True/False, will override lower-prio settings. + # These flags, if set to None should be interpreted as 'I don't care' and, + # will allow "pass-through" even of lower-prio cmdsets' explicitly True/False + # options. If this is set to True/False however, priority matters. no_exits = None no_objs = None no_channels = None - # same as above, but if left at None in the final merged set, the - # cmdhandler will auto-assume True for Objects and stay False for all - # other entities. + # The .duplicates setting does not propagate and since duplicates can only happen + # on same-prio cmdsets, there is no concept of passthrough on `None`. + # The merger of two cmdsets always return in a cmdset with `duplicates=None` + # (even if the result may have duplicated commands). + # If a final cmdset has `duplicates=None` (normal, unless the cmdset is + # created on its own with the flag set), the cmdhandler will auto-assume it to be + # True for Object-based cmdsets and stay None/False for all other entities. + # + # Example: + # A and C has .duplicates=True, B has .duplicates=None (or False) + # B + A = BA, where BA will have duplicate cmds, but BA.duplicates = None + # BA + C = BAC, where BAC will have more duplication, but BAC.duplicates = None + # + # Basically, for the `.duplicate` setting to survive throughout a + # merge-stack, every cmdset in the stack must have `duplicates` set explicitly. duplicates = None permanent = False @@ -334,7 +357,15 @@ class CmdSet(object, metaclass=_CmdSetMeta): commands (str): Representation of commands in Cmdset. """ - return ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)]) + perm = "perm" if self.permanent else "non-perm" + options = ", ".join([ + "{}:{}".format(opt, "T" if getattr(self, opt) else "F") + for opt in ("no_exits", "no_objs", "no_channels", "duplicates") + if getattr(self, opt) is not None + ]) + options = (", " + options) if options else "" + return f": " + ", ".join( + [str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)]) def __iter__(self): """ @@ -401,12 +432,15 @@ class CmdSet(object, metaclass=_CmdSetMeta): # pass through options whenever they are set, unless the merging or higher-prio # set changes the setting (i.e. has a non-None value). We don't pass through - # the duplicates setting; that is per-merge + # the duplicates setting; that is per-merge; the resulting .duplicates value + # is always None (so merging cmdsets must all have explicit values if wanting + # to cause duplicates). cmdset_c.no_channels = ( self.no_channels if cmdset_a.no_channels is None else cmdset_a.no_channels ) cmdset_c.no_exits = self.no_exits if cmdset_a.no_exits is None else cmdset_a.no_exits cmdset_c.no_objs = self.no_objs if cmdset_a.no_objs is None else cmdset_a.no_objs + cmdset_c.duplicates = None else: # B higher priority than A @@ -428,12 +462,15 @@ class CmdSet(object, metaclass=_CmdSetMeta): # pass through options whenever they are set, unless the higher-prio # set changes the setting (i.e. has a non-None value). We don't pass through - # the duplicates setting; that is per-merge + # the duplicates setting; that is per-merge; the resulting .duplicates value# + # is always None (so merging cmdsets must all have explicit values if wanting + # to cause duplicates). cmdset_c.no_channels = ( cmdset_a.no_channels if self.no_channels is None else self.no_channels ) cmdset_c.no_exits = cmdset_a.no_exits if self.no_exits is None else self.no_exits cmdset_c.no_objs = cmdset_a.no_objs if self.no_objs is None else self.no_objs + cmdset_c.duplicates = None # we store actual_mergetype since key_mergetypes # might be different from the main mergetype. diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index b2eb95c886..51853e8256 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -293,7 +293,10 @@ class CmdSetHandler(object): # the id of the "merged" current cmdset for easy access. self.key = None - # this holds the "merged" current command set + # this holds the "merged" current command set. Note that while the .update + # method updates this field in order to have it synced when operating on + # cmdsets in-code, when the game runs, this field is kept up-to-date by + # the cmdsethandler's get_and_merge_cmdsets! self.current = None # this holds a history of CommandSets self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)] @@ -311,27 +314,13 @@ class CmdSetHandler(object): Display current commands """ - string = "" + strings = [" stack:"] mergelist = [] if len(self.cmdset_stack) > 1: # We have more than one cmdset in stack; list them all for snum, cmdset in enumerate(self.cmdset_stack): - mergetype = self.mergetype_stack[snum] - permstring = "non-perm" - if cmdset.permanent: - permstring = "perm" - if mergetype != cmdset.mergetype: - mergetype = "%s^" % (mergetype) - string += "\n %i: <%s (%s, prio %i, %s)>: %s" % ( - snum, - cmdset.key, - mergetype, - cmdset.priority, - permstring, - cmdset, - ) - mergelist.append(str(snum)) - string += "\n" + mergelist.append(str(snum + 1)) + strings.append(f" {snum + 1}: {cmdset}") # Display the currently active cmdset, limited by self.obj's permissions mergetype = self.mergetype_stack[-1] @@ -339,27 +328,15 @@ class CmdSetHandler(object): merged_on = self.cmdset_stack[-2].key mergetype = _("custom {mergetype} on cmdset '{cmdset}'") mergetype = mergetype.format(mergetype=mergetype, cmdset=merged_on) + if mergelist: - tmpstring = _(" : {current}") - string += tmpstring.format( - mergelist="+".join(mergelist), - mergetype=mergetype, - prio=self.current.priority, - current=self.current, - ) + # current is a result of mergers + mergelist="+".join(mergelist) + strings.append(f" : {self.current}") else: - permstring = "non-perm" - if self.current.permanent: - permstring = "perm" - tmpstring = _(" <{key} ({mergetype}, prio {prio}, {permstring})>:\n {keylist}") - string += tmpstring.format( - key=self.current.key, - mergetype=mergetype, - prio=self.current.priority, - permstring=permstring, - keylist=", ".join(cmd.key for cmd in sorted(self.current, key=lambda o: o.key)), - ) - return string.strip() + # current is a single cmdset + strings.append(" " + str(self.current)) + return "\n".join(strings).rstrip() def _import_cmdset(self, cmdset_path, emit_to_obj=None): """ @@ -381,12 +358,22 @@ class CmdSetHandler(object): def update(self, init_mode=False): """ Re-adds all sets in the handler to have an updated current - set. Args: init_mode (bool, optional): Used automatically right after this handler was created; it imports all permanent cmdsets from the database. + + Notes: + This method is necessary in order to always have a `.current` + cmdset when working with the cmdsethandler in code. But the + CmdSetHandler doesn't (cannot) consider external cmdsets and game + state. This means that the .current calculated from this method + will likely not match the true current cmdset as determined at + run-time by `cmdhandler.get_and_merge_cmdsets()`. So in a running + game the responsibility of keeping `.current` upt-to-date belongs + to the central `cmdhandler.get_and_merge_cmdsets()`! + """ if init_mode: # reimport all permanent cmdsets diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index e79b0d3cce..babf583391 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2429,13 +2429,13 @@ class CmdExamine(ObjManipCommand): ) return output - def format_output(self, obj, avail_cmdset): + def format_output(self, obj, current_cmdset): """ Helper function that creates a nice report about an object. Args: obj (any): Object to analyze. - avail_cmdset (CmdSet): Current cmdset for object. + current_cmdset (CmdSet): Current cmdset for object. Returns: str: The formatted string. @@ -2513,15 +2513,36 @@ class CmdExamine(ObjManipCommand): # cmdsets if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"): # all() returns a 'stack', so make a copy to sort. + + def _format_options(cmdset): + """helper for cmdset-option display""" + def _truefalse(string, value): + if value is None: + return "" + if value: + return f"{string}: T" + return f"{string}: F" + options = ", ".join( + _truefalse(opt, getattr(cmdset, opt)) + for opt in ("no_exits", "no_objs", "no_channels", "duplicates") + if getattr(cmdset, opt) is not None + ) + options = ", " + options if options else "" + return options + + # cmdset stored on us stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True) - output["Stored Cmdset(s)"] = "\n " + "\n ".join( - f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority})" - for cmdset in stored_cmdsets - if cmdset.key != "_EMPTY_CMDSET" - ) + stored = [] + for cmdset in stored_cmdsets: + if cmdset.key == "_EMPTY_CMDSET": + continue + options = _format_options(cmdset) + stored.append( + f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority}{options})") + output["Stored Cmdset(s)"] = "\n " + "\n ".join(stored) # this gets all components of the currently merged set - all_cmdsets = [(cmdset.key, cmdset) for cmdset in avail_cmdset.merged_from] + all_cmdsets = [(cmdset.key, cmdset) for cmdset in current_cmdset.merged_from] # we always at least try to add account- and session sets since these are ignored # if we merge on the object level. if hasattr(obj, "account") and obj.account: @@ -2551,15 +2572,24 @@ class CmdExamine(ObjManipCommand): pass all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()] all_cmdsets.sort(key=lambda x: x.priority, reverse=True) - output["Merged Cmdset(s)"] = "\n " + "\n ".join( - f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority})" - for cmdset in all_cmdsets - ) - # list the commands available to this object - avail_cmdset = sorted([cmd.key for cmd in avail_cmdset if cmd.access(obj, "cmd")]) - cmdsetstr = "\n" + utils.fill(", ".join(avail_cmdset), indent=2) + # the resulting merged cmdset + options = _format_options(current_cmdset) + merged = [ + f" ({current_cmdset.mergetype} prio {current_cmdset.priority}{options})"] + + # the merge stack + for cmdset in all_cmdsets: + options = _format_options(cmdset) + merged.append( + f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority}{options})") + output["Merged Cmdset(s)"] = "\n " + "\n ".join(merged) + + # list the commands available to this object + current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")]) + cmdsetstr = "\n" + utils.fill(", ".join(current_commands), indent=2) output[f"Commands available to {obj.key} (result of Merged CmdSets)"] = str(cmdsetstr) + # scripts if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all(): output["Scripts"] = "\n " + f"{obj.scripts}" diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 8227de27bf..0ad9a932fc 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -971,7 +971,8 @@ class TestBuilding(CommandTest): self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room") def test_list_cmdsets(self): - self.call(building.CmdListCmdSets(), "", ":") + self.call(building.CmdListCmdSets(), "", + " stack:\n :") self.call(building.CmdListCmdSets(), "NotFound", "Could not find 'NotFound'") def test_typeclass(self): diff --git a/evennia/commands/tests.py b/evennia/commands/tests.py index a1ab9809f5..bb508a31c9 100644 --- a/evennia/commands/tests.py +++ b/evennia/commands/tests.py @@ -194,27 +194,71 @@ class TestCmdSetMergers(TestCase): self.assertEqual(len(cmdset_f.commands), 4) self.assertTrue(all(True for cmd in cmdset_f.commands if cmd.from_cmdset == "A")) - def test_option_transfer(self): - "Test transfer of cmdset options" + +class TestOptionTransferTrue(TestCase): + """ + Test cmdset-merge transfer of the cmdset-special options + (no_exits/channels/objs/duplicates etc) + + cmdset A has all True options + + """ + + def setUp(self): + super().setUp() + self.cmdset_a = _CmdSetA() + self.cmdset_b = _CmdSetB() + self.cmdset_c = _CmdSetC() + self.cmdset_d = _CmdSetD() + self.cmdset_a.priority = 0 + self.cmdset_b.priority = 0 + self.cmdset_c.priority = 0 + self.cmdset_d.priority = 0 + self.cmdset_a.no_exits = True + self.cmdset_a.no_objs = True + self.cmdset_a.no_channels = True + self.cmdset_a.duplicates = True + + def test_option_transfer__reverse_sameprio_passthrough(self): + """ + A has all True options, merges last (normal reverse merge), same prio. + The options should pass through to F since none of the other cmdsets + care to change the setting from their default None. + + Since A.duplicates = True, the final result is an union of duplicate + pairs (8 commands total). + + """ a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d - # the options should pass through since none of the other cmdsets care - # to change the setting from None. - a.no_exits = True - a.no_objs = True - a.no_channels = True - a.duplicates = True cmdset_f = d + c + b + a # reverse, same-prio self.assertTrue(cmdset_f.no_exits) self.assertTrue(cmdset_f.no_objs) self.assertTrue(cmdset_f.no_channels) - self.assertTrue(cmdset_f.duplicates) + self.assertIsNone(cmdset_f.duplicates) self.assertEqual(len(cmdset_f.commands), 8) + + def test_option_transfer__forward_sameprio_passthrough(self): + """ + A has all True options, merges first (forward merge), same prio. This + should pass those options through since the other all have options set + to None. The exception is `duplicates` since that is determined by + the two last mergers in the chain both being True. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d cmdset_f = a + b + c + d # forward, same-prio self.assertTrue(cmdset_f.no_exits) self.assertTrue(cmdset_f.no_objs) self.assertTrue(cmdset_f.no_channels) - self.assertFalse(cmdset_f.duplicates) + self.assertIsNone(cmdset_f.duplicates) self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_highprio_passthrough(self): + """ + A has all True options, merges last (normal reverse merge) with the + highest prio. This should also pass through. + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d a.priority = 2 b.priority = 1 c.priority = 0 @@ -223,14 +267,35 @@ class TestCmdSetMergers(TestCase): self.assertTrue(cmdset_f.no_exits) self.assertTrue(cmdset_f.no_objs) self.assertTrue(cmdset_f.no_channels) - self.assertTrue(cmdset_f.duplicates) + self.assertIsNone(cmdset_f.duplicates) self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_highprio_passthrough(self): + """ + A has all True options, merges first (forward merge). This is a bit + synthetic since it will never happen in practice, but logic should + still make it pass through. + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 1 + c.priority = 0 + d.priority = -1 cmdset_f = a + b + c + d # forward, A top priority. This never happens in practice. self.assertTrue(cmdset_f.no_exits) self.assertTrue(cmdset_f.no_objs) self.assertTrue(cmdset_f.no_channels) - self.assertTrue(cmdset_f.duplicates) + self.assertIsNone(cmdset_f.duplicates) self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_lowprio_passthrough(self): + """ + A has all True options, merges last (normal reverse merge) with the lowest + prio. This never happens (it would always merge first) but logic should hold + and pass through since the other cmdsets have None. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d a.priority = -1 b.priority = 0 c.priority = 1 @@ -239,32 +304,678 @@ class TestCmdSetMergers(TestCase): self.assertTrue(cmdset_f.no_exits) self.assertTrue(cmdset_f.no_objs) self.assertTrue(cmdset_f.no_channels) - self.assertFalse(cmdset_f.duplicates) + self.assertIsNone(cmdset_f.duplicates) self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_lowprio_passthrough(self): + """ + A has all True options, merges first (forward merge) with lowest prio. This + is the normal behavior for a low-prio cmdset. Passthrough should happen. + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = -1 + b.priority = 0 + c.priority = 1 + d.priority = 2 cmdset_f = a + b + c + d # forward, A low prio self.assertTrue(cmdset_f.no_exits) self.assertTrue(cmdset_f.no_objs) self.assertTrue(cmdset_f.no_channels) - self.assertFalse(cmdset_f.duplicates) + self.assertIsNone(cmdset_f.duplicates) self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_highprio_block_passthrough(self): + """ + A has all True options, other cmdsets has False. A merges last with high + prio. A should retain its option values and override the others + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 1 + c.priority = 0 + d.priority = -1 c.no_exits = False b.no_objs = False d.duplicates = False # higher-prio sets will change the option up the chain + cmdset_f = d + c + b + a # reverse, high prio + self.assertTrue(cmdset_f.no_exits) + self.assertTrue(cmdset_f.no_objs) + self.assertTrue(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_highprio_block_passthrough(self): + """ + A has all True options, other cmdsets has False. A merges last with high + prio. This situation should never happen, but logic should hold - the highest + prio's options should survive the merge process. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 1 + c.priority = 0 + d.priority = -1 + c.no_exits = False + b.no_channels = False + b.no_objs = False + d.duplicates = False + # higher-prio sets will change the option up the chain + cmdset_f = a + b + c + d # forward, high prio, never happens + self.assertTrue(cmdset_f.no_exits) + self.assertTrue(cmdset_f.no_objs) + self.assertTrue(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_lowprio_block(self): + """ + A has all True options, other cmdsets has False. A merges last with low + prio. This should result in its values being blocked and come out False. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = -1 + b.priority = 0 + c.priority = 1 + d.priority = 2 + c.no_exits = False + c.no_channels = False + b.no_objs = False + d.duplicates = False + # higher-prio sets will change the option up the chain + cmdset_f = a + b + c + d # forward, A low prio + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_lowprio_block_partial(self): + """ + A has all True options, other cmdsets has False excet C which has a None + for `no_channels`. A merges last with low + prio. This should result in its values being blocked and come out False + except for no_channels which passes through. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = -1 + b.priority = 0 + c.priority = 1 + d.priority = 2 + c.no_exits = False + c.no_channels = None # passthrough + b.no_objs = False + d.duplicates = False + # higher-prio sets will change the option up the chain cmdset_f = a + b + c + d # forward, A low prio self.assertFalse(cmdset_f.no_exits) self.assertFalse(cmdset_f.no_objs) self.assertTrue(cmdset_f.no_channels) - self.assertFalse(cmdset_f.duplicates) + self.assertIsNone(cmdset_f.duplicates) self.assertEqual(len(cmdset_f.commands), 4) - a.priority = 0 - b.priority = 0 + + def test_option_transfer__reverse_highprio_sameprio_order_last(self): + """ + A has all True options and highest prio, D has False and lowest prio, + others are passthrough. B has the same prio as A, with passthrough. + + Since A is merged last, this should give prio to A's options + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 2 c.priority = 0 - d.priority = 0 + d.priority = -1 + d.no_channels = False + d.no_exits = False + d.no_objs = None + d.duplicates = False + # higher-prio sets will change the option up the chain + cmdset_f = d + c + b + a # reverse, A same prio, merged after b + self.assertTrue(cmdset_f.no_exits) + self.assertTrue(cmdset_f.no_objs) + self.assertTrue(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 8) + + def test_option_transfer__reverse_highprio_sameprio_order_first(self): + """ + A has all True options and highest prio, D has False and lowest prio, + others are passthrough. B has the same prio as A, with passthrough. + + While B, with None-values, is merged after A, A's options should have + replaced those of D at that point, and since B has passthrough the + final result should contain A's True options. + + Note that despite A having duplicates=True, there is no duplication in + the DB + A merger since they have different priorities. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 2 + c.priority = 0 + d.priority = -1 + d.no_channels = False + d.no_exits = False + d.no_objs = False + d.duplicates = False + # higher-prio sets will change the option up the chain + cmdset_f = d + c + a + b # reverse, A same prio, merged before b + self.assertTrue(cmdset_f.no_exits) + self.assertTrue(cmdset_f.no_objs) + self.assertTrue(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_lowprio_block(self): + """ + A has all True options, other cmdsets has False. A merges last with low + prio. This usually doesn't happen- it should merge last. But logic should + hold and the low-prio cmdset's values should be blocked and come out False. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = -1 + b.priority = 0 + c.priority = 1 + d.priority = 2 + c.no_exits = False + d.no_channels = False + b.no_objs = False + d.duplicates = False + # higher-prio sets will change the option up the chain + cmdset_f = d + c + b + a # reverse, A low prio, never happens + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + +class TestOptionTransferFalse(TestCase): + """ + Test cmdset-merge transfer of the cmdset-special options + (no_exits/channels/objs/duplicates etc) + + cmdset A has all False options + + """ + + def setUp(self): + super().setUp() + self.cmdset_a = _CmdSetA() + self.cmdset_b = _CmdSetB() + self.cmdset_c = _CmdSetC() + self.cmdset_d = _CmdSetD() + self.cmdset_a.priority = 0 + self.cmdset_b.priority = 0 + self.cmdset_c.priority = 0 + self.cmdset_d.priority = 0 + self.cmdset_a.no_exits = False + self.cmdset_a.no_objs = False + self.cmdset_a.no_channels = False + self.cmdset_a.duplicates = False + + def test_option_transfer__reverse_sameprio_passthrough(self): + """ + A has all False options, merges last (normal reverse merge), same prio. + The options should pass through to F since none of the other cmdsets + care to change the setting from their default None. + + Since A has duplicates=False, the result is a unique union of 4 cmds. + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + cmdset_f = d + c + b + a # reverse, same-prio + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_sameprio_passthrough(self): + """ + A has all False options, merges first (forward merge), same prio. This + should pass those options through since the other all have options set + to None. The exception is `duplicates` since that is determined by + the two last mergers in the chain both being . + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + cmdset_f = a + b + c + d # forward, same-prio + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_highprio_passthrough(self): + """ + A has all False options, merges last (normal reverse merge) with the + highest prio. This should also pass through. + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 1 + c.priority = 0 + d.priority = -1 + cmdset_f = d + c + b + a # reverse, A top priority + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_highprio_passthrough(self): + """ + A has all False options, merges first (forward merge). This is a bit + synthetic since it will never happen in practice, but logic should + still make it pass through. + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 1 + c.priority = 0 + d.priority = -1 + cmdset_f = a + b + c + d # forward, A top priority. This never happens in practice. + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_lowprio_passthrough(self): + """ + A has all False options, merges last (normal reverse merge) with the lowest + prio. This never happens (it would always merge first) but logic should hold + and pass through since the other cmdsets have None. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = -1 + b.priority = 0 + c.priority = 1 + d.priority = 2 + cmdset_f = d + c + b + a # reverse, A low prio. This never happens in practice. + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_lowprio_passthrough(self): + """ + A has all False options, merges first (forward merge) with lowest prio. This + is the normal behavior for a low-prio cmdset. Passthrough should happen. + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = -1 + b.priority = 0 + c.priority = 1 + d.priority = 2 + cmdset_f = a + b + c + d # forward, A low prio + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_highprio_block_passthrough(self): + """ + A has all False options, other cmdsets has True. A merges last with high + prio. A should retain its option values and override the others + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 1 + c.priority = 0 + d.priority = -1 + c.no_exits = True + b.no_objs = True + d.duplicates = True + # higher-prio sets will change the option up the chain + cmdset_f = d + c + b + a # reverse, high prio + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_highprio_block_passthrough(self): + """ + A has all False options, other cmdsets has True. A merges last with high + prio. This situation should never happen, but logic should hold - the highest + prio's options should survive the merge process. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 1 + c.priority = 0 + d.priority = -1 + c.no_exits = True + b.no_channels = True + b.no_objs = True + d.duplicates = True + # higher-prio sets will change the option up the chain + cmdset_f = a + b + c + d # forward, high prio, never happens + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_lowprio_block(self): + """ + A has all False options, other cmdsets has True. A merges last with low + prio. This should result in its values being blocked and come out False. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = -1 + b.priority = 0 + c.priority = 1 + d.priority = 2 + c.no_exits = True + c.no_channels = True + b.no_objs = True + d.duplicates = True + # higher-prio sets will change the option up the chain + cmdset_f = a + b + c + d # forward, A low prio + self.assertTrue(cmdset_f.no_exits) + self.assertTrue(cmdset_f.no_objs) + self.assertTrue(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__forward_lowprio_block_partial(self): + """ + A has all False options, other cmdsets has True excet C which has a None + for `no_channels`. A merges last with low + prio. This should result in its values being blocked and come out True + except for no_channels which passes through. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = -1 + b.priority = 0 + c.priority = 1 + d.priority = 2 + c.no_exits = True + c.no_channels = None # passthrough + b.no_objs = True + d.duplicates = True + # higher-prio sets will change the option up the chain + cmdset_f = a + b + c + d # forward, A low prio + self.assertTrue(cmdset_f.no_exits) + self.assertTrue(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_sameprio_order_last(self): + """ + A has all False options and highest prio, D has True and lowest prio, + others are passthrough. B has the same prio as A, with passthrough. + + Since A is merged last, this should give prio to A's False options + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 2 + c.priority = 0 + d.priority = -1 + d.no_channels = True + d.no_exits = True + d.no_objs = True + d.duplicates = False + # higher-prio sets will change the option up the chain + cmdset_f = d + c + b + a # reverse, A high prio, merged after b + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_sameprio_order_first(self): + """ + A has all False options and highest prio, D has True and lowest prio, + others are passthrough. B has the same prio as A, with passthrough. + + While B, with None-values, is merged after A, A's options should have + replaced those of D at that point, and since B has passthrough the + final result should contain A's False options. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 2 + c.priority = 0 + d.priority = -1 + d.no_channels = True + d.no_exits = True + d.no_objs = True + d.duplicates = False + + # higher-prio sets will change the option up the chain + cmdset_f = d + c + a + b # reverse, A high prio, merged before b + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + def test_option_transfer__reverse_lowprio_block(self): + """ + A has all False options, other cmdsets has True. A merges last with low + prio. This usually doesn't happen- it should merge last. But logic should + hold and the low-prio cmdset's values should be blocked and come out True. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = -1 + b.priority = 0 + c.priority = 1 + d.priority = 2 + c.no_exits = True + d.no_channels = True + b.no_objs = True + d.duplicates = True + # higher-prio sets will change the option up the chain + cmdset_f = d + c + b + a # reverse, A low prio, never happens + self.assertTrue(cmdset_f.no_exits) + self.assertTrue(cmdset_f.no_objs) + self.assertTrue(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + + +class TestDuplicateBehavior(TestCase): + """ + Test behavior of .duplicate option, which is a bit special in that it + doesn't propagate. + + `A.duplicates=True` for all tests. + + """ + + def setUp(self): + super().setUp() + self.cmdset_a = _CmdSetA() + self.cmdset_b = _CmdSetB() + self.cmdset_c = _CmdSetC() + self.cmdset_d = _CmdSetD() + self.cmdset_a.priority = 0 + self.cmdset_b.priority = 0 + self.cmdset_c.priority = 0 + self.cmdset_d.priority = 0 + self.cmdset_a.duplicates = True + + def test_reverse_sameprio_duplicate(self): + """ + Test of `duplicates` transfer which does not propagate. Only + A has duplicates=True. + + D + B = DB (no duplication, DB.duplication=None) + DB + C = DBC (no duplication, DBC.duplication=None) + DBC + A = final (duplication, final.duplication=None) + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + cmdset_f = d + b + c + a # two last mergers duplicates=True + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 8) + + def test_reverse_sameprio_duplicate(self): + """ + Test of `duplicates` transfer, which does not propagate. + C.duplication=True + + D + B = DB (no duplication, DB.duplication=None) + DB + C = DBC (duplication, DBC.duplication=None) + DBC + A = final (duplication, final.duplication=None) + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d c.duplicates = True cmdset_f = d + b + c + a # two last mergers duplicates=True + self.assertIsNone(cmdset_f.duplicates) self.assertEqual(len(cmdset_f.commands), 10) + def test_forward_sameprio_duplicate(self): + """ + Test of `duplicates` transfer which does not propagate. + C.duplication=True, merges later than A + + D + B = DB (no duplication, DB.duplication=None) + DB + A = DBA (duplication, DBA.duplication=None) + DBA + C = final (duplication, final.duplication=None) + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + c.duplicates = True + cmdset_f = d + b + a + c # two last mergers duplicates=True + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 10) + + def test_reverse_sameprio_duplicate_reverse(self): + """ + Test of `duplicates` transfer which does not propagate. + C.duplication=False (explicit), merges before A. This behavior is the + same as if C.duplication=None, since A merges later and takes + precedence. + + D + B = DB (no duplication, DB.duplication=None) + DB + C = DBC (no duplication, DBC.duplication=None) + DBC + A = final (duplication, final.duplication=None) + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + c.duplicates = False + cmdset_f = d + b + c + a # a merges last, takes precedence + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 8) + + def test_reverse_sameprio_duplicate_forward(self): + """ + Test of `duplicates` transfer which does not propagate. + C.duplication=False (explicit), merges after A. This just means + only A causes duplicates, earlier in the chain. + + D + B = DB (no duplication, DB.duplication=None) + DB + A = DBA (duplication, DBA.duplication=None) + DBA + C = final (no duplication, final.duplication=None) + + Note that DBA has 8 cmds due to A merging onto DB with duplication, + but since C merges onto this with no duplication, the union will hold + 6 commands, since C has two commands that replaces the 4 duplicates + with uniques copies from C. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + c.duplicates = False + cmdset_f = d + b + a + c # a merges before c + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 6) + + +class TestOptionTransferReplace(TestCase): + """ + Test option transfer through more complex merge types. + """ + def setUp(self): + super().setUp() + self.cmdset_a = _CmdSetA() + self.cmdset_b = _CmdSetB() + self.cmdset_c = _CmdSetC() + self.cmdset_d = _CmdSetD() + self.cmdset_a.priority = 0 + self.cmdset_b.priority = 0 + self.cmdset_c.priority = 0 + self.cmdset_d.priority = 0 + self.cmdset_a.no_exits = True + self.cmdset_a.no_objs = True + self.cmdset_a.no_channels = True + self.cmdset_a.duplicates = True + + def test_option_transfer__replace_reverse_highprio(self): + """ + A has all options True and highest priority. C has them False and is + Replace-type. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.priority = 2 + b.priority = 2 + c.priority = 0 + c.mergetype = "Replace" + c.no_channels = False + c.no_exits = False + c.no_objs = False + c.duplicates = False + d.priority = -1 + + cmdset_f = d + c + b + a # reverse, A high prio, C Replace + self.assertTrue(cmdset_f.no_exits) + self.assertTrue(cmdset_f.no_objs) + self.assertTrue(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 7) + + def test_option_transfer__replace_reverse_highprio_from_false(self): + """ + Inverse of previous test: A has all options False and highest priority. + C has them True and is Replace-type. + + """ + a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d + a.no_exits = False + a.no_objs = False + a.no_channels = False + a.duplicates = False + + a.priority = 2 + b.priority = 2 + c.priority = 0 + c.mergetype = "Replace" + c.no_channels = True + c.no_exits = True + c.no_objs = True + c.duplicates = True + d.priority = -1 + + cmdset_f = d + c + b + a # reverse, A high prio, C Replace + self.assertFalse(cmdset_f.no_exits) + self.assertFalse(cmdset_f.no_objs) + self.assertFalse(cmdset_f.no_channels) + self.assertIsNone(cmdset_f.duplicates) + self.assertEqual(len(cmdset_f.commands), 4) + # test cmdhandler functions diff --git a/evennia/contrib/tutorial_world/tutorialmenu.py b/evennia/contrib/tutorial_world/tutorialmenu.py index e40eb1a028..6d886d06e0 100644 --- a/evennia/contrib/tutorial_world/tutorialmenu.py +++ b/evennia/contrib/tutorial_world/tutorialmenu.py @@ -179,6 +179,7 @@ class DemoCommandSetRoom(CmdSet): key = "cmd_demo_cmdset_room" priority = 2 no_exits = False + no_objs = False def at_cmdset_creation(self): from evennia import default_cmds diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index d525172b4f..6f9bf37020 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -57,7 +57,7 @@ both one or two arguments interchangeably. It also accepts nodes that takes **kwargs. The menu tree itself is available on the caller as -`caller.ndb._menutree`. This makes it a convenient place to store +`caller.ndb._evmenu`. This makes it a convenient place to store temporary state variables between nodes, since this NAttribute is deleted when the menu is exited. @@ -390,28 +390,28 @@ class CmdEvMenuNode(Command): caller = self.caller # we store Session on the menu since this can be hard to # get in multisession environemtns if caller is an Account. - menu = caller.ndb._menutree + menu = caller.ndb._evmenu if not menu: if _restore(caller): return orig_caller = caller caller = caller.account if hasattr(caller, "account") else None - menu = caller.ndb._menutree if caller else None + menu = caller.ndb._evmenu if caller else None if not menu: if caller and _restore(caller): return caller = self.session - menu = caller.ndb._menutree + menu = caller.ndb._evmenu if not menu: # can't restore from a session - err = "Menu object not found as %s.ndb._menutree!" % orig_caller + err = "Menu object not found as %s.ndb._evmenu!" % orig_caller orig_caller.msg( err ) # don't give the session as a kwarg here, direct to original raise EvMenuError(err) # we must do this after the caller with the menu has been correctly identified since it # can be either Account, Object or Session (in the latter case this info will be superfluous). - caller.ndb._menutree._session = self.session + caller.ndb._evmenu._session = self.session # we have a menu, use it. menu.parse_input(self.raw_string) @@ -539,16 +539,16 @@ class EvMenu: `persistent` flag is deactivated. Kwargs: - any (any): All kwargs will become initialization variables on `caller.ndb._menutree`, + any (any): All kwargs will become initialization variables on `caller.ndb._evmenu`, to be available at run. Raises: EvMenuError: If the start/end node is not found in menu tree. Notes: - While running, the menu is stored on the caller as `caller.ndb._menutree`. Also + While running, the menu is stored on the caller as `caller.ndb._evmenu`. Also the current Session (from the Command, so this is still valid in multisession - environments) is available through `caller.ndb._menutree._session`. The `_menutree` + environments) is available through `caller.ndb._evmenu._session`. The `_evmenu` property is a good one for storing intermediary data on between nodes since it will be automatically deleted when the menu closes. @@ -621,15 +621,18 @@ class EvMenu: for key, val in kwargs.items(): setattr(self, key, val) - if self.caller.ndb._menutree: + if self.caller.ndb._evmenu: # an evmenu already exists - we try to close it cleanly. Note that this will # not fire the previous menu's end node. try: - self.caller.ndb._menutree.close_menu() + self.caller.ndb._evmenu.close_menu() except Exception: pass # store ourself on the object + self.caller.ndb._evmenu = self + + # DEPRECATED - for backwards-compatibility self.caller.ndb._menutree = self if persistent: @@ -649,7 +652,7 @@ class EvMenu: 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) + self.msg(_ERROR_PERSISTENT_SAVING.format(error=err)) logger.log_trace(_TRACE_PERSISTENT_SAVING) persistent = False @@ -761,7 +764,7 @@ class EvMenu: ret = callback(self.caller) except EvMenuError: errmsg = _ERR_GENERAL.format(nodename=callback) - self.caller.msg(errmsg, self._session) + self.msg(errmsg) logger.log_trace() raise @@ -786,7 +789,7 @@ class EvMenu: try: node = self._menutree[nodename] except KeyError: - self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session) + self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename)) raise EvMenuError try: kwargs["_current_nodename"] = nodename @@ -796,11 +799,11 @@ class EvMenu: else: nodetext, options = ret, None except KeyError: - self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session) + self.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename)) logger.log_trace() raise EvMenuError except Exception: - self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session) + self.msg(_ERR_GENERAL.format(nodename=nodename)) logger.log_trace() raise @@ -810,6 +813,23 @@ class EvMenu: return nodetext, options + def msg(self, txt): + """ + This is a central point for sending return texts to the caller. It + allows for a central point to add custom messaging when creating custom + EvMenu overrides. + + Args: + txt (str): The text to send. + + Notes: + By default this will send to the same session provided to EvMenu + (if `session` kwarg was provided to `EvMenu.__init__`). It will + also send it with a `type=menu` for the benefit of OOB/webclient. + + """ + self.caller.msg(text=(txt, {"type": "menu"}), session=self._session) + def run_exec(self, nodename, raw_string, **kwargs): """ NOTE: This is deprecated. Use `goto` directly instead. @@ -856,7 +876,7 @@ class EvMenu: ret, kwargs = ret[:2] except EvMenuError as err: errmsg = "Error in exec '%s' (input: '%s'): %s" % (nodename, raw_string.rstrip(), err) - self.caller.msg("|r%s|n" % errmsg) + self.msg("|r%s|n" % errmsg) logger.log_trace(errmsg) return @@ -1038,7 +1058,7 @@ class EvMenu: # avoid multiple calls from different sources self._quitting = True self.caller.cmdset.remove(EvMenuCmdSet) - del self.caller.ndb._menutree + del self.caller.ndb._evmenu if self._persistent: self.caller.attributes.remove("_menutree_saved") self.caller.attributes.remove("_menutree_saved_startnode") @@ -1102,7 +1122,7 @@ class EvMenu: ) + "\n |y... END MENU DEBUG|n" ) - self.caller.msg(debugtxt) + self.msg(debugtxt) def parse_input(self, raw_string): """ @@ -1137,17 +1157,17 @@ class EvMenu: 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) + self.msg(_HELP_NO_OPTION_MATCH) except EvMenuGotoAbortMessage as err: # custom interrupt from inside a goto callable - print the message and # stay on the current node. - self.caller.msg(str(err), session=self._session) + self.msg(str(err)) def display_nodetext(self): - self.caller.msg(self.nodetext, session=self._session) + self.msg(self.nodetext) def display_helptext(self): - self.caller.msg(self.helptext, session=self._session) + self.msg(self.helptext) # formatters - override in a child class @@ -1732,7 +1752,6 @@ def parse_menu_template(caller, menu_template, goto_callables=None): # if we have a pattern, build the arguments for _default later pattern = main_key[len(_OPTION_INPUT_MARKER):].strip() inputparsemap[pattern] = goto - print(f"main_key {main_key} {pattern} {goto}") else: # a regular goto string/callable target option = {