From ddd56cdeb312768197676fddfbbaa474b11ebccb Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 4 Mar 2018 11:39:55 +0100 Subject: [PATCH] First version of expanded spawn command with storage --- evennia/commands/default/building.py | 175 ++++++++++++---- evennia/commands/default/muxcommand.py | 2 +- evennia/utils/evmore.py | 30 ++- evennia/utils/olc/olc_storage.py | 269 ------------------------- evennia/utils/spawner.py | 33 ++- 5 files changed, 181 insertions(+), 328 deletions(-) delete mode 100644 evennia/utils/olc/olc_storage.py diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 9bc7915497..dfa78ea074 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -13,7 +13,7 @@ from evennia.utils import create, utils, search from evennia.utils.utils import inherits_from, class_from_module from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore -from evennia.utils.spawner import spawn, search_prototype, list_prototypes +from evennia.utils.spawner import spawn, search_prototype, list_prototypes, store_prototype from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2735,11 +2735,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): @spawn[/noloc] @spawn[/noloc] - @spawn/search [query] + @spawn/search [key][;tag[,tag]] @spawn/list [tag, tag] @spawn/show [] - @spawn/save [;desc[;tag,tag,..[;lockstring]]] + @spawn/save [;desc[;tag,tag[,...][;lockstring]]] = @spawn/menu Switches: @@ -2786,73 +2786,164 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): def func(self): """Implements the spawner""" - def _show_prototypes(prototypes): - """Helper to show a list of available prototypes""" - prots = ", ".join(sorted(prototypes.keys())) - return "\nAvailable prototypes (case sensitive): %s" % ( - "\n" + utils.fill(prots) if prots else "None") + def _parse_prototype(inp, allow_key=False): + try: + # make use of _convert_from_string from the SetAttribute command + prototype = _convert_from_string(self, inp) + except SyntaxError: + # this means literal_eval tried to parse a faulty string + string = ("|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") + self.caller.msg(string) + return None + if isinstance(prototype, 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 don't have access to " + "use the 'exec' prototype key.") + return None + elif isinstance(prototype, basestring): + # a prototype key + if allow_key: + return prototype + else: + self.caller.msg("The prototype must be defined as a Python dictionary.") + else: + caller.msg("The prototype must be given either as a Python dictionary or a key") + return None + + + def _search_show_prototype(query): + # prototype detail + strings = [] + metaprots = search_prototype(key=query, return_meta=True) + if metaprots: + for metaprot in metaprots: + header = ( + "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" + "|cdesc:|n {} \n|cprototype:|n ".format( + metaprot.key, ", ".join(metaprot.tags), + metaprot.locks, metaprot.desc)) + prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value) + for key, value in + sorted(metaprot.prototype.items())).rstrip(","))) + strings.append(header + prototype) + return "\n".join(strings) + else: + return False + caller = self.caller + if 'search' in self.switches: + # query for a key match + 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, unicode(list_prototypes(caller, key=key, tags=tags)), + exit_on_lastpage=True) + return if 'show' in self.switches: # the argument is a key in this case (may be a partial key) if not self.args: self.switches.append('list') else: - EvMore(caller, unicode(list_prototypes(key=self.args), exit_on_lastpage=True)) + matchstring = _search_show_prototype(self.args) + if matchstring: + caller.msg(matchstring) + else: + caller.msg("No prototype '{}' was found.".format(self.args)) return if 'list' in self.switches: # for list, all optional arguments are tags - EvMore(caller, unicode(list_prototypes(tags=self.lhslist)), exit_on_lastpage=True) + EvMore(caller, unicode(list_prototypes(caller, + tags=self.lhslist)), exit_on_lastpage=True) + return + + if 'save' in self.switches: + if not self.args or not self.rhs: + caller.msg("Usage: @spawn/save [;desc[;tag,tag[,...][;lockstring]]] = ") + return + + # handle lhs + parts = self.rhs.split(";", 3) + key, desc, tags, lockstring = "", "", [], "" + nparts = len(parts) + if nparts == 1: + key = parts.strip() + elif nparts == 2: + key, desc = (part.strip() for part in parts) + elif nparts == 3: + key, desc, tags = (part.strip() for part in parts) + tags = [tag.strip().lower() for tag in tags.split(",")] + else: + # lockstrings can itself contain ; + key, desc, tags, lockstring = (part.strip() for part in parts) + tags = [tag.strip().lower() for tag in tags.split(",")] + + # handle rhs: + prototype = _parse_prototype(caller, self.rhs) + if not prototype: + return + + # check for existing prototype + matchstring = _search_show_prototype(key) + if matchstring: + caller.msg("|yExisting saved prototype found:|n\n{}".format(matchstring)) + answer = ("Do you want to replace the existing prototype? Y/[N]") + if not answer.lower() not in ["y", "yes"]: + caller.msg("Save cancelled.") + + # all seems ok. Try to save. + try: + store_prototype(caller, key, prototype, desc=desc, tags=tags, locks=lockstring) + except PermissionError as err: + caller.msg("|rError saving:|R {}|n".format(err)) + return + caller.msg("Saved prototype:") + caller.execute_cmd("spawn/show {}".format(key)) return if not self.args: ncount = len(search_prototype()) - caller.msg("Usage: @spawn or {key: value, ...}" + caller.msg("Usage: @spawn or {{key: value, ...}}" "\n ({} existing prototypes. Use /list to inspect)".format(ncount)) return + # A direct creation of an object from a given prototype - - prototypes = spawn(return_prototypes=True) - if not self.args: - string = "Usage: @spawn {key:value, key, value, ... }" - self.caller.msg(string + _show_prototypes(prototypes)) - return - try: - # make use of _convert_from_string from the SetAttribute command - prototype = _convert_from_string(self, self.args) - except SyntaxError: - # this means literal_eval tried to parse a faulty string - string = "|RCritical Python syntax error in argument. " - string += "Only primitive Python structures are allowed. " - string += "\nYou also need to use correct Python syntax. " - string += "Remember especially to put quotes around all " - string += "strings inside lists and dicts.|n" - self.caller.msg(string) + prototype = _parse_prototype(self.args, allow_key=True) + if not prototype: return if isinstance(prototype, basestring): - # A prototype key - keystr = prototype - prototype = prototypes.get(prototype, None) - if not prototype: - string = "No prototype named '%s'." % keystr - self.caller.msg(string + _show_prototypes(prototypes)) + # A prototype key we are looking to apply + metaprotos = search_prototype(prototype) + nprots = len(metaprotos) + if not metaprotos: + caller.msg("No prototype named '%s'." % prototype) return - elif isinstance(prototype, dict): - # we got the prototype on the command line. We must make sure to not allow - # the 'exec' key unless we are developers or higher. - if "exec" in prototype and not self.caller.check_permstring("Developer"): - self.caller.msg("Spawn aborted: You don't have access to use the 'exec' prototype key.") + elif nprots > 1: + caller.msg("Found {} prototypes matching '{}':\n {}".format( + nprots, prototype, ", ".join(metaproto.key for metaproto in metaprotos))) return - else: - self.caller.msg("The prototype must be a prototype key or a Python dictionary.") - return + # we have a metaprot, check access + metaproto = metaprotos[0] + if not caller.locks.check_lockstring(caller, metaproto.locks, access_type='use'): + caller.msg("You don't have access to use this prototype.") + return + prototype = metaproto.prototype if "noloc" not in self.switches and "location" not in prototype: prototype["location"] = self.caller.location + # proceed to spawning for obj in spawn(prototype): self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) diff --git a/evennia/commands/default/muxcommand.py b/evennia/commands/default/muxcommand.py index 5d8d4b2890..b3a0d066d5 100644 --- a/evennia/commands/default/muxcommand.py +++ b/evennia/commands/default/muxcommand.py @@ -113,7 +113,7 @@ class MuxCommand(Command): # check for arg1, arg2, ... = argA, argB, ... constructs lhs, rhs = args, None - lhslist, rhslist = [arg.strip() for arg in args.split(',')], [] + lhslist, rhslist = [arg.strip() for arg in args.split(',') if arg], [] if args and '=' in args: lhs, rhs = [arg.strip() for arg in args.split('=', 1)] lhslist = [arg.strip() for arg in lhs.split(',')] diff --git a/evennia/utils/evmore.py b/evennia/utils/evmore.py index 169091396b..e0ec091005 100644 --- a/evennia/utils/evmore.py +++ b/evennia/utils/evmore.py @@ -202,15 +202,18 @@ class EvMore(object): # goto top of the text self.page_top() - def display(self): + def display(self, show_footer=True): """ Pretty-print the page. """ pos = self._pos text = self._pages[pos] - page = _DISPLAY.format(text=text, - pageno=pos + 1, - pagemax=self._npages) + if show_footer: + page = _DISPLAY.format(text=text, + pageno=pos + 1, + pagemax=self._npages) + else: + page = text # check to make sure our session is still valid sessions = self._caller.sessions.get() if not sessions: @@ -245,9 +248,11 @@ class EvMore(object): self.page_quit() else: self._pos += 1 - self.display() - if self.exit_on_lastpage and self._pos >= self._npages - 1: - self.page_quit() + if self.exit_on_lastpage and self._pos >= (self._npages - 1): + self.display(show_footer=False) + self.page_quit(quiet=True) + else: + self.display() def page_back(self): """ @@ -256,16 +261,18 @@ class EvMore(object): self._pos = max(0, self._pos - 1) self.display() - def page_quit(self): + def page_quit(self, quiet=False): """ Quit the pager """ del self._caller.ndb._more - self._caller.msg(text=self._exit_msg, **self._kwargs) + if not quiet: + self._caller.msg(text=self._exit_msg, **self._kwargs) self._caller.cmdset.remove(CmdSetMore) -def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, **kwargs): +def msg(caller, text="", always_page=False, session=None, + justify_kwargs=None, exit_on_lastpage=True, **kwargs): """ More-supported version of msg, mimicking the normal msg method. @@ -280,9 +287,10 @@ def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, * justify_kwargs (dict, bool or None, optional): If given, this should be valid keyword arguments to the utils.justify() function. If False, no justification will be done. + exit_on_lastpage (bool, optional): Immediately exit pager when reaching the last page. kwargs (any, optional): These will be passed on to the `caller.msg` method. """ EvMore(caller, text, always_page=always_page, session=session, - justify_kwargs=justify_kwargs, **kwargs) + justify_kwargs=justify_kwargs, exit_on_lastpage=exit_on_lastpage, **kwargs) diff --git a/evennia/utils/olc/olc_storage.py b/evennia/utils/olc/olc_storage.py deleted file mode 100644 index cda1e6d0eb..0000000000 --- a/evennia/utils/olc/olc_storage.py +++ /dev/null @@ -1,269 +0,0 @@ -""" -OLC storage and sharing mechanism. - -This sets up a central storage for prototypes. The idea is to make these -available in a repository for buildiers to use. Each prototype is stored -in a Script so that it can be tagged for quick sorting/finding and locked for limiting -access. - -This system also takes into consideration prototypes defined and stored in modules. -Such prototypes are considered 'read-only' to the system and can only be modified -in code. To replace a default prototype, add the same-name prototype in a -custom module read later in the settings.PROTOTYPE_MODULES list. To remove a default -prototype, override its name with an empty dict. - -""" - -from collections import namedtuple -from django.conf import settings -from evennia.scripts.scripts import DefaultScript -from evennia.utils.create import create_script -from evennia.utils.utils import make_iter, all_from_module -from evennia.utils.evtable import EvTable - -# prepare the available prototypes defined in modules - -_READONLY_PROTOTYPES = {} -_READONLY_PROTOTYPE_MODULES = {} - -# storage of meta info about the prototype -MetaProto = namedtuple('MetaProto', ['key', 'desc', 'locks', 'tags', 'prototype']) - -for mod in settings.PROTOTYPE_MODULES: - # to remove a default prototype, override it with an empty dict. - # internally we store as (key, desc, locks, tags, prototype_dict) - prots = [(key, prot) for key, prot in all_from_module(mod).items() - if prot and isinstance(prot, dict)] - _READONLY_PROTOTYPES.update( - {key.lower(): MetaProto( - key.lower(), - prot['prototype_desc'] if 'prototype_desc' in prot else mod, - prot['prototype_lock'] if 'prototype_lock' in prot else "use:all()", - set(make_iter( - prot['prototype_tags']) if 'prototype_tags' in prot else ["base-prototype"]), - prot) - for key, prot in prots}) - _READONLY_PROTOTYPE_MODULES.update({tup[0]: mod for tup in prots}) - - -class PersistentPrototype(DefaultScript): - """ - This stores a single prototype - """ - def at_script_creation(self): - self.key = "empty prototype" - self.desc = "A prototype" - - -def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete=False): - """ - Store a prototype persistently. - - Args: - caller (Account or Object): Caller aiming to store prototype. At this point - the caller should have permission to 'add' new prototypes, but to edit - an existing prototype, the 'edit' lock must be passed on that prototype. - key (str): Name of prototype to store. - prototype (dict): Prototype dict. - desc (str, optional): Description of prototype, to use in listing. - tags (list, optional): Tag-strings to apply to prototype. These are always - applied with the 'persistent_prototype' category. - locks (str, optional): Locks to apply to this prototype. Used locks - are 'use' and 'edit' - delete (bool, optional): Delete an existing prototype identified by 'key'. - This requires `caller` to pass the 'edit' lock of the prototype. - Returns: - stored (StoredPrototype or None): The resulting prototype (new or edited), - or None if deleting. - Raises: - PermissionError: If edit lock was not passed by caller. - - - """ - key_orig = key - key = key.lower() - locks = locks if locks else "use:all();edit:id({}) or edit:perm(Admin)".format(caller.id) - tags = [(tag, "persistent_prototype") for tag in make_iter(tags)] - - if key in _READONLY_PROTOTYPES: - mod = _READONLY_PROTOTYPE_MODULES.get(key, "N/A") - raise PermissionError("{} is a read-only prototype " - "(defined as code in {}).".format(key_orig, mod)) - - stored_prototype = PersistentPrototype.objects.filter(db_key=key) - - if stored_prototype: - stored_prototype = stored_prototype[0] - if not stored_prototype.access(caller, 'edit'): - raise PermissionError("{} does not have permission to " - "edit prototype {}".format(caller, key)) - - if delete: - stored_prototype.delete() - return - - if desc: - stored_prototype.desc = desc - if tags: - stored_prototype.tags.batch_add(*tags) - if locks: - stored_prototype.locks.add(locks) - if prototype: - stored_prototype.attributes.add("prototype", prototype) - else: - stored_prototype = create_script( - PersistentPrototype, key=key, desc=desc, persistent=True, - locks=locks, tags=tags, attributes=[("prototype", prototype)]) - return stored_prototype - - -def search_persistent_prototype(key=None, tags=None): - """ - Find persistent (database-stored) prototypes based on key and/or tags. - - Kwargs: - key (str): An exact or partial key to query for. - tags (str or list): Tag key or keys to query for. These - will always be applied with the 'persistent_protototype' - tag category. - Return: - matches (queryset): All found PersistentPrototypes - - Note: - This will not include read-only prototypes defined in modules. - - """ - if tags: - # exact match on tag(s) - tags = make_iter(tags) - tag_categories = ["persistent_prototype" for _ in tags] - matches = PersistentPrototype.objects.get_by_tag(tags, tag_categories) - else: - matches = PersistentPrototype.objects.all() - if key: - # partial match on key - matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key) - return matches - - -def search_readonly_prototype(key=None, tags=None): - """ - Find read-only prototypes, defined in modules. - - Kwargs: - key (str): An exact or partial key to query for. - tags (str or list): Tag key to query for. - - Return: - matches (list): List of MetaProto tuples that includes - prototype metadata, - - """ - matches = {} - if tags: - # use tags to limit selection - tagset = set(tags) - matches = {key: metaproto for key, metaproto in _READONLY_PROTOTYPES.items() - if tagset.intersection(metaproto.tags)} - else: - matches = _READONLY_PROTOTYPES - - if key: - if key in matches: - # exact match - return [matches[key]] - else: - # fuzzy matching - return [metaproto for pkey, metaproto in matches.items() if key in pkey] - else: - return [match for match in matches.values()] - - -def search_prototype(key=None, tags=None): - """ - Find prototypes based on key and/or tags. - - Kwargs: - key (str): An exact or partial key to query for. - tags (str or list): Tag key or keys to query for. These - will always be applied with the 'persistent_protototype' - tag category. - Return: - matches (list): All found prototype dicts. - - Note: - The available prototypes is a combination of those supplied in - PROTOTYPE_MODULES and those stored from in-game. For the latter, - this will use the tags to make a subselection before attempting - to match on the key. So if key/tags don't match up nothing will - be found. - - """ - matches = [] - if key and key in _READONLY_PROTOTYPES: - matches.append(_READONLY_PROTOTYPES[key][3]) - else: - matches.extend([prot.attributes.get("prototype") - for prot in search_persistent_prototype(key, tags)]) - return matches - - -def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non_edit=True): - """ - Collate a list of found prototypes based on search criteria and access. - - Args: - caller (Account or Object): The object requesting the list. - key (str, optional): Exact or partial key to query for. - tags (str or list, optional): Tag key or keys to query for. - show_non_use (bool, optional): Show also prototypes the caller may not use. - show_non_edit (bool, optional): Show also prototypes the caller may not edit. - Returns: - table (EvTable or None): An EvTable representation of the prototypes. None - if no prototypes were found. - - """ - # handle read-only prototypes separately - readonly_prototypes = search_readonly_prototype(key, tags) - - # get use-permissions of readonly attributes (edit is always False) - readonly_prototypes = [ - (metaproto.key, - metaproto.desc, - ("{}/N".format('Y' - if caller.locks.check_lockstring(caller, metaproto.locks, access_type='use') else 'N')), - ",".join(metaproto.tags)) - for metaproto in sorted(readonly_prototypes, key=lambda o: o.key)] - - # next, handle db-stored prototypes - prototypes = search_persistent_prototype(key, tags) - - # gather access permissions as (key, desc, tags, can_use, can_edit) - prototypes = [(prototype.key, prototype.desc, - "{}/{}".format('Y' if prototype.access(caller, "use") else 'N', - 'Y' if prototype.access(caller, "edit") else 'N'), - ",".join(prototype.tags.get(category="persistent_prototype"))) - for prototype in sorted(prototypes, key=lambda o: o.key)] - - prototypes = prototypes + readonly_prototypes - - if not prototypes: - return None - - if not show_non_use: - prototypes = [metaproto for metaproto in prototypes if metaproto[2].split("/", 1)[0] == 'Y'] - if not show_non_edit: - prototypes = [metaproto for metaproto in prototypes if metaproto[2].split("/", 1)[1] == 'Y'] - - if not prototypes: - return None - - table = [] - for i in range(len(prototypes[0])): - table.append([str(metaproto[i]) for metaproto in prototypes]) - table = EvTable("Key", "Desc", "Use/Edit", "Tags", table=table, crop=True, width=78) - table.reformat_column(0, width=28) - table.reformat_column(1, width=40) - table.reformat_column(2, width=11, align='r') - table.reformat_column(3, width=20) - return table diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index d1d4bea920..3b2bec932c 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -483,7 +483,7 @@ def search_readonly_prototype(key=None, tags=None): return [match for match in matches.values()] -def search_prototype(key=None, tags=None): +def search_prototype(key=None, tags=None, return_meta=True): """ Find prototypes based on key and/or tags. @@ -492,8 +492,11 @@ def search_prototype(key=None, tags=None): tags (str or list): Tag key or keys to query for. These will always be applied with the 'persistent_protototype' tag category. + return_meta (bool): If False, only return prototype dicts, if True + return MetaProto namedtuples including prototype meta info + Return: - matches (list): All found prototype dicts. + matches (list): All found prototype dicts or MetaProtos Note: The available prototypes is a combination of those supplied in @@ -505,10 +508,30 @@ def search_prototype(key=None, tags=None): """ matches = [] if key and key in _READONLY_PROTOTYPES: - matches.append(_READONLY_PROTOTYPES[key][3]) + if return_meta: + matches.append(_READONLY_PROTOTYPES[key]) + else: + matches.append(_READONLY_PROTOTYPES[key][3]) + elif tags: + if return_meta: + matches.extend( + [MetaProto(prot.key, prot.desc, prot.locks.all(), + prot.tags.all(), prot.attributes.get("prototype")) + for prot in search_persistent_prototype(key, tags)]) + else: + matches.extend([prot.attributes.get("prototype") + for prot in search_persistent_prototype(key, tags)]) else: - matches.extend([prot.attributes.get("prototype") - for prot in search_persistent_prototype(key, tags)]) + # neither key nor tags given. Return all. + if return_meta: + matches = [MetaProto(prot.key, prot.desc, prot.locks.all(), + prot.tags.all(), prot.attributes.get("prototype")) + for prot in search_persistent_prototype(key, tags)] + \ + list(_READONLY_PROTOTYPES.values()) + else: + matches = [prot.attributes.get("prototype") + for prot in search_persistent_prototype()] + \ + [metaprot[3] for metaprot in _READONLY_PROTOTYPES.values()] return matches