From cb989934baae94c5ce00a08ead3019098ae2fa50 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 23 Mar 2020 23:19:46 +0100 Subject: [PATCH 001/361] Fixing protoype spawning --- evennia/commands/default/building.py | 478 ++++++++++++++++----------- evennia/prototypes/prototypes.py | 27 +- evennia/prototypes/spawner.py | 101 +++++- evennia/server/webserver.py | 17 +- 4 files changed, 411 insertions(+), 212 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 1ab8e8714a..0f8756440b 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -13,13 +13,12 @@ from evennia.utils.utils import ( class_from_module, get_all_typeclasses, variable_from_module, - dbref, + dbref, interactive ) 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) @@ -2099,10 +2098,10 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): # 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]) + txt = spawner.format_diff(diff) prompt = ( "Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n" - % (prototype["key"], obj.name, "\n".join(txt)) + % (prototype["key"], obj.name, txt) ) if not reset: prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state." @@ -3227,6 +3226,10 @@ class CmdTag(COMMAND_DEFAULT_CLASS): self.caller.msg(string) +# helper functions for spawn + + + class CmdSpawn(COMMAND_DEFAULT_CLASS): """ spawn objects from prototype @@ -3256,7 +3259,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): them with latest version of given prototype. If given with /save, will auto-update all objects with the old version of the prototype without asking first. - edit, olc - create/manipulate prototype in a menu interface. + edit, menu, olc - create/manipulate prototype in a menu interface. Example: spawn GOBLIN @@ -3309,56 +3312,203 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" + def _search_prototype(self, prototype_key, quiet=False): + """ + Search for prototype and handle no/multi-match and access. + + Returns a single found prototype or None - in the + case, the caller has already been informed of the + search error we need not do any further action. + + """ + prototypes = protlib.search_prototype(prototype_key) + nprots = len(prototypes) + + # handle the search result + err = None + if not prototypes: + err = f"No prototype named '{prototype_key}'." + elif nprots > 1: + err = "Found {} prototypes matching '{}':\n {}".format( + nprots, + prototype_key, + ", ".join(proto.get("prototype_key", "") for proto in prototypes), + ) + else: + # we have a single prototype, check access + prototype = prototypes[0] + if not self.caller.locks.check_lockstring( + self.caller, prototype.get("prototype_locks", ""), + access_type="spawn", default=True): + err = "You don't have access to use this prototype." + + if err: + # return None on any error + if not quiet: + self.caller.msg(err) + return + return prototype + + def _parse_prototype(self, inp, expect=dict): + """ + Parse a prototype dict or key from the input and convert it safely + into a dict if appropriate. + + Args: + inp (str): The input from user. + expect (type, optional): + Returns: + prototype (dict, str or None): The parsed prototype. If None, the error + was already reported. + + """ + eval_err = None + try: + prototype = _LITERAL_EVAL(inp) + except (SyntaxError, ValueError) as err: + # treat as string + eval_err = err + prototype = utils.to_str(inp) + finally: + # it's possible that the input was a prototype-key, in which case + # it's okay for the LITERAL_EVAL to fail. Only if the result does not + # match the expected type do we have a problem. + if not isinstance(prototype, expect): + if eval_err: + string = ( + f"{inp}\n{eval_err}\n|RCritical Python syntax error in argument. Only primitive " + "Python structures are allowed. \nMake sure to use correct " + "Python syntax. Remember especially to put quotes around all " + "strings inside lists and dicts.|n For more advanced uses, embed " + "inlinefuncs in the strings." + ) + else: + string = "Expected {}, got {}.".format(expect, type(prototype)) + self.caller.msg(string) + return + + if expect == dict: + # an actual prototype. We need to make sure it's safe, + # so don't allow exec. + # TODO: Exec support is deprecated. Remove completely for 1.0. + if "exec" in prototype and not self.caller.check_permstring("Developer"): + self.caller.msg( + "Spawn aborted: You are not allowed to " "use the 'exec' prototype key." + ) + return + try: + # we homogenize the protoype first, to be more lenient with free-form + protlib.validate_prototype(protlib.homogenize_prototype(prototype)) + except RuntimeError as err: + self.caller.msg(str(err)) + return + return prototype + + def _get_prototype_detail(self, query=None, prototypes=None): + """ + Display the detailed specs of one or more prototypes. + + Args: + query (str, optional): If this is given and `prototypes` is not, search for + the prototype(s) by this query. This may be a partial query which + may lead to multiple matches, all being displayed. + prototypes (list, optional): If given, ignore `query` and only show these + prototype-details. + Returns: + display (str, None): A formatted string of one or more prototype details. + If None, the caller was already informed of the error. + + + """ + if not prototypes: + # we need to query. Note that if query is None, all prototypes will + # be returned. + prototypes = protlib.search_prototype(key=query) + if prototypes: + return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes) + elif query: + self.caller.msg(f"No prototype found to match the query '{query}'.") + else: + self.caller.msg(f"No prototypes found.") + + def _list_prototypes(self, key=None, tags=None): + """Display prototypes as a list, optionally limited by key/tags. """ + EvMore( + self.caller, + str(protlib.list_prototypes(self.caller, key=key, tags=tags)), + exit_on_lastpage=True, + justify_kwargs=False, + ) + + @interactive + def _update_existing_objects(self, caller, prototype_key, quiet=False): + """ + Update existing objects (if any) with this prototype-key to the latest + prototype version. + + Args: + caller (Object): This is necessary for @interactive to work. + prototype_key (str): The prototype to update. + quiet (bool, optional): If set, don't report to user if no + old objects were found to update. + Returns: + n_updated (int): Number of updated objects. + + """ + prototype = self._search_prototype(prototype_key) + if not prototype: + return + + existing_objects = protlib.search_objects_with_prototype(prototype_key) + if not existing_objects: + if not quiet: + caller.msg("No existing objects found with an older version of this prototype.") + return + + if existing_objects: + n_existing = len(existing_objects) + slow = " (note that this may be slow)" if n_existing > 10 else "" + string = ( + f"There are {n_existing} existing object(s) with an older version " + f"of prototype '{prototype_key}'. Should it be re-applied to them{slow}? [Y]/N" + ) + answer = yield (string) + if answer.lower() in ["n", "no"]: + caller.msg( + "|rNo update was done of existing objects. " + "Use spawn/update to apply later as needed.|n" + ) + return + n_updated = spawner.batch_update_objects_with_prototype( + prototype, objects=existing_objects) + caller.msg(f"{n_updated} objects were updated.") + return + + def _parse_key_desc_tags(self, argstring, desc=True): + """ + Parse ;-separated input list. + """ + key, desc, tags = "", "", [] + if ";" in self.args: + parts = (part.strip().lower() for part in self.args.split(";")) + if len(parts) > 1 and desc: + key = parts[0] + desc = parts[1] + tags = parts[2:] + else: + key = parts[0] + tags = parts[1:] + else: + key = argstring.strip().lower() + return key, desc, tags + def func(self): """Implements the spawner""" - def _parse_prototype(inp, expect=dict): - err = None - try: - prototype = _LITERAL_EVAL(inp) - except (SyntaxError, ValueError) as err: - # treat as string - prototype = utils.to_str(inp) - finally: - if not isinstance(prototype, expect): - if err: - string = ( - "{}\n|RCritical Python syntax error in argument. Only primitive " - "Python structures are allowed. \nYou also need to use correct " - "Python syntax. Remember especially to put quotes around all " - "strings inside lists and dicts.|n For more advanced uses, embed " - "inline functions in the strings.".format(err) - ) - else: - string = "Expected {}, got {}.".format(expect, type(prototype)) - self.caller.msg(string) - return None - if expect == dict: - # an actual prototype. We need to make sure it's safe. Don't allow exec - if "exec" in prototype and not self.caller.check_permstring("Developer"): - self.caller.msg( - "Spawn aborted: You are not allowed to " "use the 'exec' prototype key." - ) - return None - try: - # we homogenize first, to be more lenient - protlib.validate_prototype(protlib.homogenize_prototype(prototype)) - except RuntimeError as err: - self.caller.msg(str(err)) - return - return prototype - - def _search_show_prototype(query, prototypes=None): - # prototype detail - if not prototypes: - prototypes = protlib.search_prototype(key=query) - if prototypes: - return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes) - else: - return False - caller = self.caller + noloc = "noloc" in self.switches + # run the menu/olc if ( self.cmdstring == "olc" or "menu" in self.switches @@ -3368,63 +3518,42 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # OLC menu mode prototype = None if self.lhs: - key = self.lhs - 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)) + prototype_key = self.lhs + prototype = self._search_prototype(prototype_key) + if not prototype: return olc_menus.start_olc(caller, session=self.session, prototype=prototype) return if "search" in self.switches: - # query for a key match + # query for a key match. The arg is a search query or nothing. + if not self.args: - self.switches.append("list") - else: - key, tags = self.args.strip(), None - if ";" in self.args: - key, tags = (part.strip().lower() for part in self.args.split(";", 1)) - tags = [tag.strip() for tag in tags.split(",")] if tags else None - EvMore( - caller, - str(protlib.list_prototypes(caller, key=key, tags=tags)), - exit_on_lastpage=True, - ) + # an empty search returns the full list + self._list_prototypes() return + # search for key;tag combinations + key, _, tags = self._parse_key_desc_tags(self.args, desc=False) + self._list_prototypes(key, tags) + return + if "show" in self.switches or "examine" in self.switches: - # the argument is a key in this case (may be a partial key) + # show a specific prot detail. The argument is a search query or empty. if not self.args: - self.switches.append("list") - else: - matchstring = _search_show_prototype(self.args) - if matchstring: - caller.msg(matchstring) - else: - caller.msg("No prototype '{}' was found.".format(self.args)) + # we don't show the list of all details, that's too spammy. + caller.msg("You need to specify a prototype-key to show.") return - if "list" in self.switches: - # for list, all optional arguments are tags - # import pudb; pudb.set_trace() + detail_string = self._get_prototype_detail(self.args) + if not detail_string: + return + caller.msg(detail_string) + return - EvMore( - caller, - str(protlib.list_prototypes(caller, tags=self.lhslist)), - exit_on_lastpage=True, - justify_kwargs=False, - ) + if "list" in self.switches: + # for list, all optional arguments are tags. + self._list_prototypes(tags=self.lhslist) return if "save" in self.switches: @@ -3435,27 +3564,41 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): ) return + prototype_key, prototype_desc, prototype_tags = self._parse_key_desc_tags(self.lhs) + # handle rhs: - prototype = _parse_prototype(self.lhs.strip()) + prototype = self._parse_prototype(self.rhs.strip()) if not prototype: return - # present prototype to save - new_matchstring = _search_show_prototype("", prototypes=[prototype]) - string = "|yCreating new prototype:|n\n{}".format(new_matchstring) - question = "\nDo you want to continue saving? [Y]/N" + if prototype.get("prototype_key") != prototype_key: + caller.msg("(Replacing `prototype_key` in prototype with given key.)") + prototype['prototype_key'] = prototype_key + if prototype_desc and prototype.get("prototype_desc") != prototype_desc: + caller.msg("(Replacing `prototype_desc` in prototype with given desc.)") + prototype['prototype_desc'] = prototype_desc + if prototype_tags and prototype.get("prototype_tags") != prototype_tags: + caller.msg("(Replacing `prototype_tags` in prototype with given tag(s))" ) + prototype['prototype_tags'] = prototype_tags - prototype_key = prototype.get("prototype_key") - if not prototype_key: - caller.msg("\n|yTo save a prototype it must have the 'prototype_key' set.") - return + string = "" + # check for existing prototype (exact match) + old_prototype = self._search_prototype(prototype_key, quiet=True) - # check for existing prototype, - old_matchstring = _search_show_prototype(prototype_key) + print("old_prototype", old_prototype) + print("new_prototype", prototype) + diff = spawner.prototype_diff(old_prototype, prototype, homogenize=True) + print("diff", diff) + diffstr = spawner.format_diff(diff) + new_prototype_detail = self._get_prototype_detail(prototypes=[prototype]) - if old_matchstring: - string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring) - question = "\n|yDo you want to replace the existing prototype?|n [Y]/N" + if old_prototype: + string = (f"|yExisting prototype \"{prototype_key}\" found. Change:|n\n{diffstr}\n" + f"|yNew changed prototype:|n\n{new_prototype_detail}") + question = "\n|yDo you want to apply the change to the existing prototype?|n [Y]/N" + else: + string = f"|yCreating new prototype:|n\n{new_prototype_detail}" + question = "\nDo you want to continue saving? [Y]/N" answer = yield (string + question) if answer.lower() in ["n", "no"]: @@ -3474,82 +3617,52 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): caller.msg("|gSaved prototype:|n {}".format(prototype_key)) # check if we want to update existing objects - existing_objects = protlib.search_objects_with_prototype(prototype_key) - if existing_objects: - if "update" not in self.switches: - n_existing = len(existing_objects) - slow = " (note that this may be slow)" if n_existing > 10 else "" - string = ( - "There are {} objects already created with an older version " - "of prototype {}. Should it be re-applied to them{}? [Y]/N".format( - n_existing, prototype_key, slow - ) - ) - answer = yield (string) - if answer.lower() in ["n", "no"]: - caller.msg( - "|rNo update was done of existing objects. " - "Use spawn/update to apply later as needed.|n" - ) - return - n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key) - caller.msg("{} objects were updated.".format(n_updated)) + + self._update_existing_objects(self.caller, prototype_key, quiet=True) return if not self.args: + # all switches beyond this point gets a common non-arg return ncount = len(protlib.search_prototype()) caller.msg( "Usage: spawn or {{key: value, ...}}" - "\n ({} existing prototypes. Use /list to inspect)".format(ncount) + f"\n ({ncount} existing prototypes. Use /list to inspect)" ) return if "delete" in self.switches: # remove db-based prototype - matchstring = _search_show_prototype(self.args) - if matchstring: - string = "|rDeleting prototype:|n\n{}".format(matchstring) - question = "\nDo you want to continue deleting? [Y]/N" - answer = yield (string + question) - if answer.lower() in ["n", "no"]: - caller.msg("|rDeletion cancelled.|n") - return - try: - success = protlib.delete_prototype(self.args) - except protlib.PermissionError as err: - caller.msg("|rError deleting:|R {}|n".format(err)) - caller.msg( - "Deletion {}.".format( - "successful" if success else "failed (does the prototype exist?)" - ) - ) + prototype_detail = self._get_prototype_detail(self.args) + if not prototype_detail: return + + string = f"|rDeleting prototype:|n\n{prototype_detail}" + question = "\nDo you want to continue deleting? [Y]/N" + answer = yield (string + question) + if answer.lower() in ["n", "no"]: + caller.msg("|rDeletion cancelled.|n") + return + + try: + success = protlib.delete_prototype(self.args) + except protlib.PermissionError as err: + retmsg = f"|rError deleting:|R {err}|n" else: - caller.msg("Could not find prototype '{}'".format(key)) + retmsg = ("Deletion successful" if success else + "Deletion failed (does the prototype exist?)") + caller.msg(retmsg) + return if "update" in self.switches: # update existing prototypes - key = self.args.strip().lower() - existing_objects = protlib.search_objects_with_prototype(key) - if existing_objects: - n_existing = len(existing_objects) - slow = " (note that this may be slow)" if n_existing > 10 else "" - string = ( - "There are {} objects already created with an older version " - "of prototype {}. Should it be re-applied to them{}? [Y]/N".format( - n_existing, key, slow - ) - ) - answer = yield (string) - if answer.lower() in ["n", "no"]: - caller.msg("|rUpdate cancelled.") - return - n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key) - caller.msg("{} objects were updated.".format(n_updated)) + prototype_key = self.args.strip().lower() + self._update_existing_objects(self.caller, prototype_key) + return - # A direct creation of an object from a given prototype + # If we get to this point, we use not switches but are trying a + # direct creation of an object from a given prototype or -key - prototype = _parse_prototype( + prototype = self._parse_prototype( self.args, expect=dict if self.args.strip().startswith("{") else str ) if not prototype: @@ -3559,35 +3672,20 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): key = "" if isinstance(prototype, str): # A prototype key we are looking to apply - key = prototype - prototypes = protlib.search_prototype(prototype) - nprots = len(prototypes) - if not prototypes: - caller.msg("No prototype named '%s'." % prototype) - return - elif nprots > 1: - caller.msg( - "Found {} prototypes matching '{}':\n {}".format( - nprots, - prototype, - ", ".join(proto.get("prototype_key", "") for proto in prototypes), - ) - ) - return - # we have a prototype, check access - prototype = prototypes[0] - if not caller.locks.check_lockstring( - caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True - ): - caller.msg("You don't have access to use this prototype.") - return + prototype_key = prototype + prototype = self._search_prototype(prototype_key) - if "noloc" not in self.switches and "location" not in prototype: - prototype["location"] = self.caller.location + if not prototype: + return # proceed to spawning try: for obj in spawner.spawn(prototype): self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) + if not noloc and "location" not in prototype: + # we don't hardcode the location in the prototype (unless the user + # did so manually) - that would lead to it having to be 'removed' every + # time we try to update objects with this prototype in the future. + obj.location = caller.location except RuntimeError as err: caller.msg(err) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 94a3e04eb0..26fd99f1fd 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -56,6 +56,8 @@ _PROTOTYPE_TAG_CATEGORY = "from_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" PROT_FUNCS = {} +_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:perm(Admin)" + class PermissionError(RuntimeError): pass @@ -84,8 +86,19 @@ def homogenize_prototype(prototype, custom_keys=None): homogenizations like adding missing prototype_keys and setting a default typeclass. """ + if not prototype or not isinstance(prototype, dict): + return {} + reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ()) + # correct cases of setting None for certain values + for protkey in prototype: + if prototype[protkey] is None: + if protkey in ("attrs", "tags", "prototype_tags"): + prototype[protkey] = [] + elif protkey in ("prototype_key", "prototype_desc"): + prototype[protkey] = "" + attrs = list(prototype.get("attrs", [])) # break reference tags = make_iter(prototype.get("tags", [])) homogenized_tags = [] @@ -111,12 +124,14 @@ def homogenize_prototype(prototype, custom_keys=None): # add required missing parts that had defaults before - if "prototype_key" not in prototype: + homogenized["prototype_key"] = homogenized.get("prototype_key", # assign a random hash as key - homogenized["prototype_key"] = "prototype-{}".format( - hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7] - ) - + "prototype-{}".format( + hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7])) + homogenized["prototype_tags"] = homogenized.get("prototype_tags", []) + homogenized["prototype_locks"] = homogenized.get( + "prototype_lock", _PROTOTYPE_FALLBACK_LOCK) + homogenized["prototype_desc"] = homogenized.get("prototype_desc", "") if "typeclass" not in prototype and "prototype_parent" not in prototype: homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS @@ -230,7 +245,7 @@ def save_prototype(prototype): "prototype_desc", prototype.get("prototype_desc", "") ) prototype_locks = in_prototype.get( - "prototype_locks", prototype.get("prototype_locks", "spawn:all();edit:perm(Admin)") + "prototype_locks", prototype.get("prototype_locks", _PROTOTYPE_FALLBACK_LOCK) ) is_valid, err = validate_lockstring(prototype_locks) if not is_valid: diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index fab4d2cfdc..98fe2526d7 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -322,9 +322,9 @@ def prototype_from_object(obj): return prot -def prototype_diff(prototype1, prototype2, maxdepth=2): +def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False): """ - A 'detailed' diff specifies differences down to individual sub-sectiions + A 'detailed' diff specifies differences down to individual sub-sections of the prototype, like individual attributes, permissions etc. It is used by the menu to allow a user to customize what should be kept. @@ -334,6 +334,8 @@ def prototype_diff(prototype1, prototype2, maxdepth=2): maxdepth (int, optional): The maximum depth into the diff we go before treating the elements of iterables as individual entities to compare. This is important since a single attr/tag (for example) are represented by a tuple. + homogenize (bool, optional): Auto-homogenize both prototypes for the best comparison. + This is most useful for displaying. Returns: diff (dict): A structure detailing how to convert prototype1 to prototype2. All @@ -350,7 +352,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2): old_type = type(old) new_type = type(new) - if old_type != new_type: + if old_type == new_type and not (old and new): + # both old and new are unset, like [] or None + return (old, new, "KEEP") + elif old_type != new_type: if old and not new: if depth < maxdepth and old_type == dict: return {key: (part, None, "REMOVE") for key, part in old.items()} @@ -387,7 +392,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2): else: return (old, new, "KEEP") - diff = _recursive_diff(prototype1, prototype2) + prot1 = protlib.homogenize_prototype(prototype1) if homogenize else prototype1 + prot2 = protlib.homogenize_prototype(prototype2) if homogenize else prototype2 + + diff = _recursive_diff(prot1, prot2) return diff @@ -490,6 +498,80 @@ def prototype_diff_from_object(prototype, obj): return diff, obj_prototype +def format_diff(diff, minimal=True): + """ + Reformat a diff for presentation. This is a shortened version + of the olc _format_diff_text_and_options without the options. + + Args: + diff (dict): A diff as produced by `prototype_diff`. + minimal (bool, optional): Only show changes (remove KEEPs) + + Returns: + texts (str): The formatted text. + + """ + valid_instructions = ("KEEP", "REMOVE", "ADD", "UPDATE") + + def _visualize(obj, rootname, get_name=False): + if is_iter(obj): + if not obj: + return str(obj) + if get_name: + return obj[0] if obj[0] else "" + if rootname == "attrs": + return "{} |w=|n {} |w(category:|n |n{}|w, locks:|n {}|w)|n".format(*obj) + elif rootname == "tags": + return "{} |w(category:|n {}|w)|n".format(obj[0], obj[1]) + return "{}".format(obj) + + def _parse_diffpart(diffpart, rootname): + typ = type(diffpart) + texts = [] + if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions: + old, new, instruction = diffpart + if instruction == "KEEP": + if not minimal: + texts.append(" |gKEEP|n: {old}".format(old=_visualize(old, rootname))) + elif instruction == "ADD": + texts.append(" |yADD|n: {new}".format(new=_visualize(new, rootname))) + elif instruction == "REMOVE" and not new: + texts.append(" |rREMOVE|n: {old}".format(old=_visualize(old, rootname))) + else: + vold = _visualize(old, rootname) + vnew = _visualize(new, rootname) + vsep = "" if len(vold) < 78 else "\n" + vinst = " |rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction) + varrow = "|r->|n" if instruction == "REMOVE" else "|y->|n" + texts.append( + " {inst}|W:|n {old} |W{varrow}|n{sep} {new}".format( + inst=vinst, old=vold, varrow=varrow, sep=vsep, new=vnew + ) + ) + else: + for key in sorted(list(diffpart.keys())): + subdiffpart = diffpart[key] + text = _parse_diffpart(subdiffpart, rootname) + texts.extend(text) + return texts + + texts = [] + + for root_key in sorted(diff): + diffpart = diff[root_key] + text = _parse_diffpart(diffpart, root_key) + if text or not minimal: + heading = "- |w{}:|n\n".format(root_key) + if text: + text = [heading + text[0]] + text[1:] + else: + text = [heading] + + texts.extend(text) + + return "\n ".join(line for line in texts if line) + + def batch_update_objects_with_prototype(prototype, diff=None, objects=None): """ Update existing objects with the latest version of the prototype. @@ -525,15 +607,13 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): # make sure the diff is flattened diff = flatten_diff(diff) + changed = 0 for obj in objects: do_save = False old_prot_key = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) old_prot_key = old_prot_key[0] if old_prot_key else None - if prototype_key != old_prot_key: - obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) - obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) for key, directive in diff.items(): if directive in ("UPDATE", "REPLACE"): @@ -623,6 +703,11 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): pass else: obj.attributes.remove(key) + + # we must always make sure to re-add the prototype tag + obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) + obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + if do_save: changed += 1 obj.save() @@ -707,7 +792,7 @@ def spawn(*prototypes, **kwargs): Args: 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. + 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 diff --git a/evennia/server/webserver.py b/evennia/server/webserver.py index 4dc3f71e35..ff7f61baa0 100644 --- a/evennia/server/webserver.py +++ b/evennia/server/webserver.py @@ -70,14 +70,15 @@ class HTTPChannelWithXForwardedFor(http.HTTPChannel): Check to see if this is a reverse proxied connection. """ - CLIENT = 0 - http.HTTPChannel.allHeadersReceived(self) - req = self.requests[-1] - client_ip, port = self.transport.client - proxy_chain = req.getHeader("X-FORWARDED-FOR") - if proxy_chain and client_ip in _UPSTREAM_IPS: - forwarded = proxy_chain.split(", ", 1)[CLIENT] - self.transport.client = (forwarded, port) + if self.requests: + CLIENT = 0 + http.HTTPChannel.allHeadersReceived(self) + req = self.requests[-1] + client_ip, port = self.transport.client + proxy_chain = req.getHeader("X-FORWARDED-FOR") + if proxy_chain and client_ip in _UPSTREAM_IPS: + forwarded = proxy_chain.split(", ", 1)[CLIENT] + self.transport.client = (forwarded, port) # Monkey-patch Twisted to handle X-Forwarded-For. From a43bc7e9560d35085d1a0a390d1677025d853ebc Mon Sep 17 00:00:00 2001 From: serprinss <62165076+serprinss@users.noreply.github.com> Date: Sat, 28 Mar 2020 13:34:10 +1100 Subject: [PATCH 002/361] documented return value for create_account --- evennia/utils/create.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evennia/utils/create.py b/evennia/utils/create.py index 5076f60ee3..dbf19f0332 100644 --- a/evennia/utils/create.py +++ b/evennia/utils/create.py @@ -501,6 +501,8 @@ def create_account( report_to (Object): An object with a msg() method to report errors to. If not given, errors will be logged. + Returns: + (object) The acount object of the new account. Raises: ValueError: If `key` already exists in database. From d382e4aff61b2b4cd46db8cc9aff2e7a6743f92e Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Mar 2020 17:57:04 +0100 Subject: [PATCH 003/361] Extensive cleanup and refactoring of the spawn command and obj-update functionality, as per #1879 --- CHANGELOG.md | 1 + evennia/commands/default/building.py | 24 +-- evennia/prototypes/menus.py | 39 +++-- evennia/prototypes/prototypes.py | 16 +- evennia/prototypes/spawner.py | 219 ++++++++++++++++----------- 5 files changed, 176 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5252f8f67a..0b22fcd398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ without arguments starts a full interactive Python console. bugfixes. - Remove `dummy@example.com` as a default account email when unset, a string is no longer required by Django. +- Fixes to `spawn`, make updating an existing prototype/object work better. ## Evennia 0.9 (2018-2019) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 0f8756440b..ab931370f8 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -7,7 +7,7 @@ from django.db.models import Q, Min, Max from evennia.objects.models import ObjectDB from evennia.locks.lockhandler import LockException from evennia.commands.cmdhandler import get_and_merge_cmdsets -from evennia.utils import create, utils, search +from evennia.utils import create, utils, search, logger from evennia.utils.utils import ( inherits_from, class_from_module, @@ -3479,8 +3479,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): "Use spawn/update to apply later as needed.|n" ) return - n_updated = spawner.batch_update_objects_with_prototype( - prototype, objects=existing_objects) + try: + n_updated = spawner.batch_update_objects_with_prototype( + prototype, objects=existing_objects) + except Exception: + logger.log_trace() caller.msg(f"{n_updated} objects were updated.") return @@ -3585,17 +3588,18 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # check for existing prototype (exact match) old_prototype = self._search_prototype(prototype_key, quiet=True) - print("old_prototype", old_prototype) - print("new_prototype", prototype) diff = spawner.prototype_diff(old_prototype, prototype, homogenize=True) - print("diff", diff) diffstr = spawner.format_diff(diff) new_prototype_detail = self._get_prototype_detail(prototypes=[prototype]) if old_prototype: - string = (f"|yExisting prototype \"{prototype_key}\" found. Change:|n\n{diffstr}\n" - f"|yNew changed prototype:|n\n{new_prototype_detail}") - question = "\n|yDo you want to apply the change to the existing prototype?|n [Y]/N" + if not diffstr: + string = f"|yAlready existing Prototype:|n\n{new_prototype_detail}\n" + question = "\nThere seems to be no changes. Do you still want to (re)save? [Y]/N" + else: + string = (f"|yExisting prototype \"{prototype_key}\" found. Change:|n\n{diffstr}\n" + f"|yNew changed prototype:|n\n{new_prototype_detail}") + question = "\n|yDo you want to apply the change to the existing prototype?|n [Y]/N" else: string = f"|yCreating new prototype:|n\n{new_prototype_detail}" question = "\nDo you want to continue saving? [Y]/N" @@ -3682,7 +3686,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): try: for obj in spawner.spawn(prototype): self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) - if not noloc and "location" not in prototype: + if not prototype.get('location') and not noloc: # we don't hardcode the location in the prototype (unless the user # did so manually) - that would lead to it having to be 'removed' every # time we try to update objects with this prototype in the future. diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index bba55b1eee..abaed9ef58 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -2131,12 +2131,13 @@ def _keep_diff(caller, **kwargs): tmp[path[-1]] = tuple(list(tmp[path[-1]][:-1]) + ["KEEP"]) -def _format_diff_text_and_options(diff, **kwargs): +def _format_diff_text_and_options(diff, minimal=True, **kwargs): """ Reformat the diff in a way suitable for the olc menu. Args: diff (dict): A diff as produced by `prototype_diff`. + minimal (bool, optional): Don't show KEEPs. Kwargs: any (any): Forwarded into the generated options as arguments to the callable. @@ -2150,12 +2151,15 @@ def _format_diff_text_and_options(diff, **kwargs): def _visualize(obj, rootname, get_name=False): if utils.is_iter(obj): + if not obj: + return str(obj) if get_name: return obj[0] if obj[0] else "" if rootname == "attrs": return "{} |W=|n {} |W(category:|n {}|W, locks:|n {}|W)|n".format(*obj) elif rootname == "tags": return "{} |W(category:|n {}|W)|n".format(obj[0], obj[1]) + return "{}".format(obj) def _parse_diffpart(diffpart, optnum, *args): @@ -2166,17 +2170,33 @@ def _format_diff_text_and_options(diff, **kwargs): rootname = args[0] old, new, instruction = diffpart if instruction == "KEEP": - texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old, rootname))) + if not minimal: + texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old, rootname))) else: + # instructions we should be able to revert by a menu choice vold = _visualize(old, rootname) vnew = _visualize(new, rootname) vsep = "" if len(vold) < 78 else "\n" - vinst = "|rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction) - texts.append( - " |c[{num}] {inst}|W:|n {old} |W->|n{sep} {new}".format( - inst=vinst, num=optnum, old=vold, sep=vsep, new=vnew + + if instruction == "ADD": + texts.append(" |c[{optnum}] |yADD|n: {new}".format( + optnum=optnum, new=_visualize(new, rootname))) + elif instruction == "REMOVE" and not new: + if rootname == "tags" and old[1] == protlib._PROTOTYPE_TAG_CATEGORY: + # special exception for the prototype-tag mechanism + # this is added post-spawn automatically and should + # not be listed as REMOVE. + return texts, options, optnum + + texts.append(" |c[{optnum}] |rREMOVE|n: {old}".format( + optnum=optnum, old=_visualize(old, rootname))) + else: + vinst = "|y{}|n".format(instruction) + texts.append( + " |c[{num}] {inst}|W:|n {old} |W->|n{sep} {new}".format( + inst=vinst, num=optnum, old=vold, sep=vsep, new=vnew + ) ) - ) options.append( { "key": str(optnum), @@ -2203,11 +2223,8 @@ def _format_diff_text_and_options(diff, **kwargs): for root_key in sorted(diff): diffpart = diff[root_key] text, option, optnum = _parse_diffpart(diffpart, optnum, root_key) - heading = "- |w{}:|n ".format(root_key) - if root_key in ("attrs", "tags", "permissions"): - texts.append(heading) - elif text: + if text: text = [heading + text[0]] + text[1:] else: text = [heading] diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 26fd99f1fd..b80d972e36 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -238,9 +238,6 @@ def save_prototype(prototype): ) # make sure meta properties are included with defaults - stored_prototype = DbPrototype.objects.filter(db_key=prototype_key) - prototype = stored_prototype[0].prototype if stored_prototype else {} - in_prototype["prototype_desc"] = in_prototype.get( "prototype_desc", prototype.get("prototype_desc", "") ) @@ -260,17 +257,16 @@ def save_prototype(prototype): ] in_prototype["prototype_tags"] = prototype_tags - prototype.update(in_prototype) - + stored_prototype = DbPrototype.objects.filter(db_key=prototype_key) if stored_prototype: # edit existing prototype stored_prototype = stored_prototype[0] - stored_prototype.desc = prototype["prototype_desc"] + stored_prototype.desc = in_prototype["prototype_desc"] if prototype_tags: stored_prototype.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) - stored_prototype.tags.batch_add(*prototype["prototype_tags"]) - stored_prototype.locks.add(prototype["prototype_locks"]) - stored_prototype.attributes.add("prototype", prototype) + stored_prototype.tags.batch_add(*in_prototype["prototype_tags"]) + stored_prototype.locks.add(in_prototype["prototype_locks"]) + stored_prototype.attributes.add("prototype", in_prototype) else: # create a new prototype stored_prototype = create_script( @@ -280,7 +276,7 @@ def save_prototype(prototype): persistent=True, locks=prototype_locks, tags=prototype["prototype_tags"], - attributes=[("prototype", prototype)], + attributes=[("prototype", in_prototype)], ) return stored_prototype.prototype diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 98fe2526d7..a4625f16f0 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -138,6 +138,7 @@ from django.conf import settings import evennia from evennia.objects.models import ObjectDB +from evennia.utils import logger from evennia.utils.utils import make_iter, is_iter from evennia.prototypes import prototypes as protlib from evennia.prototypes.prototypes import ( @@ -322,7 +323,7 @@ def prototype_from_object(obj): return prot -def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False): +def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implicit_keep=False): """ A 'detailed' diff specifies differences down to individual sub-sections of the prototype, like individual attributes, permissions etc. It is used @@ -336,6 +337,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False): attr/tag (for example) are represented by a tuple. homogenize (bool, optional): Auto-homogenize both prototypes for the best comparison. This is most useful for displaying. + implicit_keep (bool, optional): If set, the resulting diff will assume KEEP unless the new + prototype explicitly change them. That is, if a key exists in `prototype1` and + not in `prototype2`, it will not be REMOVEd but set to KEEP instead. This is particularly + useful for auto-generated prototypes when updating objects. Returns: diff (dict): A structure detailing how to convert prototype1 to prototype2. All @@ -346,6 +351,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False): instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP". """ + class Unset: + def __bool__(self): + return False + _unset = Unset() def _recursive_diff(old, new, depth=0): @@ -363,6 +372,9 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False): return { part[0] if is_iter(part) else part: (part, None, "REMOVE") for part in old } + if isinstance(new, Unset) and implicit_keep: + # the new does not define any change, use implicit-keep + return (old, None, "KEEP") return (old, new, "REMOVE") elif not old and new: if depth < maxdepth and new_type == dict: @@ -376,7 +388,7 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False): elif depth < maxdepth and new_type == dict: all_keys = set(list(old.keys()) + list(new.keys())) return { - key: _recursive_diff(old.get(key), new.get(key), depth=depth + 1) + key: _recursive_diff(old.get(key), new.get(key, _unset), depth=depth + 1) for key in all_keys } elif depth < maxdepth and is_iter(new): @@ -384,7 +396,7 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False): new_map = {part[0] if is_iter(part) else part: part for part in new} all_keys = set(list(old_map.keys()) + list(new_map.keys())) return { - key: _recursive_diff(old_map.get(key), new_map.get(key), depth=depth + 1) + key: _recursive_diff(old_map.get(key), new_map.get(key, _unset), depth=depth + 1) for key in all_keys } elif old != new: @@ -468,7 +480,7 @@ def flatten_diff(diff): return flat_diff -def prototype_diff_from_object(prototype, obj): +def prototype_diff_from_object(prototype, obj, implicit_keep=True): """ Get a simple diff for a prototype compared to an object which may or may not already have a prototype (or has one but changed locally). For more complex migratations a manual diff may be @@ -482,6 +494,11 @@ def prototype_diff_from_object(prototype, obj): diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} obj_prototype (dict): The prototype calculated for the given object. The diff is how to convert this prototype into the new prototype. + implicit_keep (bool, optional): This is usually what one wants for object updating. When + set, this means the prototype diff will assume KEEP on differences + between the object-generated prototype and that which is not explicitly set in the + new prototype. This means e.g. that even though the object has a location, and the + prototype does not specify the location, it will not be unset. Notes: The `diff` is on the following form: @@ -494,7 +511,8 @@ def prototype_diff_from_object(prototype, obj): """ obj_prototype = prototype_from_object(obj) - diff = prototype_diff(obj_prototype, protlib.homogenize_prototype(prototype)) + diff = prototype_diff(obj_prototype, protlib.homogenize_prototype(prototype), + implicit_keep=implicit_keep) return diff, obj_prototype @@ -511,6 +529,7 @@ def format_diff(diff, minimal=True): texts (str): The formatted text. """ + valid_instructions = ("KEEP", "REMOVE", "ADD", "UPDATE") def _visualize(obj, rootname, get_name=False): @@ -572,7 +591,7 @@ def format_diff(diff, minimal=True): return "\n ".join(line for line in texts if line) -def batch_update_objects_with_prototype(prototype, diff=None, objects=None): +def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exact=False): """ Update existing objects with the latest version of the prototype. @@ -583,6 +602,12 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): If not given this will be constructed from the first object found. objects (list, optional): List of objects to update. If not given, query for these objects using the prototype's `prototype_key`. + exact (bool, optional): By default (`False`), keys not explicitly in the prototype will + not be applied to the object, but will be retained as-is. This is usually what is + expected - for example, one usually do not want to remove the object's location even + if it's not set in the prototype. With `exact=True`, all un-specified properties of the + objects will be removed if they exist. This will lead to a more accurate 1:1 correlation + between the object and the prototype but is usually impractical. Returns: changed (int): The number of objects that had changes applied to them. @@ -615,98 +640,108 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): old_prot_key = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) old_prot_key = old_prot_key[0] if old_prot_key else None - for key, directive in diff.items(): - if directive in ("UPDATE", "REPLACE"): + try: + for key, directive in diff.items(): - if key in _PROTOTYPE_META_NAMES: - # prototype meta keys are not stored on-object + if key not in new_prototype and not exact: + # we don't update the object if the prototype does not actually + # contain the key (the diff will report REMOVE but we ignore it + # since exact=False) continue - val = new_prototype[key] - do_save = True + if directive in ("UPDATE", "REPLACE"): - if key == "key": - obj.db_key = init_spawn_value(val, str) - elif key == "typeclass": - obj.db_typeclass_path = init_spawn_value(val, str) - elif key == "location": - obj.db_location = init_spawn_value(val, value_to_obj) - elif key == "home": - obj.db_home = init_spawn_value(val, value_to_obj) - elif key == "destination": - obj.db_destination = init_spawn_value(val, value_to_obj) - elif key == "locks": - if directive == "REPLACE": - obj.locks.clear() - obj.locks.add(init_spawn_value(val, str)) - elif key == "permissions": - if directive == "REPLACE": - obj.permissions.clear() - obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val)) - elif key == "aliases": - if directive == "REPLACE": - obj.aliases.clear() - obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val)) - elif key == "tags": - if directive == "REPLACE": - obj.tags.clear() - obj.tags.batch_add( - *( - (init_spawn_value(ttag, str), tcategory, tdata) - for ttag, tcategory, tdata in val - ) - ) - elif key == "attrs": - if directive == "REPLACE": - obj.attributes.clear() - obj.attributes.batch_add( - *( - ( - init_spawn_value(akey, str), - init_spawn_value(aval, value_to_obj), - acategory, - alocks, + if key in _PROTOTYPE_META_NAMES: + # prototype meta keys are not stored on-object + continue + + val = new_prototype[key] + do_save = True + + if key == "key": + obj.db_key = init_spawn_value(val, str) + elif key == "typeclass": + obj.db_typeclass_path = init_spawn_value(val, str) + elif key == "location": + obj.db_location = init_spawn_value(val, value_to_obj) + elif key == "home": + obj.db_home = init_spawn_value(val, value_to_obj) + elif key == "destination": + obj.db_destination = init_spawn_value(val, value_to_obj) + elif key == "locks": + if directive == "REPLACE": + obj.locks.clear() + obj.locks.add(init_spawn_value(val, str)) + elif key == "permissions": + if directive == "REPLACE": + obj.permissions.clear() + obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val)) + elif key == "aliases": + if directive == "REPLACE": + obj.aliases.clear() + obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val)) + elif key == "tags": + if directive == "REPLACE": + obj.tags.clear() + obj.tags.batch_add( + *( + (init_spawn_value(ttag, str), tcategory, tdata) + for ttag, tcategory, tdata in val ) - for akey, aval, acategory, alocks in val ) - ) - elif key == "exec": - # we don't auto-rerun exec statements, it would be huge security risk! - pass - else: - obj.attributes.add(key, init_spawn_value(val, value_to_obj)) - elif directive == "REMOVE": - do_save = True - if key == "key": - obj.db_key = "" - elif key == "typeclass": - # fall back to default - obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS - elif key == "location": - obj.db_location = None - elif key == "home": - obj.db_home = None - elif key == "destination": - obj.db_destination = None - elif key == "locks": - obj.locks.clear() - elif key == "permissions": - obj.permissions.clear() - elif key == "aliases": - obj.aliases.clear() - elif key == "tags": - obj.tags.clear() - elif key == "attrs": - obj.attributes.clear() - elif key == "exec": - # we don't auto-rerun exec statements, it would be huge security risk! - pass - else: - obj.attributes.remove(key) - - # we must always make sure to re-add the prototype tag - obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) - obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + elif key == "attrs": + if directive == "REPLACE": + obj.attributes.clear() + obj.attributes.batch_add( + *( + ( + init_spawn_value(akey, str), + init_spawn_value(aval, value_to_obj), + acategory, + alocks, + ) + for akey, aval, acategory, alocks in val + ) + ) + elif key == "exec": + # we don't auto-rerun exec statements, it would be huge security risk! + pass + else: + obj.attributes.add(key, init_spawn_value(val, value_to_obj)) + elif directive == "REMOVE": + do_save = True + if key == "key": + obj.db_key = "" + elif key == "typeclass": + # fall back to default + obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS + elif key == "location": + obj.db_location = None + elif key == "home": + obj.db_home = None + elif key == "destination": + obj.db_destination = None + elif key == "locks": + obj.locks.clear() + elif key == "permissions": + obj.permissions.clear() + elif key == "aliases": + obj.aliases.clear() + elif key == "tags": + obj.tags.clear() + elif key == "attrs": + obj.attributes.clear() + elif key == "exec": + # we don't auto-rerun exec statements, it would be huge security risk! + pass + else: + obj.attributes.remove(key) + except Exception: + logger.log_trace(f"Failed to apply prototype '{prototype_key}' to {obj}.") + finally: + # we must always make sure to re-add the prototype tag + obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) + obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) if do_save: changed += 1 From 5c3b31efb0e8edf731b7635206bc654bb4cc2c4d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Mar 2020 20:34:56 +0100 Subject: [PATCH 004/361] Handle going from location=None to unset and back in prototype, as per #2005 --- CHANGELOG.md | 3 +- evennia/commands/default/building.py | 32 +++++++++++++++++---- evennia/prototypes/menus.py | 3 +- evennia/prototypes/prototypes.py | 42 ++++++++++++++-------------- evennia/prototypes/spawner.py | 13 +++++---- 5 files changed, 59 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b22fcd398..ac78239c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,8 @@ without arguments starts a full interactive Python console. bugfixes. - Remove `dummy@example.com` as a default account email when unset, a string is no longer required by Django. -- Fixes to `spawn`, make updating an existing prototype/object work better. +- Fixes to `spawn`, make updating an existing prototype/object work better. Add `/raw` switch + to `spawn` command to extract the raw prototype dict for manual editing. ## Evennia 0.9 (2018-2019) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index ab931370f8..e0f125079e 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -13,7 +13,8 @@ from evennia.utils.utils import ( class_from_module, get_all_typeclasses, variable_from_module, - dbref, interactive + dbref, interactive, + list_to_string ) from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore @@ -3253,6 +3254,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): search - search prototype by name or tags. list - list available prototypes, optionally limit by tags. show, examine - inspect prototype by key. If not given, acts like list. + raw - show the raw dict of the prototype as a one-line string for manual editing. save - save a prototype to the database. It will be listable by /list. delete - remove a prototype from database, if allowed to. update - find existing objects with the same prototype_key and update @@ -3301,6 +3303,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): "search", "list", "show", + "raw", "examine", "save", "delete", @@ -3433,9 +3436,12 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): def _list_prototypes(self, key=None, tags=None): """Display prototypes as a list, optionally limited by key/tags. """ + table = protlib.list_prototypes(self.caller, key=key, tags=tags) + if not table: + return True EvMore( self.caller, - str(protlib.list_prototypes(self.caller, key=key, tags=tags)), + str(table), exit_on_lastpage=True, justify_kwargs=False, ) @@ -3492,8 +3498,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): Parse ;-separated input list. """ key, desc, tags = "", "", [] - if ";" in self.args: - parts = (part.strip().lower() for part in self.args.split(";")) + if ";" in argstring: + parts = [part.strip().lower() for part in argstring.split(";")] if len(parts) > 1 and desc: key = parts[0] desc = parts[1] @@ -3537,10 +3543,20 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): return # search for key;tag combinations - key, _, tags = self._parse_key_desc_tags(self.args, desc=False) + key, _, tags = self._parse_key_desc_tags(self.args, desc=False) self._list_prototypes(key, tags) return + if "raw" in self.switches: + # query for key match and return the prototype as a safe one-liner string. + if not self.args: + caller.msg("You need to specify a prototype-key to get the raw data for.") + prototype = self._search_prototype(self.args) + if not prototype: + return + caller.msg(str(prototype)) + return + if "show" in self.switches or "examine" in self.switches: # show a specific prot detail. The argument is a search query or empty. if not self.args: @@ -3556,7 +3572,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if "list" in self.switches: # for list, all optional arguments are tags. - self._list_prototypes(tags=self.lhslist) + tags = self.lhslist + err = self._list_prototypes(tags=tags) + if err: + caller.msg("No prototypes found with prototype-tag(s): {}".format( + list_to_string(tags, "or"))) return if "save" in self.switches: diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index abaed9ef58..854f9e7ced 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -2294,7 +2294,8 @@ def node_apply_diff(caller, **kwargs): if not custom_location: diff.pop("location", None) - txt, options = _format_diff_text_and_options(diff, objects=update_objects, base_obj=base_obj) + txt, options = _format_diff_text_and_options(diff, objects=update_objects, + base_obj=base_obj, prototype=prototype) if options: text = [ diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index b80d972e36..7e76ee515e 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -272,10 +272,10 @@ def save_prototype(prototype): stored_prototype = create_script( DbPrototype, key=prototype_key, - desc=prototype["prototype_desc"], + desc=in_prototype["prototype_desc"], persistent=True, locks=prototype_locks, - tags=prototype["prototype_tags"], + tags=in_prototype["prototype_tags"], attributes=[("prototype", in_prototype)], ) return stored_prototype.prototype @@ -724,15 +724,15 @@ def prototype_to_str(prototype): prototype_desc=prototype.get("prototype_desc", "|wNone|n"), prototype_parent=prototype.get("prototype_parent", "|wNone|n"), ) - - key = prototype.get("key", "") - if key: + key = aliases = attrs = tags = locks = permissions = location = home = destination = "" + if "key" in prototype: + key = prototype["key"] key = "|ckey:|n {key}".format(key=key) - aliases = prototype.get("aliases", "") - if aliases: + if "aliases" in prototype: + aliases = prototype["aliases"] aliases = "|caliases:|n {aliases}".format(aliases=", ".join(aliases)) - attrs = prototype.get("attrs", "") - if attrs: + if "attrs" in prototype: + attrs = prototype["attrs"] out = [] for (attrkey, value, category, locks) in attrs: locks = ", ".join(lock for lock in locks if lock) @@ -751,8 +751,8 @@ def prototype_to_str(prototype): ) ) attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out)) - tags = prototype.get("tags", "") - if tags: + if "tags" in prototype: + tags = prototype['tags'] out = [] for (tagkey, category, data) in tags: out.append( @@ -761,20 +761,20 @@ def prototype_to_str(prototype): ) ) tags = "|ctags:|n\n {tags}".format(tags=", ".join(out)) - locks = prototype.get("locks", "") - if locks: + if "locks" in prototype: + locks = prototype["locks"] locks = "|clocks:|n\n {locks}".format(locks=locks) - permissions = prototype.get("permissions", "") - if permissions: + if "permissions" in prototype: + permissions = prototype["permissions"] permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions)) - location = prototype.get("location", "") - if location: + if "location" in prototype: + location = prototype["location"] location = "|clocation:|n {location}".format(location=location) - home = prototype.get("home", "") - if home: + if "home" in prototype: + home = prototype["home"] home = "|chome:|n {home}".format(home=home) - destination = prototype.get("destination", "") - if destination: + if "destination" in prototype: + destination = prototype["destination"] destination = "|cdestination:|n {destination}".format(destination=destination) body = "\n".join( diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index a4625f16f0..5ba3cd4b62 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -354,6 +354,9 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implici class Unset: def __bool__(self): return False + def __str__(self): + return "" + _unset = Unset() def _recursive_diff(old, new, depth=0): @@ -361,10 +364,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implici old_type = type(old) new_type = type(new) - if old_type == new_type and not (old and new): + if old_type == new_type and not (old or new): # both old and new are unset, like [] or None - return (old, new, "KEEP") - elif old_type != new_type: + return (None, None, "KEEP") + if old_type != new_type: if old and not new: if depth < maxdepth and old_type == dict: return {key: (part, None, "REMOVE") for key, part in old.items()} @@ -388,7 +391,7 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implici elif depth < maxdepth and new_type == dict: all_keys = set(list(old.keys()) + list(new.keys())) return { - key: _recursive_diff(old.get(key), new.get(key, _unset), depth=depth + 1) + key: _recursive_diff(old.get(key, _unset), new.get(key, _unset), depth=depth + 1) for key in all_keys } elif depth < maxdepth and is_iter(new): @@ -396,7 +399,7 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implici new_map = {part[0] if is_iter(part) else part: part for part in new} all_keys = set(list(old_map.keys()) + list(new_map.keys())) return { - key: _recursive_diff(old_map.get(key), new_map.get(key, _unset), depth=depth + 1) + key: _recursive_diff(old_map.get(key, _unset), new_map.get(key, _unset), depth=depth + 1) for key in all_keys } elif old != new: From ee62a4fa93952597e62d96898c6878c3ba5c84e3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Mar 2020 20:56:01 +0100 Subject: [PATCH 005/361] Hide prototypes from CmdScripts output. Resolves #2067 --- evennia/commands/default/system.py | 4 ++++ evennia/prototypes/menus.py | 4 ++-- evennia/prototypes/prototypes.py | 6 +++--- evennia/prototypes/spawner.py | 14 +++++++------- evennia/prototypes/tests.py | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index b6ff6ba25c..4124d0423e 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -487,6 +487,8 @@ class CmdScripts(COMMAND_DEFAULT_CLASS): locks = "cmd:perm(listscripts) or perm(Admin)" help_category = "System" + excluded_typeclass_paths = ["evennia.prototypes.prototypes.DbPrototype"] + def func(self): """implement method""" @@ -519,6 +521,8 @@ class CmdScripts(COMMAND_DEFAULT_CLASS): if not scripts: caller.msg("No scripts are running.") return + # filter any found scripts by tag category. + scripts = scripts.exclude(db_typeclass_path__in=self.excluded_typeclass_paths) if not scripts: string = "No scripts found with a key '%s', or on an object named '%s'." % (args, args) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 854f9e7ced..1024b9d63f 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -1488,7 +1488,7 @@ def node_tags(caller): as the |cprototype_key|n and with a category "{tag_category}". This allows the spawner to optionally update previously spawned objects when their prototype changes. """.format( - tag_category=protlib._PROTOTYPE_TAG_CATEGORY + tag_category=protlib.PROTOTYPE_TAG_CATEGORY ) text = (text, helptext) @@ -2182,7 +2182,7 @@ def _format_diff_text_and_options(diff, minimal=True, **kwargs): texts.append(" |c[{optnum}] |yADD|n: {new}".format( optnum=optnum, new=_visualize(new, rootname))) elif instruction == "REMOVE" and not new: - if rootname == "tags" and old[1] == protlib._PROTOTYPE_TAG_CATEGORY: + if rootname == "tags" and old[1] == protlib.PROTOTYPE_TAG_CATEGORY: # special exception for the prototype-tag mechanism # this is added post-spawn automatically and should # not be listed as REMOVE. diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 7e76ee515e..fcadc55488 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -52,7 +52,7 @@ _PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + ( "tags", "attrs", ) -_PROTOTYPE_TAG_CATEGORY = "from_prototype" +PROTOTYPE_TAG_CATEGORY = "from_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" PROT_FUNCS = {} @@ -263,7 +263,7 @@ def save_prototype(prototype): stored_prototype = stored_prototype[0] stored_prototype.desc = in_prototype["prototype_desc"] if prototype_tags: - stored_prototype.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) + stored_prototype.tags.clear(category=PROTOTYPE_TAG_CATEGORY) stored_prototype.tags.batch_add(*in_prototype["prototype_tags"]) stored_prototype.locks.add(in_prototype["prototype_locks"]) stored_prototype.attributes.add("prototype", in_prototype) @@ -421,7 +421,7 @@ def search_objects_with_prototype(prototype_key): matches (Queryset): All matching objects spawned from this prototype. """ - return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + return ObjectDB.objects.get_by_tag(key=prototype_key, category=PROTOTYPE_TAG_CATEGORY) def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True): diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 5ba3cd4b62..222e4ed49c 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -145,7 +145,7 @@ from evennia.prototypes.prototypes import ( value_to_obj, value_to_obj_or_any, init_spawn_value, - _PROTOTYPE_TAG_CATEGORY, + PROTOTYPE_TAG_CATEGORY, ) @@ -269,7 +269,7 @@ def prototype_from_object(obj): """ # first, check if this object already has a prototype - prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) + prot = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True) if prot: prot = protlib.search_prototype(prot[0]) @@ -625,7 +625,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac prototype_key = new_prototype["prototype_key"] if not objects: - objects = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + objects = ObjectDB.objects.get_by_tag(prototype_key, category=PROTOTYPE_TAG_CATEGORY) if not objects: return 0 @@ -640,7 +640,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac for obj in objects: do_save = False - old_prot_key = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) + old_prot_key = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True) old_prot_key = old_prot_key[0] if old_prot_key else None try: @@ -743,8 +743,8 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac logger.log_trace(f"Failed to apply prototype '{prototype_key}' to {obj}.") finally: # we must always make sure to re-add the prototype tag - obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY) - obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + obj.tags.clear(category=PROTOTYPE_TAG_CATEGORY) + obj.tags.add(prototype_key, category=PROTOTYPE_TAG_CATEGORY) if do_save: changed += 1 @@ -927,7 +927,7 @@ def spawn(*prototypes, **kwargs): prototype_key = prototype.get("prototype_key", None) if prototype_key: # we make sure to add a tag identifying which prototype created this object - tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY)) + tags.append((prototype_key, PROTOTYPE_TAG_CATEGORY)) val = prot.pop("exec", "") execs = init_spawn_value(val, make_iter) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 107e2a127f..2c7d5f0426 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -851,7 +851,7 @@ class TestMenuModule(EvenniaTest): self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject") self.assertEqual( - obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot["prototype_key"] + obj.tags.get(category=spawner.PROTOTYPE_TAG_CATEGORY), self.test_prot["prototype_key"] ) # update helpers From 0decf7e366a958f267a96b791a0501438200e0ea Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Mar 2020 00:12:16 +0100 Subject: [PATCH 006/361] Fix unit tests --- evennia/commands/default/building.py | 40 +++++++++++++++++------ evennia/commands/default/tests.py | 20 ++++++++---- evennia/prototypes/prototypes.py | 2 +- evennia/prototypes/spawner.py | 18 +++++++---- evennia/prototypes/tests.py | 48 ++++++++++++++-------------- 5 files changed, 81 insertions(+), 47 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index e0f125079e..8ce409300f 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -3330,7 +3330,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # handle the search result err = None if not prototypes: - err = f"No prototype named '{prototype_key}'." + err = f"No prototype named '{prototype_key}' was found." elif nprots > 1: err = "Found {} prototypes matching '{}':\n {}".format( nprots, @@ -3430,7 +3430,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if prototypes: return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes) elif query: - self.caller.msg(f"No prototype found to match the query '{query}'.") + self.caller.msg(f"No prototype named '{query}' was found.") else: self.caller.msg(f"No prototypes found.") @@ -3583,24 +3583,44 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # store a prototype to the database store if not self.args: caller.msg( - "Usage: spawn/save [;desc[;tag,tag[,...][;lockstring]]] = " + "Usage: spawn/save [[;desc[;tag,tag[,...][;lockstring]]]] = " ) return + if self.rhs: + # input on the form key = prototype + prototype_key, prototype_desc, prototype_tags = self._parse_key_desc_tags(self.lhs) + prototype_key = None if not prototype_key else prototype_key + prototype_desc = None if not prototype_desc else prototype_desc + prototype_tags = None if not prototype_tags else prototype_tags + prototype_input = self.rhs.strip() + else: + prototype_key = prototype_desc = None + prototype_tags = None + prototype_input = self.lhs.strip() - prototype_key, prototype_desc, prototype_tags = self._parse_key_desc_tags(self.lhs) - - # handle rhs: - prototype = self._parse_prototype(self.rhs.strip()) + # handle parsing + prototype = self._parse_prototype(prototype_input) if not prototype: return - if prototype.get("prototype_key") != prototype_key: + prot_prototype_key = prototype.get("prototype_key") + + if not (prototype_key or prot_prototype_key): + caller.msg("A prototype_key must be given, either as `prototype_key = ` " + "or as a key 'prototype_key' inside the prototype structure.") + return + + if prototype_key is None: + prototype_key = prot_prototype_key + + if prot_prototype_key != prototype_key: caller.msg("(Replacing `prototype_key` in prototype with given key.)") prototype['prototype_key'] = prototype_key - if prototype_desc and prototype.get("prototype_desc") != prototype_desc: + + if prototype_desc is not None and prot_prototype_key != prototype_desc: caller.msg("(Replacing `prototype_desc` in prototype with given desc.)") prototype['prototype_desc'] = prototype_desc - if prototype_tags and prototype.get("prototype_tags") != prototype_tags: + if prototype_tags is not None and prototype.get("prototype_tags") != prototype_tags: caller.msg("(Replacing `prototype_tags` in prototype with given tag(s))" ) prototype['prototype_tags'] = prototype_tags diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index d41a1ca4ca..4edaccb51d 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -1228,13 +1228,22 @@ class TestBuilding(CommandTest): inputs=["y"], ) + self.call( + building.CmdSpawn(), + "/save testprot2 = {'key':'Test Char', " + "'typeclass':'evennia.objects.objects.DefaultCharacter'}", + "(Replacing `prototype_key` in prototype with given key.)|Saved prototype: testprot2", + inputs=["y"], + ) + self.call(building.CmdSpawn(), "/search ", "Key ") self.call(building.CmdSpawn(), "/search test;test2", "") self.call( building.CmdSpawn(), "/save {'key':'Test Char', " "'typeclass':'evennia.objects.objects.DefaultCharacter'}", - "To save a prototype it must have the 'prototype_key' set.", + "A prototype_key must be given, either as `prototype_key = ` or as " + "a key 'prototype_key' inside the prototype structure.", ) self.call(building.CmdSpawn(), "/list", "Key ") @@ -1312,7 +1321,7 @@ class TestBuilding(CommandTest): ball.delete() # test calling spawn with an invalid prototype. - self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'") + self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST' was found.") # Test listing commands self.call(building.CmdSpawn(), "/list", "Key ") @@ -1343,13 +1352,12 @@ class TestBuilding(CommandTest): # spawn/edit with invalid prototype msg = self.call( - building.CmdSpawn(), "/edit NO_EXISTS", "No prototype 'NO_EXISTS' was found." + building.CmdSpawn(), "/edit NO_EXISTS", "No prototype named 'NO_EXISTS' was found." ) # spawn/examine (missing prototype) # lists all prototypes that exist - msg = self.call(building.CmdSpawn(), "/examine") - assert "testball" in msg and "testprot" in msg + self.call(building.CmdSpawn(), "/examine", "You need to specify a prototype-key to show.") # spawn/examine with valid prototype # prints the prototype @@ -1358,7 +1366,7 @@ class TestBuilding(CommandTest): # spawn/examine with invalid prototype # shows error - self.call(building.CmdSpawn(), "/examine NO_EXISTS", "No prototype 'NO_EXISTS' was found.") + self.call(building.CmdSpawn(), "/examine NO_EXISTS", "No prototype named 'NO_EXISTS' was found.") class TestComms(CommandTest): diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index fcadc55488..5d29d15b44 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -56,7 +56,7 @@ PROTOTYPE_TAG_CATEGORY = "from_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" PROT_FUNCS = {} -_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:perm(Admin)" +_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:all()" class PermissionError(RuntimeError): diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 222e4ed49c..fc8015bfbd 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -166,6 +166,18 @@ _PROTOTYPE_ROOT_NAMES = ( _NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES +class Unset: + """ + Helper class representing a non-set diff element. + + """ + def __bool__(self): + return False + def __str__(self): + return "" + + + # Helper @@ -351,12 +363,6 @@ def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implici instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP". """ - class Unset: - def __bool__(self): - return False - def __str__(self): - return "" - _unset = Unset() def _recursive_diff(old, new, depth=0): diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 2c7d5f0426..dfa592adbb 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -11,7 +11,7 @@ from evennia.utils.test_resources import EvenniaTest from evennia.utils.tests.test_evmenu import TestEvMenu from evennia.prototypes import spawner, prototypes as protlib from evennia.prototypes import menus as olc_menus -from evennia.prototypes import protfuncs as protofuncs +from evennia.prototypes import protfuncs as protofuncs, spawner from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY @@ -212,22 +212,21 @@ class TestUtils(EvenniaTest): "puppet:pperm(Developer);tell:perm(Admin);view:all()", "KEEP", ), - "prototype_tags": {}, + "prototype_tags": (None, None, 'KEEP'), "attrs": { "oldtest": ( ("oldtest", "to_keep", None, ""), ("oldtest", "to_keep", None, ""), "KEEP", ), - "test": (("test", "testval", None, ""), None, "REMOVE"), - "desc": (("desc", "changed desc", None, ""), None, "REMOVE"), - "fooattr": (None, ("fooattr", "fooattrval", None, ""), "ADD"), + "desc": (("desc", "changed desc", None, ""), None, "KEEP"), + "fooattr": (Something, ("fooattr", "fooattrval", None, ""), "ADD"), "test": ( ("test", "testval", None, ""), ("test", "testval_changed", None, ""), "UPDATE", ), - "new": (None, ("new", "new_val", None, ""), "ADD"), + "new": (Something, ("new", "new_val", None, ""), "ADD"), }, "key": ("Obj", "Obj", "KEEP"), "typeclass": ( @@ -246,7 +245,7 @@ class TestUtils(EvenniaTest): spawner.flatten_diff(pdiff), { "aliases": "REMOVE", - "attrs": "REPLACE", + "attrs": "UPDATE", "home": "KEEP", "key": "KEEP", "location": "KEEP", @@ -270,7 +269,9 @@ class TestUtils(EvenniaTest): new_prot = spawner.prototype_from_object(self.obj1) self.assertEqual( { + "aliases": ['foo'], "attrs": [ + ("desc", "changed desc", None, ""), ("fooattr", "fooattrval", None, ""), ("new", "new_val", None, ""), ("oldtest", "to_keep", None, ""), @@ -293,6 +294,9 @@ class TestUtils(EvenniaTest): "view:all()", ] ), + 'tags': [ + ('footag', 'foocategory', None), + (Something, 'from_prototype', None)], "permissions": ["builder"], "prototype_desc": "Built from Obj", "prototype_key": Something, @@ -912,24 +916,20 @@ class TestMenuModule(EvenniaTest): texts, options = olc_menus._format_diff_text_and_options(obj_diff) self.assertEqual( - "\n".join(texts), - "- |wattrs:|n \n" - " |gKEEP|W:|n desc |W=|n This is User #1. |W(category:|n None|W, locks:|n |W)|n\n" - " |c[1] |yADD|n|W:|n None |W->|n foo |W=|n bar |W(category:|n None|W, locks:|n |W)|n\n" - " |gKEEP|W:|n prelogout_location |W=|n #2 |W(category:|n None|W, locks:|n |W)|n\n" - "- |whome:|n |gKEEP|W:|n #2\n" - "- |wkey:|n |gKEEP|W:|n TestChar\n" - "- |wlocks:|n |gKEEP|W:|n boot:false();call:false();control:perm(Developer);delete:false();edit:false();examine:perm(Developer);get:false();msg:all();puppet:false();tell:perm(Admin);view:all()\n" - "- |wpermissions:|n \n" - " |gKEEP|W:|n developer\n" - "- |wprototype_desc:|n |c[2] |rREMOVE|n|W:|n Testobject build |W->|n None\n" - "- |wprototype_key:|n |gKEEP|W:|n TestDiffKey\n" - "- |wprototype_locks:|n |gKEEP|W:|n spawn:all();edit:all()\n" - "- |wprototype_tags:|n \n" - "- |wtags:|n \n" - " |c[3] |yADD|n|W:|n None |W->|n foo |W(category:|n None|W)|n\n" - "- |wtypeclass:|n |gKEEP|W:|n typeclasses.characters.Character", + "\n".join(txt.strip() for txt in texts), + "- |wattrs:|n |c[1] |yADD|n: foo |W=|n bar |W(category:|n None|W, locks:|n |W)|n" + "\n- |whome:|n" + "\n- |wkey:|n" + "\n- |wlocks:|n" + "\n- |wpermissions:|n" + "\n- |wprototype_desc:|n |c[2] |rREMOVE|n: Testobject build" + "\n- |wprototype_key:|n" + "\n- |wprototype_locks:|n" + "\n- |wprototype_tags:|n" + "\n- |wtags:|n |c[3] |yADD|n: foo |W(category:|n None|W)|n" + "\n- |wtypeclass:|n" ) + self.assertEqual( options, [ From 1661fa65746f990cf317e87c83da70abeb733d24 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Mar 2020 10:06:07 +0200 Subject: [PATCH 007/361] Don't raise but instead return HttpResponseBadRequest object --- evennia/web/website/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/web/website/views.py b/evennia/web/website/views.py index 3092a6db9f..02b1d21d56 100644 --- a/evennia/web/website/views.py +++ b/evennia/web/website/views.py @@ -1113,7 +1113,7 @@ class HelpDetailView(HelpMixin, EvenniaDetailView): # Check if this object was requested in a valid manner if not obj: - raise HttpResponseBadRequest( + return HttpResponseBadRequest( "No %(verbose_name)s found matching the query" % {"verbose_name": queryset.model._meta.verbose_name} ) From d0728ee145264f864f59c02bfb6fc7adecad7f0f Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Mar 2020 14:50:01 +0200 Subject: [PATCH 008/361] Refactor EvMore to handle queryset pagination. Resolves #1994. --- CHANGELOG.md | 6 +- evennia/commands/default/system.py | 9 +- evennia/utils/evmore.py | 217 ++++++++++++++++++++--------- 3 files changed, 159 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac78239c78..92c3758b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,11 +34,13 @@ without arguments starts a full interactive Python console. - Allow running Evennia test suite from core repo with `make test`. - Return `store_key` from `TickerHandler.add` and add `store_key` as a kwarg to the `TickerHandler.remove` method. This makes it easier to manage tickers. -- EvMore `text` argument can now also be a list - each entry in the list is run - through str(eval()) and ends up on its own line. Good for paginated object lists. - EvMore auto-justify now defaults to False since this works better with all types of texts (such as tables). New `justify` bool. Old `justify_kwargs` remains but is now only used to pass extra kwargs into the justify function. +- EvMore `text` argument can now also be a list or a queryset. Querysets will be + sliced to only return the required data per page. EvMore takes a new kwarg + `page_formatter` which will be called for each page. This allows to customize + the display of queryset data, build a new EvTable per page etc. - Improve performance of `find` and `objects` commands on large data sets (strikaco) - New `CHANNEL_HANDLER_CLASS` setting allows for replacing the ChannelHandler entirely. - Made `py` interactive mode support regular quit() and more verbose. diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index 4124d0423e..2c67951f73 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -542,19 +542,20 @@ class CmdScripts(COMMAND_DEFAULT_CLASS): # import pdb # DEBUG # pdb.set_trace() # DEBUG ScriptDB.objects.validate() # just to be sure all is synced + caller.msg(string) else: # multiple matches. - string = "Multiple script matches. Please refine your search:\n" - string += format_script_list(scripts) + EvMore(caller, scripts, page_formatter=format_script_list) + caller.msg("Multiple script matches. Please refine your search") elif self.switches and self.switches[0] in ("validate", "valid", "val"): # run validation on all found scripts nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts) string = "Validated %s scripts. " % ScriptDB.objects.all().count() string += "Started %s and stopped %s scripts." % (nr_started, nr_stopped) + caller.msg(string) else: # No stopping or validation. We just want to view things. - string = format_script_list(scripts) - EvMore(caller, string) + EvMore(caller, scripts, page_formatter=format_script_list) class CmdObjects(COMMAND_DEFAULT_CLASS): diff --git a/evennia/utils/evmore.py b/evennia/utils/evmore.py index 62fd47654a..1e0e6e93aa 100644 --- a/evennia/utils/evmore.py +++ b/evennia/utils/evmore.py @@ -28,9 +28,10 @@ caller.msg() construct every time the page is updated. """ from django.conf import settings +from django.db.models.query import QuerySet from evennia import Command, CmdSet from evennia.commands import cmdhandler -from evennia.utils.utils import justify, make_iter +from evennia.utils.utils import make_iter, inherits_from, justify _CMD_NOMATCH = cmdhandler.CMD_NOMATCH _CMD_NOINPUT = cmdhandler.CMD_NOINPUT @@ -117,6 +118,11 @@ class CmdSetMore(CmdSet): self.add(CmdMoreLook()) +# resources for handling queryset inputs +def queryset_maxsize(qs): + return qs.count() + + class EvMore(object): """ The main pager object @@ -132,6 +138,7 @@ class EvMore(object): justify_kwargs=None, exit_on_lastpage=False, exit_cmd=None, + page_formatter=str, **kwargs, ): @@ -149,7 +156,7 @@ class EvMore(object): decorations will be considered in the size of the page. - Otherwise `text` is converted to an iterator, where each step is expected to be a line in the final display. Each line - will be run through repr() (so one could pass a list of objects). + will be run through `iter_callable`. always_page (bool, optional): If `False`, the pager will only kick in if `text` is too big to fit the screen. @@ -168,6 +175,12 @@ class EvMore(object): the caller when the more page exits. Note that this will be using whatever cmdset the user had *before* the evmore pager was activated (so none of the evmore commands will be available when this is run). + page_formatter (callable, optional): If given, this function will be passed the + contents of each extracted page. This is useful when paginating + data consisting something other than a string or a list of strings. Especially + queryset data is likely to always need this argument specified. Note however, + that all size calculations assume this function to return one single line + per element on the page! kwargs (any, optional): These will be passed on to the `caller.msg` method. Examples: @@ -186,13 +199,7 @@ class EvMore(object): """ self._caller = caller - self._kwargs = kwargs - self._pages = [] - self._npages = 1 - self._npos = 0 - self.exit_on_lastpage = exit_on_lastpage - self.exit_cmd = exit_cmd - self._exit_msg = "Exited |wmore|n pager." + self._always_page = always_page if not session: # if not supplied, use the first session to @@ -203,81 +210,141 @@ class EvMore(object): session = sessions[0] self._session = session + self._justify = justify + self._justify_kwargs = justify_kwargs + self.exit_on_lastpage = exit_on_lastpage + self.exit_cmd = exit_cmd + self._exit_msg = "Exited |wmore|n pager." + self._page_formatter = page_formatter + self._kwargs = kwargs + + self._data = None + self._paginator = None + self._pages = [] + self._npages = 1 + self._npos = 0 + # set up individual pages for different sessions height = max(4, session.protocol_flags.get("SCREENHEIGHT", {0: _SCREEN_HEIGHT})[0] - 4) - width = session.protocol_flags.get("SCREENWIDTH", {0: _SCREEN_WIDTH})[0] + self.width = session.protocol_flags.get("SCREENWIDTH", {0: _SCREEN_WIDTH})[0] + # always limit number of chars to 10 000 per page + self.height = min(10000 // max(1, self.width), height) - if hasattr(text, "table") and hasattr(text, "get"): - # This is an EvTable. - - table = text - - if table.height: - # enforced height of each paged table, plus space for evmore extras - height = table.height - 4 - - # convert table to string - text = str(text) - justify_kwargs = None # enforce - - if not isinstance(text, str): - # not a string - pre-set pages of some form - text = "\n".join(str(repr(element)) for element in make_iter(text)) - - if "\f" in text: - # we use \f to indicate the user wants to enforce their line breaks - # on their own. If so, we do no automatic line-breaking/justification - # at all. - self._pages = text.split("\f") - self._npages = len(self._pages) + if inherits_from(text, "evennia.utils.evtable.EvTable"): + # an EvTable + self.init_evtable(text) + elif isinstance(text, QuerySet): + # a queryset + self.init_queryset(text) + elif not isinstance(text, str): + # anything else not a str + self.init_iterable(text) + elif "\f" in text: + # string with \f line-break markers in it + self.init_f_str(text) else: - if justify: - # we must break very long lines into multiple ones. Note that this - # will also remove spurious whitespace. - justify_kwargs = justify_kwargs or {} - width = justify_kwargs.get("width", width) - justify_kwargs["width"] = width - justify_kwargs["align"] = justify_kwargs.get("align", "l") - justify_kwargs["indent"] = justify_kwargs.get("indent", 0) + # a string + self.init_str(text) - lines = [] - for line in text.split("\n"): - if len(line) > width: - lines.extend(justify(line, **justify_kwargs).split("\n")) - else: - lines.append(line) - else: - # no justification. Simple division by line - lines = text.split("\n") + # kick things into gear + self.start() - # always limit number of chars to 10 000 per page - height = min(10000 // max(1, width), height) + # page formatter - # figure out the pagination - self._pages = ["\n".join(lines[i : i + height]) for i in range(0, len(lines), height)] - self._npages = len(self._pages) + def format_page(self, page): + """ + Page formatter. Uses the page_formatter callable by default. + This allows to easier override the class if needed. + """ + return self._page_formatter(page) - if self._npages <= 1 and not always_page: - # no need for paging; just pass-through. - caller.msg(text=self._get_page(0), session=self._session, **kwargs) + # paginators - responsible for extracting a specific page number + + def paginator_index(self, pageno): + """Paginate to specific, known index""" + return self._data[pageno] + + def paginator_slice(self, pageno): + """ + Paginate by slice. This is done with an eye on memory efficiency (usually for + querysets); to avoid fetching all objects at the same time. + """ + return self._data[pageno * self.height: pageno * self.height + self.height] + + # inits for different input types + + def init_evtable(self, table): + """The input is an EvTable.""" + if table.height: + # enforced height of each paged table, plus space for evmore extras + self.height = table.height - 4 + + # convert table to string + text = str(table) + self._justify = False + self._justify_kwargs = None # enforce + self.init_str(text) + + def init_queryset(self, qs): + """The input is a queryset""" + nsize = qs.count() # we assume each will be a line + self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1) + self._data = qs + self._paginator = self.paginator_slice + + def init_iterable(self, inp): + """The input is something other than a string - convert to iterable of strings""" + inp = make_iter(inp) + nsize = len(inp) + self._npages = nsize // self.height + (0 if nsize % self.height == 0 else 1) + self._data = inp + self._paginator_slice + + def init_f_str(self, text): + """ + The input contains \f markers. We use \f to indicate the user wants to + enforce their line breaks on their own. If so, we do no automatic + line-breaking/justification at all. + """ + self._data = text.split("\f") + self._npages = len(self._data) + self._paginator = self.paginator_index + + def init_str(self, text): + """The input is a string""" + + if self._justify: + # we must break very long lines into multiple ones. Note that this + # will also remove spurious whitespace. + justify_kwargs = self._justify_kwargs or {} + width = self._justify_kwargs.get("width", self.width) + justify_kwargs["width"] = width + justify_kwargs["align"] = self._justify_kwargs.get("align", "l") + justify_kwargs["indent"] = self._justify_kwargs.get("indent", 0) + + lines = [] + for line in text.split("\n"): + if len(line) > width: + lines.extend(justify(line, **justify_kwargs).split("\n")) + else: + lines.append(line) else: - # go into paging mode - # first pass on the msg kwargs - caller.ndb._more = self - caller.cmdset.add(CmdSetMore) + # no justification. Simple division by line + lines = text.split("\n") - # goto top of the text - self.page_top() + self._data = ["\n".join(lines[i: i + self.height]) + for i in range(0, len(lines), self.height)] + self._npages = len(self._data) + self._paginator = self.paginator_index - def _get_page(self, pos): - return self._pages[pos] + # display helpers and navigation def display(self, show_footer=True): """ Pretty-print the page. """ pos = self._npos - text = self._get_page(pos) + text = self.format_page(self._paginator(pos)) if show_footer: page = _DISPLAY.format(text=text, pageno=pos + 1, pagemax=self._npages) else: @@ -340,6 +407,22 @@ class EvMore(object): if self.exit_cmd: self._caller.execute_cmd(self.exit_cmd, session=self._session) + def start(self): + """ + Starts the pagination + """ + if self._npages <= 1 and not self._always_page: + # no need for paging; just pass-through. + self.display(show_footer=False) + else: + # go into paging mode + # first pass on the msg kwargs + self._caller.ndb._more = self + self._caller.cmdset.add(CmdSetMore) + + # goto top of the text + self.page_top() + # helper function From 10a6685fdf56c25c3e175df6753506af12f40d11 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Mar 2020 15:30:28 +0200 Subject: [PATCH 009/361] Fix shift bug in ANSIString.__mult__. Resolves #2030 --- evennia/utils/ansi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/evennia/utils/ansi.py b/evennia/utils/ansi.py index 21d1abd180..7129cc8627 100644 --- a/evennia/utils/ansi.py +++ b/evennia/utils/ansi.py @@ -821,6 +821,8 @@ class ANSIString(str, metaclass=ANSIMeta): by a number. """ + if not offset: + return [] return [i + offset for i in iterable] @classmethod @@ -1063,7 +1065,7 @@ class ANSIString(str, metaclass=ANSIMeta): clean_string = self._clean_string * other code_indexes = self._code_indexes[:] char_indexes = self._char_indexes[:] - for i in range(1, other + 1): + for i in range(other): code_indexes.extend(self._shifter(self._code_indexes, i * len(self._raw_string))) char_indexes.extend(self._shifter(self._char_indexes, i * len(self._raw_string))) return ANSIString( From 2726c25e1d41996288134d57d68188ce0a38e329 Mon Sep 17 00:00:00 2001 From: trhr Date: Mon, 6 Apr 2020 01:00:06 -0500 Subject: [PATCH 010/361] Typo --- evennia/typeclasses/attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 2cb7ac8843..6fb4870dae 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -59,7 +59,7 @@ class Attribute(SharedMemoryModel): # Attribute Database Model setup # # These database fields are all set using their corresponding properties, - # named same as the field, but withtout the db_* prefix. + # named same as the field, but without the db_* prefix. db_key = models.CharField("key", max_length=255, db_index=True) db_value = PickledObjectField( "value", From f4d6ae4de33e7ddfe36d1d71212fe64a98e76491 Mon Sep 17 00:00:00 2001 From: trhr Date: Mon, 6 Apr 2020 01:18:38 -0500 Subject: [PATCH 011/361] Typo --- evennia/accounts/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index f3217e88ea..11a0aa41d6 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -4,7 +4,7 @@ Typeclass for Account objects Note that this object is primarily intended to store OOC information, not game info! This object represents the actual user (not their -character) and has NO actual precence in the +character) and has NO actual presence in the game world (this is handled by the associated character object, so you should customize that instead for most things). From d8d2ba416817b331ca7df80460a5ca3a9efab21f Mon Sep 17 00:00:00 2001 From: David Estrada Date: Thu, 9 Apr 2020 13:47:17 -0700 Subject: [PATCH 012/361] Manager.py is returning a couple of querysets instead of lists. Fix for bug #2088 --- evennia/objects/manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index 1217d0a91d..f66c1fe253 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -178,11 +178,11 @@ class ObjectDBManager(TypedObjectManager): # This doesn't work if attribute_value is an object. Workaround below if isinstance(attribute_value, (str, int, float, bool)): - return self.filter( + return list(self.filter( cand_restriction & type_restriction & Q(db_attributes__db_key=attribute_name, db_attributes__db_value=attribute_value) - ).order_by("id") + ).order_by("id")) else: # We must loop for safety since the referenced lookup gives deepcopy error if attribute value is an object. global _ATTR @@ -278,7 +278,7 @@ class ObjectDBManager(TypedObjectManager): exclude_restriction = ( Q(pk__in=[_GA(obj, "id") for obj in make_iter(excludeobj)]) if excludeobj else Q() ) - return self.filter(db_location=location).exclude(exclude_restriction).order_by("id") + return list(self.filter(db_location=location).exclude(exclude_restriction).order_by("id")) def get_objs_with_key_or_alias(self, ostring, exact=True, candidates=None, typeclasses=None): """ @@ -309,7 +309,7 @@ class ObjectDBManager(TypedObjectManager): type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q() if exact: # exact match - do direct search - return ( + return list( ( self.filter( cand_restriction From 61c3bdc7cfcd9fcffbfc358bc5c7dac9f15de9ec Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 11 Apr 2020 23:31:31 +0200 Subject: [PATCH 013/361] Make Player #1's Character have Player perm to make quell work more intuitively. --- evennia/server/initial_setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/server/initial_setup.py b/evennia/server/initial_setup.py index e0845c0859..67e5318973 100644 --- a/evennia/server/initial_setup.py +++ b/evennia/server/initial_setup.py @@ -95,7 +95,8 @@ def create_objects(): god_character.locks.add( "examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()" ) - god_character.permissions.add("Developer") + # we set this low so that quelling is more useful + god_character.permissions.add("Player") god_account.attributes.add("_first_login", True) god_account.attributes.add("_last_puppet", god_character) From 10305003bec49b13a41689c76dd9dd9157198c2d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 12 Apr 2020 12:17:06 +0200 Subject: [PATCH 014/361] Ran black on sources --- Makefile | 2 +- evennia/commands/default/building.py | 70 ++++++++++++++++------------ evennia/commands/default/tests.py | 4 +- evennia/prototypes/menus.py | 19 +++++--- evennia/prototypes/prototypes.py | 12 ++--- evennia/prototypes/spawner.py | 12 +++-- evennia/prototypes/tests.py | 10 ++-- evennia/utils/evmore.py | 7 +-- 8 files changed, 80 insertions(+), 56 deletions(-) diff --git a/Makefile b/Makefile index 23a578c27e..84c7ad3ab1 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # This is used with `make