New list_node decorator for evmenu. Tested with olc menu

This commit is contained in:
Griatch 2018-03-31 21:10:20 +02:00
parent 3c5d00ac3d
commit 34b8c0dbce
4 changed files with 152 additions and 66 deletions

View file

@ -15,7 +15,8 @@ from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore
from evennia.utils.spawner import (spawn, search_prototype, list_prototypes,
save_db_prototype, build_metaproto, validate_prototype,
delete_db_prototype, PermissionError, start_olc)
delete_db_prototype, PermissionError, start_olc,
metaproto_to_str)
from evennia.utils.ansi import raw
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -2886,21 +2887,10 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
def _search_show_prototype(query, metaprots=None):
# prototype detail
strings = []
if not metaprots:
metaprots = search_prototype(key=query, return_meta=True)
if metaprots:
for metaprot in metaprots:
header = (
"|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n"
"|cdesc:|n {} \n|cprototype:|n ".format(
metaprot.key, ", ".join(metaprot.tags),
metaprot.locks, metaprot.desc))
prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value)
for key, value in
sorted(metaprot.prototype.items())).rstrip(",")))
strings.append(header + prototype)
return "\n".join(strings)
return "\n".join(metaproto_to_str(metaprot) for metaprot in metaprots)
else:
return False

View file

@ -979,18 +979,20 @@ class EvMenu(object):
#
# -----------------------------------------------------------
def list_node(option_list, examine_processor, goto_processor, pagesize=10):
def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
"""
Decorator for making an EvMenu node into a multi-page list node. Will add new options,
prepending those options added in the node.
Args:
option_list (list): List of strings indicating the options.
examine_processor (callable): Will be called with the caller and the chosen option when
examining said option. Should return a text string to display in the node.
goto_processor (callable): Will be called with caller and
the chosen option from the optionlist. Should return the target node to goto after the
selection.
option_generator (callable or list): A list of strings indicating the options, or a callable
that is called without any arguments to produce such a list.
examine_processor (callable, optional): Will be called with the caller and the chosen option
when examining said option. Should return a text string to display in the node.
goto_processor (callable, optional): Will be called as goto_processor(caller, menuchoice)
where menuchoice is the chosen option as a string. Should return the target node to
goto after this selection.
pagesize (int): How many options to show per page.
Example:
@ -1009,6 +1011,7 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
available_choices = kwargs.get("available_choices", [])
processor = kwargs.get("selection_processor")
try:
match_ind = int(re.search(r"[0-9]+$", raw_string).group()) - 1
selection = available_choices[match_ind]
@ -1022,12 +1025,14 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
logger.log_trace()
return selection
nall_options = len(option_list)
pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)]
npages = len(pages)
def _list_node(caller, raw_string, **kwargs):
option_list = option_generator() if callable(option_generator) else option_generator
nall_options = len(option_list)
pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)]
npages = len(pages)
page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
page = pages[page_index]
@ -1042,19 +1047,21 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
# if the goto callable returns None, the same node is rerun, and
# kwargs not used by the callable are passed on to the node. This
# allows us to call ourselves over and over, using different kwargs.
if page_index > 0:
options.append({"key": ("|wb|Wack|n", "b"),
"goto": (lambda caller: None,
{"optionpage_index": page_index - 1})})
if page_index < npages - 1:
options.append({"key": ("|wn|Wext|n", "n"),
"goto": (lambda caller: None,
{"optionpage_index": page_index + 1})})
options.append({"key": ("|Wcurrent|n", "c"),
"desc": "|W({}/{})|n".format(page_index + 1, npages),
"goto": (lambda caller: None,
{"optionpage_index": page_index})})
if page_index > 0:
options.append({"key": ("|wp|Wrevious page|n", "p"),
"goto": (lambda caller: None,
{"optionpage_index": page_index - 1})})
if page_index < npages - 1:
options.append({"key": ("|wn|Wext page|n", "n"),
"goto": (lambda caller: None,
{"optionpage_index": page_index + 1})})
# this catches arbitrary input, notably to examine entries ('look 4' or 'l4' etc)
options.append({"key": "_default",
"goto": (lambda caller: None,
{"show_detail": True, "optionpage_index": page_index})})
@ -1071,6 +1078,7 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
# add data from the decorated node
text = ''
extra_options = []
try:
text, extra_options = func(caller, raw_string)
except TypeError:
@ -1080,14 +1088,16 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10):
raise
except Exception:
logger.log_trace()
print("extra_options:", extra_options)
else:
if isinstance(extra_options, {}):
extra_options = [extra_options]
else:
extra_options = make_iter(extra_options)
options.append(extra_options)
options.extend(extra_options)
text = text + "\n\n" + text_detail if text_detail else text
text += "\n\n(Make a choice or enter 'look <num>' to examine an option closer)"
return text, options

View file

@ -333,7 +333,7 @@ def search_module_prototype(key=None, tags=None):
def search_prototype(key=None, tags=None, return_meta=True):
"""
Find prototypes based on key and/or tags.
Find prototypes based on key and/or tags, or all prototypes.
Kwargs:
key (str): An exact or partial key to query for.
@ -344,7 +344,8 @@ def search_prototype(key=None, tags=None, return_meta=True):
return MetaProto namedtuples including prototype meta info
Return:
matches (list): All found prototype dicts or MetaProtos
matches (list): All found prototype dicts or MetaProtos. If no keys
or tags are given, all available prototypes/MetaProtos will be returned.
Note:
The available prototypes is a combination of those supplied in
@ -438,6 +439,25 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed
table.reformat_column(3, width=20)
return table
def metaproto_to_str(metaproto):
"""
Format a metaproto to a nice string representation.
Args:
metaproto (NamedTuple): Represents the prototype.
"""
header = (
"|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n"
"|cdesc:|n {} \n|cprototype:|n ".format(
metaproto.key, ", ".join(metaproto.tags),
metaproto.locks, metaproto.desc))
prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value)
for key, value in
sorted(metaproto.prototype.items())).rstrip(",")))
return header + prototype
# Spawner mechanism
@ -660,7 +680,13 @@ def spawn(*prototypes, **kwargs):
return _batch_create_object(*objsparams)
# prototype design menu nodes
# ------------------------------------------------------------
#
# OLC Prototype design menu
#
# ------------------------------------------------------------
# Helper functions
def _get_menu_metaprot(caller):
if hasattr(caller.ndb._menutree, "olc_metaprot"):
@ -683,7 +709,7 @@ def _set_menu_metaprot(caller, field, value):
caller.ndb._menutree.olc_metaprot = build_metaproto(**kwargs)
def _format_property(key, required=False, metaprot=None, prototype=None):
def _format_property(key, required=False, metaprot=None, prototype=None, cropper=None):
key = key.lower()
if metaprot is not None:
prop = getattr(metaprot, key) or ''
@ -700,7 +726,7 @@ def _format_property(key, required=False, metaprot=None, prototype=None):
out = ", ".join(str(pr) for pr in prop)
if not out and required:
out = "|rrequired"
return " ({}|n)".format(crop(out, _MENU_CROP_WIDTH))
return " ({}|n)".format(cropper(out) if cropper else crop(out, _MENU_CROP_WIDTH))
def _set_property(caller, raw_string, **kwargs):
@ -744,26 +770,43 @@ def _set_property(caller, raw_string, **kwargs):
metaprot = _get_menu_metaprot(caller)
prototype = metaprot.prototype
prototype[propname_low] = value
# typeclass and prototype can't co-exist
if propname_low == "typeclass":
prototype.pop("prototype", None)
if propname_low == "prototype":
prototype.pop("typeclass", None)
_set_menu_metaprot(caller, "prototype", prototype)
caller.msg("Set {prop} to {value}.".format(
prop=prop.replace("_", "-").capitalize(), value=str(value)))
return next_node
def _wizard_options(prev_node, next_node):
options = [{"desc": "forward ({})".format(next_node.replace("_", "-")),
def _wizard_options(prev_node, next_node, color="|W"):
options = [{"key": ("|wf|Worward", "f"),
"desc": "{color}({node})|n".format(
color=color, node=next_node.replace("_", "-")),
"goto": "node_{}".format(next_node)},
{"desc": "back ({})".format(prev_node.replace("_", "-")),
{"key": ("|wb|Wack", "b"),
"desc": "{color}({node})|n".format(
color=color, node=prev_node.replace("_", "-")),
"goto": "node_{}".format(prev_node)}]
if "index" not in (prev_node, next_node):
options.append({"desc": "index",
options.append({"key": ("|wi|Wndex", "i"),
"goto": "node_index"})
return options
# menu nodes
def _path_cropper(pythonpath):
"Crop path to only the last component"
return pythonpath.split('.')[-1]
# Menu nodes
def node_index(caller):
metaprot = _get_menu_metaprot(caller)
@ -784,15 +827,20 @@ def node_index(caller):
"goto": "node_meta_key"})
for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks',
'Permissions', 'Location', 'Home', 'Destination'):
req = False
required = False
cropper = None
if key in ("Prototype", "Typeclass"):
req = "prototype" not in prototype and "typeclass" not in prototype
required = "prototype" not in prototype and "typeclass" not in prototype
if key == 'Typeclass':
cropper = _path_cropper
options.append(
{"desc": "|w{}|n{}".format(key, _format_property(key, req, None, prototype)),
{"desc": "|w{}|n{}".format(
key, _format_property(key, required, None, prototype, cropper=cropper)),
"goto": "node_{}".format(key.lower())})
required = False
for key in ('Desc', 'Tags', 'Locks'):
options.append(
{"desc": "|WMeta-{}|n|n{}".format(key, _format_property(key, req, metaprot, None)),
{"desc": "|WMeta-{}|n|n{}".format(key, _format_property(key, required, metaprot, None)),
"goto": "node_meta_{}".format(key.lower())})
return text, options
@ -837,6 +885,24 @@ def node_meta_key(caller):
return text, options
def _all_prototypes():
return [mproto.key for mproto in search_prototype()]
def _prototype_examine(caller, prototype_name):
metaprot = search_prototype(key=prototype_name)
if metaprot:
return metaproto_to_str(metaprot[0])
return "Prototype not registered."
def _prototype_select(caller, prototype):
ret = _set_property(caller, prototype, prop="prototype", processor=str, next_node="node_key")
caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype))
return ret
@list_node(_all_prototypes, _prototype_examine, _prototype_select)
def node_prototype(caller):
metaprot = _get_menu_metaprot(caller)
prot = metaprot.prototype
@ -848,25 +914,43 @@ def node_prototype(caller):
else:
text.append("Parent prototype is not set")
text = "\n\n".join(text)
options = _wizard_options("meta_key", "typeclass")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="prototype",
processor=lambda s: s.strip(),
next_node="node_typeclass"))})
options = _wizard_options("meta_key", "typeclass", color="|W")
return text, options
def _typeclass_examine(caller, typeclass):
return "This is typeclass |y{}|n.".format(typeclass)
def _all_typeclasses():
return list(sorted(get_all_typeclasses().keys()))
# return list(sorted(get_all_typeclasses(parent="evennia.objects.objects.DefaultObject").keys()))
def _typeclass_examine(caller, typeclass_path):
if typeclass_path is None:
# this means we are exiting the listing
return "node_key"
typeclass = get_all_typeclasses().get(typeclass_path)
if typeclass:
docstr = []
for line in typeclass.__doc__.split("\n"):
if line.strip():
docstr.append(line)
elif docstr:
break
docstr = '\n'.join(docstr) if docstr else "<empty>"
txt = "Typeclass |y{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 txt
def _typeclass_select(caller, typeclass):
caller.msg("Selected typeclass |y{}|n.".format(typeclass))
return None
ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key")
caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass))
return ret
@list_node(list(sorted(get_all_typeclasses().keys())), _typeclass_examine, _typeclass_select)
@list_node(_all_typeclasses, _typeclass_examine, _typeclass_select)
def node_typeclass(caller):
metaprot = _get_menu_metaprot(caller)
prot = metaprot.prototype
@ -879,12 +963,7 @@ def node_typeclass(caller):
text.append("Using default typeclass {typeclass}.".format(
typeclass=settings.BASE_OBJECT_TYPECLASS))
text = "\n\n".join(text)
options = _wizard_options("prototype", "key")
options.append({"key": "_default",
"goto": (_set_property,
dict(prop="typeclass",
processor=lambda s: s.strip(),
next_node="node_key"))})
options = _wizard_options("prototype", "key", color="|W")
return text, options

View file

@ -1882,10 +1882,14 @@ def get_game_dir_path():
raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.")
def get_all_typeclasses():
def get_all_typeclasses(parent=None):
"""
List available typeclasses from all available modules.
Args:
parent (str, optional): If given, only return typeclasses inheriting (at any distance)
from this parent.
Returns:
typeclasses (dict): On the form {"typeclass.path": typeclass, ...}
@ -1898,4 +1902,7 @@ def get_all_typeclasses():
from evennia.typeclasses.models import TypedObject
typeclasses = {"{}.{}".format(model.__module__, model.__name__): model
for model in apps.get_models() if TypedObject in getmro(model)}
if parent:
typeclasses = {name: typeclass for name, typeclass in typeclasses.items()
if inherits_from(typeclass, parent)}
return typeclasses