Refactor menu up until attrs

This commit is contained in:
Griatch 2018-07-26 23:41:00 +02:00
parent f27673b741
commit 50c54501f1
4 changed files with 329 additions and 84 deletions

View file

@ -5,6 +5,7 @@ OLC Prototype menu nodes
""" """
import json import json
import re
from random import choice from random import choice
from django.conf import settings from django.conf import settings
from evennia.utils.evmenu import EvMenu, list_node from evennia.utils.evmenu import EvMenu, list_node
@ -242,6 +243,25 @@ def _format_lockfuncs():
docs=utils.justify(lockfunc.__doc__.strip(), align='l', indent=10).strip())) docs=utils.justify(lockfunc.__doc__.strip(), align='l', indent=10).strip()))
def _format_list_actions(*args, **kwargs):
"""Create footer text for nodes with extra list actions
Args:
actions (str): Available actions. The first letter of the action name will be assumed
to be a shortcut.
Kwargs:
prefix (str): Default prefix to use.
Returns:
string (str): Formatted footer for adding to the node text.
"""
actions = []
prefix = kwargs.get('prefix', "|WSelect with |w<num>|W. Other actions:|n ")
for action in args:
actions.append("|w{}|n|W{} |w<num>|n".format(action[0], action[1:]))
return prefix + "|W,|n ".join(actions)
def _get_current_value(caller, keyname, formatter=str): def _get_current_value(caller, keyname, formatter=str):
"Return current value, marking if value comes from parent or set in this prototype" "Return current value, marking if value comes from parent or set in this prototype"
prot = _get_menu_prototype(caller) prot = _get_menu_prototype(caller)
@ -255,6 +275,32 @@ def _get_current_value(caller, keyname, formatter=str):
return "[No {} set]".format(keyname) return "[No {} set]".format(keyname)
def _default_parse(raw_inp, choices, *args):
"""
Helper to parse default input to a node decorated with the node_list decorator on
the form l1, l 2, look 1, etc. Spaces are ignored, as is case.
Args:
raw_inp (str): Input from the user.
choices (list): List of available options on the node listing (list of strings).
args (tuples): The available actions, each specifed as a tuple (name, alias, ...)
Returns:
choice (str): A choice among the choices, or None if no match was found.
action (str): The action operating on the choice, or None.
"""
raw_inp = raw_inp.lower().strip()
mapping = {t.lower(): tup[0] for tup in args for t in tup}
match = re.match(r"(%s)\s*?(\d+)$" % "|".join(mapping.keys()), raw_inp)
if match:
action = mapping.get(match.group(1), None)
num = int(match.group(2)) - 1
num = num if 0 <= num < len(choices) else None
if action is not None and num is not None:
return choices[num], action
return None, None
# Menu nodes ------------------------------ # Menu nodes ------------------------------
@ -357,6 +403,26 @@ def node_validate_prototype(caller, raw_string, **kwargs):
text = (text, helptext) text = (text, helptext)
options = _wizard_options(None, prev_node, None) options = _wizard_options(None, prev_node, None)
options.append({"key": "_default",
"goto": "node_" + prev_node})
return text, options
def node_examine_entity(caller, raw_string, **kwargs):
"""
General node to view a text and then return to previous node. Kwargs should contain "text" for
the text to show and 'back" pointing to the node to return to.
"""
text = kwargs.get("text", "Nothing was found here.")
helptext = "Use |wback|n to return to the previous node."
prev_node = kwargs.get('back', 'index')
text = (text, helptext)
options = _wizard_options(None, prev_node, None)
options.append({"key": "_default",
"goto": "node_" + prev_node})
return text, options return text, options
@ -419,15 +485,64 @@ def _all_prototype_parents(caller):
for prototype in protlib.search_prototype() if "prototype_key" in prototype] for prototype in protlib.search_prototype() if "prototype_key" in prototype]
def _prototype_parent_examine(caller, prototype_name): def _prototype_parent_actions(caller, raw_inp, **kwargs):
"""Convert prototype to a string representation for closer inspection""" """Parse the default Convert prototype to a string representation for closer inspection"""
prototypes = protlib.search_prototype(key=prototype_name) choices = kwargs.get("available_choices", [])
if prototypes: prototype_parent, action = _default_parse(
ret = protlib.prototype_to_str(prototypes[0]) raw_inp, choices, ("examine", "e", "l"), ("add", "a"), ("remove", "r", 'delete', 'd'))
caller.msg(ret)
return ret if prototype_parent:
else: # a selection of parent was made
caller.msg("Prototype not registered.") prototype_parent = protlib.search_prototype(key=prototype_parent)[0]
prototype_parent_key = prototype_parent['prototype_key']
# which action to apply on the selection
if action == 'examine':
# examine the prototype
txt = protlib.prototype_to_str(prototype_parent)
kwargs['text'] = txt
kwargs['back'] = 'prototype_parent'
return "node_examine_entity", kwargs
elif action == 'add':
# add/append parent
prot = _get_menu_prototype(caller)
current_prot_parent = prot.get('prototype_parent', None)
if current_prot_parent:
current_prot_parent = utils.make_iter(current_prot_parent)
if prototype_parent_key in current_prot_parent:
caller.msg("Prototype_parent {} is already used.".format(prototype_parent_key))
return "node_prototype_parent"
else:
current_prot_parent.append(prototype_parent_key)
caller.msg("Add prototype parent for multi-inheritance.")
else:
current_prot_parent = prototype_parent_key
try:
if prototype_parent:
spawner.flatten_prototype(prototype_parent, validate=True)
else:
raise RuntimeError("Not found.")
except RuntimeError as err:
caller.msg("Selected prototype-parent {} "
"caused Error(s):\n|r{}|n".format(prototype_parent, err))
return "node_prototype_parent"
_set_prototype_value(caller, "prototype_parent", current_prot_parent)
_get_flat_menu_prototype(caller, refresh=True)
elif action == "remove":
# remove prototype parent
prot = _get_menu_prototype(caller)
current_prot_parent = prot.get('prototype_parent', None)
if current_prot_parent:
current_prot_parent = utils.make_iter(current_prot_parent)
try:
current_prot_parent.remove(prototype_parent_key)
_set_prototype_value(caller, 'prototype_parent', current_prot_parent)
_get_flat_menu_prototype(caller, refresh=True)
caller.msg("Removed prototype parent {}.".format(prototype_parent_key))
except ValueError:
caller.msg("|rPrototype-parent {} could not be removed.".format(
prototype_parent_key))
return 'node_prototype_parent'
def _prototype_parent_select(caller, new_parent): def _prototype_parent_select(caller, new_parent):
@ -440,7 +555,7 @@ def _prototype_parent_select(caller, new_parent):
else: else:
raise RuntimeError("Not found.") raise RuntimeError("Not found.")
except RuntimeError as err: except RuntimeError as err:
caller.msg("Selected prototype parent {} " caller.msg("Selected prototype-parent {} "
"caused Error(s):\n|r{}|n".format(new_parent, err)) "caused Error(s):\n|r{}|n".format(new_parent, err))
else: else:
ret = _set_property(caller, new_parent, ret = _set_property(caller, new_parent,
@ -466,6 +581,8 @@ def node_prototype_parent(caller):
parent is given, this prototype must define the typeclass (next menu node). parent is given, this prototype must define the typeclass (next menu node).
{current} {current}
{actions}
""" """
helptext = """ helptext = """
Prototypes can inherit from one another. Changes in the child replace any values set in a Prototypes can inherit from one another. Changes in the child replace any values set in a
@ -488,13 +605,14 @@ def node_prototype_parent(caller):
if not ptexts: if not ptexts:
ptexts.append("[No prototype_parent set]") ptexts.append("[No prototype_parent set]")
text = text.format(current="\n\n".join(ptexts)) text = text.format(current="\n\n".join(ptexts),
actions=_format_list_actions("examine", "add", "remove"))
text = (text, helptext) text = (text, helptext)
options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W") options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W")
options.append({"key": "_default", options.append({"key": "_default",
"goto": _prototype_parent_examine}) "goto": _prototype_parent_actions})
return text, options return text, options
@ -508,33 +626,45 @@ def _all_typeclasses(caller):
if name != "evennia.objects.models.ObjectDB") if name != "evennia.objects.models.ObjectDB")
def _typeclass_examine(caller, typeclass_path): def _typeclass_actions(caller, raw_inp, **kwargs):
"""Show info (docstring) about given typeclass.""" """Parse actions for typeclass listing"""
if typeclass_path is None:
# this means we are exiting the listing
return "node_key"
typeclass = utils.get_all_typeclasses().get(typeclass_path) choices = kwargs.get("available_choices", [])
if typeclass: typeclass_path, action = _default_parse(
docstr = [] raw_inp, choices, ("examine", "e", "l"), ("remove", "r", "delete", "d"))
for line in typeclass.__doc__.split("\n"):
if line.strip(): if typeclass_path:
docstr.append(line) if action == 'examine':
elif docstr: typeclass = utils.get_all_typeclasses().get(typeclass_path)
break if typeclass:
docstr = '\n'.join(docstr) if docstr else "<empty>" docstr = []
txt = "Typeclass |y{typeclass_path}|n; First paragraph of docstring:\n\n{docstring}".format( for line in typeclass.__doc__.split("\n"):
typeclass_path=typeclass_path, docstring=docstr) if line.strip():
else: docstr.append(line)
txt = "This is typeclass |y{}|n.".format(typeclass) elif docstr:
caller.msg(txt) break
return txt docstr = '\n'.join(docstr) if docstr else "<empty>"
txt = "Typeclass |c{typeclass_path}|n; " \
"First paragraph of docstring:\n\n{docstring}".format(
typeclass_path=typeclass_path, docstring=docstr)
else:
txt = "This is typeclass |y{}|n.".format(typeclass)
return "node_examine_entity", {"text": txt, "back": "typeclass"}
elif action == 'remove':
prototype = _get_menu_prototype(caller)
old_typeclass = prototype.pop('typeclass', None)
if old_typeclass:
_set_menu_prototype(caller, prototype)
caller.msg("Cleared typeclass {}.".format(old_typeclass))
else:
caller.msg("No typeclass to remove.")
return "node_typeclass"
def _typeclass_select(caller, typeclass): def _typeclass_select(caller, typeclass):
"""Select typeclass from list and add it to prototype. Return next node to go to.""" """Select typeclass from list and add it to prototype. Return next node to go to."""
ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key") ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key")
caller.msg("Selected typeclass |y{}|n.".format(typeclass)) caller.msg("Selected typeclass |c{}|n.".format(typeclass))
return ret return ret
@ -547,7 +677,10 @@ def node_typeclass(caller):
one of the prototype's |cparents|n. one of the prototype's |cparents|n.
{current} {current}
""".format(current=_get_current_value(caller, "typeclass"))
{actions}
""".format(current=_get_current_value(caller, "typeclass"),
actions=_format_list_actions("examine", "remove"))
helptext = """ helptext = """
A |nTypeclass|n is specified by the actual python-path to the class definition in the A |nTypeclass|n is specified by the actual python-path to the class definition in the
@ -561,7 +694,7 @@ def node_typeclass(caller):
options = _wizard_options("typeclass", "prototype_parent", "key", color="|W") options = _wizard_options("typeclass", "prototype_parent", "key", color="|W")
options.append({"key": "_default", options.append({"key": "_default",
"goto": _typeclass_examine}) "goto": _typeclass_actions})
return text, options return text, options
@ -598,16 +731,62 @@ def node_key(caller):
# aliases node # aliases node
def _all_aliases(caller):
"Get aliases in prototype"
prototype = _get_menu_prototype(caller)
return prototype.get("aliases", [])
def _aliases_select(caller, alias):
"Add numbers as aliases"
aliases = _all_aliases(caller)
try:
ind = str(aliases.index(alias) + 1)
if ind not in aliases:
aliases.append(ind)
_set_prototype_value(caller, "aliases", aliases)
caller.msg("Added alias '{}'.".format(ind))
except (IndexError, ValueError) as err:
caller.msg("Error: {}".format(err))
return "node_aliases"
def _aliases_actions(caller, raw_inp, **kwargs):
"""Parse actions for aliases listing"""
choices = kwargs.get("available_choices", [])
alias, action = _default_parse(
raw_inp, choices, ("remove", "r", "delete", "d"))
aliases = _all_aliases(caller)
if alias and action == 'remove':
try:
aliases.remove(alias)
_set_prototype_value(caller, "aliases", aliases)
caller.msg("Removed alias '{}'.".format(alias))
except ValueError:
caller.msg("No matching alias found to remove.")
else:
# if not a valid remove, add as a new alias
alias = raw_inp.lower().strip()
if alias not in aliases:
aliases.append(alias)
_set_prototype_value(caller, "aliases", aliases)
caller.msg("Added alias '{}'.".format(alias))
else:
caller.msg("Alias '{}' was already set.".format(alias))
return "node_aliases"
@list_node(_all_aliases, _aliases_select)
def node_aliases(caller): def node_aliases(caller):
text = """ text = """
|cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not |cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not
case sensitive. case sensitive.
Add multiple aliases separating with commas. {actions}
""".format(_format_list_actions("remove", prefix="|w<text>|W to add new alias. Other action: "))
{current}
""".format(current=_get_current_value(caller, "aliases"))
helptext = """ helptext = """
Aliases are fixed alternative identifiers and are stored with the new object. Aliases are fixed alternative identifiers and are stored with the new object.
@ -621,10 +800,7 @@ def node_aliases(caller):
options = _wizard_options("aliases", "key", "attrs") options = _wizard_options("aliases", "key", "attrs")
options.append({"key": "_default", options.append({"key": "_default",
"goto": (_set_property, "goto": _aliases_actions})
dict(prop="aliases",
processor=lambda s: [part.strip() for part in s.split(",")],
next_node="node_attrs"))})
return text, options return text, options
@ -633,38 +809,62 @@ def node_aliases(caller):
def _caller_attrs(caller): def _caller_attrs(caller):
prototype = _get_menu_prototype(caller) prototype = _get_menu_prototype(caller)
attrs = prototype.get("attrs", []) attrs = ["{}={}".format(tup[0], utils.crop(utils.to_str(tup[1]), width=10))
for tup in prototype.get("attrs", [])]
return attrs return attrs
def _get_tup_by_attrname(caller, attrname):
prototype = _get_menu_prototype(caller)
attrs = prototype.get("attrs", [])
try:
inp = [tup[0] for tup in attrs].index(attrname)
return attrs[inp]
except ValueError:
return None
def _display_attribute(attr_tuple): def _display_attribute(attr_tuple):
"""Pretty-print attribute tuple""" """Pretty-print attribute tuple"""
attrkey, value, category, locks = attr_tuple attrkey, value, category, locks = attr_tuple
value = protlib.protfunc_parser(value) value = protlib.protfunc_parser(value)
typ = type(value) typ = type(value)
out = ("Attribute key: '{attrkey}' (category: {category}, " out = ("|cAttribute key:|n '{attrkey}' "
"locks: {locks})\n" "(|ccategory:|n {category}, "
"Value (parsed to {typ}): {value}").format( "|clocks:|n {locks})\n"
"|cValue|n |W(parsed to {typ})|n:\n{value}").format(
attrkey=attrkey, attrkey=attrkey,
category=category, locks=locks, category=category if category else "|wNone|n",
locks=locks if locks else "|wNone|n",
typ=typ, value=value) typ=typ, value=value)
return out return out
def _add_attr(caller, attr_string, **kwargs): def _add_attr(caller, attr_string, **kwargs):
""" """
Add new attrubute, parsing input. Add new attribute, parsing input.
attr is entered on these forms
attr = value
attr;category = value
attr;category;lockstring = value
Args:
caller (Object): Caller of menu.
attr_string (str): Input from user
attr is entered on these forms
attr = value
attr;category = value
attr;category;lockstring = value
Kwargs:
delete (str): If this is set, attr_string is
considered the name of the attribute to delete and
no further parsing happens.
Returns:
result (str): Result string of action.
""" """
attrname = '' attrname = ''
category = None category = None
locks = '' locks = ''
if '=' in attr_string: if 'delete' in kwargs:
attrname = attr_string
elif '=' in attr_string:
attrname, value = (part.strip() for part in attr_string.split('=', 1)) attrname, value = (part.strip() for part in attr_string.split('=', 1))
attrname = attrname.lower() attrname = attrname.lower()
nameparts = attrname.split(";", 2) nameparts = attrname.split(";", 2)
@ -679,6 +879,15 @@ def _add_attr(caller, attr_string, **kwargs):
prot = _get_menu_prototype(caller) prot = _get_menu_prototype(caller)
attrs = prot.get('attrs', []) attrs = prot.get('attrs', [])
if 'delete' in kwargs:
try:
ind = [tup[0] for tup in attrs].index(attrname)
del attrs[ind]
_set_prototype_value(caller, "attrs", attrs)
return "Removed Attribute '{}'".format(attrname)
except IndexError:
return "Attribute to delete not found."
try: try:
# replace existing attribute with the same name in the prototype # replace existing attribute with the same name in the prototype
ind = [tup[0] for tup in attrs].index(attrname) ind = [tup[0] for tup in attrs].index(attrname)
@ -697,26 +906,47 @@ def _add_attr(caller, attr_string, **kwargs):
else: else:
text = "Attribute must be given as 'attrname[;category;locks] = <value>'." text = "Attribute must be given as 'attrname[;category;locks] = <value>'."
options = {"key": "_default", return text
"goto": lambda caller: None}
return text, options
def _edit_attr(caller, attrname, new_value, **kwargs): def _attr_select(caller, attrstr):
attrname, _ = attrstr.split("=", 1)
attrname = attrname.strip()
attr_string = "{}={}".format(attrname, new_value) attr_tup = _get_tup_by_attrname(caller, attrname)
if attr_tup:
return _add_attr(caller, attr_string, edit=True) return "node_examine_entity", \
{"text": _display_attribute(attr_tup), "back": "attrs"}
else:
caller.msg("Attribute not found.")
return "node_attrs"
def _examine_attr(caller, selection): def _attrs_actions(caller, raw_inp, **kwargs):
prot = _get_menu_prototype(caller) """Parse actions for attribute listing"""
ind = [part[0] for part in prot['attrs']].index(selection) choices = kwargs.get("available_choices", [])
attr_tuple = prot['attrs'][ind] attrstr, action = _default_parse(
return _display_attribute(attr_tuple) raw_inp, choices, ('examine', 'e'), ('remove', 'r', 'delete', 'd'))
if attrstr is None:
attrstr = raw_inp
attrname, _ = attrstr.split("=", 1)
attrname = attrname.strip()
attr_tup = _get_tup_by_attrname(caller, attrname)
if attr_tup:
if action == 'examine':
return "node_examine_entity", \
{"text": _display_attribute(attr_tup), "back": "attrs"}
elif action == 'remove':
res = _add_attr(caller, attr_tup, delete=True)
caller.msg(res)
else:
res = _add_attr(caller, raw_inp)
caller.msg(res)
return "node_attrs"
@list_node(_caller_attrs) @list_node(_caller_attrs, _attr_select)
def node_attrs(caller): def node_attrs(caller):
text = """ text = """
@ -729,8 +959,8 @@ def node_attrs(caller):
To give an attribute without a category but with a lockstring, leave that spot empty To give an attribute without a category but with a lockstring, leave that spot empty
(attrname;;lockstring=value). Attribute values can have embedded $protfuncs. (attrname;;lockstring=value). Attribute values can have embedded $protfuncs.
{current} {actions}
""".format(current=_get_current_value(caller, "attrs")) """.format(actions=_format_list_actions("examine", "remove", prefix="Actions: "))
helptext = """ helptext = """
Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types
@ -747,10 +977,7 @@ def node_attrs(caller):
options = _wizard_options("attrs", "aliases", "tags") options = _wizard_options("attrs", "aliases", "tags")
options.append({"key": "_default", options.append({"key": "_default",
"goto": (_set_property, "goto": _attrs_actions})
dict(prop="attrs",
processor=lambda s: [part.strip() for part in s.split(",")],
next_node="node_tags"))})
return text, options return text, options
@ -1410,7 +1637,7 @@ def node_prototype_load(caller, **kwargs):
options = _wizard_options("prototype_load", "prototype_save", "index") options = _wizard_options("prototype_load", "prototype_save", "index")
options.append({"key": "_default", options.append({"key": "_default",
"goto": _prototype_parent_examine}) "goto": _prototype_parent_actions})
return text, options return text, options
@ -1468,6 +1695,7 @@ def start_olc(caller, session=None, prototype=None):
""" """
menudata = {"node_index": node_index, menudata = {"node_index": node_index,
"node_validate_prototype": node_validate_prototype, "node_validate_prototype": node_validate_prototype,
"node_examine_entity": node_examine_entity,
"node_prototype_key": node_prototype_key, "node_prototype_key": node_prototype_key,
"node_prototype_parent": node_prototype_parent, "node_prototype_parent": node_prototype_parent,
"node_typeclass": node_typeclass, "node_typeclass": node_typeclass,

View file

@ -606,6 +606,8 @@ def validate_prototype(prototype, protkey=None, protparents=None,
_flags['errors'].append( _flags['errors'].append(
"{} has infinite nesting of prototypes.".format(protkey or prototype)) "{} has infinite nesting of prototypes.".format(protkey or prototype))
if _flags['errors']:
raise RuntimeError("Error: " + "\nError: ".join(_flags['errors']))
_flags['visited'].append(id(prototype)) _flags['visited'].append(id(prototype))
_flags['depth'] += 1 _flags['depth'] += 1
validate_prototype(protparent, protstring, protparents, validate_prototype(protparent, protstring, protparents,
@ -618,7 +620,7 @@ def validate_prototype(prototype, protkey=None, protparents=None,
# if we get back to the current level without a typeclass it's an error. # if we get back to the current level without a typeclass it's an error.
if strict and is_prototype_base and _flags['depth'] <= 0 and not _flags['typeclass']: if strict and is_prototype_base and _flags['depth'] <= 0 and not _flags['typeclass']:
_flags['errors'].append("Prototype {} has no `typeclass` defined anywhere in its parent " _flags['errors'].append("Prototype {} has no `typeclass` defined anywhere in its parent\n "
"chain. Add `typeclass`, or a `prototype_parent` pointing to a " "chain. Add `typeclass`, or a `prototype_parent` pointing to a "
"prototype with a typeclass.".format(protkey)) "prototype with a typeclass.".format(protkey))

View file

@ -384,6 +384,14 @@ class TestMenuModule(EvenniaTest):
{"prototype_key": "testthing", "key": "mytest"}), {"prototype_key": "testthing", "key": "mytest"}),
(True, Something)) (True, Something))
choices = ["test1", "test2", "test3", "test4"]
actions = (("examine", "e", "l"), ("add", "a"), ("foo", "f"))
self.assertEqual(olc_menus._default_parse("l4", choices, *actions), ('test4', 'examine'))
self.assertEqual(olc_menus._default_parse("add 2", choices, *actions), ('test2', 'add'))
self.assertEqual(olc_menus._default_parse("foo3", choices, *actions), ('test3', 'foo'))
self.assertEqual(olc_menus._default_parse("f3", choices, *actions), ('test3', 'foo'))
self.assertEqual(olc_menus._default_parse("f5", choices, *actions), (None, None))
def test_node_helpers(self): def test_node_helpers(self):
caller = self.caller caller = self.caller
@ -399,15 +407,20 @@ class TestMenuModule(EvenniaTest):
# prototype_parent helpers # prototype_parent helpers
self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot']) self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot'])
self.assertEqual(olc_menus._prototype_parent_examine( # self.assertEqual(olc_menus._prototype_parent_parse(
caller, 'test_prot'), # caller, 'test_prot'),
"|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() " # "|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() "
"\n|cdesc:|n None \n|cprototype:|n " # "\n|cdesc:|n None \n|cprototype:|n "
"{\n 'typeclass': 'evennia.objects.objects.DefaultObject', \n}") # "{\n 'typeclass': 'evennia.objects.objects.DefaultObject', \n}")
self.assertEqual(olc_menus._prototype_parent_select(caller, self.test_prot), "node_key")
with mock.patch("evennia.prototypes.menus.protlib.search_prototype",
new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])):
self.assertEqual(olc_menus._prototype_parent_select(caller, "goblin"), "node_prototype_parent")
self.assertEqual(olc_menus._get_menu_prototype(caller), self.assertEqual(olc_menus._get_menu_prototype(caller),
{'prototype_key': 'test_prot', {'prototype_key': 'test_prot',
'prototype_locks': 'edit:all();spawn:all()', 'prototype_locks': 'edit:all();spawn:all()',
'prototype_parent': 'goblin',
'typeclass': 'evennia.objects.objects.DefaultObject'}) 'typeclass': 'evennia.objects.objects.DefaultObject'})
# typeclass helpers # typeclass helpers
@ -423,6 +436,7 @@ class TestMenuModule(EvenniaTest):
self.assertEqual(olc_menus._get_menu_prototype(caller), self.assertEqual(olc_menus._get_menu_prototype(caller),
{'prototype_key': 'test_prot', {'prototype_key': 'test_prot',
'prototype_locks': 'edit:all();spawn:all()', 'prototype_locks': 'edit:all();spawn:all()',
'prototype_parent': 'goblin',
'typeclass': 'evennia.objects.objects.DefaultObject'}) 'typeclass': 'evennia.objects.objects.DefaultObject'})
# attr helpers # attr helpers
@ -459,7 +473,9 @@ class TestMenuModule(EvenniaTest):
protlib.save_prototype(**self.test_prot) protlib.save_prototype(**self.test_prot)
# spawn helpers # spawn helpers
obj = olc_menus._spawn(caller, prototype=self.test_prot) with mock.patch("evennia.prototypes.menus.protlib.search_prototype",
new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])):
obj = olc_menus._spawn(caller, prototype=self.test_prot)
self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject") self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject")
self.assertEqual(obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot['prototype_key']) self.assertEqual(obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot['prototype_key'])
@ -475,7 +491,6 @@ class TestMenuModule(EvenniaTest):
self.assertEqual(olc_menus._prototype_load_select(caller, self.test_prot['prototype_key']), "node_index") self.assertEqual(olc_menus._prototype_load_select(caller, self.test_prot['prototype_key']), "node_index")
@mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock( @mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(
return_value=[{"prototype_key": "TestPrototype", return_value=[{"prototype_key": "TestPrototype",
"typeclass": "TypeClassTest", "key": "TestObj"}])) "typeclass": "TypeClassTest", "key": "TestObj"}]))

View file

@ -938,7 +938,7 @@ class EvMenu(object):
for key, desc in optionlist: for key, desc in optionlist:
if not (key or desc): if not (key or desc):
continue continue
desc_string = ": %s" % desc if desc else "" desc_string = ": %s" % (desc if desc else "")
table_width_max = max(table_width_max, table_width_max = max(table_width_max,
max(m_len(p) for p in key.split("\n")) + max(m_len(p) for p in key.split("\n")) +
max(m_len(p) for p in desc_string.split("\n")) + colsep) max(m_len(p) for p in desc_string.split("\n")) + colsep)