From 0dfea46d5c4d996eb1c66ee565a809748e0ada4a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 9 Feb 2019 16:52:02 +0100 Subject: [PATCH] Change save/search_prototype, extend unittests --- CHANGELOG.md | 9 ++++++ evennia/accounts/accounts.py | 1 - evennia/accounts/tests.py | 36 +++++++++++++++++++---- evennia/commands/default/building.py | 2 +- evennia/commands/default/tests.py | 6 ++-- evennia/objects/objects.py | 8 ++++- evennia/prototypes/README.md | 2 +- evennia/prototypes/menus.py | 2 +- evennia/prototypes/prototypes.py | 44 +++++++++++++++++----------- evennia/prototypes/spawner.py | 12 ++++++-- evennia/prototypes/tests.py | 37 +++++++++++++++++++---- evennia/utils/test_resources.py | 34 +++++++++++++++++++++ 12 files changed, 154 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78cdeed50b..71f6dfc9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,15 @@ Web/Django standard initiative (@strikaco) - Bugfixes - Fixes bug on login page where error messages were not being displayed +### Prototypes + +- `evennia.prototypes.save_prototype` now takes the prototype as a normal + argument (`prototype`) instead of having to give it as `**prototype`. +- `evennia.prototypes.search_prototype` has a new kwarg `require_single=False` that + raises a KeyError exception if query gave 0 or >1 results. +- `evennia.prototypes.spawner` can now spawn by passing a `prototype_key` + + ### Typeclasses - Add new methods on all typeclasses, useful specifically for object handling from the website/admin: diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 2a8915a59c..ca79e5b10a 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -1494,7 +1494,6 @@ class DefaultGuest(DefaultAccount): characters = self.db._playable_characters for character in characters: if character: - print "deleting Character:", character character.delete() def at_post_disconnect(self, **kwargs): diff --git a/evennia/accounts/tests.py b/evennia/accounts/tests.py index 1d31e93e2c..0e2dfbad94 100644 --- a/evennia/accounts/tests.py +++ b/evennia/accounts/tests.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- -from mock import Mock, MagicMock +import sys +from mock import Mock, MagicMock, patch from random import randint from unittest import TestCase from django.test import override_settings from evennia.accounts.accounts import AccountSessionHandler from evennia.accounts.accounts import DefaultAccount, DefaultGuest -from evennia.utils.test_resources import EvenniaTest +from evennia.utils.test_resources import EvenniaTest, unload_module from evennia.utils import create from django.conf import settings @@ -60,6 +61,7 @@ class TestAccountSessionHandler(TestCase): "Check count method" self.assertEqual(self.handler.count(), len(self.handler.get())) + class TestDefaultGuest(EvenniaTest): "Check DefaultGuest class" @@ -162,6 +164,7 @@ class TestDefaultAccountAuth(EvenniaTest): self.assertFalse(account.set_password('Mxyzptlk')) account.delete() + class TestDefaultAccount(TestCase): "Check DefaultAccount class" @@ -279,13 +282,36 @@ class TestAccountPuppetDeletion(EvenniaTest): @override_settings(MULTISESSION_MODE=2) def test_puppet_deletion(self): # Check for existing chars - self.assertFalse(self.account.db._playable_characters, 'Account should not have any chars by default.') + self.assertFalse(self.account.db._playable_characters, + 'Account should not have any chars by default.') # Add char1 to account's playable characters self.account.db._playable_characters.append(self.char1) - self.assertTrue(self.account.db._playable_characters, 'Char was not added to account.') + self.assertTrue(self.account.db._playable_characters, + 'Char was not added to account.') # See what happens when we delete char1. self.char1.delete() # Playable char list should be empty. - self.assertFalse(self.account.db._playable_characters, 'Playable character list is not empty! %s' % self.account.db._playable_characters) + self.assertFalse(self.account.db._playable_characters, + 'Playable character list is not empty! %s' % self.account.db._playable_characters) + + +class TestDefaultAccountEv(EvenniaTest): + """ + Testing using the EvenniaTest parent + + """ + def test_characters_property(self): + "test existence of None in _playable_characters Attr" + self.account.db._playable_characters = [self.char1, None] + chars = self.account.characters + self.assertEqual(chars, [self.char1]) + self.assertEqual(self.account.db._playable_characters, [self.char1]) + + def test_puppet_success(self): + unload_module(DefaultAccount) + self.account.msg = MagicMock() + with patch("evennia.accounts.accounts._MULTISESSION_MODE", 2): + self.account.puppet_object(self.session, self.char1) + self.account.msg.assert_called_with("You are already puppeting this object.") diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index f1e54e82ca..f905338405 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2978,7 +2978,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # all seems ok. Try to save. try: - prot = protlib.save_prototype(**prototype) + prot = protlib.save_prototype(prototype) if not prot: caller.msg("|rError saving:|R {}.|n".format(prototype_key)) return diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 9052f2b58a..4b6e3b5726 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -680,9 +680,9 @@ class TestBuilding(CommandTest): goblin.delete() # create prototype - protlib.create_prototype(**{'key': 'Ball', - 'typeclass': 'evennia.objects.objects.DefaultCharacter', - 'prototype_key': 'testball'}) + protlib.create_prototype({'key': 'Ball', + 'typeclass': 'evennia.objects.objects.DefaultCharacter', + 'prototype_key': 'testball'}) # Tests "@spawn " self.call(building.CmdSpawn(), "testball", "Spawned Ball") diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 8a034b0e8a..2efc4f426c 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -262,7 +262,13 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): con = self.contents_cache.get(exclude=exclude) # print "contents_get:", self, con, id(self), calledby() # DEBUG return con - contents = property(contents_get) + + def contents_set(self, *args): + "You cannot replace this property" + raise AttributeError("{}.contents is read-only. Use obj.move_to or " + "obj.location to move an object here.".format(self.__class__)) + + contents = property(contents_get, contents_set, contents_set) @property def exits(self): diff --git a/evennia/prototypes/README.md b/evennia/prototypes/README.md index 0f4139aa3e..2cca210ae1 100644 --- a/evennia/prototypes/README.md +++ b/evennia/prototypes/README.md @@ -76,7 +76,7 @@ from evennia import prototypes goblin = {"prototype_key": "goblin:, ... } -prototype = prototypes.save_prototype(caller, **goblin) +prototype = prototypes.save_prototype(goblin) ``` diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index c10f32429f..0f7f72d0f1 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -2138,7 +2138,7 @@ def node_prototype_save(caller, **kwargs): # 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) + protlib.save_prototype(prototype) spawned_objects = protlib.search_objects_with_prototype(prototype_key) nspawned = spawned_objects.count() diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 178b8384a0..a92e4ffef5 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -147,13 +147,13 @@ class DbPrototype(DefaultScript): # Prototype manager functions -def save_prototype(**kwargs): +def save_prototype(prototype): """ Create/Store a prototype persistently. - Kwargs: - prototype_key (str): This is required for any storage. - All other kwargs are considered part of the new prototype dict. + Args: + prototype (dict): The prototype to save. A `prototype_key` key is + required. Returns: prototype (dict or None): The prototype stored using the given kwargs, None if deleting. @@ -166,8 +166,8 @@ def save_prototype(**kwargs): is expected to have valid permissions. """ - - kwargs = homogenize_prototype(kwargs) + in_prototype = prototype + in_prototype = homogenize_prototype(in_prototype) def _to_batchtuple(inp, *args): "build tuple suitable for batch-creation" @@ -176,7 +176,7 @@ def save_prototype(**kwargs): return inp return (inp, ) + args - prototype_key = kwargs.get("prototype_key") + prototype_key = in_prototype.get("prototype_key") if not prototype_key: raise ValidationError("Prototype requires a prototype_key") @@ -192,21 +192,21 @@ def save_prototype(**kwargs): stored_prototype = DbPrototype.objects.filter(db_key=prototype_key) prototype = stored_prototype[0].prototype if stored_prototype else {} - kwargs['prototype_desc'] = kwargs.get("prototype_desc", prototype.get("prototype_desc", "")) - prototype_locks = kwargs.get( + in_prototype['prototype_desc'] = in_prototype.get("prototype_desc", prototype.get("prototype_desc", "")) + prototype_locks = in_prototype.get( "prototype_locks", prototype.get('prototype_locks', "spawn:all();edit:perm(Admin)")) is_valid, err = validate_lockstring(prototype_locks) if not is_valid: raise ValidationError("Lock error: {}".format(err)) - kwargs['prototype_locks'] = prototype_locks + in_prototype['prototype_locks'] = prototype_locks prototype_tags = [ _to_batchtuple(tag, _PROTOTYPE_TAG_META_CATEGORY) - for tag in make_iter(kwargs.get("prototype_tags", + for tag in make_iter(in_prototype.get("prototype_tags", prototype.get('prototype_tags', [])))] - kwargs["prototype_tags"] = prototype_tags + in_prototype["prototype_tags"] = prototype_tags - prototype.update(kwargs) + prototype.update(in_prototype) if stored_prototype: # edit existing prototype @@ -261,19 +261,25 @@ def delete_prototype(prototype_key, caller=None): return True -def search_prototype(key=None, tags=None): +def search_prototype(key=None, tags=None, require_single=False): """ Find prototypes based on key and/or tags, or all prototypes. Kwargs: key (str): An exact or partial key to query for. - tags (str or list): Tag key or keys to query for. These + tags (str or list): Tag key or keys to query for. These will always be applied with the 'db_protototype' tag category. + require_single (bool): If set, raise KeyError if the result + was not found or if there are multiple matches. Return: - matches (list): All found prototype dicts. If no keys - or tags are given, all available prototypes will be returned. + matches (list): All found prototype dicts. Empty list if + no match was found. Note that if neither `key` nor `tags` + were given, *all* available prototypes will be returned. + + Raises: + KeyError: If `require_single` is True and there are 0 or >1 matches. Note: The available prototypes is a combination of those supplied in @@ -329,6 +335,10 @@ def search_prototype(key=None, tags=None): if mta.get('prototype_key') and mta['prototype_key'] == key] if filter_matches and len(filter_matches) < nmatches: matches = filter_matches + + nmatches = len(matches) + if nmatches != 1 and require_single: + raise KeyError("Found {} matching prototypes.".format(nmatches)) return matches diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 2355da1321..30344bceaf 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -23,7 +23,7 @@ prot = { "attrs": [("weapon", "sword")] } -prot = prototypes.create_prototype(**prot) +prot = prototypes.create_prototype(prot) ``` @@ -662,8 +662,9 @@ def spawn(*prototypes, **kwargs): Spawn a number of prototyped objects. Args: - prototypes (dict): Each argument should be a prototype - dictionary. + prototypes (str or dict): Each argument should either be a + prototype_key (will be used to find the prototype) or a full prototype + dictionary. These will be batched-spawned as one object each. Kwargs: prototype_modules (str or list): A python-path to a prototype module, or a list of such paths. These will be used to build @@ -686,6 +687,11 @@ def spawn(*prototypes, **kwargs): `return_parents` is set, instead return dict of prototype parents. """ + # search string (=prototype_key) from input + prototypes = [protlib.search_prototype(prot, require_single=True)[0] + if isinstance(prot, basestring) else prot + for prot in prototypes] + # get available protparents protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index e8ef8a4e6b..81e0fe6675 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -54,7 +54,7 @@ class TestSpawner(EvenniaTest): self.prot1 = {"prototype_key": "testprototype", "typeclass": "evennia.objects.objects.DefaultObject"} - def test_spawn(self): + def test_spawn_from_prot(self): obj1 = spawner.spawn(self.prot1) # check spawned objects have the right tag self.assertEqual(list(protlib.search_objects_with_prototype("testprototype")), obj1) @@ -62,6 +62,14 @@ class TestSpawner(EvenniaTest): _PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"], prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard']) + def test_spawn_from_str(self): + protlib.save_prototype(self.prot1) + obj1 = spawner.spawn(self.prot1['prototype_key']) + self.assertEqual(list(protlib.search_objects_with_prototype("testprototype")), obj1) + self.assertEqual([o.key for o in spawner.spawn( + _PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"], + prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard']) + class TestUtils(EvenniaTest): @@ -245,6 +253,7 @@ class TestProtLib(EvenniaTest): super(TestProtLib, self).setUp() self.obj1.attributes.add("testattr", "testval") self.prot = spawner.prototype_from_object(self.obj1) + def test_prototype_to_str(self): prstr = protlib.prototype_to_str(self.prot) @@ -253,6 +262,22 @@ class TestProtLib(EvenniaTest): def test_check_permission(self): pass + def test_save_prototype(self): + result = protlib.save_prototype(self.prot) + self.assertEqual(result, self.prot) + # faulty + self.prot['prototype_key'] = None + self.assertRaises(protlib.ValidationError, protlib.save_prototype, self.prot) + + def test_search_prototype(self): + protlib.save_prototype(self.prot) + match = protlib.search_prototype("NotFound") + self.assertFalse(match) + match = protlib.search_prototype() + self.assertTrue(match) + match = protlib.search_prototype(self.prot['prototype_key']) + self.assertEqual(match, [self.prot]) + @override_settings(PROT_FUNC_MODULES=['evennia.prototypes.protfuncs'], CLIENT_DEFAULT_WIDTH=20) class TestProtFuncs(EvenniaTest): @@ -424,7 +449,7 @@ class TestPrototypeStorage(EvenniaTest): def test_prototype_storage(self): # from evennia import set_trace;set_trace(term_size=(180, 50)) - prot1 = protlib.create_prototype(**self.prot1) + prot1 = protlib.create_prototype(self.prot1) self.assertTrue(bool(prot1)) self.assertEqual(prot1, self.prot1) @@ -436,7 +461,7 @@ class TestPrototypeStorage(EvenniaTest): protlib.DbPrototype.objects.get_by_tag( "foo1", _PROTOTYPE_TAG_META_CATEGORY)[0].db.prototype, prot1) - prot2 = protlib.create_prototype(**self.prot2) + prot2 = protlib.create_prototype(self.prot2) self.assertEqual( [pobj.db.prototype for pobj in protlib.DbPrototype.objects.get_by_tag( @@ -445,7 +470,7 @@ class TestPrototypeStorage(EvenniaTest): # add to existing prototype prot1b = protlib.create_prototype( - prototype_key='testprototype1', foo='bar', prototype_tags=['foo2']) + {"prototype_key": 'testprototype1', "foo": 'bar', "prototype_tags": ['foo2']}) self.assertEqual( [pobj.db.prototype @@ -457,7 +482,7 @@ class TestPrototypeStorage(EvenniaTest): self.assertNotEqual(list(protlib.search_prototype("testprototype1")), [prot1]) self.assertEqual(list(protlib.search_prototype("testprototype1")), [prot1b]) - prot3 = protlib.create_prototype(**self.prot3) + prot3 = protlib.create_prototype(self.prot3) # partial match with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}): @@ -606,7 +631,7 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._display_tag(olc_menus._get_menu_prototype(caller)['tags'][0]), Something) self.assertEqual(olc_menus._caller_tags(caller), ["foo2", "foo3"]) - protlib.save_prototype(**self.test_prot) + protlib.save_prototype(self.test_prot) # locks helpers self.assertEqual(olc_menus._lock_add(caller, "foo:false()"), "Added lock 'foo:false()'.") diff --git a/evennia/utils/test_resources.py b/evennia/utils/test_resources.py index b4124b7219..64a186ebd4 100644 --- a/evennia/utils/test_resources.py +++ b/evennia/utils/test_resources.py @@ -1,3 +1,8 @@ +""" +Various helper resources for writing unittests. + +""" +import sys from django.conf import settings from django.test import TestCase from mock import Mock @@ -14,6 +19,35 @@ SESSIONS.data_out = Mock() SESSIONS.disconnect = Mock() +def unload_module(module_or_object): + """ + Reset import so one can mock global constants. + + Args: + module_or_object (module or object): The module will + be removed so it will have to be imported again. + + Example: + # (in a test method) + unload_module(foo) + with mock.patch("foo.GLOBALTHING", "mockval"): + import foo + ... # test code using foo.GLOBALTHING, now set to 'mockval' + + + This allows for mocking constants global to the module, since + otherwise those would not be mocked (since a module is only + loaded once). + + """ + if hasattr(module_or_object, "__module__"): + modulename = module_or_object.__module__ + else: + modulename = module_or_object.__name__ + if modulename in sys.modules: + del sys.modules[modulename] + + class EvenniaTest(TestCase): """ Base test for Evennia, sets up a basic environment.