diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py index 2f666d2bbf..d0ed248dd1 100644 --- a/evennia/utils/containers.py +++ b/evennia/utils/containers.py @@ -19,8 +19,6 @@ from evennia.utils import logger from evennia.utils.utils import callables_from_module, class_from_module SCRIPTDB = None -_BASE_SCRIPT_TYPECLASS = None - class Container: """ @@ -201,27 +199,11 @@ class GlobalScriptContainer(Container): initialized. """ - if self.loaded_data: - # we don't always load this, it collides with doc generation - global _BASE_SCRIPT_TYPECLASS - if not _BASE_SCRIPT_TYPECLASS: - _BASE_SCRIPT_TYPECLASS = class_from_module(settings.BASE_SCRIPT_TYPECLASS) - if self.typeclass_storage is None: self.typeclass_storage = {} for key, data in list(self.loaded_data.items()): - try: - typeclass = data.get("typeclass", settings.BASE_SCRIPT_TYPECLASS) - script_typeclass = class_from_module(typeclass) - assert issubclass(script_typeclass, _BASE_SCRIPT_TYPECLASS) - self.typeclass_storage[key] = script_typeclass - except Exception: - logger.log_trace( - f"GlobalScriptContainer could not start import global script {key}. " - "It will be removed (skipped)." - ) - # Let's remove this key/value. We want to let other scripts load. - self.loaded_data.pop(key) + typeclass = data.get("typeclass", settings.BASE_SCRIPT_TYPECLASS) + self.typeclass_storage[key] = class_from_module(typeclass, fallback=settings.BASE_SCRIPT_TYPECLASS) def get(self, key, default=None): """ diff --git a/evennia/utils/tests/data/broken_script.py b/evennia/utils/tests/data/broken_script.py new file mode 100644 index 0000000000..76d3e7a4c7 --- /dev/null +++ b/evennia/utils/tests/data/broken_script.py @@ -0,0 +1,10 @@ +""" +Defines a script module with a broken import, to catch the specific error case +in loading global scripts where the module can be parsed but has broken +dependencies. +""" + +from evennia import nonexistent_module, DefaultScript + +class BrokenScript(DefaultScript): + pass diff --git a/evennia/utils/tests/test_containers.py b/evennia/utils/tests/test_containers.py index 0f46ec1bca..91cba86179 100644 --- a/evennia/utils/tests/test_containers.py +++ b/evennia/utils/tests/test_containers.py @@ -4,78 +4,79 @@ from evennia.utils import containers from django.conf import settings from django.test import override_settings from evennia.utils.utils import class_from_module +from evennia import DefaultScript -_BASE_SCRIPT_TYPECLASS = class_from_module(settings.BASE_SCRIPT_TYPECLASS) +_BASE_TYPECLASS = class_from_module(settings.BASE_SCRIPT_TYPECLASS) -class GoodScript(_BASE_SCRIPT_TYPECLASS): - pass +class GoodScript(DefaultScript): + pass -class BadScript: - """Not subclass of _BASE_SCRIPT_TYPECLASS,""" - pass - -class WorseScript(_BASE_SCRIPT_TYPECLASS): - """objects will fail upon call""" - @property - def objects(self): - from evennia import module_that_doesnt_exist +class InvalidScript: + pass class TestGlobalScriptContainer(unittest.TestCase): - def test_init_with_no_scripts(self): - gsc = containers.GlobalScriptContainer() + def test_init_with_no_scripts(self): + gsc = containers.GlobalScriptContainer() - self.assertEqual(len(gsc.loaded_data), 0) + self.assertEqual(len(gsc.loaded_data), 0) - @override_settings(GLOBAL_SCRIPTS={'script_name': {}}) - def test_init_with_typeclassless_script(self): + @override_settings(GLOBAL_SCRIPTS={}) + def test_start_with_no_scripts(self): + gsc = containers.GlobalScriptContainer() - gsc = containers.GlobalScriptContainer() + gsc.start() - self.assertEqual(len(gsc.loaded_data), 1) - self.assertIn('script_name', gsc.loaded_data) + self.assertEqual(len(gsc.typeclass_storage), 0) - def test_start_with_no_scripts(self): - gsc = containers.GlobalScriptContainer() + @override_settings(GLOBAL_SCRIPTS={'script_name': {}}) + def test_start_with_typeclassless_script(self): + """No specified typeclass should fallback to base""" + gsc = containers.GlobalScriptContainer() - gsc.start() + gsc.start() - self.assertEqual(len(gsc.typeclass_storage), 0) + self.assertEqual(len(gsc.typeclass_storage), 1) + self.assertIn('script_name', gsc.typeclass_storage) + self.assertEqual(gsc.typeclass_storage['script_name'], _BASE_TYPECLASS) - @override_settings(GLOBAL_SCRIPTS={'script_name': {}}) - def test_start_with_typeclassless_script_defaults_to_base(self): - gsc = containers.GlobalScriptContainer() + @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.test_containers.NoScript'}}) + def test_start_with_nonexistent_script(self): + """Missing script class should fall back to base""" + gsc = containers.GlobalScriptContainer() - gsc.start() + gsc.start() - self.assertEqual(len(gsc.typeclass_storage), 1) - self.assertIn('script_name', gsc.typeclass_storage) - self.assertEqual(gsc.typeclass_storage['script_name'], _BASE_SCRIPT_TYPECLASS) + self.assertEqual(len(gsc.typeclass_storage), 1) + self.assertIn('script_name', gsc.typeclass_storage) + self.assertEqual(gsc.typeclass_storage['script_name'], _BASE_TYPECLASS) - @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.test_containers.GoodScript'}}) - def test_start_with_typeclassed_script_loads_it(self): - gsc = containers.GlobalScriptContainer() + @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.test_containers.GoodScript'}}) + def test_start_with_valid_script(self): + gsc = containers.GlobalScriptContainer() - gsc.start() + gsc.start() - self.assertEqual(len(gsc.typeclass_storage), 1) - self.assertIn('script_name', gsc.typeclass_storage) - self.assertEqual(gsc.typeclass_storage['script_name'], GoodScript) + self.assertEqual(len(gsc.typeclass_storage), 1) + self.assertIn('script_name', gsc.typeclass_storage) + self.assertEqual(gsc.typeclass_storage['script_name'], GoodScript) - @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.test_containers.BadScript'}}) - def test_start_with_bad_typeclassed_script_skips_it(self): - gsc = containers.GlobalScriptContainer() + @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.test_containers.InvalidScript'}}) + def test_start_with_invalid_script(self): + """Script class doesn't implement required methods methods""" + gsc = containers.GlobalScriptContainer() - gsc.start() + with self.assertRaises(AttributeError) as err: + gsc.start() + # check for general attribute failure on the invalid class to preserve against future code-rder changes + self.assertTrue(str(err.exception).startswith("type object 'InvalidScript' has no attribute"), err.exception) - self.assertEqual(len(gsc.typeclass_storage), 0) - self.assertNotIn('script_name', gsc.typeclass_storage) + @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.data.broken_script.BrokenScript'}}) + def test_start_with_broken_script(self): + """Un-importable script should traceback""" + gsc = containers.GlobalScriptContainer() - @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.test_containers.WorstScript'}}) - def test_start_with_worst_typeclassed_script_skips_it(self): - gsc = containers.GlobalScriptContainer() - - gsc.start() - - self.assertEqual(len(gsc.typeclass_storage), 0) - self.assertNotIn('script_name', gsc.typeclass_storage) + with self.assertRaises(Exception) as err: + gsc.start() + # exception raised by imported module + self.assertTrue(str(err.exception).startswith("cannot import name 'nonexistent_module' from 'evennia'"), err.exception)