From 9360dc71f183a3a823ab6d90744062d4b7d6152d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 24 Jun 2018 09:50:03 +0200 Subject: [PATCH] Rename prototype to prototype_parent, fixing olc menu --- evennia/commands/default/building.py | 4 +- evennia/prototypes/menus.py | 52 ++++++++------- evennia/prototypes/prototypes.py | 94 +++++++++++++++++++++------- evennia/prototypes/spawner.py | 25 ++++---- evennia/prototypes/tests.py | 36 +++++++++++ evennia/utils/tests/test_evmenu.py | 3 +- 6 files changed, 155 insertions(+), 59 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 301bd0376..5c96ad1cf 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -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: diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index bebc6d00b..ead299abc 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -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 = ' where 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: diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index ac343b3ec..18516681b 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -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'])) diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index da4d69eeb..df07e3b15 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -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) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 0eeb236fb..0f48c3780 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -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']] diff --git a/evennia/utils/tests/test_evmenu.py b/evennia/utils/tests/test_evmenu.py index 04310c90e..d3ee14a74 100644 --- a/evennia/utils/tests/test_evmenu.py +++ b/evennia/utils/tests/test_evmenu.py @@ -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,