diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 8110a9ecb3..dba035b481 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -6,6 +6,7 @@ All commands in Evennia inherit from the 'Command' class in this module. """ import re import math +import inspect from django.conf import settings @@ -74,6 +75,13 @@ def _init_command(cls, **kwargs): cls.is_exit = False if not hasattr(cls, "help_category"): cls.help_category = "general" + # make sure to pick up the parent's docstring if the child class is + # missing one (important for auto-help) + if cls.__doc__ is None: + for parent_class in inspect.getmro(cls): + if parent_class.__doc__ is not None: + cls.__doc__ = parent_class.__doc__ + break cls.help_category = cls.help_category.lower() diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 77b704356c..44a9350c52 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -19,6 +19,7 @@ from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus from evennia.utils.ansi import raw +from evennia.prototypes.menus import _format_diff_text_and_options COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -1912,8 +1913,8 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): Usage: typeclass[/switch] [= typeclass.path] - type '' - parent '' + typeclass/prototype = prototype_key + typeclass/list/show [typeclass.path] swap - this is a shorthand for using /force/reset flags. update - this is a shorthand for using the /force/reload flag. @@ -1930,9 +1931,12 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): list - show available typeclasses. Only typeclasses in modules actually imported or used from somewhere in the code will show up here (those typeclasses are still available if you know the path) + prototype - clean and overwrite the object with the specified + prototype key - effectively making a whole new object. Example: type button = examples.red_button.RedButton + type/prototype button=a red button If the typeclass_path is not given, the current object's typeclass is assumed. @@ -1954,7 +1958,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): key = "typeclass" aliases = ["type", "parent", "swap", "update"] - switch_options = ("show", "examine", "update", "reset", "force", "list") + switch_options = ("show", "examine", "update", "reset", "force", "list", "prototype") locks = "cmd:perm(typeclass) or perm(Builder)" help_category = "Building" @@ -2038,6 +2042,27 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): new_typeclass = self.rhs or obj.path + prototype = None + if "prototype" in self.switches: + key = self.rhs + prototype = protlib.search_prototype(key=key) + if len(prototype) > 1: + caller.msg( + "More than one match for {}:\n{}".format( + key, "\n".join(proto.get("prototype_key", "") for proto in prototype) + ) + ) + return + elif prototype: + # one match + prototype = prototype[0] + else: + # no match + caller.msg("No prototype '{}' was found.".format(key)) + return + new_typeclass = prototype["typeclass"] + self.switches.append("force") + if "show" in self.switches or "examine" in self.switches: string = "%s's current typeclass is %s." % (obj.name, obj.__class__) caller.msg(string) @@ -2070,11 +2095,38 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): hooks = "at_object_creation" if update else "all" old_typeclass_path = obj.typeclass_path + # special prompt for the user in cases where we want + # to confirm changes. + if "prototype" in self.switches: + diff, _ = spawner.prototype_diff_from_object(prototype, obj) + txt, options = _format_diff_text_and_options(diff, objects=[obj]) + prompt = "Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n" % \ + ( + prototype["key"], + obj.name, + "\n".join(txt) + ) + if not reset: + prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state." + prompt += "\nAre you sure you want to apply these changes [yes]/no?" + answer = yield (prompt) + if answer and answer in ("no", "n"): + caller.msg( + "Canceled: No changes were applied." + ) + return + # we let this raise exception if needed obj.swap_typeclass( new_typeclass, clean_attributes=reset, clean_cmdsets=reset, run_start_hooks=hooks ) + if "prototype" in self.switches: + modified = spawner.batch_update_objects_with_prototype(prototype, objects=[obj]) + prototype_success = modified > 0 + if not prototype_success: + caller.msg("Prototype %s failed to apply." % prototype["key"]) + if is_same: string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.path) else: @@ -2091,6 +2143,8 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): string += " All old attributes where deleted before the swap." else: string += " Attributes set before swap were not removed." + if "prototype" in self.switches and prototype_success: + string += " Prototype '%s' was successfully applied over the object type." % prototype["key"] caller.msg(string) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index a250166530..0dd76f8d6f 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -991,6 +991,27 @@ class TestBuilding(CommandTest): "All object creation hooks were run. All old attributes where deleted before the swap.", ) + from evennia.prototypes.prototypes import homogenize_prototype + test_prototype = [homogenize_prototype( + {"prototype_key": "testkey", + "prototype_tags": [], + "typeclass": "typeclasses.objects.Object", + "key":"replaced_obj", + "attrs": [("foo", "bar", None, ""), + ("desc", "protdesc", None, "")]})] + with mock.patch("evennia.commands.default.building.protlib.search_prototype", + new=mock.MagicMock(return_value=test_prototype)) as mprot: + self.call( + building.CmdTypeclass(), + "/prototype Obj=testkey", + "replaced_obj changed typeclass from " + "evennia.objects.objects.DefaultObject to " + "typeclasses.objects.Object.\nAll object creation hooks were " + "run. Attributes set before swap were not removed. Prototype " + "'replaced_obj' was successfully applied over the object type." + ) + assert self.obj1.db.desc == "protdesc" + def test_lock(self): self.call(building.CmdLock(), "", "Usage: ") self.call(building.CmdLock(), "Obj = test:all()", "Added lock 'test:all()' to Obj.") diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 5864134b46..5810e574fc 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -388,19 +388,18 @@ if WEBSERVER_ENABLED: webclientstr = "webclient-websocket%s: %s" % (w_ifacestr, port) INFO_DICT["webclient"].append(webclientstr) - web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE) - web_root.is_portal = True + if WEB_PLUGINS_MODULE: try: web_root = WEB_PLUGINS_MODULE.at_webproxy_root_creation(web_root) - except Exception as e: # Legacy user has not added an at_webproxy_root_creation function in existing web plugins file + except Exception as e: # Legacy user has not added an at_webproxy_root_creation function in existing web plugins file INFO_DICT["errors"] = ( "WARNING: WEB_PLUGINS_MODULE is enabled but at_webproxy_root_creation() not found - " "copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf." ) - - + web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE) + web_root.is_portal = True proxy_service = internet.TCPServer(proxyport, web_root, interface=interface) proxy_service.setName("EvenniaWebProxy%s:%s" % (ifacestr, proxyport)) PORTAL.services.addService(proxy_service) diff --git a/requirements.txt b/requirements.txt index cfbf6e5af3..e569a8ac07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,9 +7,12 @@ pytz django-sekizai inflect autobahn >= 17.9.3 -model_mommy + +# try to resolve dependency issue in py3.7 +attrs >= 19.2.0 # testing and development +model_mommy mock >= 1.0.1 anything black