From ec9813c25639c89f1998ac58477ec2b59c9de963 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 3 Jul 2018 23:56:55 +0200 Subject: [PATCH] Add functionality for object-update menu node, untested --- CHANGELOG.md | 40 ++++++-- evennia/prototypes/menus.py | 166 +++++++++++++++++++++++++------ evennia/prototypes/prototypes.py | 20 ++-- evennia/prototypes/spawner.py | 18 +++- evennia/prototypes/tests.py | 85 +++++++++++++--- 5 files changed, 265 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a05d65fdc0..3c1c4cf787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,29 @@ -# Evennia Changelog +# Changelog -# Sept 2017: -Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to +## Evennia 0.8 (2018) + +### Prototype changes + +- A new form of prototype - database-stored prototypes, editable from in-game. The old, + module-created prototypes remain as read-only prototypes. +- All prototypes must have a key `prototype_key` identifying the prototype in listings. This is + checked to be server-unique. Prototypes created in a module will use the global variable name they + are assigned to if no `prototype_key` is given. +- Prototype field `prototype` was renamed to `prototype_parent` to avoid mixing terms. +- All prototypes must either have `typeclass` or `prototype_parent` defined. If using + `prototype_parent`, `typeclass` must be defined somewhere in the inheritance chain. This is a + change from Evennia 0.7 which allowed 'mixin' prototypes without `typeclass`/`prototype_key`. To + make a mixin now, give it a default typeclass, like `evennia.objects.objects.DefaultObject` and just + override in the child as needed. +- The spawn command was extended to accept a full prototype on one line. +- The spawn command got the /save switch to save the defined prototype and its key. +- The command spawn/menu will now start an OLC (OnLine Creation) menu to load/save/edit/spawn prototypes. + + +# Overviews + +## Sept 2017: +Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to 'Account', rework the website template and a slew of other updates. Info on what changed and how to migrate is found here: https://groups.google.com/forum/#!msg/evennia/0JYYNGY-NfE/cDFaIwmPBAAJ @@ -14,9 +36,9 @@ Lots of bugfixes and considerable uptick in contributors. Unittest coverage and PEP8 adoption and refactoring. ## May 2016: -Evennia 0.6 with completely reworked Out-of-band system, making +Evennia 0.6 with completely reworked Out-of-band system, making the message path completely flexible and built around input/outputfuncs. -A completely new webclient, split into the evennia.js library and a +A completely new webclient, split into the evennia.js library and a gui library, making it easier to customize. ## Feb 2016: @@ -33,15 +55,15 @@ library format with a stand-alone launcher, in preparation for making an 'evennia' pypy package and using versioning. The version we will merge with will likely be 0.5. There is also work with an expanded testing structure and the use of threading for saves. We also now -use Travis for automatic build checking. +use Travis for automatic build checking. ## Sept 2014: Updated to Django 1.7+ which means South dependency was dropped and minimum Python version upped to 2.7. MULTISESSION_MODE=3 was added -and the web customization system was overhauled using the latest -functionality of django. Otherwise, mostly bug-fixes and +and the web customization system was overhauled using the latest +functionality of django. Otherwise, mostly bug-fixes and implementation of various smaller feature requests as we got used -to github. Many new users have appeared. +to github. Many new users have appeared. ## Jan 2014: Moved Evennia project from Google Code to github.com/evennia/evennia. diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index f978aae566..af670b743a 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -5,6 +5,7 @@ OLC Prototype menu nodes """ import json +from random import choice from django.conf import settings from evennia.utils.evmenu import EvMenu, list_node from evennia.utils.ansi import strip_ansi @@ -469,7 +470,7 @@ def _caller_attrs(caller): def _display_attribute(attr_tuple): """Pretty-print attribute tuple""" - attrkey, value, category, locks, default_access = attr_tuple + attrkey, value, category, locks = attr_tuple value = protlib.protfunc_parser(value) typ = type(value) out = ("Attribute key: '{attrkey}' (category: {category}, " @@ -503,7 +504,7 @@ def _add_attr(caller, attr_string, **kwargs): attrname, category = nameparts elif nparts > 2: attrname, category, locks = nameparts - attr_tuple = (attrname, category, locks) + attr_tuple = (attrname, value, category, locks) if attrname: prot = _get_menu_prototype(caller) @@ -513,7 +514,7 @@ def _add_attr(caller, attr_string, **kwargs): # replace existing attribute with the same name in the prototype ind = [tup[0] for tup in attrs].index(attrname) attrs[ind] = attr_tuple - except IndexError: + except ValueError: attrs.append(attr_tuple) _set_prototype_value(caller, "attrs", attrs) @@ -541,7 +542,8 @@ def _edit_attr(caller, attrname, new_value, **kwargs): def _examine_attr(caller, selection): prot = _get_menu_prototype(caller) - attr_tuple = prot['attrs'][selection] + ind = [part[0] for part in prot['attrs']].index(selection) + attr_tuple = prot['attrs'][ind] return _display_attribute(attr_tuple) @@ -572,15 +574,15 @@ def node_attrs(caller): def _caller_tags(caller): prototype = _get_menu_prototype(caller) - tags = prototype.get("tags") + tags = prototype.get("tags", []) return tags def _display_tag(tag_tuple): """Pretty-print attribute tuple""" tagkey, category, data = tag_tuple - out = ("Tag: '{tagkey}' (category: {category}{})".format( - tagkey=tagkey, category=category, data=", data: {}".format(data) if data else "")) + out = ("Tag: '{tagkey}' (category: {category}{dat})".format( + tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else "")) return out @@ -613,16 +615,21 @@ def _add_tag(caller, tag, **kwargs): old_tag = kwargs.get("edit", None) - if old_tag: - # editing a tag means removing the old and replacing with new + if not old_tag: + # a fresh, new tag + tags.append(tag_tuple) + else: + # old tag exists; editing a tag means removing the old and replacing with new try: ind = [tup[0] for tup in tags].index(old_tag) del tags[ind] + if tags: + tags.insert(ind, tag_tuple) + else: + tags = [tag_tuple] except IndexError: pass - tags.append(tag_tuple) - _set_prototype_value(caller, "tags", tags) text = kwargs.get('text') @@ -814,18 +821,121 @@ def node_prototype_locks(caller): return text, options +def _update_spawned(caller, **kwargs): + """update existing objects""" + prototype = kwargs['prototype'] + objects = kwargs['objects'] + back_node = kwargs['back_key'] + num_changed = spawner.batch_update_objects_with_prototype(prototype, objects=objects) + caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed)) + return back_key + + +def _keep_diff(caller, **kwargs): + key = kwargs['key'] + diff = kwargs['diff'] + diff[key] = "KEEP" + + +def node_update_objects(caller, **kwargs): + """Offer options for updating objects""" + + def _keep_option(keyname, prototype, obj, obj_prototype, diff, objects, back_node): + """helper returning an option dict""" + options = {"desc": "Keep {} as-is".format(keyname), + "goto": (_keep_diff, + {"key": keyname, "prototype": prototype, + "obj": obj, "obj_prototype": obj_prototype, + "diff": diff, "objects": objects, "back_node": back_node})} + return options + + prototype = kwargs.get("prototype", None) + update_objects = kwargs.get("objects", None) + back_node = kwargs.get("back_node", "node_index") + obj_prototype = kwargs.get("obj_prototype", None) + diff = kwargs.get("diff", None) + + if not update_objects: + text = "There are no existing objects to update." + options = {"key": "_default", + "goto": back_node} + return text, options + + if not diff: + # use one random object as a reference to calculate a diff + obj = choice(update_objects) + diff, obj_prototype = spawner.prototype_diff_from_object(prototype, obj) + + text = ["Suggested changes to {} objects".format(len(update_objects)), + "Showing random example obj to change: {name} (#{dbref}))\n".format(obj.key, obj.dbref)] + options = [] + io = 0 + for (key, inst) in sorted(((key, val) for key, val in diff.items()), key=lambda tup: tup[0]): + line = "{iopt} |w{key}|n: {old}{sep}{new} {change}" + old_val = utils.crop(str(obj_prototype[key]), width=20) + + if inst == "KEEP": + text.append(line.format(iopt='', key=key, old=old_val, sep=" ", new='', change=inst)) + continue + + new_val = utils.crop(str(spawner.init_spawn_value(prototype[key])), width=20) + io += 1 + if inst in ("UPDATE", "REPLACE"): + text.append(line.format(iopt=io, key=key, old=old_val, + sep=" |y->|n ", new=new_val, change=inst)) + options.append(_keep_option(key, prototype, + obj, obj_prototype, diff, objects, back_node)) + elif inst == "REMOVE": + text.append(line.format(iopt=io, key=key, old=old_val, + sep=" |r->|n ", new='', change=inst)) + options.append(_keep_option(key, prototype, + obj, obj_prototype, diff, objects, back_node)) + options.extend( + [{"key": ("|wu|r update {} objects".format(len(update_objects)), "update", "u"), + "goto": (_update_spawned, {"prototype": prototype, "objects": objects, + "back_node": back_node, "diff": diff})}, + {"key": ("|wr|neset changes", "reset", "r"), + "goto": ("node_update_objects", {"prototype": prototype, "back_node": back_node, + "objects": update_objects})}, + {"key": "|wb|rack ({})".format(back_node[5:], 'b'), + "goto": back_node}]) + + return text, options + + def node_prototype_save(caller, **kwargs): """Save prototype to disk """ # these are only set if we selected 'yes' to save on a previous pass - accept_save = kwargs.get("accept", False) prototype = kwargs.get("prototype", None) + accept_save = kwargs.get("accept_save", False) if accept_save and prototype: # we already validated and accepted the save, so this node acts as a goto callback and # should now only return the next node + prototype_key = prototype.get("prototype_key") protlib.save_prototype(**prototype) - caller.msg("|gPrototype saved.|n") - return "node_spawn" + + spawned_objects = protlib.search_objects_with_prototype(prototype_key) + nspawned = spawned_objects.count() + + if nspawned: + text = ("Do you want to update {} object(s) " + "already using this prototype?".format(nspawned)) + options = ( + {"key": ("|wY|Wes|n", "yes", "y"), + "goto": ("node_update_objects", + {"accept_update": True, "objects": spawned_objects, + "prototype": prototype, "back_node": "node_prototype_save"})}, + {"key": ("[|wN|Wo|n]", "n"), + "goto": "node_spawn"}, + {"key": "_default", + "goto": "node_spawn"}) + else: + text = "|gPrototype saved.|n" + options = {"key": "_default", + "goto": "node_spawn"} + + return text, options # not validated yet prototype = _get_menu_prototype(caller) @@ -850,15 +960,13 @@ def node_prototype_save(caller, **kwargs): options = ( {"key": ("[|wY|Wes|n]", "yes", "y"), - "goto": lambda caller: - node_prototype_save(caller, - {"accept": True, "prototype": prototype})}, + "goto": ("node_prototype_save", + {"accept": True, "prototype": prototype})}, {"key": ("|wN|Wo|n", "n"), "goto": "node_spawn"}, {"key": "_default", - "goto": lambda caller: - node_prototype_save(caller, - {"accept": True, "prototype": prototype})}) + "goto": ("node_prototype_save", + {"accept": True, "prototype": prototype})}) return "\n".join(text), options @@ -869,20 +977,15 @@ def _spawn(caller, **kwargs): new_location = kwargs.get('location', None) if new_location: prototype['location'] = new_location + obj = spawner.spawn(prototype) if obj: + obj = obj[0] caller.msg("|gNew instance|n {key} ({dbref}) |gspawned.|n".format( key=obj.key, dbref=obj.dbref)) else: caller.msg("|rError: Spawner did not return a new instance.|n") - - -def _update_spawned(caller, **kwargs): - """update existing objects""" - prototype = kwargs['prototype'] - objects = kwargs['objects'] - num_changed = spawner.batch_update_objects_with_prototype(prototype, objects=objects) - caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed)) + return obj def node_prototype_spawn(caller, **kwargs): @@ -926,9 +1029,9 @@ def node_prototype_spawn(caller, **kwargs): if spawned_objects: options.append( {"desc": "Update {num} existing objects with this prototype".format(num=nspawned), - "goto": (_update_spawned, - dict(prototype=prototype, - opjects=spawned_objects))}) + "goto": ("node_update_objects", + dict(prototype=prototype, opjects=spawned_objects, + back_node="node_prototype_spawn"))}) options.extend(_wizard_options("prototype_spawn", "prototype_save", "index")) return text, options @@ -1008,6 +1111,7 @@ def start_olc(caller, session=None, prototype=None): "node_location": node_location, "node_home": node_home, "node_destination": node_destination, + "node_update_objects": node_o "node_prototype_desc": node_prototype_desc, "node_prototype_tags": node_prototype_tags, "node_prototype_locks": node_prototype_locks, diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index c02041d8bc..2457f86994 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -547,7 +547,8 @@ def validate_prototype(prototype, protkey=None, protparents=None, _flags = {"visited": [], "depth": 0, "typeclass": False, "errors": [], "warnings": []} if not protparents: - protparents = {prototype['prototype_key']: prototype for prototype in search_prototype()} + protparents = {prototype.get('prototype_key', "").lower(): prototype + for prototype in search_prototype()} protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) @@ -568,17 +569,11 @@ def validate_prototype(prototype, protkey=None, protparents=None, 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( + "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: @@ -587,8 +582,15 @@ def validate_prototype(prototype, protkey=None, protparents=None, if not protparent: _flags['errors'].append("Prototype {}'s prototype_parent '{}' was not found.".format( (protkey, protstring))) + if id(prototype) in _flags['visited']: + _flags['errors'].append( + "{} has infinite nesting of prototypes.".format(protkey or prototype)) + + _flags['visited'].append(id(prototype)) _flags['depth'] += 1 - validate_prototype(protparent, protstring, protparents, _flags) + validate_prototype(protparent, protstring, protparents, + is_prototype_base=is_prototype_base, _flags=_flags) + _flags['visited'].pop() _flags['depth'] -= 1 if typeclass and not _flags['typeclass']: diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 71aecfd61e..d826317fec 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -179,6 +179,7 @@ def prototype_from_object(obj): prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) if prot: prot = protlib.search_prototype(prot[0]) + if not prot or len(prot) > 1: # no unambiguous prototype found - build new prototype prot = {} @@ -187,6 +188,8 @@ def prototype_from_object(obj): prot['prototype_desc'] = "Built from {}".format(str(obj)) prot['prototype_locks'] = "spawn:all();edit:all()" prot['prototype_tags'] = [] + else: + prot = prot[0] prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] prot['typeclass'] = obj.db_typeclass_path @@ -233,6 +236,8 @@ def prototype_diff_from_object(prototype, obj): Returns: diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} + other_prototype (dict): The prototype for the given object. The diff is a how to convert + this prototype into the new prototype. """ prot1 = prototype @@ -253,7 +258,7 @@ def prototype_diff_from_object(prototype, obj): if key not in diff and key not in prot1: diff[key] = "REMOVE" - return diff + return diff, prot2 def batch_update_objects_with_prototype(prototype, diff=None, objects=None): @@ -475,8 +480,12 @@ def spawn(*prototypes, **kwargs): protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} # overload module's protparents with specifically given protparents - protparents.update( - {key.lower(): value for key, value in kwargs.get("prototype_parents", {}).items()}) + # we allow prototype_key to be the key of the protparent dict, to allow for module-level + # prototype imports. We need to insert prototype_key in this case + for key, protparent in kwargs.get("prototype_parents", {}).items(): + key = str(key).lower() + protparent['prototype_key'] = str(protparent.get("prototype_key", key)).lower() + protparents[key] = protparent if "return_parents" in kwargs: # only return the parents @@ -541,6 +550,9 @@ def spawn(*prototypes, **kwargs): simple_attributes = [] for key, value in ((key, value) for key, value in prot.items() if not (key.startswith("ndb_"))): + if key in _PROTOTYPE_META_NAMES: + continue + if is_iter(value) and len(value) > 1: # (value, category) simple_attributes.append((key, diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 0d5e247378..69eb495dd5 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -17,6 +17,8 @@ from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY _PROTPARENTS = { "NOBODY": {}, "GOBLIN": { + "prototype_key": "GOBLIN", + "typeclass": "evennia.objects.objects.DefaultObject", "key": "goblin grunt", "health": lambda: randint(1, 1), "resists": ["cold", "poison"], @@ -24,21 +26,22 @@ _PROTPARENTS = { "weaknesses": ["fire", "light"] }, "GOBLIN_WIZARD": { - "prototype": "GOBLIN", + "prototype_parent": "GOBLIN", "key": "goblin wizard", "spells": ["fire ball", "lighting bolt"] }, "GOBLIN_ARCHER": { - "prototype": "GOBLIN", + "prototype_parent": "GOBLIN", "key": "goblin archer", "attacks": ["short bow"] }, "ARCHWIZARD": { + "prototype_parent": "GOBLIN", "attacks": ["archwizard staff"], }, "GOBLIN_ARCHWIZARD": { "key": "goblin archwizard", - "prototype": ("GOBLIN_WIZARD", "ARCHWIZARD") + "prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD") } } @@ -47,7 +50,8 @@ class TestSpawner(EvenniaTest): def setUp(self): super(TestSpawner, self).setUp() - self.prot1 = {"prototype_key": "testprototype"} + self.prot1 = {"prototype_key": "testprototype", + "typeclass": "evennia.objects.objects.DefaultObject"} def test_spawn(self): obj1 = spawner.spawn(self.prot1) @@ -323,6 +327,7 @@ class TestMenuModule(EvenniaTest): self.caller.ndb._menutree = menutree self.test_prot = {"prototype_key": "test_prot", + "typeclass": "evennia.objects.objects.DefaultObject", "prototype_locks": "edit:all();spawn:all()"} def test_helpers(self): @@ -334,6 +339,8 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._get_menu_prototype(caller), {}) self.assertEqual(olc_menus._is_new_prototype(caller), True) + self.assertEqual(olc_menus._set_menu_prototype(caller, {}), {}) + self.assertEqual( olc_menus._set_prototype_value(caller, "key", "TestKey"), {"key": "TestKey"}) self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "TestKey"}) @@ -349,13 +356,16 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._wizard_options( "ThisNode", "PrevNode", "NextNode"), - [{'goto': 'node_PrevNode', 'key': ('|wb|Wack', 'b'), 'desc': '|W(PrevNode)|n'}, - {'goto': 'node_NextNode', 'key': ('|wf|Worward', 'f'), 'desc': '|W(NextNode)|n'}, - {'goto': 'node_index', 'key': ('|wi|Wndex', 'i')}, + [{'goto': 'node_PrevNode', 'key': ('|wB|Wack', 'b'), 'desc': '|W(PrevNode)|n'}, + {'goto': 'node_NextNode', 'key': ('|wF|Worward', 'f'), 'desc': '|W(NextNode)|n'}, + {'goto': 'node_index', 'key': ('|wI|Wndex', 'i')}, {'goto': ('node_validate_prototype', {'back': 'ThisNode'}), - 'key': ('|wv|Walidate prototype', 'v')}]) + 'key': ('|wV|Walidate prototype', 'validate', 'v')}]) - self.assertEqual(olc_menus._validate_prototype(self.test_prot, (False, Something))) + self.assertEqual(olc_menus._validate_prototype(self.test_prot), (False, Something)) + self.assertEqual(olc_menus._validate_prototype( + {"prototype_key": "testthing", "key": "mytest"}), + (True, Something)) def test_node_helpers(self): @@ -363,23 +373,27 @@ class TestMenuModule(EvenniaTest): with mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(return_value=[self.test_prot])): + # prototype_key helpers self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), "node_prototype_parent") caller.ndb._menutree.olc_new = True self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), "node_index") + # prototype_parent helpers self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot']) self.assertEqual(olc_menus._prototype_parent_examine( caller, 'test_prot'), - '|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() ' - '\n|cdesc:|n None \n|cprototype:|n {\n \n}') + "|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() " + "\n|cdesc:|n None \n|cprototype:|n " + "{\n 'typeclass': 'evennia.objects.objects.DefaultObject', \n}") self.assertEqual(olc_menus._prototype_parent_select(caller, self.test_prot), "node_key") self.assertEqual(olc_menus._get_menu_prototype(caller), {'prototype_key': 'test_prot', 'prototype_locks': 'edit:all();spawn:all()', - 'prototype_parent': "test_prot"}) + 'typeclass': 'evennia.objects.objects.DefaultObject'}) + # typeclass helpers with mock.patch("evennia.utils.utils.get_all_typeclasses", new=mock.MagicMock(return_value={"foo": None, "bar": None})): self.assertEqual(olc_menus._all_typeclasses(caller), ["bar", "foo"]) @@ -394,6 +408,53 @@ class TestMenuModule(EvenniaTest): 'prototype_locks': 'edit:all();spawn:all()', 'typeclass': 'evennia.objects.objects.DefaultObject'}) + # attr helpers + self.assertEqual(olc_menus._caller_attrs(caller), []) + self.assertEqual(olc_menus._add_attr(caller, "test1=foo1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_attr(caller, "test2;cat1=foo2"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_attr(caller, "test3;cat2;edit:false()=foo3"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_attr(caller, "test4;cat3;set:true();edit:false()=foo4"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_attr(caller, "test5;cat4;set:true();edit:false()=123"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._caller_attrs( + caller), + [("test1", "foo1", None, ''), + ("test2", "foo2", "cat1", ''), + ("test3", "foo3", "cat2", "edit:false()"), + ("test4", "foo4", "cat3", "set:true();edit:false()"), + ("test5", '123', "cat4", "set:true();edit:false()")]) + self.assertEqual(olc_menus._edit_attr(caller, "test1", "1;cat5;edit:all()"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._examine_attr(caller, "test1"), Something) + + # tag helpers + self.assertEqual(olc_menus._caller_tags(caller), []) + self.assertEqual(olc_menus._add_tag(caller, "foo1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_tag(caller, "foo2;cat1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_tag(caller, "foo3;cat2;dat1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._caller_tags( + caller), + [('foo1', None, ""), + ('foo2', 'cat1', ""), + ('foo3', 'cat2', "dat1")]) + self.assertEqual(olc_menus._edit_tag(caller, "foo1", "bar1;cat1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._display_tag(olc_menus._caller_tags(caller)[0]), Something) + self.assertEqual(olc_menus._caller_tags(caller)[0], ("bar1", "cat1", "")) + + protlib.save_prototype(**self.test_prot) + + # spawn helpers + obj = olc_menus._spawn(caller, prototype=self.test_prot) + + 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(olc_menus._update_spawned(caller, prototype=self.test_prot, objects=[obj]), 0) # no changes to apply + self.test_prot['key'] = "updated key" # change prototype + self.assertEqual(self._update_spawned(caller, prototype=self.test_prot, objects=[obj]), 1) # apply change to the one obj + + + # load helpers + + + @mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock( return_value=[{"prototype_key": "TestPrototype",