Rename prototype to prototype_parent, fixing olc menu

This commit is contained in:
Griatch 2018-06-24 09:50:03 +02:00
parent e601e03884
commit 9360dc71f1
6 changed files with 155 additions and 59 deletions

View file

@ -13,7 +13,7 @@ from evennia.utils import create, utils, search
from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses
from evennia.utils.eveditor import EvEditor from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore from evennia.utils.evmore import EvMore
from evennia.prototypes import spawner, prototypes as protlib from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
from evennia.utils.ansi import raw from evennia.utils.ansi import raw
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -2917,7 +2917,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
elif prototype: elif prototype:
# one match # one match
prototype = prototype[0] prototype = prototype[0]
spawner.start_olc(caller, session=self.session, prototype=prototype) olc_menus.start_olc(caller, session=self.session, prototype=prototype)
return return
if 'search' in self.switches: if 'search' in self.switches:

View file

@ -9,8 +9,8 @@ from django.conf import settings
from evennia.utils.evmenu import EvMenu, list_node from evennia.utils.evmenu import EvMenu, list_node
from evennia.utils.ansi import strip_ansi from evennia.utils.ansi import strip_ansi
from evennia.utils import utils from evennia.utils import utils
from evennia.utils.prototypes import prototypes as protlib from evennia.prototypes import prototypes as protlib
from evennia.utils.prototypes import spawner from evennia.prototypes import spawner
# ------------------------------------------------------------ # ------------------------------------------------------------
# #
@ -43,12 +43,6 @@ def _is_new_prototype(caller):
return hasattr(caller.ndb._menutree, "olc_new") return hasattr(caller.ndb._menutree, "olc_new")
def _set_menu_prototype(caller, field, value):
prototype = _get_menu_prototype(caller)
prototype[field] = value
caller.ndb._menutree.olc_prototype = prototype
def _format_property(prop, required=False, prototype=None, cropper=None): def _format_property(prop, required=False, prototype=None, cropper=None):
if prototype is not None: if prototype is not None:
@ -67,6 +61,13 @@ def _format_property(prop, required=False, prototype=None, cropper=None):
return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH)) return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH))
def _set_prototype_value(caller, field, value):
prototype = _get_menu_prototype(caller)
prototype[field] = value
caller.ndb._menutree.olc_prototype = prototype
return prototype
def _set_property(caller, raw_string, **kwargs): def _set_property(caller, raw_string, **kwargs):
""" """
Update a property. To be called by the 'goto' option variable. Update a property. To be called by the 'goto' option variable.
@ -102,22 +103,26 @@ def _set_property(caller, raw_string, **kwargs):
if not value: if not value:
return next_node return next_node
prototype = _get_menu_prototype(caller) prototype = _set_prototype_value(caller, "prototype_key", value)
# typeclass and prototype can't co-exist # typeclass and prototype_parent can't co-exist
if propname_low == "typeclass": if propname_low == "typeclass":
prototype.pop("prototype", None) prototype.pop("prototype_parent", None)
if propname_low == "prototype": if propname_low == "prototype_parent":
prototype.pop("typeclass", None) prototype.pop("typeclass", None)
caller.ndb._menutree.olc_prototype = prototype caller.ndb._menutree.olc_prototype = prototype
caller.msg("Set {prop} to '{value}'.".format(prop, value=str(value))) caller.msg("Set {prop} to '{value}'.".format(prop=prop, value=str(value)))
return next_node return next_node
def _wizard_options(curr_node, prev_node, next_node, color="|W"): def _wizard_options(curr_node, prev_node, next_node, color="|W"):
"""
Creates default navigation options available in the wizard.
"""
options = [] options = []
if prev_node: if prev_node:
options.append({"key": ("|wb|Wack", "b"), options.append({"key": ("|wb|Wack", "b"),
@ -154,8 +159,8 @@ def node_index(caller):
text = ("|c --- Prototype wizard --- |n\n\n" text = ("|c --- Prototype wizard --- |n\n\n"
"Define the |yproperties|n of the prototype. All prototype values can be " "Define the |yproperties|n of the prototype. All prototype values can be "
"over-ridden at the time of spawning an instance of the prototype, but some are " "over-ridden at the time of spawning an instance of the prototype, but some are "
"required.\n\n'|wMeta'-properties|n are not used in the prototype itself but are used " "required.\n\n'|wprototype-'-properties|n are not used in the prototype itself but are used "
"to organize and list prototypes. The 'Meta-Key' uniquely identifies the prototype " "to organize and list prototypes. The 'prototype-key' uniquely identifies the prototype "
"and allows you to edit an existing prototype or save a new one for use by you or " "and allows you to edit an existing prototype or save a new one for use by you or "
"others later.\n\n(make choice; q to abort. If unsure, start from 1.)") "others later.\n\n(make choice; q to abort. If unsure, start from 1.)")
@ -192,9 +197,12 @@ def node_validate_prototype(caller, raw_string, **kwargs):
errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)" errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)"
try: try:
# validate, don't spawn # validate, don't spawn
spawner.spawn(prototype, return_prototypes=True) spawner.spawn(prototype, only_validate=True)
except RuntimeError as err: except RuntimeError as err:
errors = "\n\n|rError: {}|n".format(err) errors = "\n\n|r{}|n".format(err)
except RuntimeWarning as err:
errors = "\n\n|y{}|n".format(err)
text = (txt + errors) text = (txt + errors)
options = _wizard_options(None, kwargs.get("back"), None) options = _wizard_options(None, kwargs.get("back"), None)
@ -287,7 +295,9 @@ def node_prototype(caller):
def _all_typeclasses(caller): def _all_typeclasses(caller):
return list(sorted(utils.get_all_typeclasses().keys())) return list(name for name in
sorted(utils.get_all_typeclasses("evennia.objects.models.ObjectDB").keys())
if name != "evennia.objects.models.ObjectDB")
def _typeclass_examine(caller, typeclass_path): def _typeclass_examine(caller, typeclass_path):
@ -403,7 +413,7 @@ def _add_attr(caller, attr_string, **kwargs):
if attrname: if attrname:
prot = _get_menu_prototype(caller) prot = _get_menu_prototype(caller)
prot['attrs'][attrname] = value prot['attrs'][attrname] = value
_set_menu_prototype(caller, "prototype", prot) _set_prototype_value(caller, "prototype", prot)
text = "Added" text = "Added"
else: else:
text = "Attribute must be given as 'attrname = <value>' where <value> uses valid Python." text = "Attribute must be given as 'attrname = <value>' where <value> uses valid Python."
@ -468,7 +478,7 @@ def _add_tag(caller, tag, **kwargs):
else: else:
tags = [tag] tags = [tag]
prototype['tags'] = tags prototype['tags'] = tags
_set_menu_prototype(caller, "prototype", prototype) _set_prototype_value(caller, "prototype", prototype)
text = kwargs.get("text") text = kwargs.get("text")
if not text: if not text:
text = "Added tag {}. (return to continue)".format(tag) text = "Added tag {}. (return to continue)".format(tag)
@ -485,7 +495,7 @@ def _edit_tag(caller, old_tag, new_tag, **kwargs):
new_tag = new_tag.strip().lower() new_tag = new_tag.strip().lower()
tags[tags.index(old_tag)] = new_tag tags[tags.index(old_tag)] = new_tag
prototype['tags'] = tags prototype['tags'] = tags
_set_menu_prototype(caller, 'prototype', prototype) _set_prototype_value(caller, 'prototype', prototype)
text = kwargs.get('text') text = kwargs.get('text')
if not text: if not text:

View file

@ -12,7 +12,8 @@ from evennia.scripts.scripts import DefaultScript
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.utils.create import create_script from evennia.utils.create import create_script
from evennia.utils.utils import ( from evennia.utils.utils import (
all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module) all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module,
get_all_typeclasses)
from evennia.locks.lockhandler import validate_lockstring, check_lockstring from evennia.locks.lockhandler import validate_lockstring, check_lockstring
from evennia.utils import logger from evennia.utils import logger
from evennia.utils import inlinefuncs from evennia.utils import inlinefuncs
@ -143,10 +144,10 @@ def prototype_to_str(prototype):
header = ( header = (
"|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n"
"|cdesc:|n {} \n|cprototype:|n ".format( "|cdesc:|n {} \n|cprototype:|n ".format(
prototype['prototype_key'], prototype.get('prototype_key', None),
", ".join(prototype['prototype_tags']), ", ".join(prototype.get('prototype_tags', ['None'])),
prototype['prototype_locks'], prototype.get('prototype_locks', None),
prototype['prototype_desc'])) prototype.get('prototype_desc', None)))
proto = ("{{\n {} \n}}".format( proto = ("{{\n {} \n}}".format(
"\n ".join( "\n ".join(
"{!r}: {!r},".format(key, value) for key, value in "{!r}: {!r},".format(key, value) for key, value in
@ -513,7 +514,8 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed
return table return table
def validate_prototype(prototype, protkey=None, protparents=None, _visited=None): def validate_prototype(prototype, protkey=None, protparents=None,
is_prototype_base=True, _flags=None):
""" """
Run validation on a prototype, checking for inifinite regress. Run validation on a prototype, checking for inifinite regress.
@ -523,33 +525,77 @@ def validate_prototype(prototype, protkey=None, protparents=None, _visited=None)
dict needs to have the `prototype_key` field set. dict needs to have the `prototype_key` field set.
protpartents (dict, optional): The available prototype parent library. If protpartents (dict, optional): The available prototype parent library. If
note given this will be determined from settings/database. note given this will be determined from settings/database.
_visited (list, optional): This is an internal work array and should not be set manually. is_prototype_base (bool, optional): We are trying to create a new object *based on this
object*. This means we can't allow 'mixin'-style prototypes without typeclass/parent
etc.
_flags (dict, optional): Internal work dict that should not be set externally.
Raises: Raises:
RuntimeError: If prototype has invalid structure. RuntimeError: If prototype has invalid structure.
RuntimeWarning: If prototype has issues that would make it unsuitable to build an object
with (it may still be useful as a mix-in prototype).
""" """
assert isinstance(prototype, dict)
if _flags is None:
_flags = {"visited": [], "depth": 0, "typeclass": False, "errors": [], "warnings": []}
if not protparents: if not protparents:
protparents = {prototype['prototype_key']: prototype for prototype in search_prototype()} protparents = {prototype['prototype_key']: prototype for prototype in search_prototype()}
if _visited is None:
_visited = []
protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) protkey = protkey and protkey.lower() or prototype.get('prototype_key', None)
assert isinstance(prototype, dict) if not bool(protkey):
_flags['errors'].append("Prototype lacks a `prototype_key`.")
protkey = "[UNSET]"
if id(prototype) in _visited: typeclass = prototype.get('typeclass')
raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype) prototype_parent = prototype.get('prototype_parent', [])
_visited.append(id(prototype)) if not (typeclass or prototype_parent):
protstrings = prototype.get("prototype") if is_prototype_base:
_flags['errors'].append("Prototype {} requires `typeclass` "
"or 'prototype_parent'.".format(protkey))
else:
_flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks "
"a typeclass or a prototype_parent.".format(protkey))
if protstrings: if typeclass and typeclass not in get_all_typeclasses("evennia.objects.models.ObjectDB"):
for protstring in make_iter(protstrings): _flags['errors'].append(
"Prototype {} is based on typeclass {} which could not be imported!".format(
protkey, typeclass))
# recursively traverese prototype_parent chain
if id(prototype) in _flags['visited']:
_flags['errors'].append(
"{} has infinite nesting of prototypes.".format(protkey or prototype))
_flags['visited'].append(id(prototype))
for protstring in make_iter(prototype_parent):
protstring = protstring.lower() protstring = protstring.lower()
if protkey is not None and protstring == protkey: if protkey is not None and protstring == protkey:
raise RuntimeError("%s tries to prototype itself." % protkey or prototype) _flags['errors'].append("Protototype {} tries to parent itself.".format(protkey))
protparent = protparents.get(protstring) protparent = protparents.get(protstring)
if not protparent: if not protparent:
raise RuntimeError( _flags['errors'].append("Prototype {}'s prototype_parent '{}' was not found.".format(
"%s's prototype '%s' was not found." % (protkey or prototype, protstring)) (protkey, protstring)))
validate_prototype(protparent, protstring, protparents, _visited) _flags['depth'] += 1
validate_prototype(protparent, protstring, protparents, _flags)
_flags['depth'] -= 1
if typeclass and not _flags['typeclass']:
_flags['typeclass'] = typeclass
# if we get back to the current level without a typeclass it's an error.
if is_prototype_base and _flags['depth'] <= 0 and not _flags['typeclass']:
_flags['errors'].append("Prototype {} has no `typeclass` defined anywhere in its parent "
"chain. Add `typeclass`, or a `prototype_parent` pointing to a "
"prototype with a typeclass.".format(protkey))
if _flags['depth'] <= 0:
if _flags['errors']:
raise RuntimeError("Error: " + "\nError: ".join(_flags['errors']))
if _flags['warnings']:
raise RuntimeWarning("Warning: " + "\nWarning: ".join(_flags['warnings']))

View file

@ -32,7 +32,7 @@ Possible keywords are:
prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype
in listings in listings
parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or
a list of parents, for multiple left-to-right inheritance. a list of parents, for multiple left-to-right inheritance.
prototype: Deprecated. Same meaning as 'parent'. prototype: Deprecated. Same meaning as 'parent'.
typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use
@ -75,13 +75,13 @@ import random
GOBLIN_WIZARD = { GOBLIN_WIZARD = {
"parent": GOBLIN, "prototype_parent": GOBLIN,
"key": "goblin wizard", "key": "goblin wizard",
"spells": ["fire ball", "lighting bolt"] "spells": ["fire ball", "lighting bolt"]
} }
GOBLIN_ARCHER = { GOBLIN_ARCHER = {
"parent": GOBLIN, "prototype_parent": GOBLIN,
"key": "goblin archer", "key": "goblin archer",
"attack_skill": (random, (5, 10))" "attack_skill": (random, (5, 10))"
"attacks": ["short bow"] "attacks": ["short bow"]
@ -97,7 +97,7 @@ ARCHWIZARD = {
GOBLIN_ARCHWIZARD = { GOBLIN_ARCHWIZARD = {
"key" : "goblin archwizard" "key" : "goblin archwizard"
"parent": (GOBLIN_WIZARD, ARCHWIZARD), "prototype_parent": (GOBLIN_WIZARD, ARCHWIZARD),
} }
``` ```
@ -460,11 +460,15 @@ def spawn(*prototypes, **kwargs):
prototype_parents (dict): A dictionary holding a custom prototype_parents (dict): A dictionary holding a custom
prototype-parent dictionary. Will overload same-named prototype-parent dictionary. Will overload same-named
prototypes from prototype_modules. prototypes from prototype_modules.
return_prototypes (bool): Only return a list of the return_parents (bool): Only return a dict of the
prototype-parents (no object creation happens) prototype-parents (no object creation happens)
only_validate (bool): Only run validation of prototype/parents
(no object creation) and return the create-kwargs.
Returns: Returns:
object (Object): Spawned object. object (Object, dict or list): Spawned object. If `only_validate` is given, return
a list of the creation kwargs to build the object(s) without actually creating it. If
`return_parents` is set, return dict of prototype parents.
""" """
# get available protparents # get available protparents
@ -474,17 +478,14 @@ def spawn(*prototypes, **kwargs):
protparents.update( protparents.update(
{key.lower(): value for key, value in kwargs.get("prototype_parents", {}).items()}) {key.lower(): value for key, value in kwargs.get("prototype_parents", {}).items()})
for key, prototype in protparents.items(): if "return_parents" in kwargs:
protlib.validate_prototype(prototype, key.lower(), protparents)
if "return_prototypes" in kwargs:
# only return the parents # only return the parents
return copy.deepcopy(protparents) return copy.deepcopy(protparents)
objsparams = [] objsparams = []
for prototype in prototypes: for prototype in prototypes:
protlib.validate_prototype(prototype, None, protparents) protlib.validate_prototype(prototype, None, protparents, is_prototype_base=True)
prot = _get_prototype(prototype, {}, protparents) prot = _get_prototype(prototype, {}, protparents)
if not prot: if not prot:
continue continue
@ -556,4 +557,6 @@ def spawn(*prototypes, **kwargs):
objsparams.append((create_kwargs, permission_string, lock_string, objsparams.append((create_kwargs, permission_string, lock_string,
alias_string, nattributes, attributes, tags, execs)) alias_string, nattributes, attributes, tags, execs))
if kwargs.get("only_validate"):
return objsparams
return batch_create_object(*objsparams) return batch_create_object(*objsparams)

View file

@ -8,7 +8,9 @@ import mock
from anything import Something from anything import Something
from django.test.utils import override_settings from django.test.utils import override_settings
from evennia.utils.test_resources import EvenniaTest from evennia.utils.test_resources import EvenniaTest
from evennia.utils.tests.test_evmenu import TestEvMenu
from evennia.prototypes import spawner, prototypes as protlib from evennia.prototypes import spawner, prototypes as protlib
from evennia.prototypes import menus as olc_menus
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
@ -304,3 +306,37 @@ class TestPrototypeStorage(EvenniaTest):
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3]) self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
self.assertTrue(str(unicode(protlib.list_prototypes(self.char1)))) self.assertTrue(str(unicode(protlib.list_prototypes(self.char1))))
@mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(
return_value=[{"prototype_key": "TestPrototype",
"typeclass": "TypeClassTest", "key": "TestObj"}]))
@mock.patch("evennia.utils.utils.get_all_typeclasses", new=mock.MagicMock(
return_value={"TypeclassTest": None}))
class TestOLCMenu(TestEvMenu):
maxDiff = None
menutree = "evennia.prototypes.menus"
startnode = "node_index"
debug_output = True
expected_node_texts = {
"node_index": "|c --- Prototype wizard --- |n"
}
expected_tree = \
['node_index',
['node_prototype_key',
'node_typeclass',
'node_aliases',
'node_attrs',
'node_tags',
'node_locks',
'node_permissions',
'node_location',
'node_home',
'node_destination',
'node_prototype_desc',
'node_prototype_tags',
'node_prototype_locks']]

View file

@ -58,7 +58,7 @@ class TestEvMenu(TestCase):
def _debug_output(self, indent, msg): def _debug_output(self, indent, msg):
if self.debug_output: if self.debug_output:
print(" " * indent + msg) print(" " * indent + ansi.strip_ansi(msg))
def _test_menutree(self, menu): def _test_menutree(self, menu):
""" """
@ -168,6 +168,7 @@ class TestEvMenu(TestCase):
self.caller2.msg = MagicMock() self.caller2.msg = MagicMock()
self.session = MagicMock() self.session = MagicMock()
self.session2 = MagicMock() self.session2 = MagicMock()
self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode, self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode,
cmdset_mergetype=self.cmdset_mergetype, cmdset_mergetype=self.cmdset_mergetype,
cmdset_priority=self.cmdset_priority, cmdset_priority=self.cmdset_priority,