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.eveditor import EvEditor
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
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -2917,7 +2917,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
elif prototype:
# one match
prototype = prototype[0]
spawner.start_olc(caller, session=self.session, prototype=prototype)
olc_menus.start_olc(caller, session=self.session, prototype=prototype)
return
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.ansi import strip_ansi
from evennia.utils import utils
from evennia.utils.prototypes import prototypes as protlib
from evennia.utils.prototypes import spawner
from evennia.prototypes import prototypes as protlib
from evennia.prototypes import spawner
# ------------------------------------------------------------
#
@ -43,12 +43,6 @@ def _is_new_prototype(caller):
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):
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))
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):
"""
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:
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":
prototype.pop("prototype", None)
if propname_low == "prototype":
prototype.pop("prototype_parent", None)
if propname_low == "prototype_parent":
prototype.pop("typeclass", None)
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
def _wizard_options(curr_node, prev_node, next_node, color="|W"):
"""
Creates default navigation options available in the wizard.
"""
options = []
if prev_node:
options.append({"key": ("|wb|Wack", "b"),
@ -154,8 +159,8 @@ def node_index(caller):
text = ("|c --- Prototype wizard --- |n\n\n"
"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 "
"required.\n\n'|wMeta'-properties|n are not used in the prototype itself but are used "
"to organize and list prototypes. The 'Meta-Key' uniquely identifies the prototype "
"required.\n\n'|wprototype-'-properties|n are not used in the prototype itself but are used "
"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 "
"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)"
try:
# validate, don't spawn
spawner.spawn(prototype, return_prototypes=True)
spawner.spawn(prototype, only_validate=True)
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)
options = _wizard_options(None, kwargs.get("back"), None)
@ -287,7 +295,9 @@ def node_prototype(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):
@ -403,7 +413,7 @@ def _add_attr(caller, attr_string, **kwargs):
if attrname:
prot = _get_menu_prototype(caller)
prot['attrs'][attrname] = value
_set_menu_prototype(caller, "prototype", prot)
_set_prototype_value(caller, "prototype", prot)
text = "Added"
else:
text = "Attribute must be given as 'attrname = <value>' where <value> uses valid Python."
@ -468,7 +478,7 @@ def _add_tag(caller, tag, **kwargs):
else:
tags = [tag]
prototype['tags'] = tags
_set_menu_prototype(caller, "prototype", prototype)
_set_prototype_value(caller, "prototype", prototype)
text = kwargs.get("text")
if not text:
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()
tags[tags.index(old_tag)] = new_tag
prototype['tags'] = tags
_set_menu_prototype(caller, 'prototype', prototype)
_set_prototype_value(caller, 'prototype', prototype)
text = kwargs.get('text')
if not text:

View file

@ -12,7 +12,8 @@ from evennia.scripts.scripts import DefaultScript
from evennia.objects.models import ObjectDB
from evennia.utils.create import create_script
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.utils import logger
from evennia.utils import inlinefuncs
@ -143,10 +144,10 @@ def prototype_to_str(prototype):
header = (
"|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n"
"|cdesc:|n {} \n|cprototype:|n ".format(
prototype['prototype_key'],
", ".join(prototype['prototype_tags']),
prototype['prototype_locks'],
prototype['prototype_desc']))
prototype.get('prototype_key', None),
", ".join(prototype.get('prototype_tags', ['None'])),
prototype.get('prototype_locks', None),
prototype.get('prototype_desc', None)))
proto = ("{{\n {} \n}}".format(
"\n ".join(
"{!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
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.
@ -523,33 +525,77 @@ def validate_prototype(prototype, protkey=None, protparents=None, _visited=None)
dict needs to have the `prototype_key` field set.
protpartents (dict, optional): The available prototype parent library. If
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:
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:
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)
assert isinstance(prototype, dict)
if not bool(protkey):
_flags['errors'].append("Prototype lacks a `prototype_key`.")
protkey = "[UNSET]"
if id(prototype) in _visited:
raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype)
typeclass = prototype.get('typeclass')
prototype_parent = prototype.get('prototype_parent', [])
_visited.append(id(prototype))
protstrings = prototype.get("prototype")
if not (typeclass or prototype_parent):
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:
for protstring in make_iter(protstrings):
protstring = protstring.lower()
if protkey is not None and protstring == protkey:
raise RuntimeError("%s tries to prototype itself." % protkey or prototype)
protparent = protparents.get(protstring)
if not protparent:
raise RuntimeError(
"%s's prototype '%s' was not found." % (protkey or prototype, protstring))
validate_prototype(protparent, protstring, protparents, _visited)
if typeclass and typeclass not in get_all_typeclasses("evennia.objects.models.ObjectDB"):
_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()
if protkey is not None and protstring == protkey:
_flags['errors'].append("Protototype {} tries to parent itself.".format(protkey))
protparent = protparents.get(protstring)
if not protparent:
_flags['errors'].append("Prototype {}'s prototype_parent '{}' was not found.".format(
(protkey, protstring)))
_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
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.
prototype: Deprecated. Same meaning as 'parent'.
typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use
@ -75,13 +75,13 @@ import random
GOBLIN_WIZARD = {
"parent": GOBLIN,
"prototype_parent": GOBLIN,
"key": "goblin wizard",
"spells": ["fire ball", "lighting bolt"]
}
GOBLIN_ARCHER = {
"parent": GOBLIN,
"prototype_parent": GOBLIN,
"key": "goblin archer",
"attack_skill": (random, (5, 10))"
"attacks": ["short bow"]
@ -97,7 +97,7 @@ ARCHWIZARD = {
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-parent dictionary. Will overload same-named
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)
only_validate (bool): Only run validation of prototype/parents
(no object creation) and return the create-kwargs.
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
@ -474,17 +478,14 @@ def spawn(*prototypes, **kwargs):
protparents.update(
{key.lower(): value for key, value in kwargs.get("prototype_parents", {}).items()})
for key, prototype in protparents.items():
protlib.validate_prototype(prototype, key.lower(), protparents)
if "return_prototypes" in kwargs:
if "return_parents" in kwargs:
# only return the parents
return copy.deepcopy(protparents)
objsparams = []
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)
if not prot:
continue
@ -556,4 +557,6 @@ def spawn(*prototypes, **kwargs):
objsparams.append((create_kwargs, permission_string, lock_string,
alias_string, nattributes, attributes, tags, execs))
if kwargs.get("only_validate"):
return objsparams
return batch_create_object(*objsparams)

View file

@ -8,7 +8,9 @@ import mock
from anything import Something
from django.test.utils import override_settings
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 menus as olc_menus
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.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):
if self.debug_output:
print(" " * indent + msg)
print(" " * indent + ansi.strip_ansi(msg))
def _test_menutree(self, menu):
"""
@ -168,6 +168,7 @@ class TestEvMenu(TestCase):
self.caller2.msg = MagicMock()
self.session = MagicMock()
self.session2 = MagicMock()
self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode,
cmdset_mergetype=self.cmdset_mergetype,
cmdset_priority=self.cmdset_priority,