From 3e314208633a5e2f04f32b40f671ab7decf7deb5 Mon Sep 17 00:00:00 2001 From: Julie Iaccarino <1904063+biscuitWizard@users.noreply.github.com> Date: Sun, 16 Feb 2020 01:43:39 -0800 Subject: [PATCH 1/9] Added a prototype flag to typeclass to allow setting existing objects to a new prototype. --- evennia/commands/default/building.py | 30 +++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 47ebaa9296..4bea968fa6 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1930,9 +1930,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 +1957,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 +2041,28 @@ 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") + self.switches.append("reset") + if "show" in self.switches or "examine" in self.switches: string = "%s's current typeclass is %s." % (obj.name, obj.__class__) caller.msg(string) @@ -2075,6 +2100,9 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): new_typeclass, clean_attributes=reset, clean_cmdsets=reset, run_start_hooks=hooks ) + if "prototype" in self.switches: + spawner.batch_update_objects_with_prototype(prototype, objects=[obj]) + if is_same: string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.path) else: From b41556fc9eb3c73966aab0f7c8272cf0abab905d Mon Sep 17 00:00:00 2001 From: Julie Iaccarino <1904063+biscuitWizard@users.noreply.github.com> Date: Sun, 16 Feb 2020 07:36:29 -0800 Subject: [PATCH 2/9] Improved the messaging for prototype applications. --- evennia/commands/default/building.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 4bea968fa6..9eba7e2807 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2101,7 +2101,9 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): ) if "prototype" in self.switches: - spawner.batch_update_objects_with_prototype(prototype, objects=[obj]) + modified = spawner.batch_update_objects_with_prototype(prototype, objects=[obj]) + if modified == 0: + caller.msg("Prototype %s failed to apply." % prototype["key"]) if is_same: string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.path) @@ -2119,6 +2121,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: + string += " Prototype '%s' was successfully applied over the object type." % prototype["key"] caller.msg(string) From 8c9d33ec84847c2fc13adec16f8009b16d1c8114 Mon Sep 17 00:00:00 2001 From: Julie Iaccarino <1904063+biscuitWizard@users.noreply.github.com> Date: Sun, 16 Feb 2020 07:38:09 -0800 Subject: [PATCH 3/9] Fixing condition where state could be only sort of successful. --- evennia/commands/default/building.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 9eba7e2807..ffa1cf97fd 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2102,7 +2102,8 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): if "prototype" in self.switches: modified = spawner.batch_update_objects_with_prototype(prototype, objects=[obj]) - if modified == 0: + prototype_success = modified > 0 + if not prototype_success: caller.msg("Prototype %s failed to apply." % prototype["key"]) if is_same: @@ -2121,7 +2122,7 @@ 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: + if "prototype" in self.switches and prototype_success: string += " Prototype '%s' was successfully applied over the object type." % prototype["key"] caller.msg(string) From 8c7b6c55f42a7b9df2a20472e5100d8314054d7e Mon Sep 17 00:00:00 2001 From: Julie Iaccarino <1904063+biscuitWizard@users.noreply.github.com> Date: Sun, 16 Feb 2020 09:15:08 -0800 Subject: [PATCH 4/9] Added additional information to the prototype application command flag. --- evennia/commands/default/building.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index ffa1cf97fd..2c56d13e3e 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) @@ -2061,7 +2062,6 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): return new_typeclass = prototype["typeclass"] self.switches.append("force") - self.switches.append("reset") if "show" in self.switches or "examine" in self.switches: string = "%s's current typeclass is %s." % (obj.name, obj.__class__) @@ -2095,6 +2095,31 @@ 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) + answer = "yes" if answer == "" else answer + if answer and answer not in ("yes", "y", "no", "n"): + caller.msg( + "Canceled: Either accept the default by pressing return or specify yes/no." + ) + return + elif answer.strip().lower() in ("n", "no"): + caller.msg("Canceled: No object was modified.") + return + # we let this raise exception if needed obj.swap_typeclass( new_typeclass, clean_attributes=reset, clean_cmdsets=reset, run_start_hooks=hooks From a4a0bf1225e62d9f94cb72215d0fef3355e2b4bd Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 19 Feb 2020 23:49:28 +0100 Subject: [PATCH 5/9] Ran black on sources --- evennia/server/portal/portal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 5864134b46..df6ad016c5 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -394,13 +394,12 @@ if WEBSERVER_ENABLED: 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." ) - proxy_service = internet.TCPServer(proxyport, web_root, interface=interface) proxy_service.setName("EvenniaWebProxy%s:%s" % (ifacestr, proxyport)) PORTAL.services.addService(proxy_service) From a6940fca90b05aeff4a385db0103094135c8be15 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 24 Feb 2020 08:32:49 +0100 Subject: [PATCH 6/9] Add unittest for typeclass/prototype, some cleanup --- evennia/commands/default/building.py | 12 ++++-------- evennia/commands/default/tests.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index bbbfe42b7f..44a9350c52 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1913,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. @@ -2110,15 +2110,11 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): 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) - answer = "yes" if answer == "" else answer - if answer and answer not in ("yes", "y", "no", "n"): + if answer and answer in ("no", "n"): caller.msg( - "Canceled: Either accept the default by pressing return or specify yes/no." + "Canceled: No changes were applied." ) return - elif answer.strip().lower() in ("n", "no"): - caller.msg("Canceled: No object was modified.") - return # we let this raise exception if needed obj.swap_typeclass( 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.") From 162326fe1b7cc17e8d489dfb1a34920a1cb4ae1b Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 24 Feb 2020 16:29:04 +0100 Subject: [PATCH 7/9] Try different requirement to resolve trello build conflict --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 From e0f217de6babe1338df44b7cf9df47b359c56608 Mon Sep 17 00:00:00 2001 From: trhr Date: Wed, 26 Feb 2020 22:29:24 -0600 Subject: [PATCH 8/9] Makes the URL actually accessible. --- evennia/server/portal/portal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index df6ad016c5..5810e574fc 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -388,8 +388,7 @@ 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: @@ -399,7 +398,8 @@ if WEBSERVER_ENABLED: "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) From 95b4c58ecdd53035cb50020742ebe8252e7efaa2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 28 Feb 2020 23:41:57 +0100 Subject: [PATCH 9/9] Make child command class correctly pick up parent docstring if it's missing --- evennia/commands/command.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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()