From 43185d8f17ba65cf17b07e70d6ccd88122bd2eeb Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 28 Feb 2018 19:17:26 +0100 Subject: [PATCH 001/103] OLC systen. Create olc_storage mechanism --- evennia/accounts/accounts.py | 21 +++++ evennia/objects/objects.py | 6 +- evennia/scripts/scripts.py | 16 ++++ evennia/typeclasses/attributes.py | 4 +- evennia/utils/create.py | 16 +++- evennia/utils/olc/__init__.py | 0 evennia/utils/olc/olc_storage.py | 151 ++++++++++++++++++++++++++++++ 7 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 evennia/utils/olc/__init__.py create mode 100644 evennia/utils/olc/olc_storage.py diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index b86b6eb16e..602ccc2a69 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -631,10 +631,31 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): # this will only be set if the utils.create_account # function was used to create the object. cdict = self._createdict + updates = [] + if not cdict.get("key"): + if not self.db_key: + self.db_key = "#%i" % self.dbid + updates.append("db_key") + elif self.key != cdict.get("key"): + updates.append("db_key") + self.db_key = cdict["key"] + if updates: + self.save(update_fields=updates) + if cdict.get("locks"): self.locks.add(cdict["locks"]) if cdict.get("permissions"): permissions = cdict["permissions"] + if cdict.get("tags"): + # this should be a list of tags, tuples (key, category) or (key, category, data) + self.tags.batch_add(*cdict["tags"]) + if cdict.get("attributes"): + # this should be tuples (key, val, ...) + self.attributes.batch_add(*cdict["attributes"]) + if cdict.get("nattributes"): + # this should be a dict of nattrname:value + for key, value in cdict["nattributes"]: + self.nattributes.add(key, value) del self._createdict self.permissions.batch_add(*permissions) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 037f3ff019..6cb948da03 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1002,14 +1002,14 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): cdict["location"].at_object_receive(self, None) self.at_after_move(None) if cdict.get("tags"): - # this should be a list of tags + # this should be a list of tags, tuples (key, category) or (key, category, data) self.tags.batch_add(*cdict["tags"]) if cdict.get("attributes"): - # this should be a dict of attrname:value + # this should be tuples (key, val, ...) self.attributes.batch_add(*cdict["attributes"]) if cdict.get("nattributes"): # this should be a dict of nattrname:value - for key, value in cdict["nattributes"].items(): + for key, value in cdict["nattributes"]: self.nattributes.add(key, value) del self._createdict diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index db6e9652cb..24e25592fa 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -513,6 +513,22 @@ class DefaultScript(ScriptBase): updates.append("db_persistent") if updates: self.save(update_fields=updates) + + if cdict.get("permissions"): + self.permissions.batch_add(*cdict["permissions"]) + if cdict.get("locks"): + self.locks.add(cdict["locks"]) + if cdict.get("tags"): + # this should be a list of tags, tuples (key, category) or (key, category, data) + self.tags.batch_add(*cdict["tags"]) + if cdict.get("attributes"): + # this should be tuples (key, val, ...) + self.attributes.batch_add(*cdict["attributes"]) + if cdict.get("nattributes"): + # this should be a dict of nattrname:value + for key, value in cdict["nattributes"]: + self.nattributes.add(key, value) + if not cdict.get("autostart"): # don't auto-start the script return diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 3f8b4cd742..03ef255093 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -530,8 +530,8 @@ class AttributeHandler(object): repeat-calling add when having many Attributes to add. Args: - indata (tuple): Tuples of varying length representing the - Attribute to add to this object. + indata (list): List of tuples of varying length representing the + Attribute to add to this object. Supported tuples are - `(key, value)` - `(key, value, category)` - `(key, value, category, lockstring)` diff --git a/evennia/utils/create.py b/evennia/utils/create.py index 1404e4caaa..c5fb6f8416 100644 --- a/evennia/utils/create.py +++ b/evennia/utils/create.py @@ -54,7 +54,8 @@ _GA = object.__getattribute__ def create_object(typeclass=None, key=None, location=None, home=None, permissions=None, locks=None, aliases=None, tags=None, - destination=None, report_to=None, nohome=False): + destination=None, report_to=None, nohome=False, attributes=None, + nattributes=None): """ Create a new in-game object. @@ -68,13 +69,18 @@ def create_object(typeclass=None, key=None, location=None, home=None, permissions (list): A list of permission strings or tuples (permstring, category). locks (str): one or more lockstrings, separated by semicolons. aliases (list): A list of alternative keys or tuples (aliasstring, category). - tags (list): List of tag keys or tuples (tagkey, category). + tags (list): List of tag keys or tuples (tagkey, category) or (tagkey, category, data). destination (Object or str): Obj or #dbref to use as an Exit's target. report_to (Object): The object to return error messages to. nohome (bool): This allows the creation of objects without a default home location; only used when creating the default location itself or during unittests. + attributes (list): Tuples on the form (key, value) or (key, value, category), + (key, value, lockstring) or (key, value, lockstring, default_access). + to set as Attributes on the new object. + nattributes (list): Non-persistent tuples on the form (key, value). Note that + adding this rarely makes sense since this data will not survive a reload. Returns: object (Object): A newly created object of the given typeclass. @@ -122,7 +128,8 @@ def create_object(typeclass=None, key=None, location=None, home=None, # store the call signature for the signal new_object._createdict = dict(key=key, location=location, destination=destination, home=home, typeclass=typeclass.path, permissions=permissions, locks=locks, - aliases=aliases, tags=tags, report_to=report_to, nohome=nohome) + aliases=aliases, tags=tags, report_to=report_to, nohome=nohome, + attributes=attributes, nattributes=nattributes) # this will trigger the save signal which in turn calls the # at_first_save hook on the typeclass, where the _createdict can be # used. @@ -139,7 +146,8 @@ object = create_object def create_script(typeclass=None, key=None, obj=None, account=None, locks=None, interval=None, start_delay=None, repeats=None, - persistent=None, autostart=True, report_to=None, desc=None): + persistent=None, autostart=True, report_to=None, desc=None, + tags=None, attributes=None): """ Create a new script. All scripts are a combination of a database object that communicates with the database, and an typeclass that diff --git a/evennia/utils/olc/__init__.py b/evennia/utils/olc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/evennia/utils/olc/olc_storage.py b/evennia/utils/olc/olc_storage.py new file mode 100644 index 0000000000..d64956f8a0 --- /dev/null +++ b/evennia/utils/olc/olc_storage.py @@ -0,0 +1,151 @@ +""" +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. + +""" + +from evennia.scripts.scripts import DefaultScript +from evennia.utils.create import create_script +from evennia.utils.utils import make_iter +from evennia.utils.evtable import EvTable + + +class PersistentPrototype(DefaultScript): + """ + This stores a single prototype + """ + key = "persistent_prototype" + desc = "Stores a prototoype" + + +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 = 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)] + + stored_prototype = PersistentPrototype.objects.filter(db_key=key) + + if stored_prototype: + stored_prototype = stored_prototype[0] + if not stored_prototype.access(caller, 'edit'): + 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.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_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 (queryset): All found PersistentPrototypes. This will + be all prototypes if no arguments are given. + + Note: + 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 = PersistentPrototype.objects.all() + if tags: + # exact match on tag(s) + tags = make_iter(tags) + tag_categories = ("persistent_prototype" for _ in tags) + matches = matches.get_by_tag(tags, tag_categories) + if key: + # partial match on key + matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key) + 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. + + """ + prototypes = search_prototype(key, tags) + + if not prototypes: + return None + + # gather access permissions as (key, desc, can_use, can_edit) + prototypes = [(prototype.key, prototype.desc, + prototype.access(caller, "use"), prototype.access(caller, "edit")) + for prototype in prototypes] + + if not show_non_use: + prototypes = [tup for tup in prototypes if tup[2]] + if not show_non_edit: + prototypes = [tup for tup in prototypes if tup[3]] + + if not prototypes: + return None + + table = [] + for i in range(len(prototypes[0])): + table.append([tup[i] for tup in prototypes]) + table = EvTable("Key", "Desc", "Use", "Edit", table, crop=True, width=78) + table.reformat_column(0, width=28) + table.reformat_column(1, width=40) + table.reformat_column(2, width=5) + table.reformat_column(3, width=5) + return table From 4e488ff2a2375fc84199b593b85ecadf07e0c7d5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 2 Mar 2018 23:16:15 +0100 Subject: [PATCH 002/103] Correct bugs in script_creation, fix unittest for olc_storage --- evennia/scripts/scripts.py | 171 ++++++++++++++++--------------- evennia/utils/create.py | 14 ++- evennia/utils/olc/olc_storage.py | 18 ++-- 3 files changed, 110 insertions(+), 93 deletions(-) diff --git a/evennia/scripts/scripts.py b/evennia/scripts/scripts.py index 24e25592fa..67367df3bb 100644 --- a/evennia/scripts/scripts.py +++ b/evennia/scripts/scripts.py @@ -141,15 +141,6 @@ class ScriptBase(with_metaclass(TypeclassBase, ScriptDB)): """ objects = ScriptManager() - -class DefaultScript(ScriptBase): - """ - This is the base TypeClass for all Scripts. Scripts describe - events, timers and states in game, they can have a time component - or describe a state that changes under certain conditions. - - """ - def __eq__(self, other): """ Compares two Scripts. Compares dbids. @@ -239,7 +230,96 @@ class DefaultScript(ScriptBase): logger.log_trace() return None - # Public methods + def at_script_creation(self): + """ + Should be overridden in child. + + """ + pass + + def at_first_save(self, **kwargs): + """ + This is called after very first time this object is saved. + Generally, you don't need to overload this, but only the hooks + called by this method. + + Args: + **kwargs (dict): Arbitrary, optional arguments for users + overriding the call (unused by default). + + """ + self.at_script_creation() + + if hasattr(self, "_createdict"): + # this will only be set if the utils.create_script + # function was used to create the object. We want + # the create call's kwargs to override the values + # set by hooks. + cdict = self._createdict + updates = [] + if not cdict.get("key"): + if not self.db_key: + self.db_key = "#%i" % self.dbid + updates.append("db_key") + elif self.db_key != cdict["key"]: + self.db_key = cdict["key"] + updates.append("db_key") + if cdict.get("interval") and self.interval != cdict["interval"]: + self.db_interval = cdict["interval"] + updates.append("db_interval") + if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]: + self.db_start_delay = cdict["start_delay"] + updates.append("db_start_delay") + if cdict.get("repeats") and self.repeats != cdict["repeats"]: + self.db_repeats = cdict["repeats"] + updates.append("db_repeats") + if cdict.get("persistent") and self.persistent != cdict["persistent"]: + self.db_persistent = cdict["persistent"] + updates.append("db_persistent") + if cdict.get("desc") and self.desc != cdict["desc"]: + self.db_desc = cdict["desc"] + updates.append("db_desc") + if updates: + self.save(update_fields=updates) + + if cdict.get("permissions"): + self.permissions.batch_add(*cdict["permissions"]) + if cdict.get("locks"): + self.locks.add(cdict["locks"]) + if cdict.get("tags"): + # this should be a list of tags, tuples (key, category) or (key, category, data) + self.tags.batch_add(*cdict["tags"]) + if cdict.get("attributes"): + # this should be tuples (key, val, ...) + self.attributes.batch_add(*cdict["attributes"]) + if cdict.get("nattributes"): + # this should be a dict of nattrname:value + for key, value in cdict["nattributes"]: + self.nattributes.add(key, value) + + if not cdict.get("autostart"): + # don't auto-start the script + return + + # auto-start script (default) + self.start() + + +class DefaultScript(ScriptBase): + """ + This is the base TypeClass for all Scripts. Scripts describe + events, timers and states in game, they can have a time component + or describe a state that changes under certain conditions. + + """ + + def at_script_creation(self): + """ + Only called once, when script is first created. + + """ + pass + def time_until_next_repeat(self): """ @@ -472,77 +552,6 @@ class DefaultScript(ScriptBase): if task: task.force_repeat() - def at_first_save(self, **kwargs): - """ - This is called after very first time this object is saved. - Generally, you don't need to overload this, but only the hooks - called by this method. - - Args: - **kwargs (dict): Arbitrary, optional arguments for users - overriding the call (unused by default). - - """ - self.at_script_creation() - - if hasattr(self, "_createdict"): - # this will only be set if the utils.create_script - # function was used to create the object. We want - # the create call's kwargs to override the values - # set by hooks. - cdict = self._createdict - updates = [] - if not cdict.get("key"): - if not self.db_key: - self.db_key = "#%i" % self.dbid - updates.append("db_key") - elif self.db_key != cdict["key"]: - self.db_key = cdict["key"] - updates.append("db_key") - if cdict.get("interval") and self.interval != cdict["interval"]: - self.db_interval = cdict["interval"] - updates.append("db_interval") - if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]: - self.db_start_delay = cdict["start_delay"] - updates.append("db_start_delay") - if cdict.get("repeats") and self.repeats != cdict["repeats"]: - self.db_repeats = cdict["repeats"] - updates.append("db_repeats") - if cdict.get("persistent") and self.persistent != cdict["persistent"]: - self.db_persistent = cdict["persistent"] - updates.append("db_persistent") - if updates: - self.save(update_fields=updates) - - if cdict.get("permissions"): - self.permissions.batch_add(*cdict["permissions"]) - if cdict.get("locks"): - self.locks.add(cdict["locks"]) - if cdict.get("tags"): - # this should be a list of tags, tuples (key, category) or (key, category, data) - self.tags.batch_add(*cdict["tags"]) - if cdict.get("attributes"): - # this should be tuples (key, val, ...) - self.attributes.batch_add(*cdict["attributes"]) - if cdict.get("nattributes"): - # this should be a dict of nattrname:value - for key, value in cdict["nattributes"]: - self.nattributes.add(key, value) - - if not cdict.get("autostart"): - # don't auto-start the script - return - - # auto-start script (default) - self.start() - - def at_script_creation(self): - """ - Only called once, by the create function. - - """ - pass - def is_valid(self): """ Is called to check if the script is valid to run at this time. diff --git a/evennia/utils/create.py b/evennia/utils/create.py index c5fb6f8416..36db7e5a60 100644 --- a/evennia/utils/create.py +++ b/evennia/utils/create.py @@ -101,6 +101,7 @@ def create_object(typeclass=None, key=None, location=None, home=None, locks = make_iter(locks) if locks is not None else None aliases = make_iter(aliases) if aliases is not None else None tags = make_iter(tags) if tags is not None else None + attributes = make_iter(attributes) if attributes is not None else None if isinstance(typeclass, basestring): @@ -177,7 +178,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None, created or if the `start` method must be called explicitly. report_to (Object): The object to return error messages to. desc (str): Optional description of script - + tags (list): List of tags or tuples (tag, category). + attributes (list): List if tuples (key, value) or (key, value, category) + (key, value, lockstring) or (key, value, lockstring, default_access). See evennia.scripts.manager for methods to manipulate existing scripts in the database. @@ -198,9 +201,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None, if key: kwarg["db_key"] = key if account: - kwarg["db_account"] = dbid_to_obj(account, _ScriptDB) + kwarg["db_account"] = dbid_to_obj(account, _AccountDB) if obj: - kwarg["db_obj"] = dbid_to_obj(obj, _ScriptDB) + kwarg["db_obj"] = dbid_to_obj(obj, _ObjectDB) if interval: kwarg["db_interval"] = interval if start_delay: @@ -211,6 +214,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None, kwarg["db_persistent"] = persistent if desc: kwarg["db_desc"] = desc + tags = make_iter(tags) if tags is not None else None + attributes = make_iter(attributes) if attributes is not None else None # create new instance new_script = typeclass(**kwarg) @@ -218,7 +223,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None, # store the call signature for the signal new_script._createdict = dict(key=key, obj=obj, account=account, locks=locks, interval=interval, start_delay=start_delay, repeats=repeats, persistent=persistent, - autostart=autostart, report_to=report_to) + autostart=autostart, report_to=report_to, desc=desc, + tags=tags, attributes=attributes) # this will trigger the save signal which in turn calls the # at_first_save hook on the typeclass, where the _createdict # can be used. diff --git a/evennia/utils/olc/olc_storage.py b/evennia/utils/olc/olc_storage.py index d64956f8a0..f96a79edb2 100644 --- a/evennia/utils/olc/olc_storage.py +++ b/evennia/utils/olc/olc_storage.py @@ -18,8 +18,9 @@ class PersistentPrototype(DefaultScript): """ This stores a single prototype """ - key = "persistent_prototype" - desc = "Stores a prototoype" + 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): @@ -64,7 +65,7 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete if desc: stored_prototype.desc = desc if tags: - stored_prototype.tags.add(tags) + stored_prototype.tags.batch_add(*tags) if locks: stored_prototype.locks.add(locks) if prototype: @@ -95,12 +96,13 @@ def search_prototype(key=None, tags=None): be found. """ - matches = PersistentPrototype.objects.all() if tags: # exact match on tag(s) tags = make_iter(tags) - tag_categories = ("persistent_prototype" for _ in tags) - matches = matches.get_by_tag(tags, tag_categories) + 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) @@ -142,8 +144,8 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non table = [] for i in range(len(prototypes[0])): - table.append([tup[i] for tup in prototypes]) - table = EvTable("Key", "Desc", "Use", "Edit", table, crop=True, width=78) + table.append([str(tup[i]) for tup in prototypes]) + table = EvTable("Key", "Desc", "Use", "Edit", table=table, crop=True, width=78) table.reformat_column(0, width=28) table.reformat_column(1, width=40) table.reformat_column(2, width=5) From 7ea6a58f196bc3cd5679c2bfa3cb3e1c8267b0eb Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 3 Mar 2018 14:57:55 +0100 Subject: [PATCH 003/103] Refactor, include readonly prototypes --- evennia/utils/olc/olc_storage.py | 148 +++++++++++++++++++++++++++---- 1 file changed, 131 insertions(+), 17 deletions(-) diff --git a/evennia/utils/olc/olc_storage.py b/evennia/utils/olc/olc_storage.py index f96a79edb2..a10c0d508b 100644 --- a/evennia/utils/olc/olc_storage.py +++ b/evennia/utils/olc/olc_storage.py @@ -6,13 +6,41 @@ 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 django.conf import settings from evennia.scripts.scripts import DefaultScript from evennia.utils.create import create_script -from evennia.utils.utils import make_iter +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 = {} + +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(): + (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): """ @@ -46,17 +74,25 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete 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'): - PermissionError("{} does not have permission to edit prototype {}".format(caller, key)) + raise PermissionError("{} does not have permission to " + "edit prototype {}".format(caller, key)) if delete: stored_prototype.delete() @@ -77,9 +113,9 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete return stored_prototype -def search_prototype(key=None, tags=None): +def search_persistent_prototype(key=None, tags=None): """ - Find prototypes based on key and/or tags. + Find persistent (database-stored) prototypes based on key and/or tags. Kwargs: key (str): An exact or partial key to query for. @@ -87,13 +123,10 @@ def search_prototype(key=None, tags=None): will always be applied with the 'persistent_protototype' tag category. Return: - matches (queryset): All found PersistentPrototypes. This will - be all prototypes if no arguments are given. + matches (queryset): All found PersistentPrototypes Note: - 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. + This will not include read-only prototypes defined in modules. """ if tags: @@ -109,6 +142,68 @@ def search_prototype(key=None, tags=None): 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 prototype tuples that includes + prototype metadata, on the form + `(key, desc, lockstring, taglist, prototypedict)` + + """ + matches = [] + if tags: + # use tags to limit selection + tagset = set(tags) + matches = {key: tup for key, tup in _READONLY_PROTOTYPES.items() + if tagset.intersection(tup[3])} + else: + matches = _READONLY_PROTOTYPES + + if key: + if key in matches: + # exact match + return matches[key] + else: + # fuzzy matching + return [tup for pkey, tup in matches.items() if key in pkey] + return matches + + +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. @@ -124,20 +219,39 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non if no prototypes were found. """ - prototypes = search_prototype(key, tags) + # handle read-only prototypes separately + if key and key in _READONLY_PROTOTYPES: + readonly_prototypes = _READONLY_PROTOTYPES[key] + else: + readonly_prototypes = _READONLY_PROTOTYPES.values() + + # get use-permissions of readonly attributes (edit is always False) + readonly_prototypes = [ + (tup[0], + tup[1], + ("{}/N".format('Y' + if caller.locks.check_lockstring(caller, tup[2], access_type='use') else 'N')), + ",".join(tup[3])) for tup in readonly_prototypes] + + # next, handle db-stored prototypes + prototypes = search_persistent_prototype(key, tags) if not prototypes: return None - # gather access permissions as (key, desc, can_use, can_edit) + # gather access permissions as (key, desc, tags, can_use, can_edit) prototypes = [(prototype.key, prototype.desc, - prototype.access(caller, "use"), prototype.access(caller, "edit")) + "{}/{}".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 prototypes] + prototypes = prototypes + readonly_prototypes + if not show_non_use: - prototypes = [tup for tup in prototypes if tup[2]] + prototypes = [tup for tup in sorted(prototypes, key=lambda o: o[0]) if tup[2]] if not show_non_edit: - prototypes = [tup for tup in prototypes if tup[3]] + prototypes = [tup for tup in sorted(prototypes, key=lambda o: o[0]) if tup[3]] if not prototypes: return None @@ -145,9 +259,9 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non table = [] for i in range(len(prototypes[0])): table.append([str(tup[i]) for tup in prototypes]) - table = EvTable("Key", "Desc", "Use", "Edit", table=table, crop=True, width=78) + 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=5) - table.reformat_column(3, width=5) + table.reformat_column(2, width=11, align='r') + table.reformat_column(3, width=20) return table From f269e80fdca8fcf398b3d35a09ca2ce84a00b708 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 3 Mar 2018 15:19:18 +0100 Subject: [PATCH 004/103] Use namedtuples for internal meta info --- evennia/utils/olc/olc_storage.py | 37 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/evennia/utils/olc/olc_storage.py b/evennia/utils/olc/olc_storage.py index a10c0d508b..7c4adad5c4 100644 --- a/evennia/utils/olc/olc_storage.py +++ b/evennia/utils/olc/olc_storage.py @@ -14,6 +14,7 @@ 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 @@ -25,19 +26,22 @@ from evennia.utils.evtable import EvTable _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(): - (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) + {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}) @@ -151,17 +155,16 @@ def search_readonly_prototype(key=None, tags=None): tags (str or list): Tag key to query for. Return: - matches (list): List of prototype tuples that includes - prototype metadata, on the form - `(key, desc, lockstring, taglist, prototypedict)` + matches (list): List of MetaProto tuples that includes + prototype metadata, """ matches = [] if tags: # use tags to limit selection tagset = set(tags) - matches = {key: tup for key, tup in _READONLY_PROTOTYPES.items() - if tagset.intersection(tup[3])} + matches = {key: metaproto for key, metaproto in _READONLY_PROTOTYPES.items() + if tagset.intersection(metaproto.tags)} else: matches = _READONLY_PROTOTYPES @@ -171,7 +174,7 @@ def search_readonly_prototype(key=None, tags=None): return matches[key] else: # fuzzy matching - return [tup for pkey, tup in matches.items() if key in pkey] + return [metaproto for pkey, metaproto in matches.items() if key in pkey] return matches @@ -227,11 +230,11 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non # get use-permissions of readonly attributes (edit is always False) readonly_prototypes = [ - (tup[0], - tup[1], + (tup.key, + tup.desc, ("{}/N".format('Y' - if caller.locks.check_lockstring(caller, tup[2], access_type='use') else 'N')), - ",".join(tup[3])) for tup in readonly_prototypes] + if caller.locks.check_lockstring(caller, tup.locks, access_type='use') else 'N')), + ",".join(tup.tags)) for tup in readonly_prototypes] # next, handle db-stored prototypes prototypes = search_persistent_prototype(key, tags) From 98888d636a0630d31e88ed447b766e945a7fab39 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 3 Mar 2018 16:24:17 +0100 Subject: [PATCH 005/103] Use readonly-search for prototypes --- evennia/utils/olc/olc_storage.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/evennia/utils/olc/olc_storage.py b/evennia/utils/olc/olc_storage.py index 7c4adad5c4..113d0296cb 100644 --- a/evennia/utils/olc/olc_storage.py +++ b/evennia/utils/olc/olc_storage.py @@ -159,7 +159,7 @@ def search_readonly_prototype(key=None, tags=None): prototype metadata, """ - matches = [] + matches = {} if tags: # use tags to limit selection tagset = set(tags) @@ -171,11 +171,12 @@ def search_readonly_prototype(key=None, tags=None): if key: if key in matches: # exact match - return matches[key] + return [matches[key]] else: # fuzzy matching return [metaproto for pkey, metaproto in matches.items() if key in pkey] - return matches + else: + return [match for match in matches.values()] def search_prototype(key=None, tags=None): @@ -223,10 +224,7 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non """ # handle read-only prototypes separately - if key and key in _READONLY_PROTOTYPES: - readonly_prototypes = _READONLY_PROTOTYPES[key] - else: - readonly_prototypes = _READONLY_PROTOTYPES.values() + readonly_prototypes = search_readonly_prototype(key, tags) # get use-permissions of readonly attributes (edit is always False) readonly_prototypes = [ @@ -239,9 +237,6 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non # next, handle db-stored prototypes prototypes = search_persistent_prototype(key, tags) - if not prototypes: - return None - # 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', @@ -251,6 +246,9 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non prototypes = prototypes + readonly_prototypes + if not prototypes: + return None + if not show_non_use: prototypes = [tup for tup in sorted(prototypes, key=lambda o: o[0]) if tup[2]] if not show_non_edit: From 2b8b0b1b699976b026bf9d3ddcadce824a76e905 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 3 Mar 2018 18:29:37 +0100 Subject: [PATCH 006/103] Start expanding spawn command for prot-storage --- evennia/commands/default/building.py | 33 +++- evennia/utils/olc/olc_storage.py | 17 +- evennia/utils/spawner.py | 276 ++++++++++++++++++++++++++- 3 files changed, 310 insertions(+), 16 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 853ca6b88f..6cbef476d4 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -12,7 +12,7 @@ from evennia.commands.cmdhandler import get_and_merge_cmdsets 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.spawner import spawn +from evennia.utils.spawner import spawn, search_prototype, list_prototypes from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2731,17 +2731,29 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): spawn objects from prototype Usage: - @spawn - @spawn[/switch] - @spawn[/switch] {prototype dictionary} + @spawn[/noloc] + @spawn[/noloc] - Switch: + @spawn/search [query] + @spawn/list [tag, tag] + @spawn/show + + @spawn/save [;desc[;tag,tag,..[;lockstring]]] + @spawn/menu + + Switches: noloc - allow location to be None if not specified explicitly. Otherwise, location will default to caller's current location. + search - search prototype by name or tags. + list - list available prototypes, optionally limit by tags. + show - inspect prototype by key. + save - save a prototype to the database. It will be listable by /list. + menu - manipulate prototype in a menu interface. Example: @spawn GOBLIN @spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"} + @spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all() Dictionary keys: |wprototype |n - name of parent prototype to use. Can be a list for @@ -2760,12 +2772,16 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): The available prototypes are defined globally in modules set in settings.PROTOTYPE_MODULES. If @spawn is used without arguments it displays a list of available prototypes. + """ key = "@spawn" locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" + def parser(self): + super(CmdSpawn, self).parser() + def func(self): """Implements the spawner""" @@ -2774,6 +2790,13 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): prots = ", ".join(sorted(prototypes.keys())) return "\nAvailable prototypes (case sensitive): %s" % ( "\n" + utils.fill(prots) if prots else "None") + caller = self.caller + + if not self.args: + ncount = len(search_prototype()) + caller.msg("Usage: @spawn or {key: value, ...}" + "\n ({} existing prototypes. Use /list to inspect)".format(ncount)) + return prototypes = spawn(return_prototypes=True) if not self.args: diff --git a/evennia/utils/olc/olc_storage.py b/evennia/utils/olc/olc_storage.py index 113d0296cb..cda1e6d0eb 100644 --- a/evennia/utils/olc/olc_storage.py +++ b/evennia/utils/olc/olc_storage.py @@ -228,11 +228,12 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non # get use-permissions of readonly attributes (edit is always False) readonly_prototypes = [ - (tup.key, - tup.desc, + (metaproto.key, + metaproto.desc, ("{}/N".format('Y' - if caller.locks.check_lockstring(caller, tup.locks, access_type='use') else 'N')), - ",".join(tup.tags)) for tup in readonly_prototypes] + 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) @@ -242,7 +243,7 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non "{}/{}".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 prototypes] + for prototype in sorted(prototypes, key=lambda o: o.key)] prototypes = prototypes + readonly_prototypes @@ -250,16 +251,16 @@ def get_prototype_list(caller, key=None, tags=None, show_non_use=False, show_non return None if not show_non_use: - prototypes = [tup for tup in sorted(prototypes, key=lambda o: o[0]) if tup[2]] + prototypes = [metaproto for metaproto in prototypes if metaproto[2].split("/", 1)[0] == 'Y'] if not show_non_edit: - prototypes = [tup for tup in sorted(prototypes, key=lambda o: o[0]) if tup[3]] + 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(tup[i]) for tup in prototypes]) + 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) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 86b24e5e4f..d1d4bea920 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -86,6 +86,21 @@ The *goblin archwizard* will have some different attacks, but will otherwise have the same spells as a *goblin wizard* who in turn shares many traits with a normal *goblin*. + +Storage 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 __future__ import print_function @@ -96,7 +111,35 @@ import evennia from evennia.objects.models import ObjectDB from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj +from collections import namedtuple +from evennia.scripts.scripts import DefaultScript +from evennia.utils.create import create_script +from evennia.utils.evtable import EvTable + + _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") +_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}) def _handle_dbref(inp): @@ -119,7 +162,8 @@ def _validate_prototype(key, prototype, protparents, visited): raise RuntimeError("%s tries to prototype itself." % key or prototype) protparent = protparents.get(protstring) if not protparent: - raise RuntimeError("%s's prototype '%s' was not found." % (key or prototype, protstring)) + raise RuntimeError( + "%s's prototype '%s' was not found." % (key or prototype, protstring)) _validate_prototype(protstring, protparent, protparents, visited) @@ -303,9 +347,235 @@ def spawn(*prototypes, **kwargs): return _batch_create_object(*objsparams) -if __name__ == "__main__": - # testing +# Prototype storage mechanisms + +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 list_prototypes(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 + + +# Testing + +if __name__ == "__main__": protparents = { "NOBODY": {}, # "INFINITE" : { From e95387a7a98051beec2ae74995981a832f32946a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 3 Mar 2018 19:55:37 +0100 Subject: [PATCH 007/103] Continue working with new spawn additions --- evennia/commands/default/building.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 6cbef476d4..9bc7915497 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -12,6 +12,7 @@ from evennia.commands.cmdhandler import get_and_merge_cmdsets 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.ansi import raw @@ -2736,7 +2737,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): @spawn/search [query] @spawn/list [tag, tag] - @spawn/show + @spawn/show [] @spawn/save [;desc[;tag,tag,..[;lockstring]]] @spawn/menu @@ -2746,7 +2747,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): location will default to caller's current location. search - search prototype by name or tags. list - list available prototypes, optionally limit by tags. - show - inspect prototype by key. + show - inspect prototype by key. If not given, acts like list. save - save a prototype to the database. It will be listable by /list. menu - manipulate prototype in a menu interface. @@ -2792,12 +2793,28 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): "\n" + utils.fill(prots) if prots else "None") caller = self.caller + + 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)) + 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) + return + if not self.args: ncount = len(search_prototype()) caller.msg("Usage: @spawn or {key: value, ...}" "\n ({} existing prototypes. Use /list to inspect)".format(ncount)) return + + prototypes = spawn(return_prototypes=True) if not self.args: string = "Usage: @spawn {key:value, key, value, ... }" From ddd56cdeb312768197676fddfbbaa474b11ebccb Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 4 Mar 2018 11:39:55 +0100 Subject: [PATCH 008/103] 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 From 0c9b2239f906d12452bd689896110538eb97c682 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 4 Mar 2018 16:25:18 +0100 Subject: [PATCH 009/103] Improve parse of spawn arguments --- evennia/commands/default/building.py | 126 +++++++++++++++------------ evennia/utils/spawner.py | 49 +++++------ 2 files changed, 89 insertions(+), 86 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index dfa78ea074..d493e850b6 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -13,7 +13,8 @@ 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, store_prototype +from evennia.utils.spawner import (spawn, search_prototype, list_prototypes, + store_prototype, build_metaproto) from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -27,12 +28,8 @@ __all__ = ("ObjManipCommand", "CmdSetObjAlias", "CmdCopy", "CmdLock", "CmdExamine", "CmdFind", "CmdTeleport", "CmdScript", "CmdTag", "CmdSpawn") -try: - # used by @set - from ast import literal_eval as _LITERAL_EVAL -except ImportError: - # literal_eval is not available before Python 2.6 - _LITERAL_EVAL = None +# used by @set +from ast import literal_eval as _LITERAL_EVAL # used by @find CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS @@ -1450,17 +1447,16 @@ def _convert_from_string(cmd, strobj): # if nothing matches, return as-is return obj - if _LITERAL_EVAL: - # Use literal_eval to parse python structure exactly. - try: - return _LITERAL_EVAL(strobj) - except (SyntaxError, ValueError): - # treat as string - strobj = utils.to_str(strobj) - string = "|RNote: name \"|r%s|R\" was converted to a string. " \ - "Make sure this is acceptable." % strobj - cmd.caller.msg(string) - return strobj + # Use literal_eval to parse python structure exactly. + try: + return _LITERAL_EVAL(strobj) + except (SyntaxError, ValueError): + # treat as string + strobj = utils.to_str(strobj) + string = "|RNote: name \"|r%s|R\" was converted to a string. " \ + "Make sure this is acceptable." % strobj + cmd.caller.msg(string) + return strobj else: # fall back to old recursive solution (does not support # nested lists/dicts) @@ -2786,46 +2782,44 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): def func(self): """Implements the spawner""" - def _parse_prototype(inp, allow_key=False): + def _parse_prototype(inp, expect=dict): + err = None 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): + 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".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 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 + return prototype - - def _search_show_prototype(query): + def _search_show_prototype(query, metaprots=None): # prototype detail strings = [] - metaprots = search_prototype(key=query, return_meta=True) + if not metaprots: + 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)) + "; ".join(metaprot.locks), metaprot.desc)) prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value) for key, value in sorted(metaprot.prototype.items())).rstrip(","))) @@ -2869,11 +2863,12 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if 'save' in self.switches: if not self.args or not self.rhs: - caller.msg("Usage: @spawn/save [;desc[;tag,tag[,...][;lockstring]]] = ") + caller.msg( + "Usage: @spawn/save [;desc[;tag,tag[,...][;lockstring]]] = ") return # handle lhs - parts = self.rhs.split(";", 3) + parts = self.lhs.split(";", 3) key, desc, tags, lockstring = "", "", [], "" nparts = len(parts) if nparts == 1: @@ -2889,17 +2884,26 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): tags = [tag.strip().lower() for tag in tags.split(",")] # handle rhs: - prototype = _parse_prototype(caller, self.rhs) + prototype = _parse_prototype(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.") + # present prototype to save + new_matchstring = _search_show_prototype( + "", metaprots=[build_metaproto(key, desc, [lockstring], tags, prototype)]) + string = "|yCreating new prototype:|n\n{}".format(new_matchstring) + question = "\nDo you want to continue saving? [Y]/N" + + # check for existing prototype, + old_matchstring = _search_show_prototype(key) + 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" + + answer = yield(string + question) + if answer.lower() in ["n", "no"]: + caller.msg("|rSave cancelled.|n") + return # all seems ok. Try to save. try: @@ -2907,8 +2911,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): except PermissionError as err: caller.msg("|rError saving:|R {}|n".format(err)) return - caller.msg("Saved prototype:") - caller.execute_cmd("spawn/show {}".format(key)) + caller.msg("|gSaved prototype:|n {}".format(key)) return if not self.args: @@ -2919,12 +2922,16 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # A direct creation of an object from a given prototype - prototype = _parse_prototype(self.args, allow_key=True) + prototype = _parse_prototype( + self.args, expect=dict if self.args.strip().startswith("{") else basestring) if not prototype: + # this will only let through dicts or strings return + key = '' if isinstance(prototype, basestring): # A prototype key we are looking to apply + key = prototype metaprotos = search_prototype(prototype) nprots = len(metaprotos) if not metaprotos: @@ -2945,5 +2952,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): prototype["location"] = self.caller.location # proceed to spawning - for obj in spawn(prototype): - self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) + try: + for obj in spawn(prototype): + self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) + except RuntimeError as err: + caller.msg(err) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 3b2bec932c..1b0bbb63a3 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -359,6 +359,14 @@ class PersistentPrototype(DefaultScript): self.desc = "A prototype" +def build_metaproto(key, desc, locks, tags, prototype): + """ + Create a metaproto from combinant parts. + + """ + return MetaProto(key, desc, locks, tags, dict(prototype)) + + def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete=False): """ Store a prototype persistently. @@ -386,7 +394,7 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete """ key_orig = key key = key.lower() - locks = locks if locks else "use:all();edit:id({}) or edit:perm(Admin)".format(caller.id) + locks = locks if locks else "use:all();edit:id({}) or perm(Admin)".format(caller.id) tags = [(tag, "persistent_prototype") for tag in make_iter(tags)] if key in _READONLY_PROTOTYPES: @@ -506,34 +514,19 @@ def search_prototype(key=None, tags=None, return_meta=True): be found. """ - matches = [] - if key and key in _READONLY_PROTOTYPES: - 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: - # 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 + readonly_prototypes = search_readonly_prototype(key, tags) + persistent_prototypes = search_persistent_prototype(key, tags) + if return_meta: + persistent_prototypes = [ + build_metaproto(prot.key, prot.desc, prot.locks.all(), + prot.tags.all(), prot.attributes.get("prototype")) + for prot in persistent_prototypes] + else: + readonly_prototypes = [metaprot.prototyp for metaprot in readonly_prototypes] + persistent_prototypes = [prot.attributes.get("prototype") for prot in persistent_prototypes] + + return persistent_prototypes + readonly_prototypes def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True): """ From ed355e6096ffc3076ec9a9a792c5cba3d2d00e08 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 10 Mar 2018 08:40:28 +0100 Subject: [PATCH 010/103] Test refactoring of spawner (untested) --- evennia/utils/spawner.py | 423 ++++++++++++++++++++------------------- 1 file changed, 217 insertions(+), 206 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 1b0bbb63a3..00cfb25627 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -141,212 +141,6 @@ for mod in settings.PROTOTYPE_MODULES: for key, prot in prots}) _READONLY_PROTOTYPE_MODULES.update({tup[0]: mod for tup in prots}) - -def _handle_dbref(inp): - return dbid_to_obj(inp, ObjectDB) - - -def _validate_prototype(key, prototype, protparents, visited): - """ - Run validation on a prototype, checking for inifinite regress. - - """ - assert isinstance(prototype, dict) - if id(prototype) in visited: - raise RuntimeError("%s has infinite nesting of prototypes." % key or prototype) - visited.append(id(prototype)) - protstrings = prototype.get("prototype") - if protstrings: - for protstring in make_iter(protstrings): - if key is not None and protstring == key: - raise RuntimeError("%s tries to prototype itself." % key or prototype) - protparent = protparents.get(protstring) - if not protparent: - raise RuntimeError( - "%s's prototype '%s' was not found." % (key or prototype, protstring)) - _validate_prototype(protstring, protparent, protparents, visited) - - -def _get_prototype(dic, prot, protparents): - """ - Recursively traverse a prototype dictionary, including multiple - inheritance. Use _validate_prototype before this, we don't check - for infinite recursion here. - - """ - if "prototype" in dic: - # move backwards through the inheritance - for prototype in make_iter(dic["prototype"]): - # Build the prot dictionary in reverse order, overloading - new_prot = _get_prototype(protparents.get(prototype, {}), prot, protparents) - prot.update(new_prot) - prot.update(dic) - prot.pop("prototype", None) # we don't need this anymore - return prot - - -def _batch_create_object(*objparams): - """ - This is a cut-down version of the create_object() function, - optimized for speed. It does NOT check and convert various input - so make sure the spawned Typeclass works before using this! - - Args: - objsparams (tuple): Parameters for the respective creation/add - handlers in the following order: - - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`. - - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`. - - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`. - - `aliases` (list): A list of alias strings for - adding with `new_object.aliases.batch_add(*aliases)`. - - `nattributes` (list): list of tuples `(key, value)` to be loop-added to - add with `new_obj.nattributes.add(*tuple)`. - - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for - adding with `new_obj.attributes.batch_add(*attributes)`. - - `tags` (list): list of tuples `(key, category)` for adding - with `new_obj.tags.batch_add(*tags)`. - - `execs` (list): Code strings to execute together with the creation - of each object. They will be executed with `evennia` and `obj` - (the newly created object) available in the namespace. Execution - will happend after all other properties have been assigned and - is intended for calling custom handlers etc. - for the respective creation/add handlers in the following - order: (create_kwargs, permissions, locks, aliases, nattributes, - attributes, tags, execs) - - Returns: - objects (list): A list of created objects - - Notes: - The `exec` list will execute arbitrary python code so don't allow this to be available to - unprivileged users! - - """ - - # bulk create all objects in one go - - # unfortunately this doesn't work since bulk_create doesn't creates pks; - # the result would be duplicate objects at the next stage, so we comment - # it out for now: - # dbobjs = _ObjectDB.objects.bulk_create(dbobjs) - - dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] - objs = [] - for iobj, obj in enumerate(dbobjs): - # call all setup hooks on each object - objparam = objparams[iobj] - # setup - obj._createdict = {"permissions": make_iter(objparam[1]), - "locks": objparam[2], - "aliases": make_iter(objparam[3]), - "nattributes": objparam[4], - "attributes": objparam[5], - "tags": make_iter(objparam[6])} - # this triggers all hooks - obj.save() - # run eventual extra code - for code in objparam[7]: - if code: - exec(code, {}, {"evennia": evennia, "obj": obj}) - objs.append(obj) - return objs - - -def spawn(*prototypes, **kwargs): - """ - Spawn a number of prototyped objects. - - Args: - prototypes (dict): Each argument should be a prototype - dictionary. - 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 - the global protparents dictionary accessible by the input - prototypes. If not given, it will instead look for modules - defined by settings.PROTOTYPE_MODULES. - prototype_parents (dict): A dictionary holding a custom - prototype-parent dictionary. Will overload same-named - prototypes from prototype_modules. - return_prototypes (bool): Only return a list of the - prototype-parents (no object creation happens) - - """ - - protparents = {} - protmodules = make_iter(kwargs.get("prototype_modules", [])) - if not protmodules and hasattr(settings, "PROTOTYPE_MODULES"): - protmodules = make_iter(settings.PROTOTYPE_MODULES) - for prototype_module in protmodules: - protparents.update(dict((key, val) for key, val in - all_from_module(prototype_module).items() if isinstance(val, dict))) - # overload module's protparents with specifically given protparents - protparents.update(kwargs.get("prototype_parents", {})) - for key, prototype in protparents.items(): - _validate_prototype(key, prototype, protparents, []) - - if "return_prototypes" in kwargs: - # only return the parents - return copy.deepcopy(protparents) - - objsparams = [] - for prototype in prototypes: - - _validate_prototype(None, prototype, protparents, []) - prot = _get_prototype(prototype, {}, protparents) - if not prot: - continue - - # extract the keyword args we need to create the object itself. If we get a callable, - # call that to get the value (don't catch errors) - create_kwargs = {} - keyval = prot.pop("key", "Spawned Object %06i" % randint(1, 100000)) - create_kwargs["db_key"] = keyval() if callable(keyval) else keyval - - locval = prot.pop("location", None) - create_kwargs["db_location"] = locval() if callable(locval) else _handle_dbref(locval) - - homval = prot.pop("home", settings.DEFAULT_HOME) - create_kwargs["db_home"] = homval() if callable(homval) else _handle_dbref(homval) - - destval = prot.pop("destination", None) - create_kwargs["db_destination"] = destval() if callable(destval) else _handle_dbref(destval) - - typval = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) - create_kwargs["db_typeclass_path"] = typval() if callable(typval) else typval - - # extract calls to handlers - permval = prot.pop("permissions", []) - permission_string = permval() if callable(permval) else permval - lockval = prot.pop("locks", "") - lock_string = lockval() if callable(lockval) else lockval - aliasval = prot.pop("aliases", "") - alias_string = aliasval() if callable(aliasval) else aliasval - tagval = prot.pop("tags", []) - tags = tagval() if callable(tagval) else tagval - attrval = prot.pop("attrs", []) - attributes = attrval() if callable(tagval) else attrval - - exval = prot.pop("exec", "") - execs = make_iter(exval() if callable(exval) else exval) - - # extract ndb assignments - nattributes = dict((key.split("_", 1)[1], value() if callable(value) else value) - for key, value in prot.items() if key.startswith("ndb_")) - - # the rest are attributes - simple_attributes = [(key, value()) if callable(value) else (key, value) - for key, value in prot.items() if not key.startswith("ndb_")] - attributes = attributes + simple_attributes - attributes = [tup for tup in attributes if not tup[0] in _CREATE_OBJECT_KWARGS] - - # pack for call into _batch_create_object - objsparams.append((create_kwargs, permission_string, lock_string, - alias_string, nattributes, attributes, tags, execs)) - - return _batch_create_object(*objsparams) - - # Prototype storage mechanisms @@ -528,6 +322,20 @@ def search_prototype(key=None, tags=None, return_meta=True): return persistent_prototypes + readonly_prototypes + +def get_protparents(): + """ + Get prototype parents. These are a combination of meta-key and prototype-dict and are used when + a prototype refers to another parent-prototype. + + """ + # get all prototypes + metaprotos = search_prototype(return_meta=True) + # organize by key + return {metaproto.key: metaproto.prototype for metaproto in metaprotos} + + + def list_prototypes(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. @@ -588,6 +396,209 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed table.reformat_column(3, width=20) return table +# Spawner mechanism + + +def _handle_dbref(inp): + return dbid_to_obj(inp, ObjectDB) + + +def _validate_prototype(key, prototype, protparents, visited): + """ + Run validation on a prototype, checking for inifinite regress. + + """ + print("validate_prototype {}, {}, {}, {}".format(key, prototype, protparents, visited)) + assert isinstance(prototype, dict) + if id(prototype) in visited: + raise RuntimeError("%s has infinite nesting of prototypes." % key or prototype) + visited.append(id(prototype)) + protstrings = prototype.get("prototype") + if protstrings: + for protstring in make_iter(protstrings): + if key is not None and protstring == key: + raise RuntimeError("%s tries to prototype itself." % key or prototype) + protparent = protparents.get(protstring) + if not protparent: + raise RuntimeError( + "%s's prototype '%s' was not found." % (key or prototype, protstring)) + _validate_prototype(protstring, protparent, protparents, visited) + + +def _get_prototype(dic, prot, protparents): + """ + Recursively traverse a prototype dictionary, including multiple + inheritance. Use _validate_prototype before this, we don't check + for infinite recursion here. + + """ + if "prototype" in dic: + # move backwards through the inheritance + for prototype in make_iter(dic["prototype"]): + # Build the prot dictionary in reverse order, overloading + new_prot = _get_prototype(protparents.get(prototype.lower(), {}), prot, protparents) + prot.update(new_prot) + prot.update(dic) + prot.pop("prototype", None) # we don't need this anymore + return prot + + +def _batch_create_object(*objparams): + """ + This is a cut-down version of the create_object() function, + optimized for speed. It does NOT check and convert various input + so make sure the spawned Typeclass works before using this! + + Args: + objsparams (tuple): Parameters for the respective creation/add + handlers in the following order: + - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`. + - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`. + - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`. + - `aliases` (list): A list of alias strings for + adding with `new_object.aliases.batch_add(*aliases)`. + - `nattributes` (list): list of tuples `(key, value)` to be loop-added to + add with `new_obj.nattributes.add(*tuple)`. + - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for + adding with `new_obj.attributes.batch_add(*attributes)`. + - `tags` (list): list of tuples `(key, category)` for adding + with `new_obj.tags.batch_add(*tags)`. + - `execs` (list): Code strings to execute together with the creation + of each object. They will be executed with `evennia` and `obj` + (the newly created object) available in the namespace. Execution + will happend after all other properties have been assigned and + is intended for calling custom handlers etc. + for the respective creation/add handlers in the following + order: (create_kwargs, permissions, locks, aliases, nattributes, + attributes, tags, execs) + + Returns: + objects (list): A list of created objects + + Notes: + The `exec` list will execute arbitrary python code so don't allow this to be available to + unprivileged users! + + """ + + # bulk create all objects in one go + + # unfortunately this doesn't work since bulk_create doesn't creates pks; + # the result would be duplicate objects at the next stage, so we comment + # it out for now: + # dbobjs = _ObjectDB.objects.bulk_create(dbobjs) + + dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] + objs = [] + for iobj, obj in enumerate(dbobjs): + # call all setup hooks on each object + objparam = objparams[iobj] + # setup + obj._createdict = {"permissions": make_iter(objparam[1]), + "locks": objparam[2], + "aliases": make_iter(objparam[3]), + "nattributes": objparam[4], + "attributes": objparam[5], + "tags": make_iter(objparam[6])} + # this triggers all hooks + obj.save() + # run eventual extra code + for code in objparam[7]: + if code: + exec(code, {}, {"evennia": evennia, "obj": obj}) + objs.append(obj) + return objs + +def spawn(*prototypes, **kwargs): + """ + Spawn a number of prototyped objects. + + Args: + prototypes (dict): Each argument should be a prototype + dictionary. + 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 + the global protparents dictionary accessible by the input + prototypes. If not given, it will instead look for modules + defined by settings.PROTOTYPE_MODULES. + prototype_parents (dict): A dictionary holding a custom + prototype-parent dictionary. Will overload same-named + prototypes from prototype_modules. + return_prototypes (bool): Only return a list of the + prototype-parents (no object creation happens) + + """ + # get available protparents + protparents = get_protparents() + + # overload module's protparents with specifically given protparents + protparents.update(kwargs.get("prototype_parents", {})) + for key, prototype in protparents.items(): + _validate_prototype(key.lower(), prototype, protparents, []) + + if "return_prototypes" in kwargs: + # only return the parents + return copy.deepcopy(protparents) + + objsparams = [] + for prototype in prototypes: + + _validate_prototype(None, prototype, protparents, []) + prot = _get_prototype(prototype, {}, protparents) + if not prot: + continue + + # extract the keyword args we need to create the object itself. If we get a callable, + # call that to get the value (don't catch errors) + create_kwargs = {} + keyval = prot.pop("key", "Spawned Object %06i" % randint(1, 100000)) + create_kwargs["db_key"] = keyval() if callable(keyval) else keyval + + locval = prot.pop("location", None) + create_kwargs["db_location"] = locval() if callable(locval) else _handle_dbref(locval) + + homval = prot.pop("home", settings.DEFAULT_HOME) + create_kwargs["db_home"] = homval() if callable(homval) else _handle_dbref(homval) + + destval = prot.pop("destination", None) + create_kwargs["db_destination"] = destval() if callable(destval) else _handle_dbref(destval) + + typval = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) + create_kwargs["db_typeclass_path"] = typval() if callable(typval) else typval + + # extract calls to handlers + permval = prot.pop("permissions", []) + permission_string = permval() if callable(permval) else permval + lockval = prot.pop("locks", "") + lock_string = lockval() if callable(lockval) else lockval + aliasval = prot.pop("aliases", "") + alias_string = aliasval() if callable(aliasval) else aliasval + tagval = prot.pop("tags", []) + tags = tagval() if callable(tagval) else tagval + attrval = prot.pop("attrs", []) + attributes = attrval() if callable(tagval) else attrval + + exval = prot.pop("exec", "") + execs = make_iter(exval() if callable(exval) else exval) + + # extract ndb assignments + nattributes = dict((key.split("_", 1)[1], value() if callable(value) else value) + for key, value in prot.items() if key.startswith("ndb_")) + + # the rest are attributes + simple_attributes = [(key, value()) if callable(value) else (key, value) + for key, value in prot.items() if not key.startswith("ndb_")] + attributes = attributes + simple_attributes + attributes = [tup for tup in attributes if not tup[0] in _CREATE_OBJECT_KWARGS] + + # pack for call into _batch_create_object + objsparams.append((create_kwargs, permission_string, lock_string, + alias_string, nattributes, attributes, tags, execs)) + + return _batch_create_object(*objsparams) + + # Testing From 610399e233bdc1e9ce9841da3a19c201f1258542 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 10 Mar 2018 14:43:11 +0100 Subject: [PATCH 011/103] Change validation syntax, spawn mechanism not working --- evennia/commands/default/building.py | 7 ++++- evennia/utils/spawner.py | 40 +++++++++++++++++++--------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index d493e850b6..3efcc0d641 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -14,7 +14,7 @@ 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, - store_prototype, build_metaproto) + store_prototype, build_metaproto, validate_prototype) from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2806,6 +2806,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): self.caller.msg("Spawn aborted: You don't have access to " "use the 'exec' prototype key.") return None + try: + validate_prototype(prototype) + except RuntimeError as err: + self.caller.msg(str(err)) + return return prototype def _search_show_prototype(query, metaprots=None): diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 00cfb25627..412df31126 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -370,7 +370,7 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed 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"))) + ",".join(prototype.tags.get(category="persistent_prototype", return_list=True))) for prototype in sorted(prototypes, key=lambda o: o.key)] prototypes = prototypes + readonly_prototypes @@ -403,32 +403,45 @@ def _handle_dbref(inp): return dbid_to_obj(inp, ObjectDB) -def _validate_prototype(key, prototype, protparents, visited): +def validate_prototype(prototype, protkey=None, protparents=None, _visited=None): """ Run validation on a prototype, checking for inifinite regress. + Args: + prototype (dict): Prototype to validate. + protkey (str, optional): The name of the prototype definition, if any. + protpartents (dict, optional): The available prototype parent library. If + note given this will be determined from settings/database. + _visited (list, optional): This is an internal work array and should not be set manually. + Raises: + RuntimeError: If prototype has invalid structure. + """ - print("validate_prototype {}, {}, {}, {}".format(key, prototype, protparents, visited)) + print("validate_prototype {}, {}, {}, {}".format(protkey, prototype, protparents, _visited)) + if not protparents: + protparents = get_protparents() + if _visited is None: + _visited = [] assert isinstance(prototype, dict) - if id(prototype) in visited: - raise RuntimeError("%s has infinite nesting of prototypes." % key or prototype) - visited.append(id(prototype)) + if id(prototype) in _visited: + raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype) + _visited.append(id(prototype)) protstrings = prototype.get("prototype") if protstrings: for protstring in make_iter(protstrings): - if key is not None and protstring == key: - raise RuntimeError("%s tries to prototype itself." % key or prototype) + if protkey is not None and protstring == protkey: + raise RuntimeError("%s tries to prototype itself." % protkey or prototype) protparent = protparents.get(protstring) if not protparent: raise RuntimeError( - "%s's prototype '%s' was not found." % (key or prototype, protstring)) - _validate_prototype(protstring, protparent, protparents, visited) + "%s's prototype '%s' was not found." % (protkey or prototype, protstring)) + validate_prototype(protparent, protstring, protparents, _visited) def _get_prototype(dic, prot, protparents): """ Recursively traverse a prototype dictionary, including multiple - inheritance. Use _validate_prototype before this, we don't check + inheritance. Use validate_prototype before this, we don't check for infinite recursion here. """ @@ -509,6 +522,7 @@ def _batch_create_object(*objparams): objs.append(obj) return objs + def spawn(*prototypes, **kwargs): """ Spawn a number of prototyped objects. @@ -535,7 +549,7 @@ def spawn(*prototypes, **kwargs): # overload module's protparents with specifically given protparents protparents.update(kwargs.get("prototype_parents", {})) for key, prototype in protparents.items(): - _validate_prototype(key.lower(), prototype, protparents, []) + validate_prototype(prototype, key.lower(), protparents) if "return_prototypes" in kwargs: # only return the parents @@ -544,7 +558,7 @@ def spawn(*prototypes, **kwargs): objsparams = [] for prototype in prototypes: - _validate_prototype(None, prototype, protparents, []) + validate_prototype(prototype, None, protparents) prot = _get_prototype(prototype, {}, protparents) if not prot: continue From 6f5b04e85ebd1d47d68d0a4a6bfb4c80ac09d373 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 10 Mar 2018 14:57:08 +0100 Subject: [PATCH 012/103] Working spawning from both module and store --- evennia/utils/spawner.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 412df31126..3e4bf78112 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -422,13 +422,19 @@ def validate_prototype(prototype, protkey=None, protparents=None, _visited=None) protparents = get_protparents() if _visited is None: _visited = [] + protkey = protkey.lower() if protkey is not None else None + assert isinstance(prototype, dict) + if id(prototype) in _visited: raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype) + _visited.append(id(prototype)) protstrings = prototype.get("prototype") + if protstrings: for protstring in make_iter(protstrings): + protstring = protstring.lower() if protkey is not None and protstring == protkey: raise RuntimeError("%s tries to prototype itself." % protkey or prototype) protparent = protparents.get(protstring) @@ -546,6 +552,8 @@ def spawn(*prototypes, **kwargs): # get available protparents protparents = get_protparents() + print("protparents: {}".format(protparents)) + # overload module's protparents with specifically given protparents protparents.update(kwargs.get("prototype_parents", {})) for key, prototype in protparents.items(): From 2258530fde2a5f1903c917ef2ef53d91a7adbd09 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 11 Mar 2018 00:38:32 +0100 Subject: [PATCH 013/103] Start making tree-parser of prototypes --- evennia/commands/default/building.py | 2 +- evennia/utils/spawner.py | 72 ++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 3efcc0d641..9d8f0277c2 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2803,7 +2803,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): 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 don't have access to " + self.caller.msg("Spawn aborted: You are not allowed to " "use the 'exec' prototype key.") return None try: diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 3e4bf78112..9a0c95641b 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -223,7 +223,7 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete return stored_prototype -def search_persistent_prototype(key=None, tags=None): +def search_persistent_prototype(key=None, tags=None, return_metaprotos=False): """ Find persistent (database-stored) prototypes based on key and/or tags. @@ -232,8 +232,10 @@ def search_persistent_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_metaproto (bool): Return results as metaprotos. Return: - matches (queryset): All found PersistentPrototypes + matches (queryset or list): All found PersistentPrototypes. If `return_metaprotos` + is set, return a list of MetaProtos. Note: This will not include read-only prototypes defined in modules. @@ -249,6 +251,11 @@ def search_persistent_prototype(key=None, tags=None): if key: # partial match on key matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key) + if return_metaprotos: + return [build_metaproto(match.key, match.desc, match.locks.all(), + match.tags.get(category="persistent_prototype", return_list=True), + match.attributes.get("prototype")) + for match in matches] return matches @@ -335,8 +342,30 @@ def get_protparents(): return {metaproto.key: metaproto.prototype for metaproto in metaprotos} +def gather_prototype_tree(metaprotos): + """ + Build nested structure of metaprotos, starting from the roots with no parents. -def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True): + Args: + metaprotos (list): All metaprotos to structure. + Returns: + tree (list): A list of lists representing all root metaprotos and + their children. + """ + roots = [mproto for mproto in metaprotos if 'prototype' not in mproto] + + def _iterate_tree(root): + rootkey = root.key + children = [_iterate_tree(mproto) for mproto in metaprotos + if mproto.prototype.get('prototype') == rootkey] + if children: + return children + return root + return [_iterate_tree(root) for root in roots] + + +def list_prototypes(caller, key=None, tags=None, show_non_use=False, + show_non_edit=True, sort_tree=True): """ Collate a list of found prototypes based on search criteria and access. @@ -346,34 +375,38 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed 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. + sort_tree (bool, optional): Order prototypes by inheritance tree. 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 metaprotos for readonly and db-based prototypes + metaprotos = search_readonly_prototype(key, tags) + metaprotos += search_persistent_prototype(key, tags, return_metaprotos=True) + + if sort_tree: + def _print_tree(mproto, level=0): + + 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(metaprotos, key=lambda o: o.key)] + + tree = gather_prototype_tree(metaprotos) + # get use-permissions of readonly attributes (edit is always False) - readonly_prototypes = [ + 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", return_list=True))) - for prototype in sorted(prototypes, key=lambda o: o.key)] - - prototypes = prototypes + readonly_prototypes + for metaproto in sorted(metaprotos, key=lambda o: o.key)] if not prototypes: return None @@ -417,7 +450,6 @@ def validate_prototype(prototype, protkey=None, protparents=None, _visited=None) RuntimeError: If prototype has invalid structure. """ - print("validate_prototype {}, {}, {}, {}".format(protkey, prototype, protparents, _visited)) if not protparents: protparents = get_protparents() if _visited is None: From 40d9bd4ff50c7e12ab67218b53c6b3d234f73fc5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 15 Mar 2018 21:19:06 +0100 Subject: [PATCH 014/103] Start refining tree display --- evennia/utils/spawner.py | 59 ++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 9a0c95641b..5a154ef4c1 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -111,7 +111,7 @@ import evennia from evennia.objects.models import ObjectDB from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj -from collections import namedtuple +from collections import namedtuple, defaultdict from evennia.scripts.scripts import DefaultScript from evennia.utils.create import create_script from evennia.utils.evtable import EvTable @@ -342,7 +342,7 @@ def get_protparents(): return {metaproto.key: metaproto.prototype for metaproto in metaprotos} -def gather_prototype_tree(metaprotos): +def get_prototype_tree(metaprotos): """ Build nested structure of metaprotos, starting from the roots with no parents. @@ -352,16 +352,40 @@ def gather_prototype_tree(metaprotos): tree (list): A list of lists representing all root metaprotos and their children. """ - roots = [mproto for mproto in metaprotos if 'prototype' not in mproto] + mapping = {mproto.key.lower(): mproto for mproto in metaprotos} + parents = defaultdict(list) + + for key, mproto in mapping: + proto = mproto.prototype.get('prototype', None) + if isinstance(proto, basestring): + parents[key].append(proto.lower()) + elif isinstance(proto, (tuple, list)): + parents[key].extend([pro.lower() for pro in proto]) + + def _iterate(root): + prts = parents[root] + + + + return parents + + roots = [root for root in metaprotos if not root.prototype.get('prototype')] def _iterate_tree(root): - rootkey = root.key - children = [_iterate_tree(mproto) for mproto in metaprotos - if mproto.prototype.get('prototype') == rootkey] + rootkey = root.key.lower() + children = [ + _iterate_tree(mproto) for mproto in metaprotos + if rootkey in [mp.lower() for mp in make_iter(mproto.prototype.get('prototype', ''))]] if children: return children return root - return [_iterate_tree(root) for root in roots] + tree = [] + for root in roots: + tree.append(root) + branch = _iterate_tree(root) + if branch: + tree.append(branch) + return tree def list_prototypes(caller, key=None, tags=None, show_non_use=False, @@ -386,18 +410,17 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, metaprotos += search_persistent_prototype(key, tags, return_metaprotos=True) if sort_tree: - def _print_tree(mproto, level=0): - - 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(metaprotos, key=lambda o: o.key)] - - tree = gather_prototype_tree(metaprotos) + def _print_tree(struct, level=0): + indent = " " * level + if isinstance(struct, list): + # a sub-branch + return "\n".join("{}{}".format( + indent, _print_tree(leaf, level + 2)) for leaf in struct) + else: + # an actual mproto + return "{}{}".format(indent, struct.key) + print(_print_tree(get_prototype_tree(metaprotos))) # get use-permissions of readonly attributes (edit is always False) prototypes = [ From 5d313b0cac51929955cfb9ac6d56caf3a67cf0ae Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 15 Mar 2018 22:41:18 +0100 Subject: [PATCH 015/103] Test with different tree solution --- evennia/utils/spawner.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 5a154ef4c1..6b12f99ac6 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -362,12 +362,14 @@ def get_prototype_tree(metaprotos): elif isinstance(proto, (tuple, list)): parents[key].extend([pro.lower() for pro in proto]) - def _iterate(root): - prts = parents[root] + def _iterate(child, level=0): + tree = [_iterate(parent, level + 1) for parent in parents[key]] + return tree if tree else level * " " + child + for key in parents: + print("Mproto {}:\n{}".format(_iterate(key, level=0))) - - return parents + return [] roots = [root for root in metaprotos if not root.prototype.get('prototype')] From ceee65eb0f7ccdf993575f85829ebb1043e7b2d7 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 17 Mar 2018 18:17:28 +0100 Subject: [PATCH 016/103] Bug fixes for spawner olc --- evennia/commands/default/building.py | 7 ++- evennia/utils/spawner.py | 87 +++++----------------------- 2 files changed, 18 insertions(+), 76 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 9d8f0277c2..3730cc934e 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2743,8 +2743,9 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): location will default to caller's current location. search - search prototype by name or tags. list - list available prototypes, optionally limit by tags. - show - inspect prototype by key. If not given, acts like list. + show, examine - inspect prototype by key. If not given, acts like list. save - save a prototype to the database. It will be listable by /list. + delete - remove a prototype from database, if allowed to. menu - manipulate prototype in a menu interface. Example: @@ -2824,7 +2825,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" "|cdesc:|n {} \n|cprototype:|n ".format( metaprot.key, ", ".join(metaprot.tags), - "; ".join(metaprot.locks), metaprot.desc)) + metaprot.locks, metaprot.desc)) prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value) for key, value in sorted(metaprot.prototype.items())).rstrip(","))) @@ -2848,7 +2849,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): exit_on_lastpage=True) return - if 'show' in self.switches: + if 'show' in self.switches or 'examine' in self.switches: # the argument is a key in this case (may be a partial key) if not self.args: self.switches.append('list') diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 6b12f99ac6..92f1261f7e 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -158,7 +158,7 @@ def build_metaproto(key, desc, locks, tags, prototype): Create a metaproto from combinant parts. """ - return MetaProto(key, desc, locks, tags, dict(prototype)) + return MetaProto(key, desc, make_iter(locks), tags, dict(prototype)) def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete=False): @@ -249,7 +249,7 @@ def search_persistent_prototype(key=None, tags=None, return_metaprotos=False): else: matches = PersistentPrototype.objects.all() if key: - # partial match on key + # exact or partial match on key matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key) if return_metaprotos: return [build_metaproto(match.key, match.desc, match.locks.all(), @@ -316,18 +316,20 @@ def search_prototype(key=None, tags=None, return_meta=True): """ readonly_prototypes = search_readonly_prototype(key, tags) - persistent_prototypes = search_persistent_prototype(key, tags) + persistent_prototypes = search_persistent_prototype(key, tags, return_metaprotos=True) - if return_meta: - persistent_prototypes = [ - build_metaproto(prot.key, prot.desc, prot.locks.all(), - prot.tags.all(), prot.attributes.get("prototype")) - for prot in persistent_prototypes] - else: - readonly_prototypes = [metaprot.prototyp for metaprot in readonly_prototypes] - persistent_prototypes = [prot.attributes.get("prototype") for prot in persistent_prototypes] + matches = persistent_prototypes + readonly_prototypes + if len(matches) > 1 and key: + key = key.lower() + # avoid duplicates if an exact match exist between the two types + filter_matches = [mta for mta in matches if mta.key == key] + if len(filter_matches) < len(matches): + matches = filter_matches - return persistent_prototypes + readonly_prototypes + if not return_meta: + matches = [mta.prototype for mta in matches] + + return matches def get_protparents(): @@ -342,54 +344,6 @@ def get_protparents(): return {metaproto.key: metaproto.prototype for metaproto in metaprotos} -def get_prototype_tree(metaprotos): - """ - Build nested structure of metaprotos, starting from the roots with no parents. - - Args: - metaprotos (list): All metaprotos to structure. - Returns: - tree (list): A list of lists representing all root metaprotos and - their children. - """ - mapping = {mproto.key.lower(): mproto for mproto in metaprotos} - parents = defaultdict(list) - - for key, mproto in mapping: - proto = mproto.prototype.get('prototype', None) - if isinstance(proto, basestring): - parents[key].append(proto.lower()) - elif isinstance(proto, (tuple, list)): - parents[key].extend([pro.lower() for pro in proto]) - - def _iterate(child, level=0): - tree = [_iterate(parent, level + 1) for parent in parents[key]] - return tree if tree else level * " " + child - - for key in parents: - print("Mproto {}:\n{}".format(_iterate(key, level=0))) - - return [] - - roots = [root for root in metaprotos if not root.prototype.get('prototype')] - - def _iterate_tree(root): - rootkey = root.key.lower() - children = [ - _iterate_tree(mproto) for mproto in metaprotos - if rootkey in [mp.lower() for mp in make_iter(mproto.prototype.get('prototype', ''))]] - if children: - return children - return root - tree = [] - for root in roots: - tree.append(root) - branch = _iterate_tree(root) - if branch: - tree.append(branch) - return tree - - def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True, sort_tree=True): """ @@ -411,19 +365,6 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, metaprotos = search_readonly_prototype(key, tags) metaprotos += search_persistent_prototype(key, tags, return_metaprotos=True) - if sort_tree: - def _print_tree(struct, level=0): - indent = " " * level - if isinstance(struct, list): - # a sub-branch - return "\n".join("{}{}".format( - indent, _print_tree(leaf, level + 2)) for leaf in struct) - else: - # an actual mproto - return "{}{}".format(indent, struct.key) - - print(_print_tree(get_prototype_tree(metaprotos))) - # get use-permissions of readonly attributes (edit is always False) prototypes = [ (metaproto.key, From a4966c7edacd20b15cb93233ffde57fed833000c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 17 Mar 2018 19:47:44 +0100 Subject: [PATCH 017/103] Spawner/olc mechanism working --- evennia/commands/default/building.py | 28 +++++++++++++++++++++---- evennia/utils/spawner.py | 31 ++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 3730cc934e..9ea9707498 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -14,7 +14,8 @@ 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, - store_prototype, build_metaproto, validate_prototype) + store_prototype, build_metaproto, validate_prototype, + delete_prototype, PermissionError) from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2777,9 +2778,6 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" - def parser(self): - super(CmdSpawn, self).parser() - def func(self): """Implements the spawner""" @@ -2867,7 +2865,28 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): tags=self.lhslist)), exit_on_lastpage=True) return + if 'delete' in self.switches: + # remove db-based prototype + matchstring = _search_show_prototype(self.args) + if matchstring: + question = "\nDo you want to continue deleting? [Y]/N" + string = "|rDeleting prototype:|n\n{}".format(matchstring) + answer = yield(string + question) + if answer.lower() in ["n", "no"]: + caller.msg("|rDeletion cancelled.|n") + return + try: + success = delete_prototype(caller, self.args) + except PermissionError as err: + caller.msg("|rError deleting:|R {}|n".format(err)) + caller.msg("Deletion {}.".format( + 'successful' if success else 'failed (does the prototype exist?)')) + return + else: + caller.msg("Could not find prototype '{}'".format(key)) + if 'save' in self.switches: + # store a prototype to the database store if not self.args or not self.rhs: caller.msg( "Usage: @spawn/save [;desc[;tag,tag[,...][;lockstring]]] = ") @@ -2902,6 +2921,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # check for existing prototype, old_matchstring = _search_show_prototype(key) + 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" diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 92f1261f7e..2a003069a4 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -122,6 +122,9 @@ _READONLY_PROTOTYPES = {} _READONLY_PROTOTYPE_MODULES = {} +class PermissionError(RuntimeError): + pass + # storage of meta info about the prototype MetaProto = namedtuple('MetaProto', ['key', 'desc', 'locks', 'tags', 'prototype']) @@ -199,14 +202,16 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete stored_prototype = PersistentPrototype.objects.filter(db_key=key) if stored_prototype: + # edit existing 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)) + "edit prototype {}.".format(caller, key)) if delete: + # delete prototype stored_prototype.delete() - return + return True if desc: stored_prototype.desc = desc @@ -216,13 +221,33 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete stored_prototype.locks.add(locks) if prototype: stored_prototype.attributes.add("prototype", prototype) + elif delete: + # didn't find what to delete + return False else: + # create a new prototype stored_prototype = create_script( PersistentPrototype, key=key, desc=desc, persistent=True, locks=locks, tags=tags, attributes=[("prototype", prototype)]) return stored_prototype +def delete_prototype(caller, key): + """ + Delete a stored prototype + + Args: + caller (Account or Object): Caller aiming to delete a prototype. + key (str): The persistent prototype to delete. + Returns: + success (bool): If deletion worked or not. + Raises: + PermissionError: If 'edit' lock was not passed. + + """ + return store_prototype(caller, key, None, delete=True) + + def search_persistent_prototype(key=None, tags=None, return_metaprotos=False): """ Find persistent (database-stored) prototypes based on key and/or tags. @@ -550,8 +575,6 @@ def spawn(*prototypes, **kwargs): # get available protparents protparents = get_protparents() - print("protparents: {}".format(protparents)) - # overload module's protparents with specifically given protparents protparents.update(kwargs.get("prototype_parents", {})) for key, prototype in protparents.items(): From 60578dc5576dd401ad166bd77c57431474ff77cc Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 17 Mar 2018 23:29:16 +0100 Subject: [PATCH 018/103] Add typeclass/list to list all available typeclasses --- evennia/commands/default/building.py | 23 ++++++++++++++++++++++- evennia/utils/utils.py | 22 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 9ea9707498..9e8199d546 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -10,7 +10,7 @@ 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.utils import inherits_from, class_from_module +from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.spawner import (spawn, search_prototype, list_prototypes, @@ -1702,6 +1702,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): object - basically making this a new clean object. force - change to the typeclass also if the object already has a typeclass of the same name. + list - show available typeclasses. Example: @type button = examples.red_button.RedButton @@ -1733,6 +1734,26 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): caller = self.caller + if 'list' in self.switches: + tclasses = get_all_typeclasses() + print(list(tclasses.keys())) + contribs = [key for key in sorted(tclasses) + if key.startswith("evennia.contrib")] or [""] + core = [key for key in sorted(tclasses) + if key.startswith("evennia") and key not in contribs] or [""] + game = [key for key in sorted(tclasses) + if not key.startswith("evennia")] or [""] + string = ("|wCore typeclasses|n\n" + " {core}\n" + "|wLoaded Contrib typeclasses|n\n" + " {contrib}\n" + "|wGame-dir typeclasses|n\n" + " {game}").format(core="\n ".join(core), + contrib="\n ".join(contribs), + game="\n ".join(game)) + caller.msg(string) + return + if not self.args: caller.msg("Usage: %s [= typeclass]" % self.cmdstring) return diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 10621f0feb..a8d2171f75 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -20,12 +20,13 @@ import textwrap import random from os.path import join as osjoin from importlib import import_module -from inspect import ismodule, trace, getmembers, getmodule +from inspect import ismodule, trace, getmembers, getmodule, getmro from collections import defaultdict, OrderedDict from twisted.internet import threads, reactor, task from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext as _ +from django.apps import apps from evennia.utils import logger _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE @@ -1879,3 +1880,22 @@ def get_game_dir_path(): else: os.chdir(os.pardir) raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.") + + +def get_all_typeclasses(): + """ + List available typeclasses from all available modules. + + Returns: + typeclasses (dict): On the form {"typeclass.path": typeclass, ...} + + Notes: + This will dynamicall retrieve all abstract django models inheriting at any distance + from the TypedObject base (aka a Typeclass) so it will work fine with any custom + classes being added. + + """ + from evennia.typeclasses.models import TypedObject + typeclasses = {"{}.{}".format(model.__module__, model.__name__): model + for model in apps.get_models() if TypedObject in getmro(model)} + return typeclasses From 6b383c863b82dd3295ed859b860a0490e4bdc9d4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 18 Mar 2018 08:21:40 +0100 Subject: [PATCH 019/103] Expand typeclass/show to view typeclass docstrings --- evennia/commands/default/building.py | 35 ++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index beaaeba5a5..a948da14a9 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1700,11 +1700,13 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): @typeclass[/switch] [= typeclass.path] @type '' @parent '' + @typeclass/list/show [typeclass.path] @swap - this is a shorthand for using /force/reset flags. @update - this is a shorthand for using the /force/reload flag. Switch: - show - display the current typeclass of object (default) + show, examine - display the current typeclass of object (default) or, if + given a typeclass path, show the docstring of that typeclass. update - *only* re-run at_object_creation on this object meaning locks or other properties set later may remain. reset - clean out *all* the attributes and properties on the @@ -1712,6 +1714,8 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): force - change to the typeclass also if the object already has a typeclass of the same name. list - show available typeclasses. + + Example: @type button = examples.red_button.RedButton @@ -1767,6 +1771,33 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): caller.msg("Usage: %s [= typeclass]" % self.cmdstring) return + if "show" in self.switches or "examine" in self.switches: + oquery = self.lhs + obj = caller.search(oquery, quiet=True) + if not obj: + # no object found to examine, see if it's a typeclass-path instead + tclasses = get_all_typeclasses() + matches = [(key, tclass) + for key, tclass in tclasses.items() if key.endswith(oquery)] + nmatches = len(matches) + if nmatches > 1: + caller.msg("Multiple typeclasses found matching {}:\n {}".format( + oquery, "\n ".join(tup[0] for tup in matches))) + elif not matches: + caller.msg("No object or typeclass path found to match '{}'".format(oquery)) + else: + # one match found + caller.msg("Docstring for typeclass '{}':\n{}".format( + oquery, matches[0][1].__doc__)) + else: + # do the search again to get the error handling in case of multi-match + obj = caller.search(oquery) + if not obj: + return + caller.msg("{}'s current typeclass is '{}.{}'".format( + obj.name, obj.__class__.__module__, obj.__class__.__name__)) + return + # get object to swap on obj = caller.search(self.lhs) if not obj: @@ -1779,7 +1810,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): new_typeclass = self.rhs or obj.path - if "show" in self.switches: + if "show" in self.switches or "examine" in self.switches: string = "%s's current typeclass is %s." % (obj.name, obj.__class__) caller.msg(string) return From b75cd81eda87f33e582a26cd537c88dc46957f43 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 18 Mar 2018 12:53:38 +0100 Subject: [PATCH 020/103] Fix unit tests --- evennia/commands/default/building.py | 23 ++++---- evennia/commands/default/tests.py | 8 ++- evennia/utils/spawner.py | 81 ++++++++++++++-------------- 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index a948da14a9..c2200470c3 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -14,8 +14,8 @@ from evennia.utils.utils import inherits_from, class_from_module, get_all_typecl from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.spawner import (spawn, search_prototype, list_prototypes, - store_prototype, build_metaproto, validate_prototype, - delete_prototype, PermissionError) + save_db_prototype, build_metaproto, validate_prototype, + delete_db_prototype, PermissionError) from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -1739,6 +1739,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): key = "@typeclass" aliases = ["@type", "@parent", "@swap", "@update"] + switch_options = ("show", "examine", "update", "reset", "force", "list") locks = "cmd:perm(typeclass) or perm(Builder)" help_category = "Building" @@ -1749,7 +1750,6 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): if 'list' in self.switches: tclasses = get_all_typeclasses() - print(list(tclasses.keys())) contribs = [key for key in sorted(tclasses) if key.startswith("evennia.contrib")] or [""] core = [key for key in sorted(tclasses) @@ -1764,7 +1764,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): " {game}").format(core="\n ".join(core), contrib="\n ".join(contribs), game="\n ".join(game)) - caller.msg(string) + EvMore(caller, string, exit_on_lastpage=True) return if not self.args: @@ -2841,7 +2841,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): """ key = "@spawn" - switch_options = ("noloc", ) + switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu") locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" @@ -2912,7 +2912,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): 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 + return if 'show' in self.switches or 'examine' in self.switches: # the argument is a key in this case (may be a partial key) @@ -2943,7 +2943,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): caller.msg("|rDeletion cancelled.|n") return try: - success = delete_prototype(caller, self.args) + success = delete_db_prototype(caller, self.args) except PermissionError as err: caller.msg("|rError deleting:|R {}|n".format(err)) caller.msg("Deletion {}.".format( @@ -2961,10 +2961,12 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # handle lhs parts = self.lhs.split(";", 3) - key, desc, tags, lockstring = "", "", [], "" + key, desc, tags, lockstring = ( + "", "User-created prototype", ["user-created"], + "edit:id({}) or perm(Admin); use:all()".format(caller.id)) nparts = len(parts) if nparts == 1: - key = parts.strip() + key = parts[0].strip() elif nparts == 2: key, desc = (part.strip() for part in parts) elif nparts == 3: @@ -3000,7 +3002,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # all seems ok. Try to save. try: - store_prototype(caller, key, prototype, desc=desc, tags=tags, locks=lockstring) + save_db_prototype(caller, key, prototype, desc=desc, tags=tags, locks=lockstring) except PermissionError as err: caller.msg("|rError saving:|R {}|n".format(err)) return @@ -3038,6 +3040,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): 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.") + print("spawning2 {}:{} - {}".format(self.cmdstring, self.args, prototype)) return prototype = metaproto.prototype diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index bf01ac3039..fe738a3e07 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -27,6 +27,7 @@ from evennia.utils import ansi, utils from evennia.server.sessionhandler import SESSIONS from evennia import search_object from evennia import DefaultObject, DefaultCharacter +from evennia.utils import spawner # set up signal here since we are not starting the server @@ -390,8 +391,10 @@ class TestBuilding(CommandTest): self.assertEqual(goblin.location, spawnLoc) goblin.delete() + spawner.save_db_prototype(self.char1, "ball", {'key': 'Ball', 'prototype': 'GOBLIN'}) + # Tests "@spawn " - self.call(building.CmdSpawn(), "'BALL'", "Spawned Ball") + self.call(building.CmdSpawn(), "ball", "Spawned Ball") ball = getObject(self, "Ball") self.assertEqual(ball.location, self.char1.location) self.assertIsInstance(ball, DefaultObject) @@ -416,6 +419,9 @@ class TestBuilding(CommandTest): # test calling spawn with an invalid prototype. self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'") + # Test listing commands + self.call(building.CmdSpawn(), "/list", "| Key ") + class TestComms(CommandTest): diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 2a003069a4..6426aa0acc 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -109,17 +109,17 @@ from django.conf import settings from random import randint import evennia from evennia.objects.models import ObjectDB -from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj +from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj, is_iter -from collections import namedtuple, defaultdict +from collections import namedtuple from evennia.scripts.scripts import DefaultScript from evennia.utils.create import create_script from evennia.utils.evtable import EvTable _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") -_READONLY_PROTOTYPES = {} -_READONLY_PROTOTYPE_MODULES = {} +_MODULE_PROTOTYPES = {} +_MODULE_PROTOTYPE_MODULES = {} class PermissionError(RuntimeError): @@ -133,7 +133,7 @@ for mod in settings.PROTOTYPE_MODULES: # 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( + _MODULE_PROTOTYPES.update( {key.lower(): MetaProto( key.lower(), prot['prototype_desc'] if 'prototype_desc' in prot else mod, @@ -142,12 +142,12 @@ for mod in settings.PROTOTYPE_MODULES: 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}) + _MODULE_PROTOTYPE_MODULES.update({tup[0]: mod for tup in prots}) # Prototype storage mechanisms -class PersistentPrototype(DefaultScript): +class DbPrototype(DefaultScript): """ This stores a single prototype """ @@ -161,10 +161,10 @@ def build_metaproto(key, desc, locks, tags, prototype): Create a metaproto from combinant parts. """ - return MetaProto(key, desc, make_iter(locks), tags, dict(prototype)) + return MetaProto(key, desc, ";".join(locks) if is_iter(locks) else locks, tags, dict(prototype)) -def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete=False): +def save_db_prototype(caller, key, prototype, desc="", tags=None, locks="", delete=False): """ Store a prototype persistently. @@ -176,7 +176,7 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete 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. + applied with the 'db_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'. @@ -192,14 +192,14 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete key_orig = key key = key.lower() locks = locks if locks else "use:all();edit:id({}) or perm(Admin)".format(caller.id) - tags = [(tag, "persistent_prototype") for tag in make_iter(tags)] + tags = [(tag, "db_prototype") for tag in make_iter(tags)] - if key in _READONLY_PROTOTYPES: - mod = _READONLY_PROTOTYPE_MODULES.get(key, "N/A") + if key in _MODULE_PROTOTYPES: + mod = _MODULE_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) + stored_prototype = DbPrototype.objects.filter(db_key=key) if stored_prototype: # edit existing prototype @@ -227,12 +227,12 @@ def store_prototype(caller, key, prototype, desc="", tags=None, locks="", delete else: # create a new prototype stored_prototype = create_script( - PersistentPrototype, key=key, desc=desc, persistent=True, + DbPrototype, key=key, desc=desc, persistent=True, locks=locks, tags=tags, attributes=[("prototype", prototype)]) return stored_prototype -def delete_prototype(caller, key): +def delete_db_prototype(caller, key): """ Delete a stored prototype @@ -245,21 +245,21 @@ def delete_prototype(caller, key): PermissionError: If 'edit' lock was not passed. """ - return store_prototype(caller, key, None, delete=True) + return save_db_prototype(caller, key, None, delete=True) -def search_persistent_prototype(key=None, tags=None, return_metaprotos=False): +def search_db_prototype(key=None, tags=None, return_metaprotos=False): """ 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' + will always be applied with the 'db_protototype' tag category. return_metaproto (bool): Return results as metaprotos. Return: - matches (queryset or list): All found PersistentPrototypes. If `return_metaprotos` + matches (queryset or list): All found DbPrototypes. If `return_metaprotos` is set, return a list of MetaProtos. Note: @@ -269,22 +269,22 @@ def search_persistent_prototype(key=None, tags=None, return_metaprotos=False): 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) + tag_categories = ["db_prototype" for _ in tags] + matches = DbPrototype.objects.get_by_tag(tags, tag_categories) else: - matches = PersistentPrototype.objects.all() + matches = DbPrototype.objects.all() if key: # exact or partial match on key matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key) if return_metaprotos: return [build_metaproto(match.key, match.desc, match.locks.all(), - match.tags.get(category="persistent_prototype", return_list=True), + match.tags.get(category="db_prototype", return_list=True), match.attributes.get("prototype")) for match in matches] return matches -def search_readonly_prototype(key=None, tags=None): +def search_module_prototype(key=None, tags=None): """ Find read-only prototypes, defined in modules. @@ -301,10 +301,10 @@ def search_readonly_prototype(key=None, tags=None): if tags: # use tags to limit selection tagset = set(tags) - matches = {key: metaproto for key, metaproto in _READONLY_PROTOTYPES.items() + matches = {key: metaproto for key, metaproto in _MODULE_PROTOTYPES.items() if tagset.intersection(metaproto.tags)} else: - matches = _READONLY_PROTOTYPES + matches = _MODULE_PROTOTYPES if key: if key in matches: @@ -324,7 +324,7 @@ def search_prototype(key=None, tags=None, return_meta=True): 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' + will always be applied with the 'db_protototype' tag category. return_meta (bool): If False, only return prototype dicts, if True return MetaProto namedtuples including prototype meta info @@ -340,15 +340,15 @@ def search_prototype(key=None, tags=None, return_meta=True): be found. """ - readonly_prototypes = search_readonly_prototype(key, tags) - persistent_prototypes = search_persistent_prototype(key, tags, return_metaprotos=True) + module_prototypes = search_module_prototype(key, tags) + db_prototypes = search_db_prototype(key, tags, return_metaprotos=True) - matches = persistent_prototypes + readonly_prototypes + matches = db_prototypes + module_prototypes if len(matches) > 1 and key: key = key.lower() # avoid duplicates if an exact match exist between the two types filter_matches = [mta for mta in matches if mta.key == key] - if len(filter_matches) < len(matches): + if filter_matches and len(filter_matches) < len(matches): matches = filter_matches if not return_meta: @@ -369,8 +369,7 @@ def get_protparents(): return {metaproto.key: metaproto.prototype for metaproto in metaprotos} -def list_prototypes(caller, key=None, tags=None, show_non_use=False, - show_non_edit=True, sort_tree=True): +def list_prototypes(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. @@ -380,22 +379,27 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, 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. - sort_tree (bool, optional): Order prototypes by inheritance tree. Returns: table (EvTable or None): An EvTable representation of the prototypes. None if no prototypes were found. """ + # this allows us to pass lists of empty strings + tags = [tag for tag in make_iter(tags) if tag] + # get metaprotos for readonly and db-based prototypes - metaprotos = search_readonly_prototype(key, tags) - metaprotos += search_persistent_prototype(key, tags, return_metaprotos=True) + metaprotos = search_module_prototype(key, tags) + metaprotos += search_db_prototype(key, tags, return_metaprotos=True) # get use-permissions of readonly attributes (edit is always False) prototypes = [ (metaproto.key, metaproto.desc, ("{}/N".format('Y' - if caller.locks.check_lockstring(caller, metaproto.locks, access_type='use') else 'N')), + if caller.locks.check_lockstring( + caller, + metaproto.locks, + access_type='use') else 'N')), ",".join(metaproto.tags)) for metaproto in sorted(metaprotos, key=lambda o: o.key)] @@ -642,7 +646,6 @@ def spawn(*prototypes, **kwargs): return _batch_create_object(*objsparams) - # Testing if __name__ == "__main__": From 2689beedae6a68650da8044bd04f5521dd15e844 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 18 Mar 2018 16:31:01 +0100 Subject: [PATCH 021/103] Add lockhandler.append to update lock string --- evennia/commands/default/building.py | 5 ++- evennia/locks/lockhandler.py | 48 +++++++++++++++++++++++----- evennia/utils/spawner.py | 7 +++- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index c2200470c3..c2aaa16a4c 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -3002,7 +3002,10 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # all seems ok. Try to save. try: - save_db_prototype(caller, key, prototype, desc=desc, tags=tags, locks=lockstring) + prot = save_db_prototype(caller, key, prototype, desc=desc, tags=tags, locks=lockstring) + prot.locks.append("edit", "perm(Admin)") + if not prot.locks.get("use"): + prot.locks.add("use:all()") except PermissionError as err: caller.msg("|rError saving:|R {}|n".format(err)) return diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index b8801f9655..6b1a30ab03 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -421,6 +421,28 @@ class LockHandler(object): self._cache_locks(self.obj.lock_storage) self.cache_lock_bypass(self.obj) + def append(self, access_type, lockstring, op='or'): + """ + Append a lock definition to access_type if it doesn't already exist. + + Args: + access_type (str): Access type. + lockstring (str): A valid lockstring, without the operator to + link it to an eventual existing lockstring. + op (str): An operator 'and', 'or', 'and not', 'or not' used + for appending the lockstring to an existing access-type. + Note: + The most common use of this method is for use in commands where + the user can specify their own lockstrings. This method allows + the system to auto-add things like Admin-override access. + + """ + old_lockstring = self.get(access_type) + if not lockstring.strip().lower() in old_lockstring.lower(): + lockstring = "{old} {op} {new}".format( + old=old_lockstring, op=op, new=lockstring.strip()) + self.add(lockstring) + def check(self, accessing_obj, access_type, default=False, no_superuser_bypass=False): """ Checks a lock of the correct type by passing execution off to @@ -459,9 +481,13 @@ class LockHandler(object): return True except AttributeError: # happens before session is initiated. - if not no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or - (hasattr(accessing_obj, 'account') and hasattr(accessing_obj.account, 'is_superuser') and accessing_obj.account.is_superuser) or - (hasattr(accessing_obj, 'get_account') and (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))): + if not no_superuser_bypass and ( + (hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or + (hasattr(accessing_obj, 'account') and + hasattr(accessing_obj.account, 'is_superuser') and + accessing_obj.account.is_superuser) or + (hasattr(accessing_obj, 'get_account') and + (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))): return True # no superuser or bypass -> normal lock operation @@ -469,7 +495,8 @@ class LockHandler(object): # we have a lock, test it. evalstring, func_tup, raw_string = self.locks[access_type] # execute all lock funcs in the correct order, producing a tuple of True/False results. - true_false = tuple(bool(tup[0](accessing_obj, self.obj, *tup[1], **tup[2])) for tup in func_tup) + true_false = tuple(bool( + tup[0](accessing_obj, self.obj, *tup[1], **tup[2])) for tup in func_tup) # the True/False tuple goes into evalstring, which combines them # with AND/OR/NOT in order to get the final result. return eval(evalstring % true_false) @@ -520,9 +547,13 @@ class LockHandler(object): if accessing_obj.locks.lock_bypass and not no_superuser_bypass: return True except AttributeError: - if no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or - (hasattr(accessing_obj, 'account') and hasattr(accessing_obj.account, 'is_superuser') and accessing_obj.account.is_superuser) or - (hasattr(accessing_obj, 'get_account') and (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))): + if no_superuser_bypass and ( + (hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or + (hasattr(accessing_obj, 'account') and + hasattr(accessing_obj.account, 'is_superuser') and + accessing_obj.account.is_superuser) or + (hasattr(accessing_obj, 'get_account') and + (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))): return True if ":" not in lockstring: lockstring = "%s:%s" % ("_dummy", lockstring) @@ -538,7 +569,8 @@ class LockHandler(object): else: # if no access types was given and multiple locks were # embedded in the lockstring we assume all must be true - return all(self._eval_access_type(accessing_obj, locks, access_type) for access_type in locks) + return all(self._eval_access_type( + accessing_obj, locks, access_type) for access_type in locks) def _test(): diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 6426aa0acc..ed92dfadd5 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -161,7 +161,12 @@ def build_metaproto(key, desc, locks, tags, prototype): Create a metaproto from combinant parts. """ - return MetaProto(key, desc, ";".join(locks) if is_iter(locks) else locks, tags, dict(prototype)) + if locks: + locks = (";".join(locks) if is_iter(locks) else locks) + else: + locks = [] + prototype = dict(prototype) if prototype else {} + return MetaProto(key, desc, locks, tags, dict(prototype)) def save_db_prototype(caller, key, prototype, desc="", tags=None, locks="", delete=False): From 1ce077d8312eadad278742342050fb2898941e8b Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 18 Mar 2018 17:28:52 +0100 Subject: [PATCH 022/103] Further stabilizing of spawner storage mechanism and error checking --- evennia/commands/default/building.py | 39 +++++++++++++------ evennia/locks/lockhandler.py | 56 ++++++++++++++++++++++------ evennia/utils/spawner.py | 6 +++ 3 files changed, 79 insertions(+), 22 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index c2aaa16a4c..94362ec58e 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2229,12 +2229,15 @@ class CmdExamine(ObjManipCommand): else: things.append(content) if exits: - string += "\n|wExits|n: %s" % ", ".join(["%s(%s)" % (exit.name, exit.dbref) for exit in exits]) + string += "\n|wExits|n: %s" % ", ".join( + ["%s(%s)" % (exit.name, exit.dbref) for exit in exits]) if pobjs: - string += "\n|wCharacters|n: %s" % ", ".join(["|c%s|n(%s)" % (pobj.name, pobj.dbref) for pobj in pobjs]) + string += "\n|wCharacters|n: %s" % ", ".join( + ["|c%s|n(%s)" % (pobj.name, pobj.dbref) for pobj in pobjs]) if things: - string += "\n|wContents|n: %s" % ", ".join(["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents - if cont not in exits and cont not in pobjs]) + string += "\n|wContents|n: %s" % ", ".join( + ["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents + if cont not in exits and cont not in pobjs]) separator = "-" * _DEFAULT_WIDTH # output info return '%s\n%s\n%s' % (separator, string.strip(), separator) @@ -2961,9 +2964,6 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # handle lhs parts = self.lhs.split(";", 3) - key, desc, tags, lockstring = ( - "", "User-created prototype", ["user-created"], - "edit:id({}) or perm(Admin); use:all()".format(caller.id)) nparts = len(parts) if nparts == 1: key = parts[0].strip() @@ -2971,11 +2971,25 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): 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(",")] + tags = [tag.strip().lower() for tag in tags.split(",") if tag] else: # lockstrings can itself contain ; key, desc, tags, lockstring = (part.strip() for part in parts) - tags = [tag.strip().lower() for tag in tags.split(",")] + tags = [tag.strip().lower() for tag in tags.split(",") if tag] + if not key: + caller.msg("The prototype must have a key.") + return + if not desc: + desc = "User-created prototype" + if not tags: + tags = ["user"] + if not lockstring: + lockstring = "edit:id({}) or perm(Admin); use:all()".format(caller.id) + + is_valid, err = caller.locks.validate(lockstring) + if not is_valid: + caller.msg("|rLock error|n: {}".format(err)) + return # handle rhs: prototype = _parse_prototype(self.rhs) @@ -3002,7 +3016,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # all seems ok. Try to save. try: - prot = save_db_prototype(caller, key, prototype, desc=desc, tags=tags, locks=lockstring) + prot = save_db_prototype( + caller, key, prototype, desc=desc, tags=tags, locks=lockstring) + if not prot: + caller.msg("|rError saving:|R {}.|n".format(key)) + return prot.locks.append("edit", "perm(Admin)") if not prot.locks.get("use"): prot.locks.add("use:all()") @@ -3043,7 +3061,6 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): 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.") - print("spawning2 {}:{} - {}".format(self.cmdstring, self.args, prototype)) return prototype = metaproto.prototype diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index 6b1a30ab03..20eb117e42 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -287,7 +287,7 @@ class LockHandler(object): """ self.lock_bypass = hasattr(obj, "is_superuser") and obj.is_superuser - def add(self, lockstring): + def add(self, lockstring, validate_only=False): """ Add a new lockstring to handler. @@ -296,10 +296,12 @@ class LockHandler(object): `":"`. Multiple access types should be separated by semicolon (`;`). Alternatively, a list with lockstrings. - + validate_only (bool, optional): If True, validate the lockstring but + don't actually store it. Returns: success (bool): The outcome of the addition, `False` on - error. + error. If `validate_only` is True, this will be a tuple + (bool, error), for pass/fail and a string error. """ if isinstance(lockstring, basestring): @@ -308,21 +310,41 @@ class LockHandler(object): lockdefs = [lockdef for locks in lockstring for lockdef in locks.split(";")] lockstring = ";".join(lockdefs) + err = "" # sanity checks for lockdef in lockdefs: if ':' not in lockdef: - self._log_error(_("Lock: '%s' contains no colon (:).") % lockdef) - return False + err = _("Lock: '{lockdef}' contains no colon (:).").format(lockdef=lockdef) + if validate_only: + return False, err + else: + self._log_error(err) + return False access_type, rhs = [part.strip() for part in lockdef.split(':', 1)] if not access_type: - self._log_error(_("Lock: '%s' has no access_type (left-side of colon is empty).") % lockdef) - return False + err = _("Lock: '{lockdef}' has no access_type " + "(left-side of colon is empty).").format(lockdef=lockdef) + if validate_only: + return False, err + else: + self._log_error(err) + return False if rhs.count('(') != rhs.count(')'): - self._log_error(_("Lock: '%s' has mismatched parentheses.") % lockdef) - return False + err = _("Lock: '{lockdef}' has mismatched parentheses.").format(lockdef=lockdef) + if validate_only: + return False, err + else: + self._log_error(err) + return False if not _RE_FUNCS.findall(rhs): - self._log_error(_("Lock: '%s' has no valid lock functions.") % lockdef) - return False + err = _("Lock: '{lockdef}' has no valid lock functions.").format(lockdef=lockdef) + if validate_only: + return False, err + else: + self._log_error(err) + return False + if validate_only: + return True, None # get the lock string storage_lockstring = self.obj.lock_storage if storage_lockstring: @@ -334,6 +356,18 @@ class LockHandler(object): self._save_locks() return True + def validate(self, lockstring): + """ + Validate lockstring syntactically, without saving it. + + Args: + lockstring (str): Lockstring to validate. + Returns: + valid (bool): If validation passed or not. + + """ + return self.add(lockstring, validate_only=True) + def replace(self, lockstring): """ Replaces the lockstring entirely. diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index ed92dfadd5..01212a38d4 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -197,6 +197,12 @@ def save_db_prototype(caller, key, prototype, desc="", tags=None, locks="", dele key_orig = key key = key.lower() locks = locks if locks else "use:all();edit:id({}) or perm(Admin)".format(caller.id) + + is_valid, err = caller.locks.validate(locks) + if not is_valid: + caller.msg("Lock error: {}".format(err)) + return False + tags = [(tag, "db_prototype") for tag in make_iter(tags)] if key in _MODULE_PROTOTYPES: From c05f9463ed1cbb97c84d6f4e602187d7970a9bc6 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 18 Mar 2018 21:16:39 +0100 Subject: [PATCH 023/103] Start adding menu OLC mechanic for spawner. The EvMenu behaves strangely; going from desc->tags by setting the description means that the back-option no longer works, giving an error that the desc-node is not defined ... --- evennia/commands/default/building.py | 26 +++- evennia/utils/evmenu.py | 4 +- evennia/utils/spawner.py | 204 ++++++++++++++++++++++++++- 3 files changed, 227 insertions(+), 7 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 94362ec58e..dc70e52cea 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -15,7 +15,7 @@ from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.spawner import (spawn, search_prototype, list_prototypes, save_db_prototype, build_metaproto, validate_prototype, - delete_db_prototype, PermissionError) + delete_db_prototype, PermissionError, start_olc) from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2806,7 +2806,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): @spawn/show [] @spawn/save [;desc[;tag,tag[,...][;lockstring]]] = - @spawn/menu + @spawn/menu [] + @olc - equivalent to @spawn/menu Switches: noloc - allow location to be None if not specified explicitly. Otherwise, @@ -2816,7 +2817,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): show, examine - inspect prototype by key. If not given, acts like list. save - save a prototype to the database. It will be listable by /list. delete - remove a prototype from database, if allowed to. - menu - manipulate prototype in a menu interface. + menu, olc - create/manipulate prototype in a menu interface. Example: @spawn GOBLIN @@ -2844,7 +2845,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): """ key = "@spawn" - switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu") + aliases = ["@olc"] + switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu", "olc") locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" @@ -2904,6 +2906,22 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): caller = self.caller + if self.cmdstring == "olc" or 'menu' in self.switches or 'olc' in self.switches: + # OLC menu mode + metaprot = None + if self.lhs: + key = self.lhs + metaprot = search_prototype(key=key, return_meta=True) + if len(metaprot) > 1: + caller.msg("More than one match for {}:\n{}".format( + key, "\n".join(mproto.key for mproto in metaprot))) + return + elif metaprot: + # one match + metaprot = metaprot[0] + start_olc(caller, self.session, metaprot) + return + if 'search' in self.switches: # query for a key match if not self.args: diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 3d8fb6b789..9509bbd884 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -945,9 +945,11 @@ class EvMenu(object): node (str): The formatted node to display. """ + screen_width = self._session.protocol_flags.get("SCREENWIDTH", {0: 78})[0] + nodetext_width_max = max(m_len(line) for line in nodetext.split("\n")) options_width_max = max(m_len(line) for line in optionstext.split("\n")) - total_width = max(options_width_max, nodetext_width_max) + total_width = min(screen_width, max(options_width_max, nodetext_width_max)) separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 01212a38d4..e4d403157e 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -109,17 +109,19 @@ from django.conf import settings from random import randint import evennia from evennia.objects.models import ObjectDB -from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj, is_iter +from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj, is_iter, crop from collections import namedtuple from evennia.scripts.scripts import DefaultScript from evennia.utils.create import create_script from evennia.utils.evtable import EvTable +from evennia.utils.evmenu import EvMenu _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") _MODULE_PROTOTYPES = {} _MODULE_PROTOTYPE_MODULES = {} +_MENU_CROP_WIDTH = 15 class PermissionError(RuntimeError): @@ -156,7 +158,7 @@ class DbPrototype(DefaultScript): self.desc = "A prototype" -def build_metaproto(key, desc, locks, tags, prototype): +def build_metaproto(key='', desc='', locks='', tags=None, prototype=None): """ Create a metaproto from combinant parts. @@ -657,6 +659,204 @@ def spawn(*prototypes, **kwargs): return _batch_create_object(*objsparams) +# prototype design menu nodes + +def _get_menu_metaprot(caller): + if hasattr(caller.ndb._menutree, "olc_metaprot"): + return caller.ndb._menutree.olc_metaprot + else: + metaproto = build_metaproto(None, '', [], [], None) + caller.ndb._menutree.olc_metaprot = metaproto + caller.ndb._menutree.olc_new = True + return metaproto + + +def _set_menu_metaprot(caller, field, value): + metaprot = _get_menu_metaprot(caller) + kwargs = dict(metaprot.__dict__) + kwargs[field] = value + caller.ndb._menutree.olc_metaprot = build_metaproto(**kwargs) + + +def node_index(caller): + metaprot = _get_menu_metaprot(caller) + key = "|g{}|n".format( + crop(metaprot.key, _MENU_CROP_WIDTH)) if metaprot.key else "|rundefined, required|n" + desc = "|g{}|n".format( + crop(metaprot.desc, _MENU_CROP_WIDTH)) if metaprot.desc else "''" + tags = "|g{}|n".format( + crop(", ".join(metaprot.tags), _MENU_CROP_WIDTH)) if metaprot.tags else [] + locks = "|g{}|n".format( + crop(", ".join(metaprot.locks), _MENU_CROP_WIDTH)) if metaprot.tags else [] + prot = "|gdefined|n" if metaprot.prototype else "|rundefined, required|n" + + text = ("|c --- Prototype wizard --- |n\n" + "(make choice; q to abort, h for help)") + options = ( + {"desc": "Key ({})".format(key), "goto": "node_key"}, + {"desc": "Description ({})".format(desc), "goto": "node_desc"}, + {"desc": "Tags ({})".format(tags), "goto": "node_tags"}, + {"desc": "Locks ({})".format(locks), "goto": "node_locks"}, + {"desc": "Prototype ({})".format(prot), "goto": "node_prototype_index"}) + return text, options + + +def _node_check_key(caller, key): + old_metaprot = search_prototype(key) + olc_new = caller.ndb._menutree.olc_new + key = key.strip().lower() + if old_metaprot: + # we are starting a new prototype that matches an existing + if not caller.locks.check_lockstring(caller, old_metaprot.locks, access_type='edit'): + # return to the node_key to try another key + caller.msg("Prototype '{key}' already exists and you don't " + "have permission to edit it.".format(key=key)) + return "node_key" + elif olc_new: + # we are selecting an existing prototype to edit. Reset to index. + del caller.ndb._menutree.olc_new + caller.ndb._menutree.olc_metaprot = old_metaprot + caller.msg("Prototype already exists. Reloading.") + return "node_index" + + # continue on + _set_menu_metaprot(caller, 'key', key) + caller.msg("Key '{key}' was set.".format(key=key)) + return "node_desc" + + +def node_key(caller): + metaprot = _get_menu_metaprot(caller) + text = ["The |ckey|n must be unique and is used to find and use " + "the prototype to spawn new entities. It is not case sensitive."] + old_key = metaprot.key + if old_key: + text.append("Current key is '|y{key}|n'".format(key=old_key)) + else: + text.append("The key is currently unset.") + text.append("Enter text or make choice (q for quit, h for help)") + text = "\n".join(text) + options = ({"desc": "forward (desc)", + "goto": "node_desc"}, + {"desc": "back (index)", + "goto": "node_index"}, + {"key": "_default", + "desc": "enter a key", + "goto": _node_check_key}) + return text, options + + +def _node_check_desc(caller, desc): + desc = desc.strip() + _set_menu_metaprot(caller, 'desc', desc) + caller.msg("Description was set to '{desc}'.".format(desc=desc)) + return "node_tags" + + +def node_desc(caller): + metaprot = _get_menu_metaprot(caller) + text = ["|cDescribe|n briefly the prototype for viewing in listings."] + desc = metaprot.desc + + if desc: + text.append("The current desc is:\n\"|y{desc}|n\"".format(desc)) + else: + text.append("Description is currently unset.") + text = "\n".join(text) + options = ({"desc": "forward (tags)", + "goto": "node_tags"}, + {"desc": "back (key)", + "goto": "node_key"}, + {"key": "_default", + "desc": "enter a description", + "goto": _node_check_desc}) + + return text, options + + +def _node_check_tags(caller, tags): + tags = [part.strip().lower() for part in tags.split(",")] + _set_menu_metaprot(caller, 'tags', tags) + caller.msg("Tags {tags} were set".format(tags=tags)) + return "node_locks" + + +def node_tags(caller): + metaprot = _get_menu_metaprot(caller) + text = ["|cTags|n can be used to find prototypes. They are case-insitive. " + "Separate multiple by tags by commas."] + tags = metaprot.tags + + if tags: + text.append("The current tags are:\n|y{tags}|n".format(tags)) + else: + text.append("No tags are currently set.") + text = "\n".join(text) + options = ({"desc": "forward (locks)", + "goto": "node_locks"}, + {"desc": "back (desc)", + "goto": "node_desc"}, + {"key": "_default", + "desc": "enter tags separated by commas", + "goto": _node_check_tags}) + return text, options + + +def _node_check_locks(caller, lockstring): + # TODO - have a way to validate lock string here + _set_menu_metaprot(caller, 'locks', lockstring) + caller.msg("Set lockstring '{lockstring}'.".format(lockstring=lockstring)) + return "node_prototype_index" + + +def node_locks(caller): + metaprot = _get_menu_metaprot(caller) + text = ["Set |ylocks|n on the prototype. There are two valid lock types: " + "'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n" + "(If you are unsure, leave as default.)"] + locks = metaprot.locks + if locks: + text.append("Current lock is |y'{lockstring}'|n".format(lockstring=locks)) + else: + text.append("Lock unset - if not changed the default lockstring will be set as\n" + " |y'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) + text = "\n".join(text) + options = ({"desc": "forward (prototype)", + "goto": "node_prototype_index"}, + {"desc": "back (tags)", + "goto": "node_tags"}, + {"key": "_default", + "desc": "enter lockstring", + "goto": _node_check_locks}) + + return text, options + + +def node_prototype_index(caller): + pass + + +def start_olc(caller, session=None, metaproto=None): + """ + Start menu-driven olc system for prototypes. + + Args: + caller (Object or Account): The entity starting the menu. + session (Session, optional): The individual session to get data. + metaproto (MetaProto, optional): Given when editing an existing + prototype rather than creating a new one. + + """ + + menudata = {"node_index": node_index, + "node_key": node_key, + "node_desc": node_desc, + "node_tags": node_tags, + "node_locks": node_locks, + "node_prototype_index": node_prototype_index} + EvMenu(caller, menudata, startnode='node_index', session=session, olc_metaproto=metaproto) + + # Testing if __name__ == "__main__": From 5410640de3888d63ae36da8b2c7d49f5d64fd7ee Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 19 Mar 2018 20:27:55 +0100 Subject: [PATCH 024/103] [fix] Add better error reporting from EvMenu --- evennia/utils/evmenu.py | 6 +++++- evennia/utils/spawner.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 9509bbd884..a6a77a871f 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -182,7 +182,8 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT # i18n from django.utils.translation import ugettext as _ -_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is not implemented. Make another choice.") +_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is either not implemented or " + "caused an error. Make another choice.") _ERR_GENERAL = _("Error in menu node '{nodename}'.") _ERR_NO_OPTION_DESC = _("No description.") _HELP_FULL = _("Commands: , help, quit") @@ -573,6 +574,7 @@ class EvMenu(object): except EvMenuError: errmsg = _ERR_GENERAL.format(nodename=callback) self.caller.msg(errmsg, self._session) + logger.log_trace() raise return ret @@ -606,9 +608,11 @@ class EvMenu(object): nodetext, options = ret, None except KeyError: self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session) + logger.log_trace() raise EvMenuError except Exception: self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session) + logger.log_trace() raise # store options to make them easier to test diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index e4d403157e..33fbb91346 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -754,12 +754,13 @@ def _node_check_desc(caller, desc): def node_desc(caller): + metaprot = _get_menu_metaprot(caller) text = ["|cDescribe|n briefly the prototype for viewing in listings."] desc = metaprot.desc if desc: - text.append("The current desc is:\n\"|y{desc}|n\"".format(desc)) + text.append("The current desc is:\n\"|y{desc}|n\"".format(desc=desc)) else: text.append("Description is currently unset.") text = "\n".join(text) @@ -788,7 +789,7 @@ def node_tags(caller): tags = metaprot.tags if tags: - text.append("The current tags are:\n|y{tags}|n".format(tags)) + text.append("The current tags are:\n|y{tags}|n".format(tags=tags)) else: text.append("No tags are currently set.") text = "\n".join(text) From b4a2713333ac3f98b1fb6b6c7e0e8262c219f5b3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 19 Mar 2018 20:59:32 +0100 Subject: [PATCH 025/103] Separate prototype meta-properties from prototype properties in menu --- evennia/utils/spawner.py | 73 +++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 33fbb91346..8ce91fc970 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -678,7 +678,7 @@ def _set_menu_metaprot(caller, field, value): caller.ndb._menutree.olc_metaprot = build_metaproto(**kwargs) -def node_index(caller): +def node_meta_index(caller): metaprot = _get_menu_metaprot(caller) key = "|g{}|n".format( crop(metaprot.key, _MENU_CROP_WIDTH)) if metaprot.key else "|rundefined, required|n" @@ -693,11 +693,11 @@ def node_index(caller): text = ("|c --- Prototype wizard --- |n\n" "(make choice; q to abort, h for help)") options = ( - {"desc": "Key ({})".format(key), "goto": "node_key"}, - {"desc": "Description ({})".format(desc), "goto": "node_desc"}, - {"desc": "Tags ({})".format(tags), "goto": "node_tags"}, - {"desc": "Locks ({})".format(locks), "goto": "node_locks"}, - {"desc": "Prototype ({})".format(prot), "goto": "node_prototype_index"}) + {"desc": "Key ({})".format(key), "goto": "node_meta_key"}, + {"desc": "Description ({})".format(desc), "goto": "node_meta_desc"}, + {"desc": "Tags ({})".format(tags), "goto": "node_meta_tags"}, + {"desc": "Locks ({})".format(locks), "goto": "node_meta_locks"}, + {"desc": "Prototype[menu] ({})".format(prot), "goto": "node_prototype_index"}) return text, options @@ -708,38 +708,39 @@ def _node_check_key(caller, key): if old_metaprot: # we are starting a new prototype that matches an existing if not caller.locks.check_lockstring(caller, old_metaprot.locks, access_type='edit'): - # return to the node_key to try another key + # return to the node_meta_key to try another key caller.msg("Prototype '{key}' already exists and you don't " "have permission to edit it.".format(key=key)) - return "node_key" + return "node_meta_key" elif olc_new: # we are selecting an existing prototype to edit. Reset to index. del caller.ndb._menutree.olc_new caller.ndb._menutree.olc_metaprot = old_metaprot caller.msg("Prototype already exists. Reloading.") - return "node_index" + return "node_meta_index" # continue on _set_menu_metaprot(caller, 'key', key) caller.msg("Key '{key}' was set.".format(key=key)) - return "node_desc" + return "node_meta_desc" -def node_key(caller): +def node_meta_key(caller): metaprot = _get_menu_metaprot(caller) - text = ["The |ckey|n must be unique and is used to find and use " - "the prototype to spawn new entities. It is not case sensitive."] + text = ["The prototype name, or |ckey|n, uniquely identifies the prototype. " + "It is used to find and use the prototype to spawn new entities. " + "It is not case sensitive."] old_key = metaprot.key if old_key: text.append("Current key is '|y{key}|n'".format(key=old_key)) else: text.append("The key is currently unset.") - text.append("Enter text or make choice (q for quit, h for help)") + text.append("Enter text or make a choice (q for quit, h for help)") text = "\n".join(text) options = ({"desc": "forward (desc)", - "goto": "node_desc"}, + "goto": "node_meta_desc"}, {"desc": "back (index)", - "goto": "node_index"}, + "goto": "node_meta_index"}, {"key": "_default", "desc": "enter a key", "goto": _node_check_key}) @@ -750,24 +751,24 @@ def _node_check_desc(caller, desc): desc = desc.strip() _set_menu_metaprot(caller, 'desc', desc) caller.msg("Description was set to '{desc}'.".format(desc=desc)) - return "node_tags" + return "node_meta_tags" -def node_desc(caller): +def node_meta_desc(caller): metaprot = _get_menu_metaprot(caller) text = ["|cDescribe|n briefly the prototype for viewing in listings."] desc = metaprot.desc if desc: - text.append("The current desc is:\n\"|y{desc}|n\"".format(desc=desc)) + text.append("The current meta desc is:\n\"|y{desc}|n\"".format(desc=desc)) else: text.append("Description is currently unset.") text = "\n".join(text) options = ({"desc": "forward (tags)", - "goto": "node_tags"}, + "goto": "node_meta_tags"}, {"desc": "back (key)", - "goto": "node_key"}, + "goto": "node_meta_key"}, {"key": "_default", "desc": "enter a description", "goto": _node_check_desc}) @@ -779,12 +780,12 @@ def _node_check_tags(caller, tags): tags = [part.strip().lower() for part in tags.split(",")] _set_menu_metaprot(caller, 'tags', tags) caller.msg("Tags {tags} were set".format(tags=tags)) - return "node_locks" + return "node_meta_locks" -def node_tags(caller): +def node_meta_tags(caller): metaprot = _get_menu_metaprot(caller) - text = ["|cTags|n can be used to find prototypes. They are case-insitive. " + text = ["|cTags|n can be used to classify and find prototypes. Tags are case-insensitive. " "Separate multiple by tags by commas."] tags = metaprot.tags @@ -794,9 +795,9 @@ def node_tags(caller): text.append("No tags are currently set.") text = "\n".join(text) options = ({"desc": "forward (locks)", - "goto": "node_locks"}, + "goto": "node_meta_locks"}, {"desc": "back (desc)", - "goto": "node_desc"}, + "goto": "node_meta_desc"}, {"key": "_default", "desc": "enter tags separated by commas", "goto": _node_check_tags}) @@ -810,7 +811,7 @@ def _node_check_locks(caller, lockstring): return "node_prototype_index" -def node_locks(caller): +def node_meta_locks(caller): metaprot = _get_menu_metaprot(caller) text = ["Set |ylocks|n on the prototype. There are two valid lock types: " "'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n" @@ -825,7 +826,7 @@ def node_locks(caller): options = ({"desc": "forward (prototype)", "goto": "node_prototype_index"}, {"desc": "back (tags)", - "goto": "node_tags"}, + "goto": "node_meta_tags"}, {"key": "_default", "desc": "enter lockstring", "goto": _node_check_locks}) @@ -834,6 +835,10 @@ def node_locks(caller): def node_prototype_index(caller): + metaprot = _get_menu_metaprot(caller) + text = [" |c--- Prototype menu --- |n" + ] + pass @@ -849,13 +854,13 @@ def start_olc(caller, session=None, metaproto=None): """ - menudata = {"node_index": node_index, - "node_key": node_key, - "node_desc": node_desc, - "node_tags": node_tags, - "node_locks": node_locks, + menudata = {"node_meta_index": node_meta_index, + "node_meta_key": node_meta_key, + "node_meta_desc": node_meta_desc, + "node_meta_tags": node_meta_tags, + "node_meta_locks": node_meta_locks, "node_prototype_index": node_prototype_index} - EvMenu(caller, menudata, startnode='node_index', session=session, olc_metaproto=metaproto) + EvMenu(caller, menudata, startnode='node_meta_index', session=session, olc_metaproto=metaproto) # Testing From 535ac26c222a415866465669770f979eae0e2811 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 24 Mar 2018 17:28:56 +0100 Subject: [PATCH 026/103] Refactor spawner menu --- evennia/utils/evmenu.py | 17 ++- evennia/utils/spawner.py | 245 +++++++++++++++++++++++++-------------- 2 files changed, 171 insertions(+), 91 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index a6a77a871f..94c1467419 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -43,13 +43,18 @@ command definition too) with function definitions: def node_with_other_name(caller, input_string): # code return text, options + + def another_node(caller, input_string, **kwargs): + # code + return text, options ``` Where caller is the object using the menu and input_string is the command entered by the user on the *previous* node (the command entered to get to this node). The node function code will only be executed once per node-visit and the system will accept nodes with -both one or two arguments interchangeably. +both one or two arguments interchangeably. It also accepts nodes +that takes **kwargs. The menu tree itself is available on the caller as `caller.ndb._menutree`. This makes it a convenient place to store @@ -82,12 +87,14 @@ menu is immediately exited and the default "look" command is called. the callable. Those kwargs will also be passed into the next node if possible. Such a callable should return either a str or a (str, dict), where the string is the name of the next node to go to and the dict is the new, - (possibly modified) kwarg to pass into the next node. + (possibly modified) kwarg to pass into the next node. If the callable returns + None or the empty string, the current node will be revisited. - `exec` (str, callable or tuple, optional): This takes the same input as `goto` above and runs before it. If given a node name, the node will be executed but will not be considered the next node. If node/callback returns str or (str, dict), these will replace the `goto` step (`goto` callbacks will not fire), with the string being the next node name and the optional dict acting as the kwargs-input for the next node. + If an exec callable returns the empty string (only), the current node is re-run. If key is not given, the option will automatically be identified by its number 1..N. @@ -669,6 +676,9 @@ class EvMenu(object): if isinstance(ret, basestring): # only return a value if a string (a goto target), ignore all other returns + if not ret: + # an empty string - rerun the same node + return self.nodename return ret, kwargs return None @@ -718,6 +728,9 @@ class EvMenu(object): raise EvMenuError( "{}: goto callable must return str or (str, dict)".format(inp_nodename)) nodename, kwargs = nodename[:2] + if not nodename: + # no nodename return. Re-run current node + nodename = self.nodename try: # execute the found node, make use of the returns. nodetext, options = self._execute_node(nodename, raw_string, **kwargs) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 8ce91fc970..2f31da0f49 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -671,6 +671,10 @@ def _get_menu_metaprot(caller): return metaproto +def _is_new_prototype(caller): + return hasattr(caller.ndb._menutree, "olc_new") + + def _set_menu_metaprot(caller, field, value): metaprot = _get_menu_metaprot(caller) kwargs = dict(metaprot.__dict__) @@ -678,30 +682,121 @@ def _set_menu_metaprot(caller, field, value): caller.ndb._menutree.olc_metaprot = build_metaproto(**kwargs) -def node_meta_index(caller): - metaprot = _get_menu_metaprot(caller) - key = "|g{}|n".format( - crop(metaprot.key, _MENU_CROP_WIDTH)) if metaprot.key else "|rundefined, required|n" - desc = "|g{}|n".format( - crop(metaprot.desc, _MENU_CROP_WIDTH)) if metaprot.desc else "''" - tags = "|g{}|n".format( - crop(", ".join(metaprot.tags), _MENU_CROP_WIDTH)) if metaprot.tags else [] - locks = "|g{}|n".format( - crop(", ".join(metaprot.locks), _MENU_CROP_WIDTH)) if metaprot.tags else [] - prot = "|gdefined|n" if metaprot.prototype else "|rundefined, required|n" +def _format_property(key, required=False, metaprot=None, prototype=None): + key = key.lower() + if metaprot is not None: + prop = getattr(metaprot, key) or '' + elif prototype is not None: + prop = prototype.get(key, '') + + out = prop + if callable(prop): + if hasattr(prop, '__name__'): + out = "<{}>".format(prop.__name__) + else: + out = repr(prop) + if is_iter(prop): + out = ", ".join(str(pr) for pr in prop) + if not out and required: + out = "|rrequired" + return " ({}|n)".format(crop(out, _MENU_CROP_WIDTH)) + + +def _set_property(caller, raw_string, **kwargs): + """ + Update a property. To be called by the 'goto' option variable. + + Args: + caller (Object, Account): The user of the wizard. + raw_string (str): Input from user on given node - the new value to set. + Kwargs: + prop (str): Property name to edit with `raw_string`. + processor (callable): Converts `raw_string` to a form suitable for saving. + next_node (str): Where to redirect to after this has run. + Returns: + next_node (str): Next node to go to. + + """ + prop = kwargs.get("prop", "meta_key") + processor = kwargs.get("processor", None) + next_node = kwargs.get("next_node", "node_index") + + propname_low = prop.strip().lower() + meta = propname_low.startswith("meta_") + if meta: + propname_low = propname_low[5:] + raw_string = raw_string.strip() + + if callable(processor): + try: + value = processor(raw_string) + except Exception as err: + caller.msg("Could not set {prop} to {value} ({err})".format( + prop=prop.replace("_", "-").capitalize(), value=raw_string, err=str(err))) + # this means we'll re-run the current node. + return None + else: + value = raw_string + + if meta: + _set_menu_metaprot(caller, propname_low, value) + else: + metaprot = _get_menu_metaprot(caller) + prototype = metaprot.prototype + prototype[propname_low] = value + _set_menu_metaprot(caller, "prototype", prototype) + + caller.msg("Set {prop} to {value}.".format( + prop=prop.replace("_", "-").capitalize(), value=str(value))) + + return next_node + + +def _wizard_options(prev_node, next_node): + options = [{"desc": "forward ({})".format(next_node.replace("_", "-")), + "goto": "node_{}".format(next_node)}, + {"desc": "back ({})".format(prev_node.replace("_", "-")), + "goto": "node_{}".format(prev_node)}] + if "index" not in (prev_node, next_node): + options.append({"desc": "index", + "goto": "node_index"}) + return options + + +def node_index(caller): + metaprot = _get_menu_metaprot(caller) + prototype = metaprot.prototype + + text = ("|c --- Prototype wizard --- |n\n\n" + "Define properties of the prototype. All prototype values can be over-ridden at " + "the time of spawning an instance of the prototype, but some are required.\n\n" + "'Meta'-properties are not used in the prototype itself but are used to organize and " + "list prototypes. The 'Meta-Key' uniquely identifies the prototype and allows you to " + "edit an existing prototype or save a new one for use by you or others later.\n\n" + "(make choice; q to abort. If unsure, start from 1.)") + + options = [] + # The meta-key goes first + options.append( + {"desc": "|WMeta-Key|n|n{}".format(_format_property("Key", True, metaprot, None)), + "goto": "node_meta_key"}) + for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Home', 'Destination', + 'Permissions', 'Locks', 'Location', 'Tags', 'Attrs'): + req = False + if key in ("Prototype", "Typeclass"): + req = "prototype" not in prototype and "typeclass" not in prototype + options.append( + {"desc": "|w{}|n{}".format(key, _format_property(key, req, None, prototype)), + "goto": "node_{}".format(key.lower())}) + for key in ('Desc', 'Tags', 'Locks'): + options.append( + {"desc": "|WMeta-{}|n|n{}".format(key, _format_property(key, req, metaprot, None)), + "goto": "node_meta_{}".format(key.lower())}) - text = ("|c --- Prototype wizard --- |n\n" - "(make choice; q to abort, h for help)") - options = ( - {"desc": "Key ({})".format(key), "goto": "node_meta_key"}, - {"desc": "Description ({})".format(desc), "goto": "node_meta_desc"}, - {"desc": "Tags ({})".format(tags), "goto": "node_meta_tags"}, - {"desc": "Locks ({})".format(locks), "goto": "node_meta_locks"}, - {"desc": "Prototype[menu] ({})".format(prot), "goto": "node_prototype_index"}) return text, options -def _node_check_key(caller, key): +def _check_meta_key(caller, key): old_metaprot = search_prototype(key) olc_new = caller.ndb._menutree.olc_new key = key.strip().lower() @@ -719,15 +814,12 @@ def _node_check_key(caller, key): caller.msg("Prototype already exists. Reloading.") return "node_meta_index" - # continue on - _set_menu_metaprot(caller, 'key', key) - caller.msg("Key '{key}' was set.".format(key=key)) - return "node_meta_desc" + return _set_property(caller, key, prop='meta_key', next_node="node_meta_desc") def node_meta_key(caller): metaprot = _get_menu_metaprot(caller) - text = ["The prototype name, or |ckey|n, uniquely identifies the prototype. " + text = ["The prototype name, or |cmeta-key|n, uniquely identifies the prototype. " "It is used to find and use the prototype to spawn new entities. " "It is not case sensitive."] old_key = metaprot.key @@ -735,25 +827,14 @@ def node_meta_key(caller): text.append("Current key is '|y{key}|n'".format(key=old_key)) else: text.append("The key is currently unset.") - text.append("Enter text or make a choice (q for quit, h for help)") - text = "\n".join(text) - options = ({"desc": "forward (desc)", - "goto": "node_meta_desc"}, - {"desc": "back (index)", - "goto": "node_meta_index"}, - {"key": "_default", - "desc": "enter a key", - "goto": _node_check_key}) + text.append("Enter text or make a choice (q for quit)") + text = "\n\n".join(text) + options = _wizard_options("index", "meta_desc") + options.append({"key": "_default", + "goto": _check_meta_key}) return text, options -def _node_check_desc(caller, desc): - desc = desc.strip() - _set_menu_metaprot(caller, 'desc', desc) - caller.msg("Description was set to '{desc}'.".format(desc=desc)) - return "node_meta_tags" - - def node_meta_desc(caller): metaprot = _get_menu_metaprot(caller) @@ -764,25 +845,14 @@ def node_meta_desc(caller): text.append("The current meta desc is:\n\"|y{desc}|n\"".format(desc=desc)) else: text.append("Description is currently unset.") - text = "\n".join(text) - options = ({"desc": "forward (tags)", - "goto": "node_meta_tags"}, - {"desc": "back (key)", - "goto": "node_meta_key"}, - {"key": "_default", - "desc": "enter a description", - "goto": _node_check_desc}) - + text = "\n\n".join(text) + options = _wizard_options("meta_key", "meta_tags") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop='meta_desc', next_node="node_meta_tags"))}) return text, options -def _node_check_tags(caller, tags): - tags = [part.strip().lower() for part in tags.split(",")] - _set_menu_metaprot(caller, 'tags', tags) - caller.msg("Tags {tags} were set".format(tags=tags)) - return "node_meta_locks" - - def node_meta_tags(caller): metaprot = _get_menu_metaprot(caller) text = ["|cTags|n can be used to classify and find prototypes. Tags are case-insensitive. " @@ -793,24 +863,16 @@ def node_meta_tags(caller): text.append("The current tags are:\n|y{tags}|n".format(tags=tags)) else: text.append("No tags are currently set.") - text = "\n".join(text) - options = ({"desc": "forward (locks)", - "goto": "node_meta_locks"}, - {"desc": "back (desc)", - "goto": "node_meta_desc"}, - {"key": "_default", - "desc": "enter tags separated by commas", - "goto": _node_check_tags}) + text = "\n\n".join(text) + options = _wizard_options("meta_desc", "meta_locks") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="meta_tags", + processor=lambda s: [str(part.strip()) for part in s.split(",")], + next_node="node_meta_locks"))}) return text, options -def _node_check_locks(caller, lockstring): - # TODO - have a way to validate lock string here - _set_menu_metaprot(caller, 'locks', lockstring) - caller.msg("Set lockstring '{lockstring}'.".format(lockstring=lockstring)) - return "node_prototype_index" - - def node_meta_locks(caller): metaprot = _get_menu_metaprot(caller) text = ["Set |ylocks|n on the prototype. There are two valid lock types: " @@ -822,24 +884,30 @@ def node_meta_locks(caller): else: text.append("Lock unset - if not changed the default lockstring will be set as\n" " |y'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) - text = "\n".join(text) - options = ({"desc": "forward (prototype)", - "goto": "node_prototype_index"}, - {"desc": "back (tags)", - "goto": "node_meta_tags"}, - {"key": "_default", - "desc": "enter lockstring", - "goto": _node_check_locks}) - + text = "\n\n".join(text) + options = _wizard_options("meta_tags", "prototype") + options.append({"key": "_default", + "desc": "enter lockstring", + "goto": (_set_property, + dict(prop="meta_locks", + next_node="node_key"))}) return text, options -def node_prototype_index(caller): +def node_key(caller): metaprot = _get_menu_metaprot(caller) - text = [" |c--- Prototype menu --- |n" - ] + prot = metaprot.prototype + key = prot.get("key") - pass + text = ["Set the prototype's |ykey|n."] + if key: + text.append("Current key value is '|y{}|n'.") + else: + text.append("Key is currently unset.") + text = "\n\n".join(text) + options = _wizard_options("meta_locks", + + return "\n".join(text), options def start_olc(caller, session=None, metaproto=None): @@ -853,14 +921,13 @@ def start_olc(caller, session=None, metaproto=None): prototype rather than creating a new one. """ - - menudata = {"node_meta_index": node_meta_index, + menudata = {"node_index": node_index, "node_meta_key": node_meta_key, "node_meta_desc": node_meta_desc, "node_meta_tags": node_meta_tags, "node_meta_locks": node_meta_locks, - "node_prototype_index": node_prototype_index} - EvMenu(caller, menudata, startnode='node_meta_index', session=session, olc_metaproto=metaproto) + "node_key": node_key} + EvMenu(caller, menudata, startnode='node_index', session=session, olc_metaproto=metaproto) # Testing From b799b7280aef1e88b9f3a5c1f9bb8dade5bcbf60 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 24 Mar 2018 20:45:17 +0100 Subject: [PATCH 027/103] Add all spawn-menu nodes; need better validation/choices for several nodes --- evennia/utils/spawner.py | 345 ++++++++++++++++++++++++++++++++------- 1 file changed, 286 insertions(+), 59 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 2f31da0f49..25298476c6 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -725,7 +725,6 @@ def _set_property(caller, raw_string, **kwargs): meta = propname_low.startswith("meta_") if meta: propname_low = propname_low[5:] - raw_string = raw_string.strip() if callable(processor): try: @@ -763,25 +762,27 @@ def _wizard_options(prev_node, next_node): return options +# menu nodes + def node_index(caller): metaprot = _get_menu_metaprot(caller) prototype = metaprot.prototype text = ("|c --- Prototype wizard --- |n\n\n" - "Define properties of the prototype. All prototype values can be over-ridden at " - "the time of spawning an instance of the prototype, but some are required.\n\n" - "'Meta'-properties are not used in the prototype itself but are used to organize and " - "list prototypes. The 'Meta-Key' uniquely identifies the prototype and allows you to " - "edit an existing prototype or save a new one for use by you or others later.\n\n" - "(make choice; q to abort. If unsure, start from 1.)") + "Define the |yproperties|n of the prototype. All prototype values can be " + "over-ridden at the time of spawning an instance of the prototype, but some are " + "required.\n\n'|wMeta'-properties|n are not used in the prototype itself but are used " + "to organize and list prototypes. The 'Meta-Key' uniquely identifies the prototype " + "and allows you to edit an existing prototype or save a new one for use by you or " + "others later.\n\n(make choice; q to abort. If unsure, start from 1.)") options = [] # The meta-key goes first options.append( {"desc": "|WMeta-Key|n|n{}".format(_format_property("Key", True, metaprot, None)), "goto": "node_meta_key"}) - for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Home', 'Destination', - 'Permissions', 'Locks', 'Location', 'Tags', 'Attrs'): + for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', + 'Permissions', 'Location', 'Home', 'Destination'): req = False if key in ("Prototype", "Typeclass"): req = "prototype" not in prototype and "typeclass" not in prototype @@ -812,84 +813,66 @@ def _check_meta_key(caller, key): del caller.ndb._menutree.olc_new caller.ndb._menutree.olc_metaprot = old_metaprot caller.msg("Prototype already exists. Reloading.") - return "node_meta_index" + return "node_index" - return _set_property(caller, key, prop='meta_key', next_node="node_meta_desc") + return _set_property(caller, key, prop='meta_key', next_node="node_prototype") def node_meta_key(caller): metaprot = _get_menu_metaprot(caller) - text = ["The prototype name, or |cmeta-key|n, uniquely identifies the prototype. " + text = ["The prototype name, or |wMeta-Key|n, uniquely identifies the prototype. " "It is used to find and use the prototype to spawn new entities. " "It is not case sensitive."] old_key = metaprot.key if old_key: - text.append("Current key is '|y{key}|n'".format(key=old_key)) + text.append("Current key is '|w{key}|n'".format(key=old_key)) else: text.append("The key is currently unset.") text.append("Enter text or make a choice (q for quit)") text = "\n\n".join(text) - options = _wizard_options("index", "meta_desc") + options = _wizard_options("index", "prototype") options.append({"key": "_default", "goto": _check_meta_key}) return text, options -def node_meta_desc(caller): - +def node_prototype(caller): metaprot = _get_menu_metaprot(caller) - text = ["|cDescribe|n briefly the prototype for viewing in listings."] - desc = metaprot.desc + prot = metaprot.prototype + prototype = prot.get("prototype") - if desc: - text.append("The current meta desc is:\n\"|y{desc}|n\"".format(desc=desc)) + text = ["Set the prototype's parent |yPrototype|n. If this is unset, Typeclass will be used."] + if prototype: + text.append("Current prototype is |y{prototype}|n.".format(prototype=prototype)) else: - text.append("Description is currently unset.") + text.append("Parent prototype is not set") text = "\n\n".join(text) - options = _wizard_options("meta_key", "meta_tags") + options = _wizard_options("meta_key", "typeclass") options.append({"key": "_default", "goto": (_set_property, - dict(prop='meta_desc', next_node="node_meta_tags"))}) + dict(prop="prototype", + processor=lambda s: s.strip(), + next_node="node_typeclass"))}) return text, options -def node_meta_tags(caller): +def node_typeclass(caller): metaprot = _get_menu_metaprot(caller) - text = ["|cTags|n can be used to classify and find prototypes. Tags are case-insensitive. " - "Separate multiple by tags by commas."] - tags = metaprot.tags + prot = metaprot.prototype + typeclass = prot.get("typeclass") - if tags: - text.append("The current tags are:\n|y{tags}|n".format(tags=tags)) + text = ["Set the typeclass's parent |yTypeclass|n."] + if typeclass: + text.append("Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass)) else: - text.append("No tags are currently set.") + text.append("Using default typeclass {typeclass}.".format( + typeclass=settings.BASE_OBJECT_TYPECLASS)) text = "\n\n".join(text) - options = _wizard_options("meta_desc", "meta_locks") + options = _wizard_options("prototype", "key") options.append({"key": "_default", "goto": (_set_property, - dict(prop="meta_tags", - processor=lambda s: [str(part.strip()) for part in s.split(",")], - next_node="node_meta_locks"))}) - return text, options - - -def node_meta_locks(caller): - metaprot = _get_menu_metaprot(caller) - text = ["Set |ylocks|n on the prototype. There are two valid lock types: " - "'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n" - "(If you are unsure, leave as default.)"] - locks = metaprot.locks - if locks: - text.append("Current lock is |y'{lockstring}'|n".format(lockstring=locks)) - else: - text.append("Lock unset - if not changed the default lockstring will be set as\n" - " |y'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) - text = "\n\n".join(text) - options = _wizard_options("meta_tags", "prototype") - options.append({"key": "_default", - "desc": "enter lockstring", - "goto": (_set_property, - dict(prop="meta_locks", + dict(prop="typeclass", + processor=lambda s: s.strip(), next_node="node_key"))}) return text, options @@ -899,15 +882,248 @@ def node_key(caller): prot = metaprot.prototype key = prot.get("key") - text = ["Set the prototype's |ykey|n."] + text = ["Set the prototype's |yKey|n. This will retain case sensitivity."] if key: - text.append("Current key value is '|y{}|n'.") + text.append("Current key value is '|y{key}|n'.".format(key=key)) else: text.append("Key is currently unset.") text = "\n\n".join(text) - options = _wizard_options("meta_locks", + options = _wizard_options("typeclass", "aliases") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="key", + processor=lambda s: s.strip(), + next_node="node_aliases"))}) + return text, options + + +def node_aliases(caller): + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + aliases = prot.get("aliases") + + text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. " + "ill retain case sensitivity."] + if aliases: + text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases)) + else: + text.append("No aliases are set.") + text = "\n\n".join(text) + options = _wizard_options("key", "attrs") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="aliases", + processor=lambda s: [part.strip() for part in s.split(",")], + next_node="node_attrs"))}) + return text, options + + +def node_attrs(caller): + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + attrs = prot.get("attrs") + + text = ["Set the prototype's |yAttributes|n. Separate multiple attrs with commas. " + "Will retain case sensitivity."] + if attrs: + text.append("Current attrs are '|y{attrs}|n'.".format(attrs=attrs)) + else: + text.append("No attrs are set.") + text = "\n\n".join(text) + options = _wizard_options("aliases", "tags") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="attrs", + processor=lambda s: [part.strip() for part in s.split(",")], + next_node="node_tags"))}) + return text, options + + +def node_tags(caller): + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + tags = prot.get("tags") + + text = ["Set the prototype's |yTags|n. Separate multiple tags with commas. " + "Will retain case sensitivity."] + if tags: + text.append("Current tags are '|y{tags}|n'.".format(tags=tags)) + else: + text.append("No tags are set.") + text = "\n\n".join(text) + options = _wizard_options("attrs", "locks") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="tags", + processor=lambda s: [part.strip() for part in s.split(",")], + next_node="node_locks"))}) + return text, options + + +def node_locks(caller): + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + locks = prot.get("locks") + + text = ["Set the prototype's |yLock string|n. Separate multiple locks with semi-colons. " + "Will retain case sensitivity."] + if locks: + text.append("Current locks are '|y{locks}|n'.".format(locks=locks)) + else: + text.append("No locks are set.") + text = "\n\n".join(text) + options = _wizard_options("tags", "permissions") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="locks", + processor=lambda s: s.strip(), + next_node="node_permissions"))}) + return text, options + + +def node_permissions(caller): + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + permissions = prot.get("permissions") + + text = ["Set the prototype's |yPermissions|n. Separate multiple permissions with commas. " + "Will retain case sensitivity."] + if permissions: + text.append("Current permissions are '|y{permissions}|n'.".format(permissions=permissions)) + else: + text.append("No permissions are set.") + text = "\n\n".join(text) + options = _wizard_options("destination", "location") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="permissions", + processor=lambda s: [part.strip() for part in s.split(",")], + next_node="node_location"))}) + return text, options + + +def node_location(caller): + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + location = prot.get("location") + + text = ["Set the prototype's |yLocation|n"] + if location: + text.append("Current location is |y{location}|n.".format(location=location)) + else: + text.append("Default location is {}'s inventory.".format(caller)) + text = "\n\n".join(text) + options = _wizard_options("permissions", "home") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="location", + processor=lambda s: s.strip(), + next_node="node_home"))}) + return text, options + + +def node_home(caller): + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + home = prot.get("home") + + text = ["Set the prototype's |yHome location|n"] + if home: + text.append("Current home location is |y{home}|n.".format(home=home)) + else: + text.append("Default home location (|y{home}|n) used.".format(home=settings.DEFAULT_HOME)) + text = "\n\n".join(text) + options = _wizard_options("aliases", "destination") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="home", + processor=lambda s: s.strip(), + next_node="node_destination"))}) + return text, options + + +def node_destination(caller): + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + dest = prot.get("dest") + + text = ["Set the prototype's |yDestination|n. This is usually only used for Exits."] + if dest: + text.append("Current destination is |y{dest}|n.".format(dest=dest)) + else: + text.append("No destination is set (default).") + text = "\n\n".join(text) + options = _wizard_options("home", "meta_desc") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="dest", + processor=lambda s: s.strip(), + next_node="node_meta_desc"))}) + return text, options + + +def node_meta_desc(caller): + + metaprot = _get_menu_metaprot(caller) + text = ["The |wMeta-Description|n briefly describes the prototype for viewing in listings."] + desc = metaprot.desc + + if desc: + text.append("The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc)) + else: + text.append("Description is currently unset.") + text = "\n\n".join(text) + options = _wizard_options("meta_key", "meta_tags") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop='meta_desc', + processor=lambda s: s.strip(), + next_node="node_meta_tags"))}) + + return text, options + + +def node_meta_tags(caller): + metaprot = _get_menu_metaprot(caller) + text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " + "Separate multiple by tags by commas."] + tags = metaprot.tags + + if tags: + text.append("The current tags are:\n|w{tags}|n".format(tags=tags)) + else: + text.append("No tags are currently set.") + text = "\n\n".join(text) + options = _wizard_options("meta_desc", "meta_locks") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="meta_tags", + processor=lambda s: [ + str(part.strip().lower()) for part in s.split(",")], + next_node="node_meta_locks"))}) + return text, options + + +def node_meta_locks(caller): + metaprot = _get_menu_metaprot(caller) + text = ["Set |wMeta-Locks|n on the prototype. There are two valid lock types: " + "'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n" + "(If you are unsure, leave as default.)"] + locks = metaprot.locks + if locks: + text.append("Current lock is |w'{lockstring}'|n".format(lockstring=locks)) + else: + text.append("Lock unset - if not changed the default lockstring will be set as\n" + " |w'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) + text = "\n\n".join(text) + options = _wizard_options("meta_tags", "index") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="meta_locks", + processor=lambda s: s.strip().lower(), + next_node="node_index"))}) + return text, options - return "\n".join(text), options def start_olc(caller, session=None, metaproto=None): @@ -923,10 +1139,21 @@ def start_olc(caller, session=None, metaproto=None): """ menudata = {"node_index": node_index, "node_meta_key": node_meta_key, + "node_prototype": node_prototype, + "node_typeclass": node_typeclass, + "node_key": node_key, + "node_aliases": node_aliases, + "node_attrs": node_attrs, + "node_tags": node_tags, + "node_locks": node_locks, + "node_permissions": node_permissions, + "node_location": node_location, + "node_home": node_home, + "node_destination": node_destination, "node_meta_desc": node_meta_desc, "node_meta_tags": node_meta_tags, "node_meta_locks": node_meta_locks, - "node_key": node_key} + } EvMenu(caller, menudata, startnode='node_index', session=session, olc_metaproto=metaproto) From ca746f9af2fa1e123cc4c477fa9ebc338e246ebc Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 28 Mar 2018 00:02:00 +0200 Subject: [PATCH 028/103] Start add list_node EvMenu node decorator --- evennia/utils/evmenu.py | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 94c1467419..148ba4c0dc 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -166,6 +166,7 @@ evennia.utils.evmenu`. from __future__ import print_function import random from builtins import object, range +import re from textwrap import dedent from inspect import isfunction, getargspec @@ -972,6 +973,107 @@ class EvMenu(object): return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext +# ----------------------------------------------------------- +# +# List node +# +# ----------------------------------------------------------- + +def list_node(option_list, examine_processor, goto_processor, pagesize=10): + """ + Decorator for making an EvMenu node into a multi-page list node. Will add new options, + prepending those options added in the node. + + Args: + option_list (list): List of strings indicating the options. + examine_processor (callable): Will be called with the caller and the chosen option when + examining said option. Should return a text string to display in the node. + goto_processor (callable): Will be called with caller and + the chosen option from the optionlist. Should return the target node to goto after the + selection. + pagesize (int): How many options to show per page. + + Example: + + @list_node(['foo', 'bar'], examine_processor, goto_processor) + def node_index(caller): + text = "describing the list" + return text, [] + + """ + + def _rerouter(caller, raw_string): + "Parse which input was given, select from option_list" + + caller.ndb._menutree + + goto_processor + + + + def decorator(func): + + all_options = [{"desc": opt, "goto": _rerouter} for opt in option_list] + all_options = list(sorted(all_options, key=lambda d: d["desc"])) + + nall_options = len(all_options) + pages = [all_options[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)] + npages = len(pages) + + def _examine_select(caller, raw_string, **kwargs): + + match = re.search(r"[0-9]+$", raw_string) + + + page_index = kwargs.get("optionpage_index", 0) + + + def _list_node(caller, raw_string, **kwargs): + + # update text with detail, if set + + + # dynamic, multi-page option list + page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0))) + + options = pages[page_index] + + if options: + if npages > 1: + # if the goto callable returns None, the same node is rerun, and + # kwargs not used by the callable are passed on to the node. + if page_index > 0: + options.append({"desc": "prev", + "goto": (lambda caller: None, + {"optionpage_index": page_index - 1})}) + if page_index < npages - 1: + options.append({"desc": "next", + "goto": (lambda caller: None, + {"optionpage_index": page_index + 1})}) + options.append({"key": "_default", + "goto": (_examine_select, {"optionpage_index": page_index})}) + + # add data from the decorated node + + try: + text, extra_options = func(caller, raw_string) + except Exception: + logger.log_trace() + else: + if isinstance(extra_options, {}): + extra_options = [extra_options] + else: + extra_options = make_iter(extra_options) + options.append(extra_options) + + return text, options + + return _list_node + return decorator + + + + # ------------------------------------------------------------------------------------------------- # # Simple input shortcuts From 058a3085e4c0efc955b93c6a9472f5af6c110ca2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 28 Mar 2018 23:56:23 +0200 Subject: [PATCH 029/103] Almost working list_node evmenu decorator --- evennia/utils/evmenu.py | 105 +++++++++++++++++++++++---------------- evennia/utils/spawner.py | 15 +++++- 2 files changed, 76 insertions(+), 44 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 148ba4c0dc..6c8729aee1 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1002,61 +1002,82 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10): """ - def _rerouter(caller, raw_string): - "Parse which input was given, select from option_list" - - caller.ndb._menutree - - goto_processor - - - def decorator(func): - all_options = [{"desc": opt, "goto": _rerouter} for opt in option_list] - all_options = list(sorted(all_options, key=lambda d: d["desc"])) + def _input_parser(caller, raw_string, **kwargs): + "Parse which input was given, select from option_list" - nall_options = len(all_options) - pages = [all_options[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)] + available_choices = kwargs.get("available_choices", []) + processor = kwargs.get("selection_processor") + try: + match_ind = int(re.search(r"[0-9]+$", raw_string).group()) - 1 + selection = available_choices[match_ind] + except (AttributeError, KeyError, IndexError, ValueError): + return None + + if processor: + try: + return processor(caller, selection) + except Exception: + logger.log_trace() + return selection + + nall_options = len(option_list) + pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)] npages = len(pages) - def _examine_select(caller, raw_string, **kwargs): - - match = re.search(r"[0-9]+$", raw_string) - - - page_index = kwargs.get("optionpage_index", 0) - - def _list_node(caller, raw_string, **kwargs): - # update text with detail, if set - - - # dynamic, multi-page option list page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0))) + page = pages[page_index] - options = pages[page_index] + # dynamic, multi-page option list. We use _input_parser as a goto-callable, + # with the `goto_processor` redirecting when we leave the node. + options = [{"desc": opt, + "goto": (_input_parser, + {"available_choices": page, + "selection_processor": goto_processor})} for opt in page] - if options: - if npages > 1: - # if the goto callable returns None, the same node is rerun, and - # kwargs not used by the callable are passed on to the node. - if page_index > 0: - options.append({"desc": "prev", - "goto": (lambda caller: None, - {"optionpage_index": page_index - 1})}) - if page_index < npages - 1: - options.append({"desc": "next", - "goto": (lambda caller: None, - {"optionpage_index": page_index + 1})}) - options.append({"key": "_default", - "goto": (_examine_select, {"optionpage_index": page_index})}) + if npages > 1: + # if the goto callable returns None, the same node is rerun, and + # kwargs not used by the callable are passed on to the node. This + # allows us to call ourselves over and over, using different kwargs. + if page_index > 0: + options.append({"key": ("|wb|Wack|n", "b"), + "goto": (lambda caller: None, + {"optionpage_index": page_index - 1})}) + if page_index < npages - 1: + options.append({"key": ("|wn|Wext|n", "n"), + "goto": (lambda caller: None, + {"optionpage_index": page_index + 1})}) + + options.append({"key": ("|Wcurrent|n", "c"), + "desc": "|W({}/{})|n".format(page_index + 1, npages), + "goto": (lambda caller: None, + {"optionpage_index": page_index})}) + options.append({"key": "_default", + "goto": (lambda caller: None, + {"show_detail": True, "optionpage_index": page_index})}) + + # update text with detail, if set. Here we call _input_parser like a normal function + text_detail = None + if raw_string and 'show_detail' in kwargs: + text_detail = _input_parser( + caller, raw_string, **{"available_choices": page, + "selection_processor": examine_processor}) + if text_detail is None: + text_detail = "|rThat's not a valid command or option.|n" # add data from the decorated node + text = '' try: text, extra_options = func(caller, raw_string) + except TypeError: + try: + text, extra_options = func(caller) + except Exception: + raise except Exception: logger.log_trace() else: @@ -1066,14 +1087,14 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10): extra_options = make_iter(extra_options) options.append(extra_options) + text = text + "\n\n" + text_detail if text_detail else text + return text, options return _list_node return decorator - - # ------------------------------------------------------------------------------------------------- # # Simple input shortcuts diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 25298476c6..3b2d6d4353 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -109,13 +109,14 @@ from django.conf import settings from random import randint import evennia from evennia.objects.models import ObjectDB -from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj, is_iter, crop +from evennia.utils.utils import ( + make_iter, all_from_module, dbid_to_obj, is_iter, crop, get_all_typeclasses) from collections import namedtuple from evennia.scripts.scripts import DefaultScript from evennia.utils.create import create_script from evennia.utils.evtable import EvTable -from evennia.utils.evmenu import EvMenu +from evennia.utils.evmenu import EvMenu, list_node _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") @@ -856,6 +857,16 @@ def node_prototype(caller): return text, options +def _typeclass_examine(caller, typeclass): + return "This is typeclass |y{}|n.".format(typeclass) + + +def _typeclass_select(caller, typeclass): + caller.msg("Selected typeclass |y{}|n.".format(typeclass)) + return None + + +@list_node(list(sorted(get_all_typeclasses().keys())), _typeclass_examine, _typeclass_select) def node_typeclass(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype From 1689d54ff3631a8ba224ac72a86c97f3e888b522 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 31 Mar 2018 21:10:20 +0200 Subject: [PATCH 030/103] New list_node decorator for evmenu. Tested with olc menu --- evennia/commands/default/building.py | 16 +-- evennia/utils/evmenu.py | 52 ++++++---- evennia/utils/spawner.py | 141 +++++++++++++++++++++------ evennia/utils/utils.py | 9 +- 4 files changed, 152 insertions(+), 66 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index dc70e52cea..759247acc7 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -15,7 +15,8 @@ from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.spawner import (spawn, search_prototype, list_prototypes, save_db_prototype, build_metaproto, validate_prototype, - delete_db_prototype, PermissionError, start_olc) + delete_db_prototype, PermissionError, start_olc, + metaproto_to_str) from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2886,21 +2887,10 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): def _search_show_prototype(query, metaprots=None): # prototype detail - strings = [] if not metaprots: 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) + return "\n".join(metaproto_to_str(metaprot) for metaprot in metaprots) else: return False diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 6c8729aee1..cccda6798f 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -979,18 +979,20 @@ class EvMenu(object): # # ----------------------------------------------------------- -def list_node(option_list, examine_processor, goto_processor, pagesize=10): +def list_node(option_generator, examine_processor, goto_processor, pagesize=10): """ Decorator for making an EvMenu node into a multi-page list node. Will add new options, prepending those options added in the node. Args: - option_list (list): List of strings indicating the options. - examine_processor (callable): Will be called with the caller and the chosen option when - examining said option. Should return a text string to display in the node. - goto_processor (callable): Will be called with caller and - the chosen option from the optionlist. Should return the target node to goto after the - selection. + option_generator (callable or list): A list of strings indicating the options, or a callable + that is called without any arguments to produce such a list. + examine_processor (callable, optional): Will be called with the caller and the chosen option + when examining said option. Should return a text string to display in the node. + goto_processor (callable, optional): Will be called as goto_processor(caller, menuchoice) + where menuchoice is the chosen option as a string. Should return the target node to + goto after this selection. + pagesize (int): How many options to show per page. Example: @@ -1009,6 +1011,7 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10): available_choices = kwargs.get("available_choices", []) processor = kwargs.get("selection_processor") + try: match_ind = int(re.search(r"[0-9]+$", raw_string).group()) - 1 selection = available_choices[match_ind] @@ -1022,12 +1025,14 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10): logger.log_trace() return selection - nall_options = len(option_list) - pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)] - npages = len(pages) - def _list_node(caller, raw_string, **kwargs): + option_list = option_generator() if callable(option_generator) else option_generator + + nall_options = len(option_list) + pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)] + npages = len(pages) + page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0))) page = pages[page_index] @@ -1042,19 +1047,21 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10): # if the goto callable returns None, the same node is rerun, and # kwargs not used by the callable are passed on to the node. This # allows us to call ourselves over and over, using different kwargs. - if page_index > 0: - options.append({"key": ("|wb|Wack|n", "b"), - "goto": (lambda caller: None, - {"optionpage_index": page_index - 1})}) - if page_index < npages - 1: - options.append({"key": ("|wn|Wext|n", "n"), - "goto": (lambda caller: None, - {"optionpage_index": page_index + 1})}) - options.append({"key": ("|Wcurrent|n", "c"), "desc": "|W({}/{})|n".format(page_index + 1, npages), "goto": (lambda caller: None, {"optionpage_index": page_index})}) + if page_index > 0: + options.append({"key": ("|wp|Wrevious page|n", "p"), + "goto": (lambda caller: None, + {"optionpage_index": page_index - 1})}) + if page_index < npages - 1: + options.append({"key": ("|wn|Wext page|n", "n"), + "goto": (lambda caller: None, + {"optionpage_index": page_index + 1})}) + + + # this catches arbitrary input, notably to examine entries ('look 4' or 'l4' etc) options.append({"key": "_default", "goto": (lambda caller: None, {"show_detail": True, "optionpage_index": page_index})}) @@ -1071,6 +1078,7 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10): # add data from the decorated node text = '' + extra_options = [] try: text, extra_options = func(caller, raw_string) except TypeError: @@ -1080,14 +1088,16 @@ def list_node(option_list, examine_processor, goto_processor, pagesize=10): raise except Exception: logger.log_trace() + print("extra_options:", extra_options) else: if isinstance(extra_options, {}): extra_options = [extra_options] else: extra_options = make_iter(extra_options) - options.append(extra_options) + options.extend(extra_options) text = text + "\n\n" + text_detail if text_detail else text + text += "\n\n(Make a choice or enter 'look ' to examine an option closer)" return text, options diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 3b2d6d4353..8caa9ae0c2 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -333,7 +333,7 @@ def search_module_prototype(key=None, tags=None): def search_prototype(key=None, tags=None, return_meta=True): """ - Find prototypes based on key and/or tags. + Find prototypes based on key and/or tags, or all prototypes. Kwargs: key (str): An exact or partial key to query for. @@ -344,7 +344,8 @@ def search_prototype(key=None, tags=None, return_meta=True): return MetaProto namedtuples including prototype meta info Return: - matches (list): All found prototype dicts or MetaProtos + matches (list): All found prototype dicts or MetaProtos. If no keys + or tags are given, all available prototypes/MetaProtos will be returned. Note: The available prototypes is a combination of those supplied in @@ -438,6 +439,25 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed table.reformat_column(3, width=20) return table + +def metaproto_to_str(metaproto): + """ + Format a metaproto to a nice string representation. + + Args: + metaproto (NamedTuple): Represents the prototype. + """ + header = ( + "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" + "|cdesc:|n {} \n|cprototype:|n ".format( + metaproto.key, ", ".join(metaproto.tags), + metaproto.locks, metaproto.desc)) + prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value) + for key, value in + sorted(metaproto.prototype.items())).rstrip(","))) + return header + prototype + + # Spawner mechanism @@ -660,7 +680,13 @@ def spawn(*prototypes, **kwargs): return _batch_create_object(*objsparams) -# prototype design menu nodes +# ------------------------------------------------------------ +# +# OLC Prototype design menu +# +# ------------------------------------------------------------ + +# Helper functions def _get_menu_metaprot(caller): if hasattr(caller.ndb._menutree, "olc_metaprot"): @@ -683,7 +709,7 @@ def _set_menu_metaprot(caller, field, value): caller.ndb._menutree.olc_metaprot = build_metaproto(**kwargs) -def _format_property(key, required=False, metaprot=None, prototype=None): +def _format_property(key, required=False, metaprot=None, prototype=None, cropper=None): key = key.lower() if metaprot is not None: prop = getattr(metaprot, key) or '' @@ -700,7 +726,7 @@ def _format_property(key, required=False, metaprot=None, prototype=None): out = ", ".join(str(pr) for pr in prop) if not out and required: out = "|rrequired" - return " ({}|n)".format(crop(out, _MENU_CROP_WIDTH)) + return " ({}|n)".format(cropper(out) if cropper else crop(out, _MENU_CROP_WIDTH)) def _set_property(caller, raw_string, **kwargs): @@ -744,26 +770,43 @@ def _set_property(caller, raw_string, **kwargs): metaprot = _get_menu_metaprot(caller) prototype = metaprot.prototype prototype[propname_low] = value + + # typeclass and prototype can't co-exist + if propname_low == "typeclass": + prototype.pop("prototype", None) + if propname_low == "prototype": + prototype.pop("typeclass", None) + _set_menu_metaprot(caller, "prototype", prototype) + caller.msg("Set {prop} to {value}.".format( prop=prop.replace("_", "-").capitalize(), value=str(value))) return next_node -def _wizard_options(prev_node, next_node): - options = [{"desc": "forward ({})".format(next_node.replace("_", "-")), +def _wizard_options(prev_node, next_node, color="|W"): + options = [{"key": ("|wf|Worward", "f"), + "desc": "{color}({node})|n".format( + color=color, node=next_node.replace("_", "-")), "goto": "node_{}".format(next_node)}, - {"desc": "back ({})".format(prev_node.replace("_", "-")), + {"key": ("|wb|Wack", "b"), + "desc": "{color}({node})|n".format( + color=color, node=prev_node.replace("_", "-")), "goto": "node_{}".format(prev_node)}] if "index" not in (prev_node, next_node): - options.append({"desc": "index", + options.append({"key": ("|wi|Wndex", "i"), "goto": "node_index"}) return options -# menu nodes +def _path_cropper(pythonpath): + "Crop path to only the last component" + return pythonpath.split('.')[-1] + + +# Menu nodes def node_index(caller): metaprot = _get_menu_metaprot(caller) @@ -784,15 +827,20 @@ def node_index(caller): "goto": "node_meta_key"}) for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', 'Permissions', 'Location', 'Home', 'Destination'): - req = False + required = False + cropper = None if key in ("Prototype", "Typeclass"): - req = "prototype" not in prototype and "typeclass" not in prototype + required = "prototype" not in prototype and "typeclass" not in prototype + if key == 'Typeclass': + cropper = _path_cropper options.append( - {"desc": "|w{}|n{}".format(key, _format_property(key, req, None, prototype)), + {"desc": "|w{}|n{}".format( + key, _format_property(key, required, None, prototype, cropper=cropper)), "goto": "node_{}".format(key.lower())}) + required = False for key in ('Desc', 'Tags', 'Locks'): options.append( - {"desc": "|WMeta-{}|n|n{}".format(key, _format_property(key, req, metaprot, None)), + {"desc": "|WMeta-{}|n|n{}".format(key, _format_property(key, required, metaprot, None)), "goto": "node_meta_{}".format(key.lower())}) return text, options @@ -837,6 +885,24 @@ def node_meta_key(caller): return text, options +def _all_prototypes(): + return [mproto.key for mproto in search_prototype()] + + +def _prototype_examine(caller, prototype_name): + metaprot = search_prototype(key=prototype_name) + if metaprot: + return metaproto_to_str(metaprot[0]) + return "Prototype not registered." + + +def _prototype_select(caller, prototype): + ret = _set_property(caller, prototype, prop="prototype", processor=str, next_node="node_key") + caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype)) + return ret + + +@list_node(_all_prototypes, _prototype_examine, _prototype_select) def node_prototype(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype @@ -848,25 +914,43 @@ def node_prototype(caller): else: text.append("Parent prototype is not set") text = "\n\n".join(text) - options = _wizard_options("meta_key", "typeclass") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="prototype", - processor=lambda s: s.strip(), - next_node="node_typeclass"))}) + options = _wizard_options("meta_key", "typeclass", color="|W") return text, options -def _typeclass_examine(caller, typeclass): - return "This is typeclass |y{}|n.".format(typeclass) +def _all_typeclasses(): + return list(sorted(get_all_typeclasses().keys())) + # return list(sorted(get_all_typeclasses(parent="evennia.objects.objects.DefaultObject").keys())) + + +def _typeclass_examine(caller, typeclass_path): + if typeclass_path is None: + # this means we are exiting the listing + return "node_key" + + typeclass = get_all_typeclasses().get(typeclass_path) + if typeclass: + docstr = [] + for line in typeclass.__doc__.split("\n"): + if line.strip(): + docstr.append(line) + elif docstr: + break + docstr = '\n'.join(docstr) if docstr else "" + txt = "Typeclass |y{typeclass_path}|n; First paragraph of docstring:\n\n{docstring}".format( + typeclass_path=typeclass_path, docstring=docstr) + else: + txt = "This is typeclass |y{}|n.".format(typeclass) + return txt def _typeclass_select(caller, typeclass): - caller.msg("Selected typeclass |y{}|n.".format(typeclass)) - return None + ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key") + caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass)) + return ret -@list_node(list(sorted(get_all_typeclasses().keys())), _typeclass_examine, _typeclass_select) +@list_node(_all_typeclasses, _typeclass_examine, _typeclass_select) def node_typeclass(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype @@ -879,12 +963,7 @@ def node_typeclass(caller): text.append("Using default typeclass {typeclass}.".format( typeclass=settings.BASE_OBJECT_TYPECLASS)) text = "\n\n".join(text) - options = _wizard_options("prototype", "key") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="typeclass", - processor=lambda s: s.strip(), - next_node="node_key"))}) + options = _wizard_options("prototype", "key", color="|W") return text, options diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index a8d2171f75..22d59a165f 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1882,10 +1882,14 @@ def get_game_dir_path(): raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.") -def get_all_typeclasses(): +def get_all_typeclasses(parent=None): """ List available typeclasses from all available modules. + Args: + parent (str, optional): If given, only return typeclasses inheriting (at any distance) + from this parent. + Returns: typeclasses (dict): On the form {"typeclass.path": typeclass, ...} @@ -1898,4 +1902,7 @@ def get_all_typeclasses(): from evennia.typeclasses.models import TypedObject typeclasses = {"{}.{}".format(model.__module__, model.__name__): model for model in apps.get_models() if TypedObject in getmro(model)} + if parent: + typeclasses = {name: typeclass for name, typeclass in typeclasses.items() + if inherits_from(typeclass, parent)} return typeclasses From 7dd6c2bd593947d029fe410159e4f0e6255cff09 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 31 Mar 2018 22:58:17 +0200 Subject: [PATCH 031/103] Custom OLCMenu class, validate prot from menu --- evennia/utils/spawner.py | 106 +++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 8caa9ae0c2..c9a68ea8ab 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -117,6 +117,7 @@ from evennia.scripts.scripts import DefaultScript from evennia.utils.create import create_script from evennia.utils.evtable import EvTable from evennia.utils.evmenu import EvMenu, list_node +from evennia.utils.ansi import strip_ansi _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") @@ -779,25 +780,33 @@ def _set_property(caller, raw_string, **kwargs): _set_menu_metaprot(caller, "prototype", prototype) - caller.msg("Set {prop} to {value}.".format( prop=prop.replace("_", "-").capitalize(), value=str(value))) return next_node -def _wizard_options(prev_node, next_node, color="|W"): - options = [{"key": ("|wf|Worward", "f"), - "desc": "{color}({node})|n".format( - color=color, node=next_node.replace("_", "-")), - "goto": "node_{}".format(next_node)}, - {"key": ("|wb|Wack", "b"), - "desc": "{color}({node})|n".format( - color=color, node=prev_node.replace("_", "-")), - "goto": "node_{}".format(prev_node)}] +def _wizard_options(curr_node, prev_node, next_node, color="|W"): + options = [] + if prev_node: + options.append({"key": ("|wb|Wack", "b"), + "desc": "{color}({node})|n".format( + color=color, node=prev_node.replace("_", "-")), + "goto": "node_{}".format(prev_node)}) + if next_node: + options.append({"key": ("|wf|Worward", "f"), + "desc": "{color}({node})|n".format( + color=color, node=next_node.replace("_", "-")), + "goto": "node_{}".format(next_node)}) + if "index" not in (prev_node, next_node): options.append({"key": ("|wi|Wndex", "i"), "goto": "node_index"}) + + if curr_node: + options.append({"key": ("|wv|Walidate prototype", "v"), + "goto": ("node_validate_prototype", {"back": curr_node})}) + return options @@ -846,6 +855,23 @@ def node_index(caller): return text, options +def node_validate_prototype(caller, raw_string, **kwargs): + metaprot = _get_menu_metaprot(caller) + + txt = metaproto_to_str(metaprot) + errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)" + try: + # validate, don't spawn + spawn(metaprot.prototype, return_prototypes=True) + except RuntimeError as err: + errors = "\n\n|rError: {}|n".format(err) + text = (txt + errors) + + options = _wizard_options(None, kwargs.get("back"), None) + + return text, options + + def _check_meta_key(caller, key): old_metaprot = search_prototype(key) olc_new = caller.ndb._menutree.olc_new @@ -879,7 +905,7 @@ def node_meta_key(caller): text.append("The key is currently unset.") text.append("Enter text or make a choice (q for quit)") text = "\n\n".join(text) - options = _wizard_options("index", "prototype") + options = _wizard_options("meta_key", "index", "prototype") options.append({"key": "_default", "goto": _check_meta_key}) return text, options @@ -914,7 +940,7 @@ def node_prototype(caller): else: text.append("Parent prototype is not set") text = "\n\n".join(text) - options = _wizard_options("meta_key", "typeclass", color="|W") + options = _wizard_options("prototype", "meta_key", "typeclass", color="|W") return text, options @@ -963,7 +989,7 @@ def node_typeclass(caller): text.append("Using default typeclass {typeclass}.".format( typeclass=settings.BASE_OBJECT_TYPECLASS)) text = "\n\n".join(text) - options = _wizard_options("prototype", "key", color="|W") + options = _wizard_options("typeclass", "prototype", "key", color="|W") return text, options @@ -978,7 +1004,7 @@ def node_key(caller): else: text.append("Key is currently unset.") text = "\n\n".join(text) - options = _wizard_options("typeclass", "aliases") + options = _wizard_options("key", "typeclass", "aliases") options.append({"key": "_default", "goto": (_set_property, dict(prop="key", @@ -999,7 +1025,7 @@ def node_aliases(caller): else: text.append("No aliases are set.") text = "\n\n".join(text) - options = _wizard_options("key", "attrs") + options = _wizard_options("aliases", "key", "attrs") options.append({"key": "_default", "goto": (_set_property, dict(prop="aliases", @@ -1020,7 +1046,7 @@ def node_attrs(caller): else: text.append("No attrs are set.") text = "\n\n".join(text) - options = _wizard_options("aliases", "tags") + options = _wizard_options("attrs", "aliases", "tags") options.append({"key": "_default", "goto": (_set_property, dict(prop="attrs", @@ -1041,7 +1067,7 @@ def node_tags(caller): else: text.append("No tags are set.") text = "\n\n".join(text) - options = _wizard_options("attrs", "locks") + options = _wizard_options("tags", "attrs", "locks") options.append({"key": "_default", "goto": (_set_property, dict(prop="tags", @@ -1062,7 +1088,7 @@ def node_locks(caller): else: text.append("No locks are set.") text = "\n\n".join(text) - options = _wizard_options("tags", "permissions") + options = _wizard_options("locks", "tags", "permissions") options.append({"key": "_default", "goto": (_set_property, dict(prop="locks", @@ -1083,7 +1109,7 @@ def node_permissions(caller): else: text.append("No permissions are set.") text = "\n\n".join(text) - options = _wizard_options("destination", "location") + options = _wizard_options("permissions", "destination", "location") options.append({"key": "_default", "goto": (_set_property, dict(prop="permissions", @@ -1103,7 +1129,7 @@ def node_location(caller): else: text.append("Default location is {}'s inventory.".format(caller)) text = "\n\n".join(text) - options = _wizard_options("permissions", "home") + options = _wizard_options("location", "permissions", "home") options.append({"key": "_default", "goto": (_set_property, dict(prop="location", @@ -1123,7 +1149,7 @@ def node_home(caller): else: text.append("Default home location (|y{home}|n) used.".format(home=settings.DEFAULT_HOME)) text = "\n\n".join(text) - options = _wizard_options("aliases", "destination") + options = _wizard_options("home", "aliases", "destination") options.append({"key": "_default", "goto": (_set_property, dict(prop="home", @@ -1143,7 +1169,7 @@ def node_destination(caller): else: text.append("No destination is set (default).") text = "\n\n".join(text) - options = _wizard_options("home", "meta_desc") + options = _wizard_options("destination", "home", "meta_desc") options.append({"key": "_default", "goto": (_set_property, dict(prop="dest", @@ -1163,7 +1189,7 @@ def node_meta_desc(caller): else: text.append("Description is currently unset.") text = "\n\n".join(text) - options = _wizard_options("meta_key", "meta_tags") + options = _wizard_options("meta_desc", "meta_key", "meta_tags") options.append({"key": "_default", "goto": (_set_property, dict(prop='meta_desc', @@ -1184,7 +1210,7 @@ def node_meta_tags(caller): else: text.append("No tags are currently set.") text = "\n\n".join(text) - options = _wizard_options("meta_desc", "meta_locks") + options = _wizard_options("meta_tags", "meta_desc", "meta_locks") options.append({"key": "_default", "goto": (_set_property, dict(prop="meta_tags", @@ -1206,7 +1232,7 @@ def node_meta_locks(caller): text.append("Lock unset - if not changed the default lockstring will be set as\n" " |w'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) text = "\n\n".join(text) - options = _wizard_options("meta_tags", "index") + options = _wizard_options("meta_locks", "meta_tags", "index") options.append({"key": "_default", "goto": (_set_property, dict(prop="meta_locks", @@ -1215,6 +1241,33 @@ def node_meta_locks(caller): return text, options +class OLCMenu(EvMenu): + """ + A custom EvMenu with a different formatting for the options. + + """ + def options_formatter(self, optionlist): + """ + Split the options into two blocks - olc options and normal options + + """ + olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype") + olc_options = [] + other_options = [] + for key, desc in optionlist: + raw_key = strip_ansi(key) + if raw_key in olc_keys: + desc = " {}".format(desc) if desc else "" + olc_options.append("|lc{}|lt{}|le{}".format(raw_key, key, desc)) + else: + other_options.append((key, desc)) + + olc_options = " | ".join(olc_options) + " | " + "|wq|Wuit" if olc_options else "" + other_options = super(OLCMenu, self).options_formatter(other_options) + sep = "\n\n" if olc_options and other_options else "" + + return "{}{}{}".format(olc_options, sep, other_options) + def start_olc(caller, session=None, metaproto=None): """ @@ -1228,6 +1281,7 @@ def start_olc(caller, session=None, metaproto=None): """ menudata = {"node_index": node_index, + "node_validate_prototype": node_validate_prototype, "node_meta_key": node_meta_key, "node_prototype": node_prototype, "node_typeclass": node_typeclass, @@ -1244,7 +1298,7 @@ def start_olc(caller, session=None, metaproto=None): "node_meta_tags": node_meta_tags, "node_meta_locks": node_meta_locks, } - EvMenu(caller, menudata, startnode='node_index', session=session, olc_metaproto=metaproto) + OLCMenu(caller, menudata, startnode='node_index', session=session, olc_metaproto=metaproto) # Testing From 43cec126ed6755bf6a9bdb7cc1ccf8c70212ccc6 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 1 Apr 2018 08:29:35 +0200 Subject: [PATCH 032/103] Better handle logfile cycle while tailing --- evennia/server/evennia_launcher.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index c82b922ca0..2f9206b4c1 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -1037,9 +1037,11 @@ def tail_log_files(filename1, filename2, start_lines1=20, start_lines2=20, rate= new_linecount = sum(blck.count("\n") for blck in _block(filehandle)) if new_linecount < old_linecount: - # this could happen if the file was manually deleted or edited - print("Log file has shrunk. Restart log reader.") - sys.exit() + # this happens if the file was cycled or manually deleted/edited. + print(" ** Log file {filename} has cycled or been edited. " + "Restarting log. ".format(filehandle.name)) + new_linecount = 0 + old_linecount = 0 lines_to_get = max(0, new_linecount - old_linecount) From 9e46d996b155ebdb045c7f8106aaa3d4a4ed3b97 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 1 Apr 2018 09:26:55 +0200 Subject: [PATCH 033/103] Fix olc with existing prototype --- evennia/commands/default/building.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 759247acc7..77bdb619f1 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2909,7 +2909,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): elif metaprot: # one match metaprot = metaprot[0] - start_olc(caller, self.session, metaprot) + start_olc(caller, session=self.session, metaproto=metaprot) return if 'search' in self.switches: From 34d63e9d23be8259da61837efb28207c3cfb66ba Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 1 Apr 2018 09:29:44 +0200 Subject: [PATCH 034/103] fix olc bug with single prototype --- evennia/utils/spawner.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index c9a68ea8ab..bab8cdc9a1 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -765,6 +765,9 @@ def _set_property(caller, raw_string, **kwargs): else: value = raw_string + if not value: + return next_node + if meta: _set_menu_metaprot(caller, propname_low, value) else: @@ -780,7 +783,7 @@ def _set_property(caller, raw_string, **kwargs): _set_menu_metaprot(caller, "prototype", prototype) - caller.msg("Set {prop} to {value}.".format( + caller.msg("Set {prop} to '{value}'.".format( prop=prop.replace("_", "-").capitalize(), value=str(value))) return next_node @@ -874,7 +877,7 @@ def node_validate_prototype(caller, raw_string, **kwargs): def _check_meta_key(caller, key): old_metaprot = search_prototype(key) - olc_new = caller.ndb._menutree.olc_new + olc_new = _is_new_prototype(caller) key = key.strip().lower() if old_metaprot: # we are starting a new prototype that matches an existing @@ -1298,7 +1301,7 @@ def start_olc(caller, session=None, metaproto=None): "node_meta_tags": node_meta_tags, "node_meta_locks": node_meta_locks, } - OLCMenu(caller, menudata, startnode='node_index', session=session, olc_metaproto=metaproto) + OLCMenu(caller, menudata, startnode='node_index', session=session, olc_metaprot=metaproto) # Testing From 0e9d6f9c0582ada7dbc935735a0d2e16c55e1d96 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 2 Apr 2018 18:46:55 +0200 Subject: [PATCH 035/103] Start add edit_node decorator (untested) --- evennia/utils/evmenu.py | 427 +++++++++++++++++++++++++++++---------- evennia/utils/spawner.py | 10 +- 2 files changed, 331 insertions(+), 106 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index cccda6798f..e9e3f1c8af 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -175,7 +175,7 @@ from evennia import Command, CmdSet from evennia.utils import logger from evennia.utils.evtable import EvTable from evennia.utils.ansi import strip_ansi -from evennia.utils.utils import mod_import, make_iter, pad, m_len +from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter from evennia.commands import cmdhandler # read from protocol NAWS later? @@ -683,6 +683,43 @@ class EvMenu(object): return ret, kwargs return None + def extract_goto_exec(self, nodename, option_dict): + """ + Helper: Get callables and their eventual kwargs. + + Args: + nodename (str): The current node name (used for error reporting). + option_dict (dict): The seleted option's dict. + + Returns: + goto (str, callable or None): The goto directive in the option. + goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty. + execute (callable or None): Executable given by the `exec` directive. + exec_kwargs (dict): Kwargs for `execute` if it's callable, otherwise empty. + + """ + goto_kwargs, exec_kwargs = {}, {} + goto, execute = option_dict.get("goto", None), option_dict.get("exec", None) + if goto and isinstance(goto, (tuple, list)): + if len(goto) > 1: + goto, goto_kwargs = goto[:2] # ignore any extra arguments + if not hasattr(goto_kwargs, "__getitem__"): + # not a dict-like structure + raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format( + nodename, goto_kwargs)) + else: + goto = goto[0] + if execute and isinstance(execute, (tuple, list)): + if len(execute) > 1: + execute, exec_kwargs = execute[:2] # ignore any extra arguments + if not hasattr(exec_kwargs, "__getitem__"): + # not a dict-like structure + raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format( + nodename, goto_kwargs)) + else: + execute = execute[0] + return goto, goto_kwargs, execute, exec_kwargs + def goto(self, nodename, raw_string, **kwargs): """ Run a node by name, optionally dynamically generating that name first. @@ -696,29 +733,6 @@ class EvMenu(object): argument) """ - def _extract_goto_exec(option_dict): - "Helper: Get callables and their eventual kwargs" - goto_kwargs, exec_kwargs = {}, {} - goto, execute = option_dict.get("goto", None), option_dict.get("exec", None) - if goto and isinstance(goto, (tuple, list)): - if len(goto) > 1: - goto, goto_kwargs = goto[:2] # ignore any extra arguments - if not hasattr(goto_kwargs, "__getitem__"): - # not a dict-like structure - raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format( - nodename, goto_kwargs)) - else: - goto = goto[0] - if execute and isinstance(execute, (tuple, list)): - if len(execute) > 1: - execute, exec_kwargs = execute[:2] # ignore any extra arguments - if not hasattr(exec_kwargs, "__getitem__"): - # not a dict-like structure - raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format( - nodename, goto_kwargs)) - else: - execute = execute[0] - return goto, goto_kwargs, execute, exec_kwargs if callable(nodename): # run the "goto" callable, if possible @@ -764,12 +778,12 @@ class EvMenu(object): desc = dic.get("desc", dic.get("text", None)) if "_default" in keys: keys = [key for key in keys if key != "_default"] - goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) + goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic) self.default = (goto, goto_kwargs, execute, exec_kwargs) else: # use the key (only) if set, otherwise use the running number keys = list(make_iter(dic.get("key", str(inum + 1).strip()))) - goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) + goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic) if keys: display_options.append((keys[0], desc)) for key in keys: @@ -975,11 +989,143 @@ class EvMenu(object): # ----------------------------------------------------------- # -# List node +# Edit node (decorator turning a node into an editing +# point for a given resource # # ----------------------------------------------------------- -def list_node(option_generator, examine_processor, goto_processor, pagesize=10): +def edit_node(edit_text, add_text, edit_callback, add_callback, get_choices=None): + """ + Decorator for turning an EvMenu node into an editing + page. Will add new options, prepending those options + added in the node. + + Args: + edit_text (str or callable): Will be used as text for the edit node. If + callable, it will be called as edittext(selection) + and should return the node text for the edit-node, probably listing + the current value of all editable propnames, if possible. + add_text (str) or callable: Gives text for node in add-mode. If a callable, + called as add_text() and should return the text for the node. + edit_callback (callable): Will be called as edit_callback(editable, raw_string) + and should return a boolean True/False if the setting of the property + succeeded or not. The value will always be a string and should be + converted as needed. + add_callback (callable): Will be called as add_callback(raw_string) and + should return a boolean True/False if the addition succeded. + + get_choices (callable): Produce the available editable choices. If this + is not given, the `goto` callable must have been provided with the + kwarg `available_choices` by the decorated node. + + """ + + def decorator(func): + + def _setter_goto(caller, raw_string, **kwargs): + editable = kwargs.get("editable") + mode = kwargs.get("edit_node_mode") + try: + if mode == 'edit': + is_ok = edit_callback(editable, raw_string) + else: + is_ok = add_callback(raw_string) + except Exception: + logger.log_trace() + if not is_ok: + caller.msg("|rValue could not be set.") + return None + + def _patch_goto(caller, raw_string, **kwargs): + + # parse incoming string to figure out if there is a match to edit/add + match = re.search(r"(^[a-zA-Z]*)\s*([0-9]*)$", raw_string) + cmd, number = match.groups() + edit_mode = None + available_choices = None + selection = None + + if get_choices: + available_choices = make_iter(get_choices(caller, raw_string, **kwargs)) + if not available_choices: + available_choices = kwargs.get("available_choices", []) + + if available_choices and cmd.startswith("e"): + try: + index = int(cmd) - 1 + selection = available_choices[index] + edit_mode = 'edit' + except (IndexError, TypeError): + caller.msg("|rNot a valid 'edit' command.") + + if cmd.startswith("a") and not number: + # add mode + edit_mode = "add" + + if edit_mode: + # replace with edit text/options + text = edit_text(selection) if edit_mode == "edit" else add_text() + options = ({"key": "_default", + "goto": (_setter_goto, + {"selection": selection, + "edit_node_mode": edit_mode})}) + return text, options + + # no matches - pass through to the original decorated goto instruction + + decorated_opt = kwargs.get("decorated_opt") + + if decorated_opt: + # use EvMenu's parser to get the goto/goto-kwargs out of + # the decorated option structure + dec_goto, dec_goto_kwargs, _, _ = \ + caller.ndb._menutree.extract_goto_exec("edit-node", decorated_opt) + + if callable(dec_goto): + try: + return dec_goto(caller, raw_string, + **{dec_goto_kwargs if dec_goto_kwargs else {}}) + except Exception: + caller.msg("|rThere was an error in the edit node.") + logger.log_trace() + return None + + def _edit_node(caller, raw_string, **kwargs): + + text, options = func(caller, raw_string, **kwargs) + + if options: + # find eventual _default in options and patch it with a handler for + # catching editing + + decorated_opt = None + iopt = 0 + for iopt, optdict in enumerate(options): + if optdict.get('key') == "_default": + decorated_opt = optdict + break + + if decorated_opt: + # inject our wrapper over the original goto instruction for the + # _default action (save the original) + options[iopt]["goto"] = (_patch_goto, + {"decorated_opt": decorated_opt}) + + return text, options + + return _edit_node + return decorator + + + +# ----------------------------------------------------------- +# +# List node (decorator turning a node into a list with +# look/edit/add functionality for the elements) +# +# ----------------------------------------------------------- + +def list_node(option_generator, select=None, examine=None, edit=None, add=None, pagesize=10): """ Decorator for making an EvMenu node into a multi-page list node. Will add new options, prepending those options added in the node. @@ -987,17 +1133,25 @@ def list_node(option_generator, examine_processor, goto_processor, pagesize=10): Args: option_generator (callable or list): A list of strings indicating the options, or a callable that is called without any arguments to produce such a list. - examine_processor (callable, optional): Will be called with the caller and the chosen option - when examining said option. Should return a text string to display in the node. - goto_processor (callable, optional): Will be called as goto_processor(caller, menuchoice) + select (callable, option): Will be called as select(caller, menuchoice) where menuchoice is the chosen option as a string. Should return the target node to - goto after this selection. + goto after this selection. Note that if this is not given, the decorated node must itself + provide a way to continue from the node! + examine (callable, optional): If given, allows for examining options in detail. Will + be called with examine(caller, menuchoice) and should return a text string to + display in-place in the node. + edit (callable, optional): If given, this callable will be called as edit(caller, menuchoice). + It should return the node-key to a node decorated with the `edit_node` decorator. The + menuchoice will automatically be stored on the menutree as `list_node_edit`. + add (tuple, optional): If given, this callable will be called as add(caller, menuchoice). + It should return the node-key to a node decorated with the `edit_node` decorator. The + menuchoice will automatically be stored on the menutree as `list_node_add`. pagesize (int): How many options to show per page. Example: - @list_node(['foo', 'bar'], examine_processor, goto_processor) + @list_node(['foo', 'bar'], examine, select) def node_index(caller): text = "describing the list" return text, [] @@ -1006,27 +1160,63 @@ def list_node(option_generator, examine_processor, goto_processor, pagesize=10): def decorator(func): - def _input_parser(caller, raw_string, **kwargs): - "Parse which input was given, select from option_list" - + def _select_parser(caller, raw_string, **kwargs): + """ + Parse the select action + """ available_choices = kwargs.get("available_choices", []) - processor = kwargs.get("selection_processor") try: - match_ind = int(re.search(r"[0-9]+$", raw_string).group()) - 1 - selection = available_choices[match_ind] - except (AttributeError, KeyError, IndexError, ValueError): - return None - - if processor: + index = int(raw_string.strip()) - 1 + selection = available_choices[index] + except Exception: + caller.msg("|rInvalid choice.|n") + else: try: - return processor(caller, selection) + return select(caller, selection) except Exception: logger.log_trace() - return selection + return None + + def _input_parser(caller, raw_string, **kwargs): + """ + Parse which input was given, select from option_list. + + Understood input is [cmd], where [cmd] is either empty (`select`) + or one of the supported actions `look`, `edit` or `add` depending on + which processors are available. + + """ + + available_choices = kwargs.get("available_choices", []) + match = re.search(r"(^[a-zA-Z]*)\s*([0-9]*)$", raw_string) + cmd, number = match.groups() + mode, selection = None, None + + if number: + number = int(number) - 1 + cmd = cmd.lower().strip() + if cmd.startswith("e") or cmd.startswith("a") and edit: + mode = "edit" + elif examine: + mode = "examine" + + try: + selection = available_choices[number] + except IndexError: + caller.msg("|rInvalid index") + mode = None + else: + caller.msg("|rMust supply a number.") + + return mode, selection + + def _relay_to_edit_or_add(caller, raw_string, **kwargs): + pass def _list_node(caller, raw_string, **kwargs): + mode = kwargs.get("list_mode", None) option_list = option_generator() if callable(option_generator) else option_generator nall_options = len(option_list) @@ -1035,71 +1225,104 @@ def list_node(option_generator, examine_processor, goto_processor, pagesize=10): page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0))) page = pages[page_index] + entry = None + extra_text = None - # dynamic, multi-page option list. We use _input_parser as a goto-callable, - # with the `goto_processor` redirecting when we leave the node. - options = [{"desc": opt, - "goto": (_input_parser, - {"available_choices": page, - "selection_processor": goto_processor})} for opt in page] + if mode == "arbitrary": + # freeform input, we must parse it for the allowed commands (look/edit) + mode, entry = _input_parser(caller, raw_string, + **{"available_choices": page}) - if npages > 1: - # if the goto callable returns None, the same node is rerun, and - # kwargs not used by the callable are passed on to the node. This - # allows us to call ourselves over and over, using different kwargs. - options.append({"key": ("|Wcurrent|n", "c"), - "desc": "|W({}/{})|n".format(page_index + 1, npages), - "goto": (lambda caller: None, - {"optionpage_index": page_index})}) - if page_index > 0: - options.append({"key": ("|wp|Wrevious page|n", "p"), - "goto": (lambda caller: None, - {"optionpage_index": page_index - 1})}) - if page_index < npages - 1: - options.append({"key": ("|wn|Wext page|n", "n"), - "goto": (lambda caller: None, - {"optionpage_index": page_index + 1})}) - - - # this catches arbitrary input, notably to examine entries ('look 4' or 'l4' etc) - options.append({"key": "_default", - "goto": (lambda caller: None, - {"show_detail": True, "optionpage_index": page_index})}) - - # update text with detail, if set. Here we call _input_parser like a normal function - text_detail = None - if raw_string and 'show_detail' in kwargs: - text_detail = _input_parser( - caller, raw_string, **{"available_choices": page, - "selection_processor": examine_processor}) - if text_detail is None: - text_detail = "|rThat's not a valid command or option.|n" - - # add data from the decorated node - - text = '' - extra_options = [] - try: - text, extra_options = func(caller, raw_string) - except TypeError: + if examine and mode: # == "look": + # look mode - we are examining a given entry try: - text, extra_options = func(caller) + text = examine(caller, entry) except Exception: - raise - except Exception: - logger.log_trace() - print("extra_options:", extra_options) + logger.log_trace() + text = "|rCould not view." + options = [{"key": ("|wb|Wack|n", "b"), + "goto": (lambda caller: None, + {"optionpage_index": page_index})}, + {"key": "_default", + "goto": (lambda caller: None, + {"optionpage_index": page_index})}] + return text, options + + # if edit and mode == "edit": + # pass + # elif add and mode == "add": + # # add mode - we are adding a new entry + # pass + else: - if isinstance(extra_options, {}): - extra_options = [extra_options] + # normal mode - list + pass + + if select: + # We have a processor to handle selecting an entry + + # dynamic, multi-page option list. Each selection leads to the `select` + # callback being called with a result from the available choices + options = [{"desc": opt, + "goto": (_select_parser, + {"available_choices": page})} for opt in page] + + if add: + # We have a processor to handle adding a new entry. Re-run this node + # in the 'add' mode + options.append({"key": ("|wadd|Wdd new|n", "a"), + "goto": (lambda caller: None, + {"optionpage_index": page_index, + "list_mode": "add"})}) + if npages > 1: + # if the goto callable returns None, the same node is rerun, and + # kwargs not used by the callable are passed on to the node. This + # allows us to call ourselves over and over, using different kwargs. + options.append({"key": ("|Wcurrent|n", "c"), + "desc": "|W({}/{})|n".format(page_index + 1, npages), + "goto": (lambda caller: None, + {"optionpage_index": page_index})}) + if page_index > 0: + options.append({"key": ("|wp|Wrevious page|n", "p"), + "goto": (lambda caller: None, + {"optionpage_index": page_index - 1})}) + if page_index < npages - 1: + options.append({"key": ("|wn|Wext page|n", "n"), + "goto": (lambda caller: None, + {"optionpage_index": page_index + 1})}) + + # this catches arbitrary input and reruns this node with the 'arbitrary' mode + # this could mean input on the form 'look ' or 'edit ' + options.append({"key": "_default", + "goto": (lambda caller: None, + {"optionpage_index": page_index, + "available_choices": page, + "list_mode": "arbitrary"})}) + + # add data from the decorated node + + extra_options = [] + try: + text, extra_options = func(caller, raw_string) + except TypeError: + try: + text, extra_options = func(caller) + except Exception: + raise + except Exception: + logger.log_trace() + print("extra_options:", extra_options) else: - extra_options = make_iter(extra_options) + if isinstance(extra_options, {}): + extra_options = [extra_options] + else: + extra_options = make_iter(extra_options) - options.extend(extra_options) - text = text + "\n\n" + text_detail if text_detail else text - text += "\n\n(Make a choice or enter 'look ' to examine an option closer)" + options.extend(extra_options) + text = text + "\n\n" + extra_text if extra_text else text + text += "\n\n(Make a choice or enter 'look ' to examine an option closer)" - return text, options + return text, options return _list_node return decorator diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index bab8cdc9a1..eaab84b202 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -690,9 +690,11 @@ def spawn(*prototypes, **kwargs): # Helper functions def _get_menu_metaprot(caller): + + metaproto = None if hasattr(caller.ndb._menutree, "olc_metaprot"): - return caller.ndb._menutree.olc_metaprot - else: + metaproto = caller.ndb._menutree.olc_metaprot + if not metaproto: metaproto = build_metaproto(None, '', [], [], None) caller.ndb._menutree.olc_metaprot = metaproto caller.ndb._menutree.olc_new = True @@ -931,7 +933,7 @@ def _prototype_select(caller, prototype): return ret -@list_node(_all_prototypes, _prototype_examine, _prototype_select) +@list_node(_all_prototypes, _prototype_select, examine=_prototype_examine) def node_prototype(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype @@ -979,7 +981,7 @@ def _typeclass_select(caller, typeclass): return ret -@list_node(_all_typeclasses, _typeclass_examine, _typeclass_select) +@list_node(_all_typeclasses, _typeclass_select, examine=_typeclass_examine) def node_typeclass(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype From 8777d6311e56652f30aa586fba601daf93fc00f9 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 2 Apr 2018 19:55:31 +0200 Subject: [PATCH 036/103] Partial edit_node functionality --- evennia/utils/evmenu.py | 42 ++++++++++++++++++++++++---------------- evennia/utils/spawner.py | 24 +++++++++-------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index e9e3f1c8af..6df9142bbb 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1132,7 +1132,7 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, Args: option_generator (callable or list): A list of strings indicating the options, or a callable - that is called without any arguments to produce such a list. + that is called as option_generator(caller) to produce such a list. select (callable, option): Will be called as select(caller, menuchoice) where menuchoice is the chosen option as a string. Should return the target node to goto after this selection. Note that if this is not given, the decorated node must itself @@ -1217,14 +1217,22 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, def _list_node(caller, raw_string, **kwargs): mode = kwargs.get("list_mode", None) - option_list = option_generator() if callable(option_generator) else option_generator + option_list = option_generator(caller) if callable(option_generator) else option_generator - nall_options = len(option_list) - pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)] - npages = len(pages) + npages = 0 + page_index = 0 + page = None + options = [] - page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0))) - page = pages[page_index] + if option_list: + nall_options = len(option_list) + pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)] + npages = len(pages) + + page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0))) + page = pages[page_index] + + text = "" entry = None extra_text = None @@ -1233,19 +1241,19 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, mode, entry = _input_parser(caller, raw_string, **{"available_choices": page}) - if examine and mode: # == "look": + if examine and mode: # == "look": # look mode - we are examining a given entry try: text = examine(caller, entry) except Exception: logger.log_trace() text = "|rCould not view." - options = [{"key": ("|wb|Wack|n", "b"), - "goto": (lambda caller: None, - {"optionpage_index": page_index})}, - {"key": "_default", - "goto": (lambda caller: None, - {"optionpage_index": page_index})}] + options.extend([{"key": ("|wb|Wack|n", "b"), + "goto": (lambda caller: None, + {"optionpage_index": page_index})}, + {"key": "_default", + "goto": (lambda caller: None, + {"optionpage_index": page_index})}]) return text, options # if edit and mode == "edit": @@ -1263,9 +1271,9 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, # dynamic, multi-page option list. Each selection leads to the `select` # callback being called with a result from the available choices - options = [{"desc": opt, - "goto": (_select_parser, - {"available_choices": page})} for opt in page] + options.extend([{"desc": opt, + "goto": (_select_parser, + {"available_choices": page})} for opt in page]) if add: # We have a processor to handle adding a new entry. Re-run this node diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index eaab84b202..ebf3ce5d67 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -916,7 +916,7 @@ def node_meta_key(caller): return text, options -def _all_prototypes(): +def _all_prototypes(caller): return [mproto.key for mproto in search_prototype()] @@ -949,7 +949,7 @@ def node_prototype(caller): return text, options -def _all_typeclasses(): +def _all_typeclasses(caller): return list(sorted(get_all_typeclasses().keys())) # return list(sorted(get_all_typeclasses(parent="evennia.objects.objects.DefaultObject").keys())) @@ -1060,24 +1060,17 @@ def node_attrs(caller): return text, options -def node_tags(caller): +def _caller_tags(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype tags = prot.get("tags") + return tags - text = ["Set the prototype's |yTags|n. Separate multiple tags with commas. " - "Will retain case sensitivity."] - if tags: - text.append("Current tags are '|y{tags}|n'.".format(tags=tags)) - else: - text.append("No tags are set.") - text = "\n\n".join(text) + +@list_node(_caller_tags) +def node_tags(caller): + text = "Set the prototype's |yTags|n." options = _wizard_options("tags", "attrs", "locks") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="tags", - processor=lambda s: [part.strip() for part in s.split(",")], - next_node="node_locks"))}) return text, options @@ -1204,6 +1197,7 @@ def node_meta_desc(caller): return text, options + def node_meta_tags(caller): metaprot = _get_menu_metaprot(caller) text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " From cb170ef89a97dd88c909d895ac11336e46fa2092 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 2 Apr 2018 23:35:35 +0200 Subject: [PATCH 037/103] Progress on expanding list_node with edit/add instead --- evennia/utils/evmenu.py | 88 +++++++++++++++++++++------------------- evennia/utils/spawner.py | 24 ++++++++++- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 6df9142bbb..c9f31b4688 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1135,17 +1135,15 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, that is called as option_generator(caller) to produce such a list. select (callable, option): Will be called as select(caller, menuchoice) where menuchoice is the chosen option as a string. Should return the target node to - goto after this selection. Note that if this is not given, the decorated node must itself - provide a way to continue from the node! + goto after this selection. Note that if this is not given, the decorated node must + itself provide a way to continue from the node! examine (callable, optional): If given, allows for examining options in detail. Will be called with examine(caller, menuchoice) and should return a text string to display in-place in the node. - edit (callable, optional): If given, this callable will be called as edit(caller, menuchoice). - It should return the node-key to a node decorated with the `edit_node` decorator. The - menuchoice will automatically be stored on the menutree as `list_node_edit`. - add (tuple, optional): If given, this callable will be called as add(caller, menuchoice). - It should return the node-key to a node decorated with the `edit_node` decorator. The - menuchoice will automatically be stored on the menutree as `list_node_add`. + edit (callable, optional): If given, this callable will be called as edit(caller, + menuchoice, **kwargs) and should return a complete (text, options) tuple (like a node). + add (callable optional): If given, this callable will be called as add(caller, menuchoice, + **kwargs) and should return a complete (text, options) tuple (like a node). pagesize (int): How many options to show per page. @@ -1189,25 +1187,28 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, """ available_choices = kwargs.get("available_choices", []) - match = re.search(r"(^[a-zA-Z]*)\s*([0-9]*)$", raw_string) - cmd, number = match.groups() + match = re.search(r"(^[a-zA-Z]*)\s*(.*?)$", raw_string) + cmd, args = match.groups() mode, selection = None, None + cmd = cmd.lower().strip() - if number: - number = int(number) - 1 - cmd = cmd.lower().strip() - if cmd.startswith("e") or cmd.startswith("a") and edit: - mode = "edit" - elif examine: - mode = "examine" - + if args: try: - selection = available_choices[number] - except IndexError: - caller.msg("|rInvalid index") - mode = None - else: - caller.msg("|rMust supply a number.") + number = int(args) - 1 + except ValueError: + if cmd.startswith("a") and add: + mode = "add" + selection = args + else: + if cmd.startswith("e") and edit: + mode = "edit" + elif examine: + mode = "look" + try: + selection = available_choices[number] + except IndexError: + caller.msg("|rInvalid index") + mode = None return mode, selection @@ -1233,18 +1234,18 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, page = pages[page_index] text = "" - entry = None + selection = None extra_text = None if mode == "arbitrary": # freeform input, we must parse it for the allowed commands (look/edit) - mode, entry = _input_parser(caller, raw_string, - **{"available_choices": page}) + mode, selection = _input_parser(caller, raw_string, + **{"available_choices": page}) - if examine and mode: # == "look": + if examine and mode == "look": # look mode - we are examining a given entry try: - text = examine(caller, entry) + text = examine(caller, selection) except Exception: logger.log_trace() text = "|rCould not view." @@ -1256,15 +1257,25 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, {"optionpage_index": page_index})}]) return text, options - # if edit and mode == "edit": - # pass - # elif add and mode == "add": - # # add mode - we are adding a new entry - # pass + elif add and mode == 'add': + # add mode - the selection is the new value + try: + text, options = add(caller, selection, **kwargs) + except Exception: + logger.log_trace() + text = "|rCould not add." + return text, options + + elif edit and mode == 'edit': + try: + text, options = edit(caller, selection, **kwargs) + except Exception: + logger.log_trace() + text = "|Could not edit." + return text, options else: # normal mode - list - pass if select: # We have a processor to handle selecting an entry @@ -1275,13 +1286,6 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, "goto": (_select_parser, {"available_choices": page})} for opt in page]) - if add: - # We have a processor to handle adding a new entry. Re-run this node - # in the 'add' mode - options.append({"key": ("|wadd|Wdd new|n", "a"), - "goto": (lambda caller: None, - {"optionpage_index": page_index, - "list_mode": "add"})}) if npages > 1: # if the goto callable returns None, the same node is rerun, and # kwargs not used by the callable are passed on to the node. This diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index ebf3ce5d67..2fbdbaed21 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -1066,8 +1066,30 @@ def _caller_tags(caller): tags = prot.get("tags") return tags +def _add_tags(caller, tag, **kwargs): + tag = tag.strip().lower() + metaprot = _get_menu_metaprot(caller) + tags = metaprot.tags + if tags: + if tag not in tags: + tags.append(tag) + else: + tags = [tag] + metaprot.tags = tags + text = kwargs.get("text") + if not text: + text = "Added tag {}. (return to continue)".format(tag) + options = {"key": "_default", + "goto": lambda caller: None} + return text, options -@list_node(_caller_tags) + +def _edit_tag(caller, tag, **kwargs): + tag = tag.strip().lower() + metaprot = _get_menu_metaprot(caller) + #TODO change in evmenu so one can do e 3 right away, parse & store value in kwarg + +@list_node(_caller_tags, edit=_edit_tags) def node_tags(caller): text = "Set the prototype's |yTags|n." options = _wizard_options("tags", "attrs", "locks") From 86a1f395257a58440783e46124f9340a678550eb Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 8 Apr 2018 13:21:51 +0200 Subject: [PATCH 038/103] Unworking commit for stashing --- evennia/utils/evmenu.py | 32 +++++++++++++++++--------------- evennia/utils/spawner.py | 1 - 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index c9f31b4688..2cb282a641 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1185,32 +1185,34 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, which processors are available. """ - + mode, selection, new_value = None, None, None available_choices = kwargs.get("available_choices", []) - match = re.search(r"(^[a-zA-Z]*)\s*(.*?)$", raw_string) - cmd, args = match.groups() - mode, selection = None, None - cmd = cmd.lower().strip() - if args: + cmd, args = re.search(r"(^[a-zA-Z]*)\s*(.*?)$", raw_string).groups() + + cmd = cmd.lower().strip() + if cmd.startswith('a') and add: + mode = "add" + new_value = args + else: + selection, new_value = re.search(r"(^[0-9]*)\s*(.*?)$", args).groups() try: - number = int(args) - 1 + selection = int(selection) - 1 except ValueError: - if cmd.startswith("a") and add: - mode = "add" - selection = args + caller.msg("|rInvalid input|n") else: + # edits are on the form 'edit if cmd.startswith("e") and edit: mode = "edit" elif examine: mode = "look" try: - selection = available_choices[number] + selection = available_choices[selection] except IndexError: - caller.msg("|rInvalid index") + caller.msg("|rInvalid index|n") mode = None - return mode, selection + return mode, selection, new_value def _relay_to_edit_or_add(caller, raw_string, **kwargs): pass @@ -1239,8 +1241,8 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, if mode == "arbitrary": # freeform input, we must parse it for the allowed commands (look/edit) - mode, selection = _input_parser(caller, raw_string, - **{"available_choices": page}) + mode, selection, new_value = _input_parser(caller, raw_string, + **{"available_choices": page}) if examine and mode == "look": # look mode - we are examining a given entry diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 2fbdbaed21..c4f732ebbb 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -1219,7 +1219,6 @@ def node_meta_desc(caller): return text, options - def node_meta_tags(caller): metaprot = _get_menu_metaprot(caller) text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " From 44ee41a5d12dfbcf8d5d0c84b093a0c856362cf6 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 9 Apr 2018 23:13:54 +0200 Subject: [PATCH 039/103] Add functioning, if primitive edit/add to decorator --- evennia/utils/evmenu.py | 41 ++++++++++++++++++++-------------------- evennia/utils/spawner.py | 31 +++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 2cb282a641..edbf64755b 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1170,10 +1170,11 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, except Exception: caller.msg("|rInvalid choice.|n") else: - try: - return select(caller, selection) - except Exception: - logger.log_trace() + if select: + try: + return select(caller, selection) + except Exception: + logger.log_trace() return None def _input_parser(caller, raw_string, **kwargs): @@ -1185,7 +1186,7 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, which processors are available. """ - mode, selection, new_value = None, None, None + mode, selection, args = None, None, None available_choices = kwargs.get("available_choices", []) cmd, args = re.search(r"(^[a-zA-Z]*)\s*(.*?)$", raw_string).groups() @@ -1193,13 +1194,12 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, cmd = cmd.lower().strip() if cmd.startswith('a') and add: mode = "add" - new_value = args else: - selection, new_value = re.search(r"(^[0-9]*)\s*(.*?)$", args).groups() + selection, args = re.search(r"(^[0-9]*)\s*(.*?)$", args).groups() try: selection = int(selection) - 1 except ValueError: - caller.msg("|rInvalid input|n") + mode = "look" else: # edits are on the form 'edit if cmd.startswith("e") and edit: @@ -1212,7 +1212,7 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, caller.msg("|rInvalid index|n") mode = None - return mode, selection, new_value + return mode, selection, args def _relay_to_edit_or_add(caller, raw_string, **kwargs): pass @@ -1222,9 +1222,11 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, mode = kwargs.get("list_mode", None) option_list = option_generator(caller) if callable(option_generator) else option_generator + print("option_list: {}, {}".format(option_list, mode)) + npages = 0 page_index = 0 - page = None + page = [] options = [] if option_list: @@ -1241,7 +1243,7 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, if mode == "arbitrary": # freeform input, we must parse it for the allowed commands (look/edit) - mode, selection, new_value = _input_parser(caller, raw_string, + mode, selection, args = _input_parser(caller, raw_string, **{"available_choices": page}) if examine and mode == "look": @@ -1262,7 +1264,7 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, elif add and mode == 'add': # add mode - the selection is the new value try: - text, options = add(caller, selection, **kwargs) + text, options = add(caller, args) except Exception: logger.log_trace() text = "|rCould not add." @@ -1270,7 +1272,7 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, elif edit and mode == 'edit': try: - text, options = edit(caller, selection, **kwargs) + text, options = edit(caller, selection, args) except Exception: logger.log_trace() text = "|Could not edit." @@ -1279,14 +1281,13 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, else: # normal mode - list - if select: - # We have a processor to handle selecting an entry + # We have a processor to handle selecting an entry - # dynamic, multi-page option list. Each selection leads to the `select` - # callback being called with a result from the available choices - options.extend([{"desc": opt, - "goto": (_select_parser, - {"available_choices": page})} for opt in page]) + # dynamic, multi-page option list. Each selection leads to the `select` + # callback being called with a result from the available choices + options.extend([{"desc": opt, + "goto": (_select_parser, + {"available_choices": page})} for opt in page]) if npages > 1: # if the goto callable returns None, the same node is rerun, and diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index c4f732ebbb..d5750c1629 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -1066,16 +1066,19 @@ def _caller_tags(caller): tags = prot.get("tags") return tags -def _add_tags(caller, tag, **kwargs): + +def _add_tag(caller, tag, **kwargs): tag = tag.strip().lower() metaprot = _get_menu_metaprot(caller) - tags = metaprot.tags + prot = metaprot.prototype + tags = prot.get('tags', []) if tags: if tag not in tags: tags.append(tag) else: tags = [tag] - metaprot.tags = tags + prot['tags'] = tags + _set_menu_metaprot(caller, "prototype", prot) text = kwargs.get("text") if not text: text = "Added tag {}. (return to continue)".format(tag) @@ -1084,12 +1087,26 @@ def _add_tags(caller, tag, **kwargs): return text, options -def _edit_tag(caller, tag, **kwargs): - tag = tag.strip().lower() +def _edit_tag(caller, old_tag, new_tag, **kwargs): metaprot = _get_menu_metaprot(caller) - #TODO change in evmenu so one can do e 3 right away, parse & store value in kwarg + prototype = metaprot.prototype + tags = prototype.get('tags', []) -@list_node(_caller_tags, edit=_edit_tags) + old_tag = old_tag.strip().lower() + new_tag = new_tag.strip().lower() + tags[tags.index(old_tag)] = new_tag + prototype['tags'] = tags + _set_menu_metaprot(caller, 'prototype', prototype) + + text = kwargs.get('text') + if not text: + text = "Changed tag {} to {}.".format(old_tag, new_tag) + options = {"key": "_default", + "goto": lambda caller: None} + return text, options + + +@list_node(_caller_tags, edit=_edit_tag, add=_add_tag) def node_tags(caller): text = "Set the prototype's |yTags|n." options = _wizard_options("tags", "attrs", "locks") From 4fe4f0656e2e5c1b8e572f97913a75be870ce15d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 15 Apr 2018 00:03:36 +0200 Subject: [PATCH 040/103] Simplify list_node decorator --- evennia/utils/evmenu.py | 377 +++++++++++----------------------------- 1 file changed, 102 insertions(+), 275 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index edbf64755b..60373e3a5a 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -987,137 +987,6 @@ class EvMenu(object): return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext -# ----------------------------------------------------------- -# -# Edit node (decorator turning a node into an editing -# point for a given resource -# -# ----------------------------------------------------------- - -def edit_node(edit_text, add_text, edit_callback, add_callback, get_choices=None): - """ - Decorator for turning an EvMenu node into an editing - page. Will add new options, prepending those options - added in the node. - - Args: - edit_text (str or callable): Will be used as text for the edit node. If - callable, it will be called as edittext(selection) - and should return the node text for the edit-node, probably listing - the current value of all editable propnames, if possible. - add_text (str) or callable: Gives text for node in add-mode. If a callable, - called as add_text() and should return the text for the node. - edit_callback (callable): Will be called as edit_callback(editable, raw_string) - and should return a boolean True/False if the setting of the property - succeeded or not. The value will always be a string and should be - converted as needed. - add_callback (callable): Will be called as add_callback(raw_string) and - should return a boolean True/False if the addition succeded. - - get_choices (callable): Produce the available editable choices. If this - is not given, the `goto` callable must have been provided with the - kwarg `available_choices` by the decorated node. - - """ - - def decorator(func): - - def _setter_goto(caller, raw_string, **kwargs): - editable = kwargs.get("editable") - mode = kwargs.get("edit_node_mode") - try: - if mode == 'edit': - is_ok = edit_callback(editable, raw_string) - else: - is_ok = add_callback(raw_string) - except Exception: - logger.log_trace() - if not is_ok: - caller.msg("|rValue could not be set.") - return None - - def _patch_goto(caller, raw_string, **kwargs): - - # parse incoming string to figure out if there is a match to edit/add - match = re.search(r"(^[a-zA-Z]*)\s*([0-9]*)$", raw_string) - cmd, number = match.groups() - edit_mode = None - available_choices = None - selection = None - - if get_choices: - available_choices = make_iter(get_choices(caller, raw_string, **kwargs)) - if not available_choices: - available_choices = kwargs.get("available_choices", []) - - if available_choices and cmd.startswith("e"): - try: - index = int(cmd) - 1 - selection = available_choices[index] - edit_mode = 'edit' - except (IndexError, TypeError): - caller.msg("|rNot a valid 'edit' command.") - - if cmd.startswith("a") and not number: - # add mode - edit_mode = "add" - - if edit_mode: - # replace with edit text/options - text = edit_text(selection) if edit_mode == "edit" else add_text() - options = ({"key": "_default", - "goto": (_setter_goto, - {"selection": selection, - "edit_node_mode": edit_mode})}) - return text, options - - # no matches - pass through to the original decorated goto instruction - - decorated_opt = kwargs.get("decorated_opt") - - if decorated_opt: - # use EvMenu's parser to get the goto/goto-kwargs out of - # the decorated option structure - dec_goto, dec_goto_kwargs, _, _ = \ - caller.ndb._menutree.extract_goto_exec("edit-node", decorated_opt) - - if callable(dec_goto): - try: - return dec_goto(caller, raw_string, - **{dec_goto_kwargs if dec_goto_kwargs else {}}) - except Exception: - caller.msg("|rThere was an error in the edit node.") - logger.log_trace() - return None - - def _edit_node(caller, raw_string, **kwargs): - - text, options = func(caller, raw_string, **kwargs) - - if options: - # find eventual _default in options and patch it with a handler for - # catching editing - - decorated_opt = None - iopt = 0 - for iopt, optdict in enumerate(options): - if optdict.get('key') == "_default": - decorated_opt = optdict - break - - if decorated_opt: - # inject our wrapper over the original goto instruction for the - # _default action (save the original) - options[iopt]["goto"] = (_patch_goto, - {"decorated_opt": decorated_opt}) - - return text, options - - return _edit_node - return decorator - - - # ----------------------------------------------------------- # # List node (decorator turning a node into a list with @@ -1125,7 +994,7 @@ def edit_node(edit_text, add_text, edit_callback, add_callback, get_choices=None # # ----------------------------------------------------------- -def list_node(option_generator, select=None, examine=None, edit=None, add=None, pagesize=10): +def list_node(option_generator, select=None, pagesize=10): """ Decorator for making an EvMenu node into a multi-page list node. Will add new options, prepending those options added in the node. @@ -1135,25 +1004,22 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, that is called as option_generator(caller) to produce such a list. select (callable, option): Will be called as select(caller, menuchoice) where menuchoice is the chosen option as a string. Should return the target node to - goto after this selection. Note that if this is not given, the decorated node must - itself provide a way to continue from the node! - examine (callable, optional): If given, allows for examining options in detail. Will - be called with examine(caller, menuchoice) and should return a text string to - display in-place in the node. - edit (callable, optional): If given, this callable will be called as edit(caller, - menuchoice, **kwargs) and should return a complete (text, options) tuple (like a node). - add (callable optional): If given, this callable will be called as add(caller, menuchoice, - **kwargs) and should return a complete (text, options) tuple (like a node). - + goto after this selection (or None to repeat the list-node). Note that if this is not + given, the decorated node must itself provide a way to continue from the node! pagesize (int): How many options to show per page. Example: - @list_node(['foo', 'bar'], examine, select) + @list_node(['foo', 'bar'], select) def node_index(caller): text = "describing the list" return text, [] + Notes: + All normal `goto` or `exec` callables returned from the decorated nodes will, if they accept + **kwargs, get a new kwarg 'available_choices' injected. These are the ordered list of named + options (descs) visible on the current node page. + """ def decorator(func): @@ -1177,53 +1043,44 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, logger.log_trace() return None - def _input_parser(caller, raw_string, **kwargs): - """ - Parse which input was given, select from option_list. - - Understood input is [cmd], where [cmd] is either empty (`select`) - or one of the supported actions `look`, `edit` or `add` depending on - which processors are available. - - """ - mode, selection, args = None, None, None - available_choices = kwargs.get("available_choices", []) - - cmd, args = re.search(r"(^[a-zA-Z]*)\s*(.*?)$", raw_string).groups() - - cmd = cmd.lower().strip() - if cmd.startswith('a') and add: - mode = "add" - else: - selection, args = re.search(r"(^[0-9]*)\s*(.*?)$", args).groups() - try: - selection = int(selection) - 1 - except ValueError: - mode = "look" - else: - # edits are on the form 'edit - if cmd.startswith("e") and edit: - mode = "edit" - elif examine: - mode = "look" - try: - selection = available_choices[selection] - except IndexError: - caller.msg("|rInvalid index|n") - mode = None - - return mode, selection, args - - def _relay_to_edit_or_add(caller, raw_string, **kwargs): - pass +# def _input_parser(caller, raw_string, **kwargs): +# """ +# Parse which input was given, select from option_list. +# +# +# """ +# mode, selection, args = None, None, None +# available_choices = kwargs.get("available_choices", []) +# +# cmd, args = re.search(r"(^[a-zA-Z]*)\s*(.*?)$", raw_string).groups() +# +# cmd = cmd.lower().strip() +# if cmd.startswith('a') and add: +# mode = "add" +# else: +# selection, args = re.search(r"(^[0-9]*)\s*(.*?)$", args).groups() +# try: +# selection = int(selection) - 1 +# except ValueError: +# mode = "look" +# else: +# # edits are on the form 'edit +# if cmd.startswith("e") and edit: +# mode = "edit" +# elif examine: +# mode = "look" +# try: +# selection = available_choices[selection] +# except IndexError: +# caller.msg("|rInvalid index|n") +# mode = None +# +# return mode, selection, args def _list_node(caller, raw_string, **kwargs): - mode = kwargs.get("list_mode", None) option_list = option_generator(caller) if callable(option_generator) else option_generator - print("option_list: {}, {}".format(option_list, mode)) - npages = 0 page_index = 0 page = [] @@ -1231,113 +1088,83 @@ def list_node(option_generator, select=None, examine=None, edit=None, add=None, if option_list: nall_options = len(option_list) - pages = [option_list[ind:ind + pagesize] for ind in range(0, nall_options, pagesize)] + pages = [option_list[ind:ind + pagesize] + for ind in range(0, nall_options, pagesize)] npages = len(pages) page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0))) page = pages[page_index] text = "" - selection = None extra_text = None - if mode == "arbitrary": - # freeform input, we must parse it for the allowed commands (look/edit) - mode, selection, args = _input_parser(caller, raw_string, - **{"available_choices": page}) + # dynamic, multi-page option list. Each selection leads to the `select` + # callback being called with a result from the available choices + options.extend([{"desc": opt, + "goto": (_select_parser, + {"available_choices": page})} for opt in page]) - if examine and mode == "look": - # look mode - we are examining a given entry - try: - text = examine(caller, selection) - except Exception: - logger.log_trace() - text = "|rCould not view." - options.extend([{"key": ("|wb|Wack|n", "b"), - "goto": (lambda caller: None, - {"optionpage_index": page_index})}, - {"key": "_default", - "goto": (lambda caller: None, - {"optionpage_index": page_index})}]) - return text, options - - elif add and mode == 'add': - # add mode - the selection is the new value - try: - text, options = add(caller, args) - except Exception: - logger.log_trace() - text = "|rCould not add." - return text, options - - elif edit and mode == 'edit': - try: - text, options = edit(caller, selection, args) - except Exception: - logger.log_trace() - text = "|Could not edit." - return text, options - - else: - # normal mode - list - - # We have a processor to handle selecting an entry - - # dynamic, multi-page option list. Each selection leads to the `select` - # callback being called with a result from the available choices - options.extend([{"desc": opt, - "goto": (_select_parser, - {"available_choices": page})} for opt in page]) - - if npages > 1: - # if the goto callable returns None, the same node is rerun, and - # kwargs not used by the callable are passed on to the node. This - # allows us to call ourselves over and over, using different kwargs. - options.append({"key": ("|Wcurrent|n", "c"), - "desc": "|W({}/{})|n".format(page_index + 1, npages), - "goto": (lambda caller: None, - {"optionpage_index": page_index})}) - if page_index > 0: - options.append({"key": ("|wp|Wrevious page|n", "p"), - "goto": (lambda caller: None, - {"optionpage_index": page_index - 1})}) - if page_index < npages - 1: - options.append({"key": ("|wn|Wext page|n", "n"), - "goto": (lambda caller: None, - {"optionpage_index": page_index + 1})}) - - # this catches arbitrary input and reruns this node with the 'arbitrary' mode - # this could mean input on the form 'look ' or 'edit ' - options.append({"key": "_default", + if npages > 1: + # if the goto callable returns None, the same node is rerun, and + # kwargs not used by the callable are passed on to the node. This + # allows us to call ourselves over and over, using different kwargs. + options.append({"key": ("|Wcurrent|n", "c"), + "desc": "|W({}/{})|n".format(page_index + 1, npages), "goto": (lambda caller: None, - {"optionpage_index": page_index, - "available_choices": page, - "list_mode": "arbitrary"})}) + {"optionpage_index": page_index})}) + if page_index > 0: + options.append({"key": ("|wp|Wrevious page|n", "p"), + "goto": (lambda caller: None, + {"optionpage_index": page_index - 1})}) + if page_index < npages - 1: + options.append({"key": ("|wn|Wext page|n", "n"), + "goto": (lambda caller: None, + {"optionpage_index": page_index + 1})}) - # add data from the decorated node + # add data from the decorated node - extra_options = [] + decorated_options = [] + try: + text, decorated_options = func(caller, raw_string) + except TypeError: try: - text, extra_options = func(caller, raw_string) - except TypeError: - try: - text, extra_options = func(caller) - except Exception: - raise + text, decorated_options = func(caller) except Exception: - logger.log_trace() - print("extra_options:", extra_options) + raise + except Exception: + logger.log_trace() + else: + if isinstance(decorated_options, {}): + decorated_options = [decorated_options] else: - if isinstance(extra_options, {}): - extra_options = [extra_options] - else: - extra_options = make_iter(extra_options) + decorated_options = make_iter(decorated_options) - options.extend(extra_options) - text = text + "\n\n" + extra_text if extra_text else text - text += "\n\n(Make a choice or enter 'look ' to examine an option closer)" + extra_options = [] + for eopt in decorated_options: + cback = ("goto" in eopt and "goto") or ("exec" in eopt and "exec") or None + if cback: + signature = eopt[cback] + if callable(signature): + # callable with no kwargs defined + eopt[cback] = (signature, {"available_choices": page}) + elif is_iter(signature): + if len(signature) > 1 and isinstance(signature[1], dict): + signature[1]["available_choices"] = page + eopt[cback] = signature + elif signature: + # a callable alone in a tuple (i.e. no previous kwargs) + eopt[cback] = (signature[0], {"available_choices": page}) + else: + # malformed input. + logger.log_err("EvMenu @list_node decorator found " + "malformed option to decorate: {}".format(eopt)) + extra_options.append(eopt) - return text, options + options.extend(extra_options) + text = text + "\n\n" + extra_text if extra_text else text + text += "\n\n(Make a choice or enter 'look ' to examine an option closer)" + + return text, options return _list_node return decorator From 1bbffa2fc5fab15b3c5e1db9929e5d209daeaa33 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 15 Apr 2018 20:13:38 +0200 Subject: [PATCH 041/103] non-functioning spawner --- evennia/utils/spawner.py | 68 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index d5750c1629..acc8cb2457 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -105,6 +105,7 @@ prototype, override its name with an empty dict. from __future__ import print_function import copy +from ast import literal_eval from django.conf import settings from random import randint import evennia @@ -125,6 +126,11 @@ _MODULE_PROTOTYPES = {} _MODULE_PROTOTYPE_MODULES = {} _MENU_CROP_WIDTH = 15 +_MENU_ATTR_LITERAL_EVAL_ERROR = ( + "|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n" + "You also need to use correct Python syntax. Remember especially to put quotes around all " + "strings inside lists and dicts.|n") + class PermissionError(RuntimeError): pass @@ -933,7 +939,7 @@ def _prototype_select(caller, prototype): return ret -@list_node(_all_prototypes, _prototype_select, examine=_prototype_examine) +@list_node(_all_prototypes, select=_prototype_select, examine=_prototype_examine) def node_prototype(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype @@ -1039,6 +1045,66 @@ def node_aliases(caller): return text, options +def _caller_attrs(caller): + metaprot = _get_menu_metaprot(caller) + attrs = metaprot.prototype.get("attrs", []) + return attrs + + +def _attrparse(caller, attr_string): + "attr is entering on the form 'attr = value'" + + if '=' in attr_string: + attrname, value = (part.strip() for part in attr_string.split('=', 1)) + attrname = attrname.lower() + if attrname: + try: + value = literal_eval(value) + except SyntaxError: + caller.msg(_MENU_ATTR_LITERAL_EVAL_ERROR) + else: + return attrname, value + else: + return None, None + + +def _add_attr(caller, attr_string, **kwargs): + attrname, value = _attrparse(caller, attr_string) + if attrname: + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + prot['attrs'][attrname] = value + _set_menu_metaprot(caller, "prototype", prot) + text = "Added" + else: + text = "Attribute must be given as 'attrname = ' where uses valid Python." + options = {"key": "_default", + "goto": lambda caller: None} + return text, options + + +def _edit_attr(caller, attrname, new_value, **kwargs): + attrname, value = _attrparse("{}={}".format(caller, attrname, new_value)) + if attrname: + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + prot['attrs'][attrname] = value + text = "Edited Attribute {} = {}".format(attrname, value) + else: + text = "Attribute value must be valid Python." + options = {"key": "_default", + "goto": lambda caller: None} + return text, options + + +def _examine_attr(caller, selection): + metaprot = _get_menu_metaprot(caller) + prot = metaprot.prototype + value = prot['attrs'][selection] + return "Attribute {} = {}".format(selection, value) + + +@list_node(_caller_attrs, edit=_edit_attr, add=_add_attr, examine=_examine_attr) def node_attrs(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype From 03a107d0225b6bb4d5dd19a488299341e1e19b45 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 15 Apr 2018 22:06:15 +0200 Subject: [PATCH 042/103] Made code run without traceback; in future, use select action to enter edit node, separate add command to enter add mode --- evennia/utils/spawner.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index acc8cb2457..bbed685b5c 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -929,8 +929,9 @@ def _all_prototypes(caller): def _prototype_examine(caller, prototype_name): metaprot = search_prototype(key=prototype_name) if metaprot: - return metaproto_to_str(metaprot[0]) - return "Prototype not registered." + caller.msg(metaproto_to_str(metaprot[0])) + caller.msg("Prototype not registered.") + return None def _prototype_select(caller, prototype): @@ -939,7 +940,7 @@ def _prototype_select(caller, prototype): return ret -@list_node(_all_prototypes, select=_prototype_select, examine=_prototype_examine) +@list_node(_all_prototypes, _prototype_select) def node_prototype(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype @@ -952,6 +953,9 @@ def node_prototype(caller): text.append("Parent prototype is not set") text = "\n\n".join(text) options = _wizard_options("prototype", "meta_key", "typeclass", color="|W") + options.append({"key": "_default", + "goto": _prototype_examine}) + return text, options @@ -978,7 +982,8 @@ def _typeclass_examine(caller, typeclass_path): typeclass_path=typeclass_path, docstring=docstr) else: txt = "This is typeclass |y{}|n.".format(typeclass) - return txt + caller.msg(txt) + return None def _typeclass_select(caller, typeclass): @@ -987,7 +992,7 @@ def _typeclass_select(caller, typeclass): return ret -@list_node(_all_typeclasses, _typeclass_select, examine=_typeclass_examine) +@list_node(_all_typeclasses, _typeclass_select) def node_typeclass(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype @@ -1001,6 +1006,8 @@ def node_typeclass(caller): typeclass=settings.BASE_OBJECT_TYPECLASS)) text = "\n\n".join(text) options = _wizard_options("typeclass", "prototype", "key", color="|W") + options.append({"key": "_default", + "goto": _typeclass_examine}) return text, options @@ -1104,7 +1111,7 @@ def _examine_attr(caller, selection): return "Attribute {} = {}".format(selection, value) -@list_node(_caller_attrs, edit=_edit_attr, add=_add_attr, examine=_examine_attr) +@list_node(_caller_attrs) def node_attrs(caller): metaprot = _get_menu_metaprot(caller) prot = metaprot.prototype @@ -1172,7 +1179,7 @@ def _edit_tag(caller, old_tag, new_tag, **kwargs): return text, options -@list_node(_caller_tags, edit=_edit_tag, add=_add_tag) +@list_node(_caller_tags) def node_tags(caller): text = "Set the prototype's |yTags|n." options = _wizard_options("tags", "attrs", "locks") From 9a7583a4d7313f7c0502921863c023c03c6148f5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 16 Apr 2018 20:18:21 +0200 Subject: [PATCH 043/103] Add test_spawner --- evennia/utils/tests/test_spawner.py | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 evennia/utils/tests/test_spawner.py diff --git a/evennia/utils/tests/test_spawner.py b/evennia/utils/tests/test_spawner.py new file mode 100644 index 0000000000..e29ee8c151 --- /dev/null +++ b/evennia/utils/tests/test_spawner.py @@ -0,0 +1,63 @@ +""" +Unit test for the spawner + +""" + +from evennia.utils.test_resources import EvenniaTest +from evennia.utils import spawner + + +class TestPrototypeStorage(EvenniaTest): + + def setUp(self): + super(TestPrototypeStorage, self).setUp() + self.prot1 = {"key": "testprototype"} + self.prot2 = {"key": "testprototype2"} + self.prot3 = {"key": "testprototype3"} + + def _get_metaproto( + self, key='testprototype', desc='testprototype', locks=['edit:id(6) or perm(Admin)', 'use:all()'], + tags=[], prototype={"key": "testprototype"}): + return spawner.build_metaproto(key, desc, locks, tags, prototype) + + def _to_metaproto(self, db_prototype): + return spawner.build_metaproto( + db_prototype.key, db_prototype.desc, db_prototype.locks.all(), + db_prototype.tags.get(category="db_prototype", return_list=True), + db_prototype.attributes.get("prototype")) + + def test_prototype_storage(self): + + prot = spawner.save_db_prototype(self.char1, "testprot", self.prot1, desc='testdesc0', tags=["foo"]) + + self.assertTrue(bool(prot)) + self.assertEqual(prot.db.prototype, self.prot1) + self.assertEqual(prot.desc, "testdesc0") + + prot = spawner.save_db_prototype(self.char1, "testprot", self.prot1, desc='testdesc', tags=["fooB"]) + self.assertEqual(prot.db.prototype, self.prot1) + self.assertEqual(prot.desc, "testdesc") + self.assertTrue(bool(prot.tags.get("fooB", "db_prototype"))) + + self.assertEqual(list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot]) + + prot2 = spawner.save_db_prototype(self.char1, "testprot2", self.prot2, desc='testdesc2b', tags=["foo"]) + self.assertEqual(list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot, prot2]) + + prot3 = spawner.save_db_prototype(self.char1, "testprot2", self.prot3, desc='testdesc2') + self.assertEqual(prot2.id, prot3.id) + self.assertEqual(list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot, prot2]) + + # returns DBPrototype + self.assertEqual(list(spawner.search_db_prototype("testprot")), [prot]) + + # returns metaprotos + prot = self._to_metaproto(prot) + prot3 = self._to_metaproto(prot3) + self.assertEqual(list(spawner.search_prototype("testprot")), [prot]) + self.assertEqual(list(spawner.search_prototype("testprot", return_meta=False)), [self.prot1]) + # partial match + self.assertEqual(list(spawner.search_prototype("prot")), [prot, prot3]) + self.assertEqual(list(spawner.search_prototype(tags="foo")), [prot, prot3]) + + self.assertTrue(str(unicode(spawner.list_prototypes(self.char1)))) From b4edc858da135a520f67bd40869e75ee733d1de0 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 18 Apr 2018 07:07:03 +0200 Subject: [PATCH 044/103] Cleanup --- evennia/utils/evmenu.py | 40 ++-------------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 60373e3a5a..a2a92f5e34 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -166,7 +166,6 @@ evennia.utils.evmenu`. from __future__ import print_function import random from builtins import object, range -import re from textwrap import dedent from inspect import isfunction, getargspec @@ -1009,7 +1008,6 @@ def list_node(option_generator, select=None, pagesize=10): pagesize (int): How many options to show per page. Example: - @list_node(['foo', 'bar'], select) def node_index(caller): text = "describing the list" @@ -1043,43 +1041,10 @@ def list_node(option_generator, select=None, pagesize=10): logger.log_trace() return None -# def _input_parser(caller, raw_string, **kwargs): -# """ -# Parse which input was given, select from option_list. -# -# -# """ -# mode, selection, args = None, None, None -# available_choices = kwargs.get("available_choices", []) -# -# cmd, args = re.search(r"(^[a-zA-Z]*)\s*(.*?)$", raw_string).groups() -# -# cmd = cmd.lower().strip() -# if cmd.startswith('a') and add: -# mode = "add" -# else: -# selection, args = re.search(r"(^[0-9]*)\s*(.*?)$", args).groups() -# try: -# selection = int(selection) - 1 -# except ValueError: -# mode = "look" -# else: -# # edits are on the form 'edit -# if cmd.startswith("e") and edit: -# mode = "edit" -# elif examine: -# mode = "look" -# try: -# selection = available_choices[selection] -# except IndexError: -# caller.msg("|rInvalid index|n") -# mode = None -# -# return mode, selection, args - def _list_node(caller, raw_string, **kwargs): - option_list = option_generator(caller) if callable(option_generator) else option_generator + option_list = option_generator(caller) \ + if callable(option_generator) else option_generator npages = 0 page_index = 0 @@ -1162,7 +1127,6 @@ def list_node(option_generator, select=None, pagesize=10): options.extend(extra_options) text = text + "\n\n" + extra_text if extra_text else text - text += "\n\n(Make a choice or enter 'look ' to examine an option closer)" return text, options From 41a1d6a33cb5a042fb11129095357e0615fa986a Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 18 Apr 2018 23:50:12 +0200 Subject: [PATCH 045/103] Refactor spawner to use prototype instead of metaprots --- evennia/utils/spawner.py | 454 +++++++++++++++++++-------------------- 1 file changed, 221 insertions(+), 233 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index bbed685b5c..0bce7addd8 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -122,6 +122,8 @@ from evennia.utils.ansi import strip_ansi _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") +_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") +_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES _MODULE_PROTOTYPES = {} _MODULE_PROTOTYPE_MODULES = {} _MENU_CROP_WIDTH = 15 @@ -135,24 +137,24 @@ _MENU_ATTR_LITERAL_EVAL_ERROR = ( class PermissionError(RuntimeError): pass -# 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() + prots = [(prototype_key, prot) for prototype_key, prot in all_from_module(mod).items() if prot and isinstance(prot, dict)] - _MODULE_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}) + # assign module path to each prototype_key for easy reference _MODULE_PROTOTYPE_MODULES.update({tup[0]: mod for tup in prots}) + # make sure the prototype contains all meta info + for prototype_key, prot in prots: + prot.update({ + "prototype_key": prototype_key.lower(), + "prototype_desc": prot['prototype_desc'] if 'prototype_desc' in prot else mod, + "prototype_locks": prot['prototype_locks'] if 'prototype_locks' in prot else "use:all()", + "prototype_tags": set(make_iter(prot['prototype_tags']) + if 'prototype_tags' in prot else ["base-prototype"])}) + _MODULE_PROTOTYPES.update(prot) + # Prototype storage mechanisms @@ -162,24 +164,11 @@ class DbPrototype(DefaultScript): This stores a single prototype """ def at_script_creation(self): - self.key = "empty prototype" - self.desc = "A prototype" + self.key = "empty prototype" # prototype_key + self.desc = "A prototype" # prototype_desc -def build_metaproto(key='', desc='', locks='', tags=None, prototype=None): - """ - Create a metaproto from combinant parts. - - """ - if locks: - locks = (";".join(locks) if is_iter(locks) else locks) - else: - locks = [] - prototype = dict(prototype) if prototype else {} - return MetaProto(key, desc, locks, tags, dict(prototype)) - - -def save_db_prototype(caller, key, prototype, desc="", tags=None, locks="", delete=False): +def save_db_prototype(caller, prototype, key=None, desc=None, tags=None, locks="", delete=False): """ Store a prototype persistently. @@ -187,13 +176,14 @@ def save_db_prototype(caller, key, prototype, desc="", tags=None, locks="", dele 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. + key (str): Name of prototype to store. Will be inserted as `prototype_key` in the prototype. + desc (str, optional): Description of prototype, to use in listing. Will be inserted + as `prototype_desc` in the prototype. tags (list, optional): Tag-strings to apply to prototype. These are always - applied with the 'db_prototype' category. + applied with the 'db_prototype' category. Will be inserted as `prototype_tags`. locks (str, optional): Locks to apply to this prototype. Used locks - are 'use' and 'edit' + are 'use' and 'edit'. Will be inserted as `prototype_locks` in the prototype. delete (bool, optional): Delete an existing prototype identified by 'key'. This requires `caller` to pass the 'edit' lock of the prototype. Returns: @@ -204,21 +194,40 @@ def save_db_prototype(caller, key, prototype, desc="", tags=None, locks="", dele """ - key_orig = key - key = key.lower() - locks = locks if locks else "use:all();edit:id({}) or perm(Admin)".format(caller.id) + key_orig = key or prototype.get('prototype_key', None) + if not key_orig: + caller.msg("This prototype requires a prototype_key.") + return False + key = str(key).lower() + + # we can't edit a prototype defined in a module + if key in _MODULE_PROTOTYPES: + mod = _MODULE_PROTOTYPE_MODULES.get(key, "N/A") + raise PermissionError("{} is a read-only prototype " + "(defined as code in {}).".format(key_orig, mod)) + + prototype['prototype_key'] = key + + if desc: + desc = prototype['prototype_desc'] = desc + else: + desc = prototype.get('prototype_desc', '') + + # set up locks and check they are on a valid form + locks = locks or prototype.get( + "prototype_locks", "use:all();edit:id({}) or perm(Admin)".format(caller.id)) + prototype['prototype_locks'] = locks is_valid, err = caller.locks.validate(locks) if not is_valid: caller.msg("Lock error: {}".format(err)) return False - tags = [(tag, "db_prototype") for tag in make_iter(tags)] - - if key in _MODULE_PROTOTYPES: - mod = _MODULE_PROTOTYPE_MODULES.get(key, "N/A") - raise PermissionError("{} is a read-only prototype " - "(defined as code in {}).".format(key_orig, mod)) + if tags: + tags = [(tag, "db_prototype") for tag in make_iter(tags)] + else: + tags = prototype.get('prototype_tags', []) + prototype['prototype_tags'] = tags stored_prototype = DbPrototype.objects.filter(db_key=key) @@ -269,7 +278,7 @@ def delete_db_prototype(caller, key): return save_db_prototype(caller, key, None, delete=True) -def search_db_prototype(key=None, tags=None, return_metaprotos=False): +def search_db_prototype(key=None, tags=None, return_queryset=False): """ Find persistent (database-stored) prototypes based on key and/or tags. @@ -278,13 +287,14 @@ def search_db_prototype(key=None, tags=None, return_metaprotos=False): tags (str or list): Tag key or keys to query for. These will always be applied with the 'db_protototype' tag category. - return_metaproto (bool): Return results as metaprotos. + return_queryset (bool): Return the database queryset. Return: - matches (queryset or list): All found DbPrototypes. If `return_metaprotos` - is set, return a list of MetaProtos. + matches (queryset or list): All found DbPrototypes. If `return_queryset` + is not set, this is a list of prototype dicts. Note: - This will not include read-only prototypes defined in modules. + This does not include read-only prototypes defined in modules; use + `search_module_prototype` for those. """ if tags: @@ -297,11 +307,9 @@ def search_db_prototype(key=None, tags=None, return_metaprotos=False): if key: # exact or partial match on key matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key) - if return_metaprotos: - return [build_metaproto(match.key, match.desc, match.locks.all(), - match.tags.get(category="db_prototype", return_list=True), - match.attributes.get("prototype")) - for match in matches] + if not return_queryset: + # return prototype + return [dbprot.attributes.get("prototype", {}) for dbprot in matches] return matches @@ -314,16 +322,16 @@ def search_module_prototype(key=None, tags=None): tags (str or list): Tag key to query for. Return: - matches (list): List of MetaProto tuples that includes - prototype metadata, + matches (list): List of prototypes matching the search criterion. """ matches = {} if tags: # use tags to limit selection tagset = set(tags) - matches = {key: metaproto for key, metaproto in _MODULE_PROTOTYPES.items() - if tagset.intersection(metaproto.tags)} + matches = {prototype_key: prototype + for prototype_key, prototype in _MODULE_PROTOTYPES.items() + if tagset.intersection(prototype.get("prototype_tags", []))} else: matches = _MODULE_PROTOTYPES @@ -333,12 +341,13 @@ def search_module_prototype(key=None, tags=None): return [matches[key]] else: # fuzzy matching - return [metaproto for pkey, metaproto in matches.items() if key in pkey] + return [prototype for prototype_key, prototype in matches.items() + if key in prototype_key] else: return [match for match in matches.values()] -def search_prototype(key=None, tags=None, return_meta=True): +def search_prototype(key=None, tags=None): """ Find prototypes based on key and/or tags, or all prototypes. @@ -347,12 +356,10 @@ def search_prototype(key=None, tags=None, return_meta=True): tags (str or list): Tag key or keys to query for. These will always be applied with the 'db_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 or MetaProtos. If no keys - or tags are given, all available prototypes/MetaProtos will be returned. + matches (list): All found prototype dicts. If no keys + or tags are given, all available prototypes will be returned. Note: The available prototypes is a combination of those supplied in @@ -363,32 +370,29 @@ def search_prototype(key=None, tags=None, return_meta=True): """ module_prototypes = search_module_prototype(key, tags) - db_prototypes = search_db_prototype(key, tags, return_metaprotos=True) + db_prototypes = search_db_prototype(key, tags) matches = db_prototypes + module_prototypes if len(matches) > 1 and key: key = key.lower() # avoid duplicates if an exact match exist between the two types - filter_matches = [mta for mta in matches if mta.key == key] + filter_matches = [mta for mta in matches + if mta.get('prototype_key') and mta['prototype_key'] == key] if filter_matches and len(filter_matches) < len(matches): matches = filter_matches - if not return_meta: - matches = [mta.prototype for mta in matches] - return matches -def get_protparents(): +def get_protparent_dict(): """ - Get prototype parents. These are a combination of meta-key and prototype-dict and are used when - a prototype refers to another parent-prototype. + Get prototype parents. + + Returns: + parent_dict (dict): A mapping {prototype_key: prototype} for all available prototypes. """ - # get all prototypes - metaprotos = search_prototype(return_meta=True) - # organize by key - return {metaproto.key: metaproto.prototype for metaproto in metaprotos} + return {prototype['prototype_key']: prototype for prototype in search_prototype()} def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True): @@ -410,35 +414,29 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed tags = [tag for tag in make_iter(tags) if tag] # get metaprotos for readonly and db-based prototypes - metaprotos = search_module_prototype(key, tags) - metaprotos += search_db_prototype(key, tags, return_metaprotos=True) + prototypes = search_prototype(key, tags) # get use-permissions of readonly attributes (edit is always False) - 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(metaprotos, key=lambda o: o.key)] + display_tuples = [] + for prototype in sorted(prototypes, key=lambda d: d['prototype_key']): + lock_use = caller.locks.check_lockstring(caller, prototype['locks'], access_type='use') + if not show_non_use and not lock_use: + continue + lock_edit = caller.locks.check_lockstring(caller, prototype['locks'], access_type='edit') + if not show_non_edit and not lock_edit: + continue + display_tuples.append( + (prototype.get('prototype_key', '', + prototype['prototype_desc', ''], + "{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'), + ",".join(prototype.get('prototype_tags', []))))) - 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: + if not display_tuples: return None table = [] - for i in range(len(prototypes[0])): - table.append([str(metaproto[i]) for metaproto in prototypes]) + for i in range(len(display_tuples[0])): + table.append([str(display_tuple[i]) for display_tuple in display_tuples]) 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) @@ -447,22 +445,26 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed return table -def metaproto_to_str(metaproto): +def prototype_to_str(prototype): """ - Format a metaproto to a nice string representation. + Format a prototype to a nice string representation. Args: metaproto (NamedTuple): Represents the prototype. """ + header = ( "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" "|cdesc:|n {} \n|cprototype:|n ".format( - metaproto.key, ", ".join(metaproto.tags), - metaproto.locks, metaproto.desc)) - prototype = ("{{\n {} \n}}".format("\n ".join("{!r}: {!r},".format(key, value) - for key, value in - sorted(metaproto.prototype.items())).rstrip(","))) - return header + prototype + prototype['prototype_key'], + ", ".join(prototype['prototype_tags']), + prototype['prototype_locks'], + prototype['prototype_desc'])) + proto = ("{{\n {} \n}}".format( + "\n ".join( + "{!r}: {!r},".format(key, value) for key, value in + sorted(prototype.items()) if key not in _PROTOTYPE_META_NAMES)).rstrip(",")) + return header + proto # Spawner mechanism @@ -487,10 +489,12 @@ def validate_prototype(prototype, protkey=None, protparents=None, _visited=None) """ if not protparents: - protparents = get_protparents() + protparents = get_protparent_dict() if _visited is None: _visited = [] - protkey = protkey.lower() if protkey is not None else None + protkey = protkey or prototype.get('prototype_key', None) + + protkey = protkey.lower() or prototype.get('prototype_key', None) assert isinstance(prototype, dict) @@ -537,8 +541,8 @@ def _batch_create_object(*objparams): so make sure the spawned Typeclass works before using this! Args: - objsparams (tuple): Parameters for the respective creation/add - handlers in the following order: + objsparams (tuple): Each paremter tuple will create one object instance using the parameters within. + The parameters should be given in the following order: - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`. - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`. - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`. @@ -555,9 +559,6 @@ def _batch_create_object(*objparams): (the newly created object) available in the namespace. Execution will happend after all other properties have been assigned and is intended for calling custom handlers etc. - for the respective creation/add handlers in the following - order: (create_kwargs, permissions, locks, aliases, nattributes, - attributes, tags, execs) Returns: objects (list): A list of created objects @@ -664,6 +665,10 @@ def spawn(*prototypes, **kwargs): alias_string = aliasval() if callable(aliasval) else aliasval tagval = prot.pop("tags", []) tags = tagval() if callable(tagval) else tagval + + # we make sure to add a tag identifying which prototype created this object + # tags.append(()) + attrval = prot.pop("attrs", []) attributes = attrval() if callable(tagval) else attrval @@ -676,9 +681,9 @@ def spawn(*prototypes, **kwargs): # the rest are attributes simple_attributes = [(key, value()) if callable(value) else (key, value) - for key, value in prot.items() if not key.startswith("ndb_")] + for key, value in prot.items() if not (key.startswith("ndb_"))] attributes = attributes + simple_attributes - attributes = [tup for tup in attributes if not tup[0] in _CREATE_OBJECT_KWARGS] + attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS] # pack for call into _batch_create_object objsparams.append((create_kwargs, permission_string, lock_string, @@ -695,34 +700,30 @@ def spawn(*prototypes, **kwargs): # Helper functions -def _get_menu_metaprot(caller): +def _get_menu_prototype(caller): - metaproto = None - if hasattr(caller.ndb._menutree, "olc_metaprot"): - metaproto = caller.ndb._menutree.olc_metaprot - if not metaproto: - metaproto = build_metaproto(None, '', [], [], None) - caller.ndb._menutree.olc_metaprot = metaproto + prototype = None + if hasattr(caller.ndb._menutree, "olc_prototype"): + prototype = caller.ndb._menutree.olc_prototype + if not prototype: + caller.ndb._menutree.olc_prototype = {} caller.ndb._menutree.olc_new = True - return metaproto + return prototype def _is_new_prototype(caller): return hasattr(caller.ndb._menutree, "olc_new") -def _set_menu_metaprot(caller, field, value): - metaprot = _get_menu_metaprot(caller) - kwargs = dict(metaprot.__dict__) - kwargs[field] = value - caller.ndb._menutree.olc_metaprot = build_metaproto(**kwargs) +def _set_menu_prototype(caller, field, value): + prototype = _get_menu_prototype(caller) + prototype[field] = value + caller.ndb._menutree.olc_prototype = prototype -def _format_property(key, required=False, metaprot=None, prototype=None, cropper=None): +def _format_property(key, required=False, prototype=None, cropper=None): key = key.lower() - if metaprot is not None: - prop = getattr(metaprot, key) or '' - elif prototype is not None: + if prototype is not None: prop = prototype.get(key, '') out = prop @@ -753,14 +754,11 @@ def _set_property(caller, raw_string, **kwargs): next_node (str): Next node to go to. """ - prop = kwargs.get("prop", "meta_key") + prop = kwargs.get("prop", "prototype_key") processor = kwargs.get("processor", None) next_node = kwargs.get("next_node", "node_index") propname_low = prop.strip().lower() - meta = propname_low.startswith("meta_") - if meta: - propname_low = propname_low[5:] if callable(processor): try: @@ -776,23 +774,17 @@ def _set_property(caller, raw_string, **kwargs): if not value: return next_node - if meta: - _set_menu_metaprot(caller, propname_low, value) - else: - metaprot = _get_menu_metaprot(caller) - prototype = metaprot.prototype - prototype[propname_low] = value + prototype = _get_menu_prototype(caller) - # typeclass and prototype can't co-exist - if propname_low == "typeclass": - prototype.pop("prototype", None) - if propname_low == "prototype": - prototype.pop("typeclass", None) + # typeclass and prototype can't co-exist + if propname_low == "typeclass": + prototype.pop("prototype", None) + if propname_low == "prototype": + prototype.pop("typeclass", None) - _set_menu_metaprot(caller, "prototype", prototype) + caller.ndb._menutree.olc_prototype = prototype - caller.msg("Set {prop} to '{value}'.".format( - prop=prop.replace("_", "-").capitalize(), value=str(value))) + caller.msg("Set {prop} to '{value}'.".format(prop, value=str(value))) return next_node @@ -829,8 +821,7 @@ def _path_cropper(pythonpath): # Menu nodes def node_index(caller): - metaprot = _get_menu_metaprot(caller) - prototype = metaprot.prototype + prototype = _get_menu_prototype(caller) text = ("|c --- Prototype wizard --- |n\n\n" "Define the |yproperties|n of the prototype. All prototype values can be " @@ -841,10 +832,9 @@ def node_index(caller): "others later.\n\n(make choice; q to abort. If unsure, start from 1.)") options = [] - # The meta-key goes first options.append( - {"desc": "|WMeta-Key|n|n{}".format(_format_property("Key", True, metaprot, None)), - "goto": "node_meta_key"}) + {"desc": "|WPrototype-Key|n|n{}".format(_format_property("Key", True, prototype, None)), + "goto": "node_prototype_key"}) for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', 'Permissions', 'Location', 'Home', 'Destination'): required = False @@ -860,20 +850,20 @@ def node_index(caller): required = False for key in ('Desc', 'Tags', 'Locks'): options.append( - {"desc": "|WMeta-{}|n|n{}".format(key, _format_property(key, required, metaprot, None)), - "goto": "node_meta_{}".format(key.lower())}) + {"desc": "|WPrototype-{}|n|n{}".format(key, _format_property(key, required, prototype, None)), + "goto": "node_prototype_{}".format(key.lower())}) return text, options def node_validate_prototype(caller, raw_string, **kwargs): - metaprot = _get_menu_metaprot(caller) + prototype = _get_menu_prototype(caller) - txt = metaproto_to_str(metaprot) + txt = prototype_to_str(prototype) errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)" try: # validate, don't spawn - spawn(metaprot.prototype, return_prototypes=True) + spawn(prototype, return_prototypes=True) except RuntimeError as err: errors = "\n\n|rError: {}|n".format(err) text = (txt + errors) @@ -883,42 +873,43 @@ def node_validate_prototype(caller, raw_string, **kwargs): return text, options -def _check_meta_key(caller, key): - old_metaprot = search_prototype(key) +def _check_prototype_key(caller, key): + old_prototype = search_prototype(key) olc_new = _is_new_prototype(caller) key = key.strip().lower() - if old_metaprot: + if old_prototype: # we are starting a new prototype that matches an existing - if not caller.locks.check_lockstring(caller, old_metaprot.locks, access_type='edit'): - # return to the node_meta_key to try another key + if not caller.locks.check_lockstring( + caller, old_prototype['prototype_locks'], access_type='edit'): + # return to the node_prototype_key to try another key caller.msg("Prototype '{key}' already exists and you don't " "have permission to edit it.".format(key=key)) - return "node_meta_key" + return "node_prototype_key" elif olc_new: # we are selecting an existing prototype to edit. Reset to index. del caller.ndb._menutree.olc_new - caller.ndb._menutree.olc_metaprot = old_metaprot + caller.ndb._menutree.olc_prototype = old_prototype caller.msg("Prototype already exists. Reloading.") return "node_index" - return _set_property(caller, key, prop='meta_key', next_node="node_prototype") + return _set_property(caller, key, prop='prototype_key', next_node="node_prototype") -def node_meta_key(caller): - metaprot = _get_menu_metaprot(caller) +def node_prototype_key(caller): + prototype = _get_menu_prototype(caller) text = ["The prototype name, or |wMeta-Key|n, uniquely identifies the prototype. " "It is used to find and use the prototype to spawn new entities. " "It is not case sensitive."] - old_key = metaprot.key + old_key = prototype['prototype_key'] if old_key: text.append("Current key is '|w{key}|n'".format(key=old_key)) else: text.append("The key is currently unset.") text.append("Enter text or make a choice (q for quit)") text = "\n\n".join(text) - options = _wizard_options("meta_key", "index", "prototype") + options = _wizard_options("prototype_key", "index", "prototype") options.append({"key": "_default", - "goto": _check_meta_key}) + "goto": _check_prototype_key}) return text, options @@ -927,9 +918,9 @@ def _all_prototypes(caller): def _prototype_examine(caller, prototype_name): - metaprot = search_prototype(key=prototype_name) - if metaprot: - caller.msg(metaproto_to_str(metaprot[0])) + prototypes = search_prototype(key=prototype_name) + if prototypes: + caller.msg(prototype_to_str(prototypes[0])) caller.msg("Prototype not registered.") return None @@ -942,17 +933,22 @@ def _prototype_select(caller, prototype): @list_node(_all_prototypes, _prototype_select) def node_prototype(caller): - metaprot = _get_menu_metaprot(caller) - prot = metaprot.prototype - prototype = prot.get("prototype") + prototype = _get_menu_prototype(caller) - text = ["Set the prototype's parent |yPrototype|n. If this is unset, Typeclass will be used."] - if prototype: - text.append("Current prototype is |y{prototype}|n.".format(prototype=prototype)) + prot_parent_key = prototype.get('prototype') + + text = ["Set the prototype's |yParent Prototype|n. If this is unset, Typeclass will be used."] + if prot_parent_key: + prot_parent = search_prototype(prot_parent_key) + if prot_parent: + text.append("Current parent prototype is {}:\n{}".format(prototype_to_str(prot_parent))) + else: + text.append("Current parent prototype |r{prototype}|n " + "does not appear to exist.".format(prot_parent_key)) else: text.append("Parent prototype is not set") text = "\n\n".join(text) - options = _wizard_options("prototype", "meta_key", "typeclass", color="|W") + options = _wizard_options("prototype", "prototype_key", "typeclass", color="|W") options.append({"key": "_default", "goto": _prototype_examine}) @@ -961,7 +957,6 @@ def node_prototype(caller): def _all_typeclasses(caller): return list(sorted(get_all_typeclasses().keys())) - # return list(sorted(get_all_typeclasses(parent="evennia.objects.objects.DefaultObject").keys())) def _typeclass_examine(caller, typeclass_path): @@ -994,9 +989,8 @@ def _typeclass_select(caller, typeclass): @list_node(_all_typeclasses, _typeclass_select) def node_typeclass(caller): - metaprot = _get_menu_metaprot(caller) - prot = metaprot.prototype - typeclass = prot.get("typeclass") + prototype = _get_menu_prototype(caller) + typeclass = prototype.get("typeclass") text = ["Set the typeclass's parent |yTypeclass|n."] if typeclass: @@ -1012,9 +1006,8 @@ def node_typeclass(caller): def node_key(caller): - metaprot = _get_menu_metaprot(caller) - prot = metaprot.prototype - key = prot.get("key") + prototype = _get_menu_prototype(caller) + key = prototype.get("key") text = ["Set the prototype's |yKey|n. This will retain case sensitivity."] if key: @@ -1032,9 +1025,8 @@ def node_key(caller): def node_aliases(caller): - metaprot = _get_menu_metaprot(caller) - prot = metaprot.prototype - aliases = prot.get("aliases") + prototype = _get_menu_prototype(caller) + aliases = prototype.get("aliases") text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. " "ill retain case sensitivity."] @@ -1053,8 +1045,8 @@ def node_aliases(caller): def _caller_attrs(caller): - metaprot = _get_menu_metaprot(caller) - attrs = metaprot.prototype.get("attrs", []) + prototype = _get_menu_prototype(caller) + attrs = prototype.get("attrs", []) return attrs @@ -1078,10 +1070,9 @@ def _attrparse(caller, attr_string): def _add_attr(caller, attr_string, **kwargs): attrname, value = _attrparse(caller, attr_string) if attrname: - metaprot = _get_menu_metaprot(caller) - prot = metaprot.prototype + prot = _get_menu_prototype(caller) prot['attrs'][attrname] = value - _set_menu_metaprot(caller, "prototype", prot) + _set_menu_prototype(caller, "prototype", prot) text = "Added" else: text = "Attribute must be given as 'attrname = ' where uses valid Python." @@ -1093,8 +1084,7 @@ def _add_attr(caller, attr_string, **kwargs): def _edit_attr(caller, attrname, new_value, **kwargs): attrname, value = _attrparse("{}={}".format(caller, attrname, new_value)) if attrname: - metaprot = _get_menu_metaprot(caller) - prot = metaprot.prototype + prot = _get_menu_prototype(caller) prot['attrs'][attrname] = value text = "Edited Attribute {} = {}".format(attrname, value) else: @@ -1105,16 +1095,14 @@ def _edit_attr(caller, attrname, new_value, **kwargs): def _examine_attr(caller, selection): - metaprot = _get_menu_metaprot(caller) - prot = metaprot.prototype + prot = _get_menu_prototype(caller) value = prot['attrs'][selection] return "Attribute {} = {}".format(selection, value) @list_node(_caller_attrs) def node_attrs(caller): - metaprot = _get_menu_metaprot(caller) - prot = metaprot.prototype + prot = _get_menu_prototype(caller) attrs = prot.get("attrs") text = ["Set the prototype's |yAttributes|n. Separate multiple attrs with commas. " @@ -1134,7 +1122,7 @@ def node_attrs(caller): def _caller_tags(caller): - metaprot = _get_menu_metaprot(caller) + metaprot = _get_menu_prototype(caller) prot = metaprot.prototype tags = prot.get("tags") return tags @@ -1142,7 +1130,7 @@ def _caller_tags(caller): def _add_tag(caller, tag, **kwargs): tag = tag.strip().lower() - metaprot = _get_menu_metaprot(caller) + metaprot = _get_menu_prototype(caller) prot = metaprot.prototype tags = prot.get('tags', []) if tags: @@ -1151,7 +1139,7 @@ def _add_tag(caller, tag, **kwargs): else: tags = [tag] prot['tags'] = tags - _set_menu_metaprot(caller, "prototype", prot) + _set_menu_prototype(caller, "prototype", prot) text = kwargs.get("text") if not text: text = "Added tag {}. (return to continue)".format(tag) @@ -1161,7 +1149,7 @@ def _add_tag(caller, tag, **kwargs): def _edit_tag(caller, old_tag, new_tag, **kwargs): - metaprot = _get_menu_metaprot(caller) + metaprot = _get_menu_prototype(caller) prototype = metaprot.prototype tags = prototype.get('tags', []) @@ -1169,7 +1157,7 @@ def _edit_tag(caller, old_tag, new_tag, **kwargs): new_tag = new_tag.strip().lower() tags[tags.index(old_tag)] = new_tag prototype['tags'] = tags - _set_menu_metaprot(caller, 'prototype', prototype) + _set_menu_prototype(caller, 'prototype', prototype) text = kwargs.get('text') if not text: @@ -1187,7 +1175,7 @@ def node_tags(caller): def node_locks(caller): - metaprot = _get_menu_metaprot(caller) + metaprot = _get_menu_prototype(caller) prot = metaprot.prototype locks = prot.get("locks") @@ -1208,7 +1196,7 @@ def node_locks(caller): def node_permissions(caller): - metaprot = _get_menu_metaprot(caller) + metaprot = _get_menu_prototype(caller) prot = metaprot.prototype permissions = prot.get("permissions") @@ -1229,7 +1217,7 @@ def node_permissions(caller): def node_location(caller): - metaprot = _get_menu_metaprot(caller) + metaprot = _get_menu_prototype(caller) prot = metaprot.prototype location = prot.get("location") @@ -1249,7 +1237,7 @@ def node_location(caller): def node_home(caller): - metaprot = _get_menu_metaprot(caller) + metaprot = _get_menu_prototype(caller) prot = metaprot.prototype home = prot.get("home") @@ -1269,7 +1257,7 @@ def node_home(caller): def node_destination(caller): - metaprot = _get_menu_metaprot(caller) + metaprot = _get_menu_prototype(caller) prot = metaprot.prototype dest = prot.get("dest") @@ -1279,18 +1267,18 @@ def node_destination(caller): else: text.append("No destination is set (default).") text = "\n\n".join(text) - options = _wizard_options("destination", "home", "meta_desc") + options = _wizard_options("destination", "home", "prototype_desc") options.append({"key": "_default", "goto": (_set_property, dict(prop="dest", processor=lambda s: s.strip(), - next_node="node_meta_desc"))}) + next_node="node_prototype_desc"))}) return text, options -def node_meta_desc(caller): +def node_prototype_desc(caller): - metaprot = _get_menu_metaprot(caller) + metaprot = _get_menu_prototype(caller) text = ["The |wMeta-Description|n briefly describes the prototype for viewing in listings."] desc = metaprot.desc @@ -1299,18 +1287,18 @@ def node_meta_desc(caller): else: text.append("Description is currently unset.") text = "\n\n".join(text) - options = _wizard_options("meta_desc", "meta_key", "meta_tags") + options = _wizard_options("prototype_desc", "prototype_key", "prototype_tags") options.append({"key": "_default", "goto": (_set_property, - dict(prop='meta_desc', + dict(prop='prototype_desc', processor=lambda s: s.strip(), - next_node="node_meta_tags"))}) + next_node="node_prototype_tags"))}) return text, options -def node_meta_tags(caller): - metaprot = _get_menu_metaprot(caller) +def node_prototype_tags(caller): + metaprot = _get_menu_prototype(caller) text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " "Separate multiple by tags by commas."] tags = metaprot.tags @@ -1320,18 +1308,18 @@ def node_meta_tags(caller): else: text.append("No tags are currently set.") text = "\n\n".join(text) - options = _wizard_options("meta_tags", "meta_desc", "meta_locks") + options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks") options.append({"key": "_default", "goto": (_set_property, - dict(prop="meta_tags", + dict(prop="prototype_tags", processor=lambda s: [ str(part.strip().lower()) for part in s.split(",")], - next_node="node_meta_locks"))}) + next_node="node_prototype_locks"))}) return text, options -def node_meta_locks(caller): - metaprot = _get_menu_metaprot(caller) +def node_prototype_locks(caller): + metaprot = _get_menu_prototype(caller) text = ["Set |wMeta-Locks|n on the prototype. There are two valid lock types: " "'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n" "(If you are unsure, leave as default.)"] @@ -1342,10 +1330,10 @@ def node_meta_locks(caller): text.append("Lock unset - if not changed the default lockstring will be set as\n" " |w'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) text = "\n\n".join(text) - options = _wizard_options("meta_locks", "meta_tags", "index") + options = _wizard_options("prototype_locks", "prototype_tags", "index") options.append({"key": "_default", "goto": (_set_property, - dict(prop="meta_locks", + dict(prop="prototype_locks", processor=lambda s: s.strip().lower(), next_node="node_index"))}) return text, options @@ -1392,7 +1380,7 @@ def start_olc(caller, session=None, metaproto=None): """ menudata = {"node_index": node_index, "node_validate_prototype": node_validate_prototype, - "node_meta_key": node_meta_key, + "node_prototype_key": node_prototype_key, "node_prototype": node_prototype, "node_typeclass": node_typeclass, "node_key": node_key, @@ -1404,11 +1392,11 @@ def start_olc(caller, session=None, metaproto=None): "node_location": node_location, "node_home": node_home, "node_destination": node_destination, - "node_meta_desc": node_meta_desc, - "node_meta_tags": node_meta_tags, - "node_meta_locks": node_meta_locks, + "node_prototype_desc": node_prototype_desc, + "node_prototype_tags": node_prototype_tags, + "node_prototype_locks": node_prototype_locks, } - OLCMenu(caller, menudata, startnode='node_index', session=session, olc_metaprot=metaproto) + OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=metaproto) # Testing From f3796ea6331c1dea999e0bd2e252810bde34d1a3 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 19 Apr 2018 22:23:24 +0200 Subject: [PATCH 046/103] Clean out metaprots, only use prototypes --- evennia/commands/default/building.py | 47 +++++++------- evennia/utils/spawner.py | 93 +++++++++++++--------------- 2 files changed, 67 insertions(+), 73 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 77bdb619f1..c593a6376d 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -14,9 +14,9 @@ from evennia.utils.utils import inherits_from, class_from_module, get_all_typecl from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore from evennia.utils.spawner import (spawn, search_prototype, list_prototypes, - save_db_prototype, build_metaproto, validate_prototype, + save_db_prototype, validate_prototype, delete_db_prototype, PermissionError, start_olc, - metaproto_to_str) + prototype_to_str) from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2885,12 +2885,12 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): return return prototype - def _search_show_prototype(query, metaprots=None): + def _search_show_prototype(query, prototypes=None): # prototype detail - if not metaprots: - metaprots = search_prototype(key=query, return_meta=True) - if metaprots: - return "\n".join(metaproto_to_str(metaprot) for metaprot in metaprots) + if not prototypes: + prototypes = search_prototype(key=query) + if prototypes: + return "\n".join(prototype_to_str(prot) for prot in prototypes) else: return False @@ -2898,18 +2898,18 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if self.cmdstring == "olc" or 'menu' in self.switches or 'olc' in self.switches: # OLC menu mode - metaprot = None + prototype = None if self.lhs: key = self.lhs - metaprot = search_prototype(key=key, return_meta=True) - if len(metaprot) > 1: + prototype = search_prototype(key=key, return_meta=True) + if len(prototype) > 1: caller.msg("More than one match for {}:\n{}".format( - key, "\n".join(mproto.key for mproto in metaprot))) + key, "\n".join(proto.get('prototype_key', '') for proto in prototype))) return - elif metaprot: + elif prototype: # one match - metaprot = metaprot[0] - start_olc(caller, session=self.session, metaproto=metaprot) + prototype = prototype[0] + start_olc(caller, session=self.session, prototype=prototype) return if 'search' in self.switches: @@ -3005,8 +3005,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): return # present prototype to save - new_matchstring = _search_show_prototype( - "", metaprots=[build_metaproto(key, desc, [lockstring], tags, prototype)]) + 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" @@ -3056,21 +3055,21 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if isinstance(prototype, basestring): # A prototype key we are looking to apply key = prototype - metaprotos = search_prototype(prototype) - nprots = len(metaprotos) - if not metaprotos: + prototypes = 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(metaproto.key for metaproto in metaprotos))) + nprots, prototype, ", ".join(prot.get('prototype_key', '') + for proto in prototypes))) return - # we have a metaprot, check access - metaproto = metaprotos[0] - if not caller.locks.check_lockstring(caller, metaproto.locks, access_type='use'): + # we have a prototype, check access + prototype = prototypes[0] + if not caller.locks.check_lockstring(caller, prototype.get('prototype_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 diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 0bce7addd8..1916c2210e 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -144,7 +144,7 @@ for mod in settings.PROTOTYPE_MODULES: prots = [(prototype_key, prot) for prototype_key, prot in all_from_module(mod).items() if prot and isinstance(prot, dict)] # assign module path to each prototype_key for easy reference - _MODULE_PROTOTYPE_MODULES.update({tup[0]: mod for tup in prots}) + _MODULE_PROTOTYPE_MODULES.update({prototype_key: mod for prototype_key, _ in prots}) # make sure the prototype contains all meta info for prototype_key, prot in prots: prot.update({ @@ -153,7 +153,7 @@ for mod in settings.PROTOTYPE_MODULES: "prototype_locks": prot['prototype_locks'] if 'prototype_locks' in prot else "use:all()", "prototype_tags": set(make_iter(prot['prototype_tags']) if 'prototype_tags' in prot else ["base-prototype"])}) - _MODULE_PROTOTYPES.update(prot) + _MODULE_PROTOTYPES[prototype_key] = prot # Prototype storage mechanisms @@ -413,23 +413,25 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed # this allows us to pass lists of empty strings tags = [tag for tag in make_iter(tags) if tag] - # get metaprotos for readonly and db-based prototypes + # get prototypes for readonly and db-based prototypes prototypes = search_prototype(key, tags) # get use-permissions of readonly attributes (edit is always False) display_tuples = [] - for prototype in sorted(prototypes, key=lambda d: d['prototype_key']): - lock_use = caller.locks.check_lockstring(caller, prototype['locks'], access_type='use') + for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')): + lock_use = caller.locks.check_lockstring( + caller, prototype.get('prototype_locks', ''), access_type='use') if not show_non_use and not lock_use: continue - lock_edit = caller.locks.check_lockstring(caller, prototype['locks'], access_type='edit') + lock_edit = caller.locks.check_lockstring( + caller, prototype.get('prototype_locks', ''), access_type='edit') if not show_non_edit and not lock_edit: continue display_tuples.append( - (prototype.get('prototype_key', '', - prototype['prototype_desc', ''], + (prototype.get('prototype_key', ''), + prototype.get('prototype_desc', ''), "{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'), - ",".join(prototype.get('prototype_tags', []))))) + ",".join(prototype.get('prototype_tags', [])))) if not display_tuples: return None @@ -450,7 +452,7 @@ def prototype_to_str(prototype): Format a prototype to a nice string representation. Args: - metaproto (NamedTuple): Represents the prototype. + prototype (dict): The prototype. """ header = ( @@ -706,7 +708,7 @@ def _get_menu_prototype(caller): if hasattr(caller.ndb._menutree, "olc_prototype"): prototype = caller.ndb._menutree.olc_prototype if not prototype: - caller.ndb._menutree.olc_prototype = {} + caller.ndb._menutree.olc_prototype = prototype = {} caller.ndb._menutree.olc_new = True return prototype @@ -721,10 +723,10 @@ def _set_menu_prototype(caller, field, value): caller.ndb._menutree.olc_prototype = prototype -def _format_property(key, required=False, prototype=None, cropper=None): - key = key.lower() +def _format_property(prop, required=False, prototype=None, cropper=None): + if prototype is not None: - prop = prototype.get(key, '') + prop = prototype.get(prop, '') out = prop if callable(prop): @@ -845,7 +847,7 @@ def node_index(caller): cropper = _path_cropper options.append( {"desc": "|w{}|n{}".format( - key, _format_property(key, required, None, prototype, cropper=cropper)), + key, _format_property(key, required, prototype, cropper=cropper)), "goto": "node_{}".format(key.lower())}) required = False for key in ('Desc', 'Tags', 'Locks'): @@ -900,7 +902,7 @@ def node_prototype_key(caller): text = ["The prototype name, or |wMeta-Key|n, uniquely identifies the prototype. " "It is used to find and use the prototype to spawn new entities. " "It is not case sensitive."] - old_key = prototype['prototype_key'] + old_key = prototype.get('prototype_key', None) if old_key: text.append("Current key is '|w{key}|n'".format(key=old_key)) else: @@ -914,7 +916,8 @@ def node_prototype_key(caller): def _all_prototypes(caller): - return [mproto.key for mproto in search_prototype()] + return [prototype["prototype_key"] + for prototype in search_prototype() if "prototype_key" in prototype] def _prototype_examine(caller, prototype_name): @@ -1122,17 +1125,15 @@ def node_attrs(caller): def _caller_tags(caller): - metaprot = _get_menu_prototype(caller) - prot = metaprot.prototype - tags = prot.get("tags") + prototype = _get_menu_prototype(caller) + tags = prototype.get("tags") return tags def _add_tag(caller, tag, **kwargs): tag = tag.strip().lower() - metaprot = _get_menu_prototype(caller) - prot = metaprot.prototype - tags = prot.get('tags', []) + prototype = _get_menu_prototype(caller) + tags = prototype.get('tags', []) if tags: if tag not in tags: tags.append(tag) @@ -1149,8 +1150,7 @@ def _add_tag(caller, tag, **kwargs): def _edit_tag(caller, old_tag, new_tag, **kwargs): - metaprot = _get_menu_prototype(caller) - prototype = metaprot.prototype + prototype = _get_menu_prototype(caller) tags = prototype.get('tags', []) old_tag = old_tag.strip().lower() @@ -1175,9 +1175,8 @@ def node_tags(caller): def node_locks(caller): - metaprot = _get_menu_prototype(caller) - prot = metaprot.prototype - locks = prot.get("locks") + prototype = _get_menu_prototype(caller) + locks = prototype.get("locks") text = ["Set the prototype's |yLock string|n. Separate multiple locks with semi-colons. " "Will retain case sensitivity."] @@ -1196,9 +1195,8 @@ def node_locks(caller): def node_permissions(caller): - metaprot = _get_menu_prototype(caller) - prot = metaprot.prototype - permissions = prot.get("permissions") + prototype = _get_menu_prototype(caller) + permissions = prototype.get("permissions") text = ["Set the prototype's |yPermissions|n. Separate multiple permissions with commas. " "Will retain case sensitivity."] @@ -1217,9 +1215,8 @@ def node_permissions(caller): def node_location(caller): - metaprot = _get_menu_prototype(caller) - prot = metaprot.prototype - location = prot.get("location") + prototype = _get_menu_prototype(caller) + location = prototype.get("location") text = ["Set the prototype's |yLocation|n"] if location: @@ -1237,9 +1234,8 @@ def node_location(caller): def node_home(caller): - metaprot = _get_menu_prototype(caller) - prot = metaprot.prototype - home = prot.get("home") + prototype = _get_menu_prototype(caller) + home = prototype.get("home") text = ["Set the prototype's |yHome location|n"] if home: @@ -1257,9 +1253,8 @@ def node_home(caller): def node_destination(caller): - metaprot = _get_menu_prototype(caller) - prot = metaprot.prototype - dest = prot.get("dest") + prototype = _get_menu_prototype(caller) + dest = prototype.get("dest") text = ["Set the prototype's |yDestination|n. This is usually only used for Exits."] if dest: @@ -1278,9 +1273,9 @@ def node_destination(caller): def node_prototype_desc(caller): - metaprot = _get_menu_prototype(caller) + prototype = _get_menu_prototype(caller) text = ["The |wMeta-Description|n briefly describes the prototype for viewing in listings."] - desc = metaprot.desc + desc = prototype.get("prototype_desc", None) if desc: text.append("The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc)) @@ -1298,10 +1293,10 @@ def node_prototype_desc(caller): def node_prototype_tags(caller): - metaprot = _get_menu_prototype(caller) + prototype = _get_menu_prototype(caller) text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " "Separate multiple by tags by commas."] - tags = metaprot.tags + tags = prototype.get('prototype_tags', []) if tags: text.append("The current tags are:\n|w{tags}|n".format(tags=tags)) @@ -1319,11 +1314,11 @@ def node_prototype_tags(caller): def node_prototype_locks(caller): - metaprot = _get_menu_prototype(caller) + prototype = _get_menu_prototype(caller) text = ["Set |wMeta-Locks|n on the prototype. There are two valid lock types: " "'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n" "(If you are unsure, leave as default.)"] - locks = metaprot.locks + locks = prototype.get('prototype_locks', '') if locks: text.append("Current lock is |w'{lockstring}'|n".format(lockstring=locks)) else: @@ -1367,14 +1362,14 @@ class OLCMenu(EvMenu): return "{}{}{}".format(olc_options, sep, other_options) -def start_olc(caller, session=None, metaproto=None): +def start_olc(caller, session=None, prototype=None): """ Start menu-driven olc system for prototypes. Args: caller (Object or Account): The entity starting the menu. session (Session, optional): The individual session to get data. - metaproto (MetaProto, optional): Given when editing an existing + prototype (dict, optional): Given when editing an existing prototype rather than creating a new one. """ @@ -1396,7 +1391,7 @@ def start_olc(caller, session=None, metaproto=None): "node_prototype_tags": node_prototype_tags, "node_prototype_locks": node_prototype_locks, } - OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=metaproto) + OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=prototype) # Testing From c298f1182fbae620da0cf4e288b0d8cde9434a5f Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 10 May 2018 20:37:21 +0200 Subject: [PATCH 047/103] Auto-tag spawned objects. Clean up unit tests --- evennia/settings_default.py | 6 +- evennia/utils/inlinefuncs.py | 11 +- evennia/utils/spawner.py | 272 +++++++++++++++++++++------- evennia/utils/tests/test_spawner.py | 48 +++-- 4 files changed, 251 insertions(+), 86 deletions(-) diff --git a/evennia/settings_default.py b/evennia/settings_default.py index a5c4b7255d..1d7adb4375 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -513,7 +513,7 @@ TIME_GAME_EPOCH = None TIME_IGNORE_DOWNTIMES = False ###################################################################### -# Inlinefunc +# Inlinefunc & PrototypeFuncs ###################################################################### # Evennia supports inline function preprocessing. This allows users # to supply inline calls on the form $func(arg, arg, ...) to do @@ -525,6 +525,10 @@ INLINEFUNC_ENABLED = False # is loaded from left-to-right, same-named functions will overload INLINEFUNC_MODULES = ["evennia.utils.inlinefuncs", "server.conf.inlinefuncs"] +# Module holding handlers for OLCFuncs. These allow for embedding +# functional code in prototypes +PROTOTYPEFUNC_MODULES = ["evennia.utils.prototypefuncs", + "server.conf.prototypefuncs"] ###################################################################### # Default Account setup and access diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index e103e217d7..2646fb3991 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -257,7 +257,7 @@ class InlinefuncError(RuntimeError): pass -def parse_inlinefunc(string, strip=False, **kwargs): +def parse_inlinefunc(string, strip=False, _available_funcs=None, **kwargs): """ Parse the incoming string. @@ -265,6 +265,8 @@ def parse_inlinefunc(string, strip=False, **kwargs): string (str): The incoming string to parse. strip (bool, optional): Whether to strip function calls rather than execute them. + _available_funcs(dict, optional): Define an alterinative source of functions to parse for. + If unset, use the functions found through `settings.INLINEFUNC_MODULES`. Kwargs: session (Session): This is sent to this function by Evennia when triggering it. It is passed to the inlinefunc. @@ -273,6 +275,9 @@ def parse_inlinefunc(string, strip=False, **kwargs): """ global _PARSING_CACHE + + _available_funcs = _INLINE_FUNCS if _available_funcs is None else _available_funcs + if string in _PARSING_CACHE: # stack is already cached stack = _PARSING_CACHE[string] @@ -309,9 +314,9 @@ def parse_inlinefunc(string, strip=False, **kwargs): funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1) try: # try to fetch the matching inlinefunc from storage - stack.append(_INLINE_FUNCS[funcname]) + stack.append(_available_funcs[funcname]) except KeyError: - stack.append(_INLINE_FUNCS["nomatch"]) + stack.append(_available_funcs["nomatch"]) stack.append(funcname) ncallable += 1 elif gdict["escaped"]: diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 1916c2210e..daf4b23c3f 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -22,28 +22,41 @@ GOBLIN = { ``` Possible keywords are: - prototype - string parent prototype - key - string, the main object identifier - typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS` - location - this should be a valid object or #dbref - home - valid object or #dbref - destination - only valid for exits (object or dbref) + prototype_key (str): name of this prototype. This is used when storing prototypes and should + be unique. This should always be defined but for prototypes defined in modules, the + variable holding the prototype dict will become the prototype_key if it's not explicitly + given. + prototype_desc (str, optional): describes prototype in listings + prototype_locks (str, optional): locks for restricting access to this prototype. Locktypes + supported are 'edit' and 'use'. + prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype + in listings - permissions - string or list of permission strings - locks - a lock-string - aliases - string or list of strings - exec - this is a string of python code to execute or a list of such codes. - This can be used e.g. to trigger custom handlers on the object. The - execution namespace contains 'evennia' for the library and 'obj' - tags - string or list of strings or tuples `(tagstr, category)`. Plain - strings will be result in tags with no category (default tags). - attrs - tuple or list of tuples of Attributes to add. This form allows - more complex Attributes to be set. Tuples at least specify `(key, value)` - but can also specify up to `(key, value, category, lockstring)`. If - you want to specify a lockstring but not a category, set the category - to `None`. - ndb_ - value of a nattribute (ndb_ is stripped) - other - any other name is interpreted as the key of an Attribute with + prototype (str or callable, optional): bame (prototype_key) of eventual parent prototype + typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use + `settings.BASE_OBJECT_TYPECLASS` + key (str or callable, optional): the name of the spawned object. If not given this will set to a + random hash + location (obj, str or callable, optional): location of the object - a valid object or #dbref + home (obj, str or callable, optional): valid object or #dbref + destination (obj, str or callable, optional): only valid for exits (object or #dbref) + + permissions (str, list or callable, optional): which permissions for spawned object to have + locks (str or callable, optional): lock-string for the spawned object + aliases (str, list or callable, optional): Aliases for the spawned object + exec (str or callable, optional): this is a string of python code to execute or a list of such + codes. This can be used e.g. to trigger custom handlers on the object. The execution + namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit + this functionality to Developer/superusers. Usually it's better to use callables or + prototypefuncs instead of this. + tags (str, tuple, list or callable, optional): string or list of strings or tuples + `(tagstr, category)`. Plain strings will be result in tags with no category (default tags). + attrs (tuple, list or callable, optional): tuple or list of tuples of Attributes to add. This + form allows more complex Attributes to be set. Tuples at least specify `(key, value)` + but can also specify up to `(key, value, category, lockstring)`. If you want to specify a + lockstring but not a category, set the category to `None`. + ndb_ (any): value of a nattribute (ndb_ is stripped) + other (any): any other name is interpreted as the key of an Attribute with its value. Such Attributes have no categories. Each value can also be a callable that takes no arguments. It should @@ -56,6 +69,9 @@ that prototype, inheritng all prototype slots it does not explicitly define itself, while overloading those that it does specify. ```python +import random + + GOBLIN_WIZARD = { "prototype": GOBLIN, "key": "goblin wizard", @@ -65,6 +81,7 @@ GOBLIN_WIZARD = { GOBLIN_ARCHER = { "prototype": GOBLIN, "key": "goblin archer", + "attack_skill": (random, (5, 10))" "attacks": ["short bow"] } ``` @@ -105,15 +122,18 @@ prototype, override its name with an empty dict. from __future__ import print_function import copy +import hashlib +import time from ast import literal_eval from django.conf import settings from random import randint import evennia from evennia.objects.models import ObjectDB from evennia.utils.utils import ( - make_iter, all_from_module, dbid_to_obj, is_iter, crop, get_all_typeclasses) + make_iter, all_from_module, callables_from_module, dbid_to_obj, + is_iter, crop, get_all_typeclasses) +from evennia.utils import inlinefuncs -from collections import namedtuple from evennia.scripts.scripts import DefaultScript from evennia.utils.create import create_script from evennia.utils.evtable import EvTable @@ -126,7 +146,9 @@ _PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "p _NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES _MODULE_PROTOTYPES = {} _MODULE_PROTOTYPE_MODULES = {} +_PROTOTYPEFUNCS = {} _MENU_CROP_WIDTH = 15 +_PROTOTYPE_TAG_CATEGORY = "spawned_by_prototype" _MENU_ATTR_LITERAL_EVAL_ERROR = ( "|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n" @@ -138,6 +160,9 @@ class PermissionError(RuntimeError): pass +# load resources + + 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) @@ -148,7 +173,7 @@ for mod in settings.PROTOTYPE_MODULES: # make sure the prototype contains all meta info for prototype_key, prot in prots: prot.update({ - "prototype_key": prototype_key.lower(), + "prototype_key": prot.get('prototype_key', prototype_key.lower()), "prototype_desc": prot['prototype_desc'] if 'prototype_desc' in prot else mod, "prototype_locks": prot['prototype_locks'] if 'prototype_locks' in prot else "use:all()", "prototype_tags": set(make_iter(prot['prototype_tags']) @@ -156,6 +181,81 @@ for mod in settings.PROTOTYPE_MODULES: _MODULE_PROTOTYPES[prototype_key] = prot +for mod in settings.PROTOTYPEFUNC_MODULES: + try: + _PROTOTYPEFUNCS.update(callables_from_module(mod)) + except ImportError: + pass + + +# Helper functions + + +def olcfunc_parser(value, available_functions=None, **kwargs): + """ + This is intended to be used by the in-game olc mechanism. It will parse the prototype + value for function tokens like `$olcfunc(arg, arg, ...)`. These functions behave all the + parameters of `inlinefuncs` but they are *not* passed a Session since this is not guaranteed to + be available at the time of spawning. They may also return other structures than strings. + + Available olcfuncs are specified as callables in one of the modules of + `settings.PROTOTYPEFUNC_MODULES`, or specified on the command line. + + Args: + value (string): The value to test for a parseable olcfunc. + available_functions (dict, optional): Mapping of name:olcfunction to use for this parsing. + + Kwargs: + any (any): Passed on to the inlinefunc. + + Returns: + any (any): A structure to replace the string on the prototype level. If this is a + callable or a (callable, (args,)) structure, it will be executed as if one had supplied + it to the prototype directly. + + """ + if not isinstance(basestring, value): + return value + available_functions = _PROTOTYPEFUNCS if available_functions is None else available_functions + return inlinefuncs.parse_inlinefunc(value, _available_funcs=available_functions) + + +def _to_obj(value, force=True): + return dbid_to_obj(value, ObjectDB) + + +def _to_obj_or_any(value): + obj = dbid_to_obj(value, ObjectDB) + return obj if obj is not None else value + + +def validate_spawn_value(value, validator=None): + """ + Analyze the value and produce a value for use at the point of spawning. + + Args: + value (any): This can be:j + callable - will be called as callable() + (callable, (args,)) - will be called as callable(*args) + other - will be assigned depending on the variable type + validator (callable, optional): If given, this will be called with the value to + check and guarantee the outcome is of a given type. + + Returns: + any (any): The (potentially pre-processed value to use for this prototype key) + + """ + validator = validator if validator else lambda o: o + if callable(value): + return validator(value()) + elif value and is_iter(value) and callable(value[0]): + # a structure (callable, (args, )) + args = value[1:] + return validator(value[0](*make_iter(args))) + else: + return validator(value) + + # Prototype storage mechanisms @@ -384,6 +484,20 @@ def search_prototype(key=None, tags=None): return matches +def search_objects_with_prototype(prototype_key): + """ + Retrieve all object instances created by a given prototype. + + Args: + prototype_key (str): The exact (and unique) prototype identifier to query for. + + Returns: + matches (Queryset): All matching objects spawned from this prototype. + + """ + return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + + def get_protparent_dict(): """ Get prototype parents. @@ -401,7 +515,7 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed Args: caller (Account or Object): The object requesting the list. - key (str, optional): Exact or partial key to query for. + key (str, optional): Exact or partial prototype 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. @@ -427,23 +541,34 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed caller, prototype.get('prototype_locks', ''), access_type='edit') if not show_non_edit and not lock_edit: continue + ptags = [] + for ptag in prototype.get('prototype_tags', []): + if is_iter(ptag): + if len(ptag) > 1: + ptags.append("{} (category: {}".format(ptag[0], ptag[1])) + else: + ptags.append(ptag[0]) + else: + ptags.append(str(ptag)) + display_tuples.append( (prototype.get('prototype_key', ''), prototype.get('prototype_desc', ''), "{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'), - ",".join(prototype.get('prototype_tags', [])))) + ",".join(ptags))) if not display_tuples: return None table = [] + width = 78 for i in range(len(display_tuples[0])): table.append([str(display_tuple[i]) for display_tuple in display_tuples]) - 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) + table = EvTable("Key", "Desc", "Use/Edit", "Tags", table=table, crop=True, width=width) + table.reformat_column(0, width=22) + table.reformat_column(1, width=31) + table.reformat_column(2, width=9, align='r') + table.reformat_column(3, width=16) return table @@ -472,17 +597,14 @@ def prototype_to_str(prototype): # Spawner mechanism -def _handle_dbref(inp): - return dbid_to_obj(inp, ObjectDB) - - def validate_prototype(prototype, protkey=None, protparents=None, _visited=None): """ Run validation on a prototype, checking for inifinite regress. Args: prototype (dict): Prototype to validate. - protkey (str, optional): The name of the prototype definition, if any. + protkey (str, optional): The name of the prototype definition. If not given, the prototype + dict needs to have the `prototype_key` field set. protpartents (dict, optional): The available prototype parent library. If note given this will be determined from settings/database. _visited (list, optional): This is an internal work array and should not be set manually. @@ -494,9 +616,8 @@ def validate_prototype(prototype, protkey=None, protparents=None, _visited=None) protparents = get_protparent_dict() if _visited is None: _visited = [] - protkey = protkey or prototype.get('prototype_key', None) - protkey = protkey.lower() or prototype.get('prototype_key', None) + protkey = protkey and protkey.lower() or prototype.get('prototype_key', "") assert isinstance(prototype, dict) @@ -619,9 +740,12 @@ def spawn(*prototypes, **kwargs): return_prototypes (bool): Only return a list of the prototype-parents (no object creation happens) + Returns: + object (Object): Spawned object. + """ # get available protparents - protparents = get_protparents() + protparents = get_protparent_dict() # overload module's protparents with specifically given protparents protparents.update(kwargs.get("prototype_parents", {})) @@ -643,47 +767,61 @@ def spawn(*prototypes, **kwargs): # extract the keyword args we need to create the object itself. If we get a callable, # call that to get the value (don't catch errors) create_kwargs = {} - keyval = prot.pop("key", "Spawned Object %06i" % randint(1, 100000)) - create_kwargs["db_key"] = keyval() if callable(keyval) else keyval + # we must always add a key, so if not given we use a shortened md5 hash. There is a (small) + # chance this is not unique but it should usually not be a problem. + val = prot.pop("key", "Spawned-{}".format( + hashlib.md5(str(time.time())).hexdigest()[:6])) + create_kwargs["db_key"] = validate_spawn_value(val, str) - locval = prot.pop("location", None) - create_kwargs["db_location"] = locval() if callable(locval) else _handle_dbref(locval) + val = prot.pop("location", None) + create_kwargs["db_location"] = validate_spawn_value(val, _to_obj) - homval = prot.pop("home", settings.DEFAULT_HOME) - create_kwargs["db_home"] = homval() if callable(homval) else _handle_dbref(homval) + val = prot.pop("home", settings.DEFAULT_HOME) + create_kwargs["db_home"] = validate_spawn_value(val, _to_obj) - destval = prot.pop("destination", None) - create_kwargs["db_destination"] = destval() if callable(destval) else _handle_dbref(destval) + val = prot.pop("destination", None) + create_kwargs["db_destination"] = validate_spawn_value(val, _to_obj) - typval = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) - create_kwargs["db_typeclass_path"] = typval() if callable(typval) else typval + val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) + create_kwargs["db_typeclass_path"] = validate_spawn_value(val, str) # extract calls to handlers - permval = prot.pop("permissions", []) - permission_string = permval() if callable(permval) else permval - lockval = prot.pop("locks", "") - lock_string = lockval() if callable(lockval) else lockval - aliasval = prot.pop("aliases", "") - alias_string = aliasval() if callable(aliasval) else aliasval - tagval = prot.pop("tags", []) - tags = tagval() if callable(tagval) else tagval + val = prot.pop("permissions", []) + permission_string = validate_spawn_value(val, make_iter) + val = prot.pop("locks", "") + lock_string = validate_spawn_value(val, str) + val = prot.pop("aliases", []) + alias_string = validate_spawn_value(val, make_iter) + + val = prot.pop("tags", []) + tags = validate_spawn_value(val, make_iter) # we make sure to add a tag identifying which prototype created this object - # tags.append(()) + tags.append((prototype['prototype_key'], _PROTOTYPE_TAG_CATEGORY)) - attrval = prot.pop("attrs", []) - attributes = attrval() if callable(tagval) else attrval - - exval = prot.pop("exec", "") - execs = make_iter(exval() if callable(exval) else exval) + val = prot.pop("exec", "") + execs = validate_spawn_value(val, make_iter) # extract ndb assignments - nattributes = dict((key.split("_", 1)[1], value() if callable(value) else value) - for key, value in prot.items() if key.startswith("ndb_")) + nattributes = dict((key.split("_", 1)[1], validate_spawn_value(val, _to_obj)) + for key, val in prot.items() if key.startswith("ndb_")) # the rest are attributes - simple_attributes = [(key, value()) if callable(value) else (key, value) - for key, value in prot.items() if not (key.startswith("ndb_"))] + val = prot.pop("attrs", []) + attributes = validate_spawn_value(val, list) + + simple_attributes = [] + for key, value in ((key, value) for key, value in prot.items() + if not (key.startswith("ndb_"))): + if is_iter(value) and len(value) > 1: + # (value, category) + simple_attributes.append((key, + validate_spawn_value(value[0], _to_obj_or_any), + validate_spawn_value(value[1], str))) + else: + simple_attributes.append((key, + validate_spawn_value(value, _to_obj_or_any))) + attributes = attributes + simple_attributes attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS] diff --git a/evennia/utils/tests/test_spawner.py b/evennia/utils/tests/test_spawner.py index e29ee8c151..4d680a9e8a 100644 --- a/evennia/utils/tests/test_spawner.py +++ b/evennia/utils/tests/test_spawner.py @@ -7,16 +7,29 @@ from evennia.utils.test_resources import EvenniaTest from evennia.utils import spawner +class TestSpawner(EvenniaTest): + + def setUp(self): + super(TestSpawner, self).setUp() + self.prot1 = {"prototype_key": "testprototype"} + + def test_spawn(self): + obj1 = spawner.spawn(self.prot1) + # check spawned objects have the right tag + self.assertEqual(list(spawner.search_objects_with_prototype("testprototype")), obj1) + + class TestPrototypeStorage(EvenniaTest): def setUp(self): super(TestPrototypeStorage, self).setUp() - self.prot1 = {"key": "testprototype"} - self.prot2 = {"key": "testprototype2"} - self.prot3 = {"key": "testprototype3"} + self.prot1 = {"prototype_key": "testprototype"} + self.prot2 = {"prototype_key": "testprototype2"} + self.prot3 = {"prototype_key": "testprototype3"} def _get_metaproto( - self, key='testprototype', desc='testprototype', locks=['edit:id(6) or perm(Admin)', 'use:all()'], + self, key='testprototype', desc='testprototype', + locks=['edit:id(6) or perm(Admin)', 'use:all()'], tags=[], prototype={"key": "testprototype"}): return spawner.build_metaproto(key, desc, locks, tags, prototype) @@ -28,34 +41,39 @@ class TestPrototypeStorage(EvenniaTest): def test_prototype_storage(self): - prot = spawner.save_db_prototype(self.char1, "testprot", self.prot1, desc='testdesc0', tags=["foo"]) + prot = spawner.save_db_prototype(self.char1, self.prot1, "testprot", + desc='testdesc0', tags=["foo"]) self.assertTrue(bool(prot)) self.assertEqual(prot.db.prototype, self.prot1) self.assertEqual(prot.desc, "testdesc0") - prot = spawner.save_db_prototype(self.char1, "testprot", self.prot1, desc='testdesc', tags=["fooB"]) + prot = spawner.save_db_prototype(self.char1, self.prot1, "testprot", + desc='testdesc', tags=["fooB"]) self.assertEqual(prot.db.prototype, self.prot1) self.assertEqual(prot.desc, "testdesc") self.assertTrue(bool(prot.tags.get("fooB", "db_prototype"))) self.assertEqual(list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot]) - prot2 = spawner.save_db_prototype(self.char1, "testprot2", self.prot2, desc='testdesc2b', tags=["foo"]) - self.assertEqual(list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot, prot2]) + prot2 = spawner.save_db_prototype(self.char1, self.prot2, "testprot2", + desc='testdesc2b', tags=["foo"]) + self.assertEqual( + list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot, prot2]) - prot3 = spawner.save_db_prototype(self.char1, "testprot2", self.prot3, desc='testdesc2') + prot3 = spawner.save_db_prototype(self.char1, self.prot3, "testprot2", desc='testdesc2') self.assertEqual(prot2.id, prot3.id) - self.assertEqual(list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot, prot2]) + self.assertEqual( + list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot, prot2]) # returns DBPrototype - self.assertEqual(list(spawner.search_db_prototype("testprot")), [prot]) + self.assertEqual(list(spawner.search_db_prototype("testprot", return_queryset=True)), [prot]) - # returns metaprotos - prot = self._to_metaproto(prot) - prot3 = self._to_metaproto(prot3) + prot = prot.db.prototype + prot3 = prot3.db.prototype self.assertEqual(list(spawner.search_prototype("testprot")), [prot]) - self.assertEqual(list(spawner.search_prototype("testprot", return_meta=False)), [self.prot1]) + self.assertEqual( + list(spawner.search_prototype("testprot")), [self.prot1]) # partial match self.assertEqual(list(spawner.search_prototype("prot")), [prot, prot3]) self.assertEqual(list(spawner.search_prototype(tags="foo")), [prot, prot3]) From 02067583bd1e6b14241edc1837dbbd3e69fa18ed Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 10 May 2018 22:28:16 +0200 Subject: [PATCH 048/103] Work to test functionality --- evennia/commands/default/building.py | 8 +++++++- evennia/commands/default/tests.py | 5 +++-- evennia/utils/spawner.py | 30 +++++++++++++++++----------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 8616b7dafa..b41b5c40e9 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2840,8 +2840,14 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): |wdestination|n - only valid for exits (object or dbref) |wpermissions|n - string or list of permission strings |wlocks |n - a lock-string - |waliases |n - string or list of strings + |waliases |n - string or list of strings. |wndb_|n - value of a nattribute (ndb_ is stripped) + + |wprototype_key|n - name of this prototype. Used to store/retrieve from db + |wprototype_desc|n - desc of this prototype. Used in listings + |wprototype_locks|n - locks of this prototype. Limits who may use prototype + |wprototype_tags|n - tags of this prototype. Used to find prototype + any other keywords are interpreted as Attributes and their values. The available prototypes are defined globally in modules set in diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 950b934125..ffb877c3e3 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -369,7 +369,8 @@ class TestBuilding(CommandTest): # Tests "@spawn " without specifying location. self.call(building.CmdSpawn(), - "{'key':'goblin', 'typeclass':'evennia.DefaultCharacter'}", "Spawned goblin") + "{'prototype_key': 'testprot', 'key':'goblin', " + "'typeclass':'evennia.DefaultCharacter'}", "Spawned goblin") goblin = getObject(self, "goblin") # Tests that the spawned object's type is a DefaultCharacter. @@ -394,7 +395,7 @@ class TestBuilding(CommandTest): self.assertEqual(goblin.location, spawnLoc) goblin.delete() - spawner.save_db_prototype(self.char1, "ball", {'key': 'Ball', 'prototype': 'GOBLIN'}) + spawner.save_db_prototype(self.char1, {'key': 'Ball', 'prototype': 'GOBLIN'}, 'ball') # Tests "@spawn " self.call(building.CmdSpawn(), "ball", "Spawned Ball") diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index daf4b23c3f..335eea9341 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -172,13 +172,14 @@ for mod in settings.PROTOTYPE_MODULES: _MODULE_PROTOTYPE_MODULES.update({prototype_key: mod for prototype_key, _ in prots}) # make sure the prototype contains all meta info for prototype_key, prot in prots: + actual_prot_key = prot.get('prototype_key', prototype_key).lower() prot.update({ - "prototype_key": prot.get('prototype_key', prototype_key.lower()), + "prototype_key": actual_prot_key, "prototype_desc": prot['prototype_desc'] if 'prototype_desc' in prot else mod, - "prototype_locks": prot['prototype_locks'] if 'prototype_locks' in prot else "use:all()", - "prototype_tags": set(make_iter(prot['prototype_tags']) - if 'prototype_tags' in prot else ["base-prototype"])}) - _MODULE_PROTOTYPES[prototype_key] = prot + "prototype_locks": (prot['prototype_locks'] + if 'prototype_locks' in prot else "use:all();edit:false()"), + "prototype_tags": list(set(make_iter(prot.get('prototype_tags', [])) + ["module"]))}) + _MODULE_PROTOTYPES[actual_prot_key] = prot for mod in settings.PROTOTYPEFUNC_MODULES: @@ -537,8 +538,11 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed caller, prototype.get('prototype_locks', ''), access_type='use') if not show_non_use and not lock_use: continue - lock_edit = caller.locks.check_lockstring( - caller, prototype.get('prototype_locks', ''), access_type='edit') + if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES: + lock_edit = False + else: + lock_edit = caller.locks.check_lockstring( + caller, prototype.get('prototype_locks', ''), access_type='edit') if not show_non_edit and not lock_edit: continue ptags = [] @@ -566,8 +570,8 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed table.append([str(display_tuple[i]) for display_tuple in display_tuples]) table = EvTable("Key", "Desc", "Use/Edit", "Tags", table=table, crop=True, width=width) table.reformat_column(0, width=22) - table.reformat_column(1, width=31) - table.reformat_column(2, width=9, align='r') + table.reformat_column(1, width=29) + table.reformat_column(2, width=11, align='c') table.reformat_column(3, width=16) return table @@ -617,7 +621,7 @@ def validate_prototype(prototype, protkey=None, protparents=None, _visited=None) if _visited is None: _visited = [] - protkey = protkey and protkey.lower() or prototype.get('prototype_key', "") + protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) assert isinstance(prototype, dict) @@ -796,8 +800,10 @@ def spawn(*prototypes, **kwargs): val = prot.pop("tags", []) tags = validate_spawn_value(val, make_iter) - # we make sure to add a tag identifying which prototype created this object - tags.append((prototype['prototype_key'], _PROTOTYPE_TAG_CATEGORY)) + 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)) val = prot.pop("exec", "") execs = validate_spawn_value(val, make_iter) From 78ce1d21028c9004ae03291898960d69a420cc2f Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 12 May 2018 12:19:47 +0200 Subject: [PATCH 049/103] Fix unit tests --- evennia/utils/spawner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 335eea9341..c63ddaf868 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -388,7 +388,7 @@ def search_db_prototype(key=None, tags=None, return_queryset=False): tags (str or list): Tag key or keys to query for. These will always be applied with the 'db_protototype' tag category. - return_queryset (bool): Return the database queryset. + return_queryset (bool, optional): Return the database queryset. Return: matches (queryset or list): All found DbPrototypes. If `return_queryset` is not set, this is a list of prototype dicts. @@ -410,7 +410,7 @@ def search_db_prototype(key=None, tags=None, return_queryset=False): matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key) if not return_queryset: # return prototype - return [dbprot.attributes.get("prototype", {}) for dbprot in matches] + matches = [dict(dbprot.attributes.get("prototype", {})) for dbprot in matches] return matches From 436ad4d8a588b845e3d6de3cb7d30c37e8f17423 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 13 May 2018 14:50:48 +0200 Subject: [PATCH 050/103] Unittests pass --- evennia/utils/spawner.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index c63ddaf868..aa62a1dcad 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -192,19 +192,19 @@ for mod in settings.PROTOTYPEFUNC_MODULES: # Helper functions -def olcfunc_parser(value, available_functions=None, **kwargs): +def protfunc_parser(value, available_functions=None, **kwargs): """ This is intended to be used by the in-game olc mechanism. It will parse the prototype - value for function tokens like `$olcfunc(arg, arg, ...)`. These functions behave all the + value for function tokens like `$protfunc(arg, arg, ...)`. These functions behave all the parameters of `inlinefuncs` but they are *not* passed a Session since this is not guaranteed to be available at the time of spawning. They may also return other structures than strings. - Available olcfuncs are specified as callables in one of the modules of + Available protfuncs are specified as callables in one of the modules of `settings.PROTOTYPEFUNC_MODULES`, or specified on the command line. Args: - value (string): The value to test for a parseable olcfunc. - available_functions (dict, optional): Mapping of name:olcfunction to use for this parsing. + value (string): The value to test for a parseable protfunc. + available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. Kwargs: any (any): Passed on to the inlinefunc. @@ -215,7 +215,7 @@ def olcfunc_parser(value, available_functions=None, **kwargs): it to the prototype directly. """ - if not isinstance(basestring, value): + if not isinstance(value, basestring): return value available_functions = _PROTOTYPEFUNCS if available_functions is None else available_functions return inlinefuncs.parse_inlinefunc(value, _available_funcs=available_functions) @@ -246,6 +246,7 @@ def validate_spawn_value(value, validator=None): any (any): The (potentially pre-processed value to use for this prototype key) """ + value = protfunc_parser(value) validator = validator if validator else lambda o: o if callable(value): return validator(value()) From eeeef27283b6b90d707c5d0ee9a6811a2827d216 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 15 May 2018 15:42:04 +0200 Subject: [PATCH 051/103] Start work on prototype updating --- evennia/commands/default/building.py | 123 +++++++++++++++++---------- evennia/utils/spawner.py | 25 ++++++ 2 files changed, 105 insertions(+), 43 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index b41b5c40e9..4aabc861b1 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -13,10 +13,7 @@ from evennia.utils import create, utils, search from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore -from evennia.utils.spawner import (spawn, search_prototype, list_prototypes, - save_db_prototype, validate_prototype, - delete_db_prototype, PermissionError, start_olc, - prototype_to_str) +from evennia.utils import spawner from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2792,12 +2789,6 @@ class CmdTag(COMMAND_DEFAULT_CLASS): string = "No tags attached to %s." % obj self.caller.msg(string) -# -# To use the prototypes with the @spawn function set -# PROTOTYPE_MODULES = ["commands.prototypes"] -# Reload the server and the prototypes should be available. -# - class CmdSpawn(COMMAND_DEFAULT_CLASS): """ @@ -2810,6 +2801,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): @spawn/search [key][;tag[,tag]] @spawn/list [tag, tag] @spawn/show [] + @spawn/update @spawn/save [;desc[;tag,tag[,...][;lockstring]]] = @spawn/menu [] @@ -2823,6 +2815,10 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): show, examine - inspect prototype by key. If not given, acts like list. 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 + 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. menu, olc - create/manipulate prototype in a menu interface. Example: @@ -2843,7 +2839,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): |waliases |n - string or list of strings. |wndb_|n - value of a nattribute (ndb_ is stripped) - |wprototype_key|n - name of this prototype. Used to store/retrieve from db + |wprototype_key|n - name of this prototype. Unique. Used to store/retrieve from db + and update existing prototyped objects if desired. |wprototype_desc|n - desc of this prototype. Used in listings |wprototype_locks|n - locks of this prototype. Limits who may use prototype |wprototype_tags|n - tags of this prototype. Used to find prototype @@ -2858,7 +2855,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): key = "@spawn" aliases = ["@olc"] - switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu", "olc") + switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu", "olc", "update") locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" @@ -2890,7 +2887,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): "use the 'exec' prototype key.") return None try: - validate_prototype(prototype) + spawner.validate_prototype(prototype) except RuntimeError as err: self.caller.msg(str(err)) return @@ -2899,9 +2896,9 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): def _search_show_prototype(query, prototypes=None): # prototype detail if not prototypes: - prototypes = search_prototype(key=query) + prototypes = spawner.search_prototype(key=query) if prototypes: - return "\n".join(prototype_to_str(prot) for prot in prototypes) + return "\n".join(spawner.prototype_to_str(prot) for prot in prototypes) else: return False @@ -2912,7 +2909,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): prototype = None if self.lhs: key = self.lhs - prototype = search_prototype(key=key, return_meta=True) + prototype = spawner.search_prototype(key=key, return_meta=True) if len(prototype) > 1: caller.msg("More than one match for {}:\n{}".format( key, "\n".join(proto.get('prototype_key', '') for proto in prototype))) @@ -2920,7 +2917,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): elif prototype: # one match prototype = prototype[0] - start_olc(caller, session=self.session, prototype=prototype) + spawner.start_olc(caller, session=self.session, prototype=prototype) return if 'search' in self.switches: @@ -2932,7 +2929,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): 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)), + EvMore(caller, unicode(spawner.list_prototypes(caller, key=key, tags=tags)), exit_on_lastpage=True) return @@ -2950,30 +2947,10 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if 'list' in self.switches: # for list, all optional arguments are tags - EvMore(caller, unicode(list_prototypes(caller, + EvMore(caller, unicode(spawner.list_prototypes(caller, tags=self.lhslist)), exit_on_lastpage=True) return - if 'delete' in self.switches: - # remove db-based prototype - matchstring = _search_show_prototype(self.args) - if matchstring: - question = "\nDo you want to continue deleting? [Y]/N" - string = "|rDeleting prototype:|n\n{}".format(matchstring) - answer = yield(string + question) - if answer.lower() in ["n", "no"]: - caller.msg("|rDeletion cancelled.|n") - return - try: - success = delete_db_prototype(caller, self.args) - except PermissionError as err: - caller.msg("|rError deleting:|R {}|n".format(err)) - caller.msg("Deletion {}.".format( - 'successful' if success else 'failed (does the prototype exist?)')) - return - else: - caller.msg("Could not find prototype '{}'".format(key)) - if 'save' in self.switches: # store a prototype to the database store if not self.args or not self.rhs: @@ -3015,6 +2992,12 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if not prototype: return + # inject the prototype_* keys into the prototype to save + prototype['prototype_key'] = prototype.get('prototype_key', key) + prototype['prototype_desc'] = prototype.get('prototype_desc', desc) + prototype['prototype_tags'] = prototype.get('prototype_tags', tags) + prototype['prototype_locks'] = prototype.get('prototype_locks', lockstring) + # present prototype to save new_matchstring = _search_show_prototype("", prototypes=[prototype]) string = "|yCreating new prototype:|n\n{}".format(new_matchstring) @@ -3034,7 +3017,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # all seems ok. Try to save. try: - prot = save_db_prototype( + prot = spawner.save_db_prototype( caller, key, prototype, desc=desc, tags=tags, locks=lockstring) if not prot: caller.msg("|rError saving:|R {}.|n".format(key)) @@ -3046,14 +3029,68 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): caller.msg("|rError saving:|R {}|n".format(err)) return caller.msg("|gSaved prototype:|n {}".format(key)) + + # check if we want to update existing objects + existing_objects = spawner.search_objects_with_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, 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)) return if not self.args: - ncount = len(search_prototype()) + ncount = len(spawner.search_prototype()) caller.msg("Usage: @spawn or {{key: value, ...}}" "\n ({} existing prototypes. Use /list to inspect)".format(ncount)) 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 = spawner.delete_db_prototype(caller, self.args) + except PermissionError as err: + caller.msg("|rError deleting:|R {}|n".format(err)) + caller.msg("Deletion {}.".format( + 'successful' if success else 'failed (does the prototype exist?)')) + return + else: + caller.msg("Could not find prototype '{}'".format(key)) + + if 'update' in self.switches: + # update existing prototypes + key = self.args.strip().lower() + existing_objects = spawner.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)) + # A direct creation of an object from a given prototype prototype = _parse_prototype( @@ -3066,7 +3103,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if isinstance(prototype, basestring): # A prototype key we are looking to apply key = prototype - prototypes = search_prototype(prototype) + prototypes = spawner.search_prototype(prototype) nprots = len(prototypes) if not prototypes: caller.msg("No prototype named '%s'." % prototype) @@ -3087,7 +3124,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # proceed to spawning try: - for obj in spawn(prototype): + for obj in spawner.spawn(prototype): self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) except RuntimeError as err: caller.msg(err) diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index aa62a1dcad..06cb59c178 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -270,6 +270,9 @@ class DbPrototype(DefaultScript): self.desc = "A prototype" # prototype_desc + + + def save_db_prototype(caller, prototype, key=None, desc=None, tags=None, locks="", delete=False): """ Store a prototype persistently. @@ -662,6 +665,28 @@ def _get_prototype(dic, prot, protparents): return prot +def batch_update_objects_with_prototype(prototype, objects=None): + """ + Update existing objects with the latest version of the prototype. + + Args: + prototype (str or dict): Either the `prototype_key` to use or the + prototype dict itself. + objects (list): List of objects to update. If not given, query for these + objects using the prototype's `prototype_key`. + Returns: + changed (int): The number of objects that had changes applied to them. + """ + prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] + prototype_obj = search_db_prototype(prototype_key, return_queryset=True) + prototype_obj = prototype_obj[0] if prototype_obj else None + new_prototype = prototype_obj.db.prototype + + + + return 0 + + def _batch_create_object(*objparams): """ This is a cut-down version of the create_object() function, From 37c679d2f24e9e40a3bad407579f48a1ddc477fc Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 22 May 2018 22:28:03 +0200 Subject: [PATCH 052/103] Update-objs with prototype, first version, no testing yet --- evennia/typeclasses/attributes.py | 1 + evennia/utils/spawner.py | 159 +++++++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 5 deletions(-) diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 4ed68a1fe8..eb698e6f0e 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -435,6 +435,7 @@ class AttributeHandler(object): def __init__(self): self.key = None self.value = default + self.category = None self.strvalue = str(default) if default is not None else None ret = [] diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index 06cb59c178..3c269ca742 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -235,7 +235,7 @@ def validate_spawn_value(value, validator=None): Analyze the value and produce a value for use at the point of spawning. Args: - value (any): This can be:j + value (any): This can be: callable - will be called as callable() (callable, (args,)) - will be called as callable(*args) other - will be assigned depending on the variable type @@ -602,6 +602,44 @@ def prototype_to_str(prototype): return header + proto +def prototype_from_object(obj): + """ + Guess a minimal prototype from an existing object. + + Args: + obj (Object): An object to analyze. + + Returns: + prototype (dict): A prototype estimating the current state of the object. + + """ + # first, check if this object already has a prototype + + prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) + prot = search_prototype(prot) + if not prot or len(prot) > 1: + # no unambiguous prototype found - build new prototype + prot = {} + prot['prototype_key'] = "From-Object-{}-{}".format( + obj.key, hashlib.md5(str(time.time())).hexdigest()[:6]) + prot['prototype_desc'] = "Built from {}".format(str(obj)) + prot['prototype_locks'] = "use:all();edit:all()" + + prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] + prot['location'] = obj.db_location + prot['home'] = obj.db_home + prot['destination'] = obj.db_destination + prot['typeclass'] = obj.db_typeclass_path + prot['locks'] = obj.locks.all() + prot['permissions'] = obj.permissions.get() + prot['aliases'] = obj.aliases.get() + prot['tags'] = [(tag.key, tag.category, tag.data) + for tag in obj.tags.get(return_tagobj=True, return_list=True)] + prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks) + for attr in obj.attributes.get(return_obj=True, return_list=True)] + + return prot + # Spawner mechanism @@ -665,26 +703,137 @@ def _get_prototype(dic, prot, protparents): return prot -def batch_update_objects_with_prototype(prototype, objects=None): +def prototype_diff_from_object(prototype, obj): + """ + 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 + needed. + + Args: + prototype (dict): Prototype. + obj (Object): Object to + + Returns: + diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} + + """ + prot1 = prototype + prot2 = prototype_from_object(obj) + + diff = {} + for key, value in prot1.items(): + diff[key] = "KEEP" + if key in prot2: + if callable(prot2[key]) or value != prot2[key]: + diff[key] = "UPDATE" + elif key not in prot2: + diff[key] = "REMOVE" + + return diff + + +def batch_update_objects_with_prototype(prototype, diff=None, objects=None): """ Update existing objects with the latest version of the prototype. Args: prototype (str or dict): Either the `prototype_key` to use or the prototype dict itself. - objects (list): List of objects to update. If not given, query for these + diff (dict, optional): This a diff structure that describes how to update the protototype. 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`. Returns: changed (int): The number of objects that had changes applied to them. + """ prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] prototype_obj = search_db_prototype(prototype_key, return_queryset=True) prototype_obj = prototype_obj[0] if prototype_obj else None new_prototype = prototype_obj.db.prototype + objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + if not objs: + return 0 + if not diff: + diff = prototype_diff_from_object(new_prototype, objs[0]) - return 0 + changed = 0 + for obj in objs: + do_save = False + for key, directive in diff.items(): + val = new_prototype[key] + if directive in ('UPDATE', 'REPLACE'): + do_save = True + if key == 'key': + obj.db_key = validate_spawn_value(val, str) + elif key == 'typeclass': + obj.db_typeclass_path = validate_spawn_value(val, str) + elif key == 'location': + obj.db_location = validate_spawn_value(val, _to_obj) + elif key == 'home': + obj.db_home = validate_spawn_value(val, _to_obj) + elif key == 'destination': + obj.db_destination = validate_spawn_value(val, _to_obj) + elif key == 'locks': + if directive == 'REPLACE': + obj.locks.clear() + obj.locks.add(validate_spawn_value(val, str)) + elif key == 'permissions': + if directive == 'REPLACE': + obj.permissions.clear() + obj.permissions.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'aliases': + if directive == 'REPLACE': + obj.aliases.clear() + obj.aliases.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'tags': + if directive == 'REPLACE': + obj.tags.clear() + obj.tags.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'attrs': + if directive == 'REPLACE': + obj.attributes.clear() + obj.attributes.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'exec': + # we don't auto-rerun exec statements, it would be huge security risk! + pass + else: + obj.attributes.add(key, validate_spawn_value(val, _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) + if do_save: + changed += 1 + obj.save() + + return changed def _batch_create_object(*objparams): @@ -835,7 +984,7 @@ def spawn(*prototypes, **kwargs): execs = validate_spawn_value(val, make_iter) # extract ndb assignments - nattributes = dict((key.split("_", 1)[1], validate_spawn_value(val, _to_obj)) + nattribute = dict((key.split("_", 1)[1], validate_spawn_value(val, _to_obj)) for key, val in prot.items() if key.startswith("ndb_")) # the rest are attributes From 7c4a9a03e0b17d272177018d2eb9a8bd6467b888 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 6 Jun 2018 19:15:20 +0200 Subject: [PATCH 053/103] Refactor prototype-functionality into its own package --- evennia/locks/lockhandler.py | 16 + evennia/prototypes/README.md | 145 +++ evennia/prototypes/__init__.py | 0 evennia/prototypes/menus.py | 709 ++++++++++++ evennia/prototypes/protfuncs.py | 78 ++ evennia/prototypes/prototypes.py | 280 +++++ evennia/prototypes/spawner.py | 600 ++++++++++ evennia/prototypes/utils.py | 150 +++ evennia/utils/spawner.py | 1752 ------------------------------ 9 files changed, 1978 insertions(+), 1752 deletions(-) create mode 100644 evennia/prototypes/README.md create mode 100644 evennia/prototypes/__init__.py create mode 100644 evennia/prototypes/menus.py create mode 100644 evennia/prototypes/protfuncs.py create mode 100644 evennia/prototypes/prototypes.py create mode 100644 evennia/prototypes/spawner.py create mode 100644 evennia/prototypes/utils.py delete mode 100644 evennia/utils/spawner.py diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index c65b30c131..4822dde1b6 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -647,6 +647,22 @@ def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False, default=default, access_type=access_type) +def validate_lockstring(lockstring): + """ + Validate so lockstring is on a valid form. + + Args: + lockstring (str): Lockstring to validate. + + Returns: + is_valid (bool): If the lockstring is valid or not. + error (str or None): A string describing the error, or None + if no error was found. + + """ + return _LOCK_HANDLER.valdate(lockstring) + + def _test(): # testing diff --git a/evennia/prototypes/README.md b/evennia/prototypes/README.md new file mode 100644 index 0000000000..0f4139aa3e --- /dev/null +++ b/evennia/prototypes/README.md @@ -0,0 +1,145 @@ +# Prototypes + +A 'Prototype' is a normal Python dictionary describing unique features of individual instance of a +Typeclass. The prototype is used to 'spawn' a new instance with custom features detailed by said +prototype. This allows for creating variations without having to create a large number of actual +Typeclasses. It is a good way to allow Builders more freedom of creation without giving them full +Python access to create Typeclasses. + +For example, if a Typeclass 'Cat' describes all the coded differences between a Cat and +other types of animals, then prototypes could be used to quickly create unique individual cats with +different Attributes/properties (like different colors, stats, names etc) without having to make a new +Typeclass for each. Prototypes have inheritance and can be scripted when they are applied to create +a new instance of a typeclass - a common example would be to randomize stats and name. + +The prototype is a normal dictionary with specific keys. Almost all values can be callables +triggered when the prototype is used to spawn a new instance. Below is an example: + +``` +{ +# meta-keys - these are used only when listing prototypes in-game. Only prototype_key is mandatory, +# but it must be globally unique. + + "prototype_key": "base_goblin", + "prototype_desc": "A basic goblin", + "prototype_locks": "edit:all();spawn:all()", + "prototype_tags": "mobs", + +# fixed-meaning keys, modifying the spawned instance. 'typeclass' may be +# replaced by 'parent', referring to the prototype_key of an existing prototype +# to inherit from. + + "typeclass": "types.objects.Monster", + "key": "goblin grunt", + "tags": ["mob", "evil", ('greenskin','mob')] # tags as well as tags with category etc + "attrs": [("weapon", "sword")] # this allows to set Attributes with categories etc + +# non-fixed keys are interpreted as Attributes and their + + "health": lambda: randint(20,30), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + } + +``` +## Using prototypes + +Prototypes are generally used as inputs to the `spawn` command: + + @spawn prototype_key + +This will spawn a new instance of the prototype in the caller's current location unless the +`location` key of the prototype was set (see below). The caller must pass the prototype's 'spawn' +lock to be able to use it. + + @spawn/list [prototype_key] + +will show all available prototypes along with meta info, or look at a specific prototype in detail. + + +## Creating prototypes + +The `spawn` command can also be used to directly create/update prototypes from in-game. + + spawn/save {"prototype_key: "goblin", ... } + +but it is probably more convenient to use the menu-driven prototype wizard: + + spawn/menu goblin + +In code: + +```python + +from evennia import prototypes + +goblin = {"prototype_key": "goblin:, ... } + +prototype = prototypes.save_prototype(caller, **goblin) + +``` + +Prototypes will normally be stored in the database (internally this is done using a Script, holding +the meta-info and the prototype). One can also define prototypes outside of the game by assigning +the prototype dictionary to a global variable in a module defined by `settings.PROTOTYPE_MODULES`: + +```python +# in e.g. mygame/world/prototypes.py + +GOBLIN = { + "prototype_key": "goblin", + ... + } + +``` + +Such prototypes cannot be modified from inside the game no matter what `edit` lock they are given +(we refer to them as 'readonly') but can be a fast and efficient way to give builders a starting +library of prototypes to inherit from. + +## Valid Prototype keys + +Every prototype key also accepts a callable (taking no arguments) for producing its value or a +string with an $protfunc definition. That callable/protfunc must then return a value on a form the +prototype key expects. + + - `prototype_key` (str): name of this prototype. This is used when storing prototypes and should + be unique. This should always be defined but for prototypes defined in modules, the + variable holding the prototype dict will become the prototype_key if it's not explicitly + given. + - `prototype_desc` (str, optional): describes prototype in listings + - `prototype_locks` (str, optional): locks for restricting access to this prototype. Locktypes + supported are 'edit' and 'use'. + - `prototype_tags` (list, optional): List of tags or tuples (tag, category) used to group prototype + in listings + + - `parent` (str or tuple, optional): name (`prototype_key`) of eventual parent prototype, or a + list of parents for multiple left-to-right inheritance. + - `prototype`: Deprecated. Same meaning as 'parent'. + - `typeclass` (str, optional): if not set, will use typeclass of parent prototype or use + `settings.BASE_OBJECT_TYPECLASS` + - `key` (str, optional): the name of the spawned object. If not given this will set to a + random hash + - `location` (obj, optional): location of the object - a valid object or #dbref + - `home` (obj or str, optional): valid object or #dbref + - `destination` (obj or str, optional): only valid for exits (object or #dbref) + + - `permissions` (str or list, optional): which permissions for spawned object to have + - `locks` (str, optional): lock-string for the spawned object + - `aliases` (str or list, optional): Aliases for the spawned object. + - `exec` (str, optional): this is a string of python code to execute or a list of such + codes. This can be used e.g. to trigger custom handlers on the object. The execution + namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit + this functionality to Developer/superusers. Usually it's better to use callables or + prototypefuncs instead of this. + - `tags` (str, tuple or list, optional): string or list of strings or tuples + `(tagstr, category)`. Plain strings will be result in tags with no category (default tags). + - `attrs` (tuple or list, optional): tuple or list of tuples of Attributes to add. This + form allows more complex Attributes to be set. Tuples at least specify `(key, value)` + but can also specify up to `(key, value, category, lockstring)`. If you want to specify a + lockstring but not a category, set the category to `None`. + - `ndb_` (any): value of a nattribute (`ndb_` is stripped). This is usually not useful to + put in a prototype unless the NAttribute is used immediately upon spawning. + - `other` (any): any other name is interpreted as the key of an Attribute with + its value. Such Attributes have no categories. diff --git a/evennia/prototypes/__init__.py b/evennia/prototypes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py new file mode 100644 index 0000000000..85e7f3f574 --- /dev/null +++ b/evennia/prototypes/menus.py @@ -0,0 +1,709 @@ +""" + +OLC Prototype menu nodes + +""" + +from evennia.utils.evmenu import EvMenu, list_node +from evennia.utils.ansi import strip_ansi + +# ------------------------------------------------------------ +# +# OLC Prototype design menu +# +# ------------------------------------------------------------ + +# Helper functions + + +def _get_menu_prototype(caller): + + prototype = None + if hasattr(caller.ndb._menutree, "olc_prototype"): + prototype = caller.ndb._menutree.olc_prototype + if not prototype: + caller.ndb._menutree.olc_prototype = prototype = {} + caller.ndb._menutree.olc_new = True + return prototype + + +def _is_new_prototype(caller): + return hasattr(caller.ndb._menutree, "olc_new") + + +def _set_menu_prototype(caller, field, value): + prototype = _get_menu_prototype(caller) + prototype[field] = value + caller.ndb._menutree.olc_prototype = prototype + + +def _format_property(prop, required=False, prototype=None, cropper=None): + + if prototype is not None: + prop = prototype.get(prop, '') + + out = prop + if callable(prop): + if hasattr(prop, '__name__'): + out = "<{}>".format(prop.__name__) + else: + out = repr(prop) + if is_iter(prop): + out = ", ".join(str(pr) for pr in prop) + if not out and required: + out = "|rrequired" + return " ({}|n)".format(cropper(out) if cropper else crop(out, _MENU_CROP_WIDTH)) + + +def _set_property(caller, raw_string, **kwargs): + """ + Update a property. To be called by the 'goto' option variable. + + Args: + caller (Object, Account): The user of the wizard. + raw_string (str): Input from user on given node - the new value to set. + Kwargs: + prop (str): Property name to edit with `raw_string`. + processor (callable): Converts `raw_string` to a form suitable for saving. + next_node (str): Where to redirect to after this has run. + Returns: + next_node (str): Next node to go to. + + """ + prop = kwargs.get("prop", "prototype_key") + processor = kwargs.get("processor", None) + next_node = kwargs.get("next_node", "node_index") + + propname_low = prop.strip().lower() + + if callable(processor): + try: + value = processor(raw_string) + except Exception as err: + caller.msg("Could not set {prop} to {value} ({err})".format( + prop=prop.replace("_", "-").capitalize(), value=raw_string, err=str(err))) + # this means we'll re-run the current node. + return None + else: + value = raw_string + + if not value: + return next_node + + prototype = _get_menu_prototype(caller) + + # typeclass and prototype can't co-exist + if propname_low == "typeclass": + prototype.pop("prototype", None) + if propname_low == "prototype": + prototype.pop("typeclass", None) + + caller.ndb._menutree.olc_prototype = prototype + + caller.msg("Set {prop} to '{value}'.".format(prop, value=str(value))) + + return next_node + + +def _wizard_options(curr_node, prev_node, next_node, color="|W"): + options = [] + if prev_node: + options.append({"key": ("|wb|Wack", "b"), + "desc": "{color}({node})|n".format( + color=color, node=prev_node.replace("_", "-")), + "goto": "node_{}".format(prev_node)}) + if next_node: + options.append({"key": ("|wf|Worward", "f"), + "desc": "{color}({node})|n".format( + color=color, node=next_node.replace("_", "-")), + "goto": "node_{}".format(next_node)}) + + if "index" not in (prev_node, next_node): + options.append({"key": ("|wi|Wndex", "i"), + "goto": "node_index"}) + + if curr_node: + options.append({"key": ("|wv|Walidate prototype", "v"), + "goto": ("node_validate_prototype", {"back": curr_node})}) + + return options + + +def _path_cropper(pythonpath): + "Crop path to only the last component" + return pythonpath.split('.')[-1] + + +# Menu nodes + +def node_index(caller): + prototype = _get_menu_prototype(caller) + + text = ("|c --- Prototype wizard --- |n\n\n" + "Define the |yproperties|n of the prototype. All prototype values can be " + "over-ridden at the time of spawning an instance of the prototype, but some are " + "required.\n\n'|wMeta'-properties|n are not used in the prototype itself but are used " + "to organize and list prototypes. The 'Meta-Key' uniquely identifies the prototype " + "and allows you to edit an existing prototype or save a new one for use by you or " + "others later.\n\n(make choice; q to abort. If unsure, start from 1.)") + + options = [] + options.append( + {"desc": "|WPrototype-Key|n|n{}".format(_format_property("Key", True, prototype, None)), + "goto": "node_prototype_key"}) + for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', + 'Permissions', 'Location', 'Home', 'Destination'): + required = False + cropper = None + if key in ("Prototype", "Typeclass"): + required = "prototype" not in prototype and "typeclass" not in prototype + if key == 'Typeclass': + cropper = _path_cropper + options.append( + {"desc": "|w{}|n{}".format( + key, _format_property(key, required, prototype, cropper=cropper)), + "goto": "node_{}".format(key.lower())}) + required = False + for key in ('Desc', 'Tags', 'Locks'): + options.append( + {"desc": "|WPrototype-{}|n|n{}".format(key, _format_property(key, required, prototype, None)), + "goto": "node_prototype_{}".format(key.lower())}) + + return text, options + + +def node_validate_prototype(caller, raw_string, **kwargs): + prototype = _get_menu_prototype(caller) + + txt = prototype_to_str(prototype) + errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)" + try: + # validate, don't spawn + spawn(prototype, return_prototypes=True) + except RuntimeError as err: + errors = "\n\n|rError: {}|n".format(err) + text = (txt + errors) + + options = _wizard_options(None, kwargs.get("back"), None) + + return text, options + + +def _check_prototype_key(caller, key): + old_prototype = search_prototype(key) + olc_new = _is_new_prototype(caller) + key = key.strip().lower() + if old_prototype: + # we are starting a new prototype that matches an existing + if not caller.locks.check_lockstring( + caller, old_prototype['prototype_locks'], access_type='edit'): + # return to the node_prototype_key to try another key + caller.msg("Prototype '{key}' already exists and you don't " + "have permission to edit it.".format(key=key)) + return "node_prototype_key" + elif olc_new: + # we are selecting an existing prototype to edit. Reset to index. + del caller.ndb._menutree.olc_new + caller.ndb._menutree.olc_prototype = old_prototype + caller.msg("Prototype already exists. Reloading.") + return "node_index" + + return _set_property(caller, key, prop='prototype_key', next_node="node_prototype") + + +def node_prototype_key(caller): + prototype = _get_menu_prototype(caller) + text = ["The prototype name, or |wMeta-Key|n, uniquely identifies the prototype. " + "It is used to find and use the prototype to spawn new entities. " + "It is not case sensitive."] + old_key = prototype.get('prototype_key', None) + if old_key: + text.append("Current key is '|w{key}|n'".format(key=old_key)) + else: + text.append("The key is currently unset.") + text.append("Enter text or make a choice (q for quit)") + text = "\n\n".join(text) + options = _wizard_options("prototype_key", "index", "prototype") + options.append({"key": "_default", + "goto": _check_prototype_key}) + return text, options + + +def _all_prototypes(caller): + return [prototype["prototype_key"] + for prototype in search_prototype() if "prototype_key" in prototype] + + +def _prototype_examine(caller, prototype_name): + prototypes = search_prototype(key=prototype_name) + if prototypes: + caller.msg(prototype_to_str(prototypes[0])) + caller.msg("Prototype not registered.") + return None + + +def _prototype_select(caller, prototype): + ret = _set_property(caller, prototype, prop="prototype", processor=str, next_node="node_key") + caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype)) + return ret + + +@list_node(_all_prototypes, _prototype_select) +def node_prototype(caller): + prototype = _get_menu_prototype(caller) + + prot_parent_key = prototype.get('prototype') + + text = ["Set the prototype's |yParent Prototype|n. If this is unset, Typeclass will be used."] + if prot_parent_key: + prot_parent = search_prototype(prot_parent_key) + if prot_parent: + text.append("Current parent prototype is {}:\n{}".format(prototype_to_str(prot_parent))) + else: + text.append("Current parent prototype |r{prototype}|n " + "does not appear to exist.".format(prot_parent_key)) + else: + text.append("Parent prototype is not set") + text = "\n\n".join(text) + options = _wizard_options("prototype", "prototype_key", "typeclass", color="|W") + options.append({"key": "_default", + "goto": _prototype_examine}) + + return text, options + + +def _all_typeclasses(caller): + return list(sorted(get_all_typeclasses().keys())) + + +def _typeclass_examine(caller, typeclass_path): + if typeclass_path is None: + # this means we are exiting the listing + return "node_key" + + typeclass = get_all_typeclasses().get(typeclass_path) + if typeclass: + docstr = [] + for line in typeclass.__doc__.split("\n"): + if line.strip(): + docstr.append(line) + elif docstr: + break + docstr = '\n'.join(docstr) if docstr else "" + txt = "Typeclass |y{typeclass_path}|n; First paragraph of docstring:\n\n{docstring}".format( + typeclass_path=typeclass_path, docstring=docstr) + else: + txt = "This is typeclass |y{}|n.".format(typeclass) + caller.msg(txt) + return None + + +def _typeclass_select(caller, typeclass): + ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key") + caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass)) + return ret + + +@list_node(_all_typeclasses, _typeclass_select) +def node_typeclass(caller): + prototype = _get_menu_prototype(caller) + typeclass = prototype.get("typeclass") + + text = ["Set the typeclass's parent |yTypeclass|n."] + if typeclass: + text.append("Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass)) + else: + text.append("Using default typeclass {typeclass}.".format( + typeclass=settings.BASE_OBJECT_TYPECLASS)) + text = "\n\n".join(text) + options = _wizard_options("typeclass", "prototype", "key", color="|W") + options.append({"key": "_default", + "goto": _typeclass_examine}) + return text, options + + +def node_key(caller): + prototype = _get_menu_prototype(caller) + key = prototype.get("key") + + text = ["Set the prototype's |yKey|n. This will retain case sensitivity."] + if key: + text.append("Current key value is '|y{key}|n'.".format(key=key)) + else: + text.append("Key is currently unset.") + text = "\n\n".join(text) + options = _wizard_options("key", "typeclass", "aliases") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="key", + processor=lambda s: s.strip(), + next_node="node_aliases"))}) + return text, options + + +def node_aliases(caller): + prototype = _get_menu_prototype(caller) + aliases = prototype.get("aliases") + + text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. " + "ill retain case sensitivity."] + if aliases: + text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases)) + else: + text.append("No aliases are set.") + text = "\n\n".join(text) + options = _wizard_options("aliases", "key", "attrs") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="aliases", + processor=lambda s: [part.strip() for part in s.split(",")], + next_node="node_attrs"))}) + return text, options + + +def _caller_attrs(caller): + prototype = _get_menu_prototype(caller) + attrs = prototype.get("attrs", []) + return attrs + + +def _attrparse(caller, attr_string): + "attr is entering on the form 'attr = value'" + + if '=' in attr_string: + attrname, value = (part.strip() for part in attr_string.split('=', 1)) + attrname = attrname.lower() + if attrname: + try: + value = literal_eval(value) + except SyntaxError: + caller.msg(_MENU_ATTR_LITERAL_EVAL_ERROR) + else: + return attrname, value + else: + return None, None + + +def _add_attr(caller, attr_string, **kwargs): + attrname, value = _attrparse(caller, attr_string) + if attrname: + prot = _get_menu_prototype(caller) + prot['attrs'][attrname] = value + _set_menu_prototype(caller, "prototype", prot) + text = "Added" + else: + text = "Attribute must be given as 'attrname = ' where uses valid Python." + options = {"key": "_default", + "goto": lambda caller: None} + return text, options + + +def _edit_attr(caller, attrname, new_value, **kwargs): + attrname, value = _attrparse("{}={}".format(caller, attrname, new_value)) + if attrname: + prot = _get_menu_prototype(caller) + prot['attrs'][attrname] = value + text = "Edited Attribute {} = {}".format(attrname, value) + else: + text = "Attribute value must be valid Python." + options = {"key": "_default", + "goto": lambda caller: None} + return text, options + + +def _examine_attr(caller, selection): + prot = _get_menu_prototype(caller) + value = prot['attrs'][selection] + return "Attribute {} = {}".format(selection, value) + + +@list_node(_caller_attrs) +def node_attrs(caller): + prot = _get_menu_prototype(caller) + attrs = prot.get("attrs") + + text = ["Set the prototype's |yAttributes|n. Separate multiple attrs with commas. " + "Will retain case sensitivity."] + if attrs: + text.append("Current attrs are '|y{attrs}|n'.".format(attrs=attrs)) + else: + text.append("No attrs are set.") + text = "\n\n".join(text) + options = _wizard_options("attrs", "aliases", "tags") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="attrs", + processor=lambda s: [part.strip() for part in s.split(",")], + next_node="node_tags"))}) + return text, options + + +def _caller_tags(caller): + prototype = _get_menu_prototype(caller) + tags = prototype.get("tags") + return tags + + +def _add_tag(caller, tag, **kwargs): + tag = tag.strip().lower() + prototype = _get_menu_prototype(caller) + tags = prototype.get('tags', []) + if tags: + if tag not in tags: + tags.append(tag) + else: + tags = [tag] + prot['tags'] = tags + _set_menu_prototype(caller, "prototype", prot) + text = kwargs.get("text") + if not text: + text = "Added tag {}. (return to continue)".format(tag) + options = {"key": "_default", + "goto": lambda caller: None} + return text, options + + +def _edit_tag(caller, old_tag, new_tag, **kwargs): + prototype = _get_menu_prototype(caller) + tags = prototype.get('tags', []) + + old_tag = old_tag.strip().lower() + new_tag = new_tag.strip().lower() + tags[tags.index(old_tag)] = new_tag + prototype['tags'] = tags + _set_menu_prototype(caller, 'prototype', prototype) + + text = kwargs.get('text') + if not text: + text = "Changed tag {} to {}.".format(old_tag, new_tag) + options = {"key": "_default", + "goto": lambda caller: None} + return text, options + + +@list_node(_caller_tags) +def node_tags(caller): + text = "Set the prototype's |yTags|n." + options = _wizard_options("tags", "attrs", "locks") + return text, options + + +def node_locks(caller): + prototype = _get_menu_prototype(caller) + locks = prototype.get("locks") + + text = ["Set the prototype's |yLock string|n. Separate multiple locks with semi-colons. " + "Will retain case sensitivity."] + if locks: + text.append("Current locks are '|y{locks}|n'.".format(locks=locks)) + else: + text.append("No locks are set.") + text = "\n\n".join(text) + options = _wizard_options("locks", "tags", "permissions") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="locks", + processor=lambda s: s.strip(), + next_node="node_permissions"))}) + return text, options + + +def node_permissions(caller): + prototype = _get_menu_prototype(caller) + permissions = prototype.get("permissions") + + text = ["Set the prototype's |yPermissions|n. Separate multiple permissions with commas. " + "Will retain case sensitivity."] + if permissions: + text.append("Current permissions are '|y{permissions}|n'.".format(permissions=permissions)) + else: + text.append("No permissions are set.") + text = "\n\n".join(text) + options = _wizard_options("permissions", "destination", "location") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="permissions", + processor=lambda s: [part.strip() for part in s.split(",")], + next_node="node_location"))}) + return text, options + + +def node_location(caller): + prototype = _get_menu_prototype(caller) + location = prototype.get("location") + + text = ["Set the prototype's |yLocation|n"] + if location: + text.append("Current location is |y{location}|n.".format(location=location)) + else: + text.append("Default location is {}'s inventory.".format(caller)) + text = "\n\n".join(text) + options = _wizard_options("location", "permissions", "home") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="location", + processor=lambda s: s.strip(), + next_node="node_home"))}) + return text, options + + +def node_home(caller): + prototype = _get_menu_prototype(caller) + home = prototype.get("home") + + text = ["Set the prototype's |yHome location|n"] + if home: + text.append("Current home location is |y{home}|n.".format(home=home)) + else: + text.append("Default home location (|y{home}|n) used.".format(home=settings.DEFAULT_HOME)) + text = "\n\n".join(text) + options = _wizard_options("home", "aliases", "destination") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="home", + processor=lambda s: s.strip(), + next_node="node_destination"))}) + return text, options + + +def node_destination(caller): + prototype = _get_menu_prototype(caller) + dest = prototype.get("dest") + + text = ["Set the prototype's |yDestination|n. This is usually only used for Exits."] + if dest: + text.append("Current destination is |y{dest}|n.".format(dest=dest)) + else: + text.append("No destination is set (default).") + text = "\n\n".join(text) + options = _wizard_options("destination", "home", "prototype_desc") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="dest", + processor=lambda s: s.strip(), + next_node="node_prototype_desc"))}) + return text, options + + +def node_prototype_desc(caller): + + prototype = _get_menu_prototype(caller) + text = ["The |wMeta-Description|n briefly describes the prototype for viewing in listings."] + desc = prototype.get("prototype_desc", None) + + if desc: + text.append("The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc)) + else: + text.append("Description is currently unset.") + text = "\n\n".join(text) + options = _wizard_options("prototype_desc", "prototype_key", "prototype_tags") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop='prototype_desc', + processor=lambda s: s.strip(), + next_node="node_prototype_tags"))}) + + return text, options + + +def node_prototype_tags(caller): + prototype = _get_menu_prototype(caller) + text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " + "Separate multiple by tags by commas."] + tags = prototype.get('prototype_tags', []) + + if tags: + text.append("The current tags are:\n|w{tags}|n".format(tags=tags)) + else: + text.append("No tags are currently set.") + text = "\n\n".join(text) + options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="prototype_tags", + processor=lambda s: [ + str(part.strip().lower()) for part in s.split(",")], + next_node="node_prototype_locks"))}) + return text, options + + +def node_prototype_locks(caller): + prototype = _get_menu_prototype(caller) + text = ["Set |wMeta-Locks|n on the prototype. There are two valid lock types: " + "'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n" + "(If you are unsure, leave as default.)"] + locks = prototype.get('prototype_locks', '') + if locks: + text.append("Current lock is |w'{lockstring}'|n".format(lockstring=locks)) + else: + text.append("Lock unset - if not changed the default lockstring will be set as\n" + " |w'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) + text = "\n\n".join(text) + options = _wizard_options("prototype_locks", "prototype_tags", "index") + options.append({"key": "_default", + "goto": (_set_property, + dict(prop="prototype_locks", + processor=lambda s: s.strip().lower(), + next_node="node_index"))}) + return text, options + + +class OLCMenu(EvMenu): + """ + A custom EvMenu with a different formatting for the options. + + """ + def options_formatter(self, optionlist): + """ + Split the options into two blocks - olc options and normal options + + """ + olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype") + olc_options = [] + other_options = [] + for key, desc in optionlist: + raw_key = strip_ansi(key) + if raw_key in olc_keys: + desc = " {}".format(desc) if desc else "" + olc_options.append("|lc{}|lt{}|le{}".format(raw_key, key, desc)) + else: + other_options.append((key, desc)) + + olc_options = " | ".join(olc_options) + " | " + "|wq|Wuit" if olc_options else "" + other_options = super(OLCMenu, self).options_formatter(other_options) + sep = "\n\n" if olc_options and other_options else "" + + return "{}{}{}".format(olc_options, sep, other_options) + + +def start_olc(caller, session=None, prototype=None): + """ + Start menu-driven olc system for prototypes. + + Args: + caller (Object or Account): The entity starting the menu. + session (Session, optional): The individual session to get data. + prototype (dict, optional): Given when editing an existing + prototype rather than creating a new one. + + """ + menudata = {"node_index": node_index, + "node_validate_prototype": node_validate_prototype, + "node_prototype_key": node_prototype_key, + "node_prototype": node_prototype, + "node_typeclass": node_typeclass, + "node_key": node_key, + "node_aliases": node_aliases, + "node_attrs": node_attrs, + "node_tags": node_tags, + "node_locks": node_locks, + "node_permissions": node_permissions, + "node_location": node_location, + "node_home": node_home, + "node_destination": node_destination, + "node_prototype_desc": node_prototype_desc, + "node_prototype_tags": node_prototype_tags, + "node_prototype_locks": node_prototype_locks, + } + OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=prototype) + diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py new file mode 100644 index 0000000000..057f5f770f --- /dev/null +++ b/evennia/prototypes/protfuncs.py @@ -0,0 +1,78 @@ +""" +Protfuncs are function-strings embedded in a prototype and allows for a builder to create a +prototype with custom logics without having access to Python. The Protfunc is parsed using the +inlinefunc parser but is fired at the moment the spawning happens, using the creating object's +session as input. + +In the prototype dict, the protfunc is specified as a string inside the prototype, e.g.: + + { ... + + "key": "$funcname(arg1, arg2, ...)" + + ... } + +and multiple functions can be nested (no keyword args are supported). The result will be used as the +value for that prototype key for that individual spawn. + +Available protfuncs are callables in one of the modules of `settings.PROTOTYPEFUNC_MODULES`. They +are specified as functions + + def funcname (*args, **kwargs) + +where *args are the arguments given in the prototype, and **kwargs are inserted by Evennia: + + - session (Session): The Session of the entity spawning using this prototype. + - prototype_key (str): The currently spawning prototype-key. + - prototype (dict): The dict this protfunc is a part of. + +Any traceback raised by this function will be handled at the time of spawning and abort the spawn +before any object is created/updated. It must otherwise return the value to store for the specified +prototype key (this value must be possible to serialize in an Attribute). + +""" + +from django.conf import settings +from evennia.utils import inlinefuncs +from evennia.utils.utils import callables_from_module + + +_PROTOTYPEFUNCS = {} + +for mod in settings.PROTOTYPEFUNC_MODULES: + try: + callables = callables_from_module(mod) + if mod == __name__: + callables.pop("protfunc_parser") + _PROTOTYPEFUNCS.update(callables) + except ImportError: + pass + + +def protfunc_parser(value, available_functions=None, **kwargs): + """ + Parse a prototype value string for a protfunc and process it. + + Available protfuncs are specified as callables in one of the modules of + `settings.PROTOTYPEFUNC_MODULES`, or specified on the command line. + + Args: + value (string): The value to test for a parseable protfunc. + available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. + + Kwargs: + any (any): Passed on to the inlinefunc. + + Returns: + any (any): A structure to replace the string on the prototype level. If this is a + callable or a (callable, (args,)) structure, it will be executed as if one had supplied + it to the prototype directly. + + """ + if not isinstance(value, basestring): + return value + available_functions = _PROTOTYPEFUNCS if available_functions is None else available_functions + return inlinefuncs.parse_inlinefunc(value, _available_funcs=available_functions, **kwargs) + + +# default protfuncs diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py new file mode 100644 index 0000000000..60e194861b --- /dev/null +++ b/evennia/prototypes/prototypes.py @@ -0,0 +1,280 @@ +""" + +Handling storage of prototypes, both database-based ones (DBPrototypes) and those defined in modules +(Read-only prototypes). + +""" + +from django.conf import settings +from evennia.scripts.scripts import DefaultScript +from evennia.objects.models import ObjectDB +from evennia.utils.create import create_script +from evennia.utils.utils import all_from_module, make_iter, callables_from_module, is_iter +from evennia.locks.lockhandler import validate_lockstring, check_lockstring +from evennia.utils import logger + + +_MODULE_PROTOTYPE_MODULES = {} +_MODULE_PROTOTYPES = {} + + +class ValidationError(RuntimeError): + """ + Raised on prototype validation errors + """ + pass + + +# module-based prototypes + +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 = [(prototype_key, prot) for prototype_key, prot in all_from_module(mod).items() + if prot and isinstance(prot, dict)] + # assign module path to each prototype_key for easy reference + _MODULE_PROTOTYPE_MODULES.update({prototype_key: mod for prototype_key, _ in prots}) + # make sure the prototype contains all meta info + for prototype_key, prot in prots: + actual_prot_key = prot.get('prototype_key', prototype_key).lower() + prot.update({ + "prototype_key": actual_prot_key, + "prototype_desc": prot['prototype_desc'] if 'prototype_desc' in prot else mod, + "prototype_locks": (prot['prototype_locks'] + if 'prototype_locks' in prot else "use:all();edit:false()"), + "prototype_tags": list(set(make_iter(prot.get('prototype_tags', [])) + ["module"]))}) + _MODULE_PROTOTYPES[actual_prot_key] = prot + + +# Db-based prototypes + + +class DbPrototype(DefaultScript): + """ + This stores a single prototype, in an Attribute `prototype`. + """ + def at_script_creation(self): + self.key = "empty prototype" # prototype_key + self.desc = "A prototype" # prototype_desc + self.db.prototype = {} # actual prototype + + +# General prototype functions + +def check_permission(prototype_key, action, default=True): + """ + Helper function to check access to actions on given prototype. + + Args: + prototype_key (str): The prototype to affect. + action (str): One of "spawn" or "edit". + default (str): If action is unknown or prototype has no locks + + Returns: + passes (bool): If permission for action is granted or not. + + """ + if action == 'edit': + if prototype_key in _MODULE_PROTOTYPES: + mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A") + logger.log_err("{} is a read-only prototype " + "(defined as code in {}).".format(prototype_key, mod)) + return False + + prototype = search_prototype(key=prototype_key) + if not prototype: + logger.log_err("Prototype {} not found.".format(prototype_key)) + return False + + lockstring = prototype.get("prototype_locks") + + if lockstring: + return check_lockstring(None, lockstring, default=default, access_type=action) + return default + + +def create_prototype(**kwargs): + """ + Store a prototype persistently. + + Kwargs: + prototype_key (str): This is required for any storage. + All other kwargs are considered part of the new prototype dict. + + Returns: + prototype (dict or None): The prototype stored using the given kwargs, None if deleting. + + Raises: + prototypes.ValidationError: If prototype does not validate. + + Note: + No edit/spawn locks will be checked here - if this function is called the caller + is expected to have valid permissions. + + """ + + def _to_batchtuple(inp, *args): + "build tuple suitable for batch-creation" + if is_iter(inp): + # already a tuple/list, use as-is + return inp + return (inp, ) + args + + prototype_key = kwargs.get("prototype_key") + if not prototype_key: + raise ValidationError("Prototype requires a prototype_key") + + prototype_key = str(prototype_key).lower() + + # we can't edit a prototype defined in a module + if prototype_key in _MODULE_PROTOTYPES: + mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A") + raise PermissionError("{} is a read-only prototype " + "(defined as code in {}).".format(prototype_key, mod)) + + # want to create- or edit + prototype = kwargs + + # make sure meta properties are included with defaults + prototype['prototype_desc'] = prototype.get('prototype_desc', '') + locks = prototype.get('prototype_locks', "spawn:all();edit:perm(Admin)") + is_valid, err = validate_lockstring(locks) + if not is_valid: + raise ValidationError("Lock error: {}".format(err)) + prototype["prototype_locks"] = locks + prototype["prototype_tags"] = [ + _to_batchtuple(tag, "db_prototype") + for tag in make_iter(prototype.get("prototype_tags", []))] + + 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.tags.batch_add(*prototype['prototype_tags']) + stored_prototype.locks.add(prototype['prototype_locks']) + stored_prototype.attributes.add('prototype', prototype) + else: + # create a new prototype + stored_prototype = create_script( + DbPrototype, key=prototype_key, desc=prototype['prototype_desc'], persistent=True, + locks=locks, tags=prototype['prototype_tags'], attributes=[("prototype", prototype)]) + return stored_prototype + + +def delete_prototype(key, caller=None): + """ + Delete a stored prototype + + Args: + key (str): The persistent prototype to delete. + caller (Account or Object, optionsl): Caller aiming to delete a prototype. + Note that no locks will be checked if`caller` is not passed. + Returns: + success (bool): If deletion worked or not. + Raises: + PermissionError: If 'edit' lock was not passed or deletion failed for some other reason. + + """ + if prototype_key in _MODULE_PROTOTYPES: + mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A") + raise PermissionError("{} is a read-only prototype " + "(defined as code in {}).".format(prototype_key, mod)) + + stored_prototype = DbPrototype.objects.filter(db_key=prototype_key) + + if not stored_prototype: + raise PermissionError("Prototype {} was not found.".format(prototype_key)) + if caller: + if not stored_prototype.access(caller, 'edit'): + raise PermissionError("{} does not have permission to " + "delete prototype {}.".format(caller, prototype_key)) + stored_prototype.delete() + return True + + +def search_prototype(key=None, tags=None): + """ + Find prototypes based on key and/or tags, or all prototypes. + + 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 'db_protototype' + tag category. + + Return: + matches (list): All found prototype dicts. If no keys + or tags are given, all available prototypes will be returned. + + Note: + The available prototypes is a combination of those supplied in + PROTOTYPE_MODULES and those stored in the database. Note that if + tags are given and the prototype has no tags defined, it will not + be found as a match. + + """ + # search module prototypes + + mod_matches = {} + if tags: + # use tags to limit selection + tagset = set(tags) + mod_matches = {prototype_key: prototype + for prototype_key, prototype in _MODULE_PROTOTYPES.items() + if tagset.intersection(prototype.get("prototype_tags", []))} + else: + mod_matches = _MODULE_PROTOTYPES + if key: + if key in mod_matches: + # exact match + module_prototypes = [mod_matches[key]] + else: + # fuzzy matching + module_prototypes = [prototype for prototype_key, prototype in mod_matches.items() + if key in prototype_key] + else: + module_prototypes = [match for match in mod_matches.values()] + + # search db-stored prototypes + + if tags: + # exact match on tag(s) + tags = make_iter(tags) + tag_categories = ["db_prototype" for _ in tags] + db_matches = DbPrototype.objects.get_by_tag(tags, tag_categories) + else: + db_matches = DbPrototype.objects.all() + if key: + # exact or partial match on key + db_matches = db_matches.filter(db_key=key) or db_matches.filter(db_key__icontains=key) + # return prototype + db_prototypes = [dict(dbprot.attributes.get("prototype", {})) for dbprot in db_matches] + + matches = db_prototypes + module_prototypes + nmatches = len(matches) + if nmatches > 1 and key: + key = key.lower() + # avoid duplicates if an exact match exist between the two types + filter_matches = [mta for mta in matches + if mta.get('prototype_key') and mta['prototype_key'] == key] + if filter_matches and len(filter_matches) < nmatches: + matches = filter_matches + + return matches + + +def search_objects_with_prototype(prototype_key): + """ + Retrieve all object instances created by a given prototype. + + Args: + prototype_key (str): The exact (and unique) prototype identifier to query for. + + Returns: + matches (Queryset): All matching objects spawned from this prototype. + + """ + return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY) diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py new file mode 100644 index 0000000000..062e15ee92 --- /dev/null +++ b/evennia/prototypes/spawner.py @@ -0,0 +1,600 @@ +""" +Spawner + +The spawner takes input files containing object definitions in +dictionary forms. These use a prototype architecture to define +unique objects without having to make a Typeclass for each. + +The main function is `spawn(*prototype)`, where the `prototype` +is a dictionary like this: + +```python +GOBLIN = { + "typeclass": "types.objects.Monster", + "key": "goblin grunt", + "health": lambda: randint(20,30), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + "tags": ["mob", "evil", ('greenskin','mob')] + "attrs": [("weapon", "sword")] + } +``` + +Possible keywords are: + prototype_key (str): name of this prototype. This is used when storing prototypes and should + be unique. This should always be defined but for prototypes defined in modules, the + variable holding the prototype dict will become the prototype_key if it's not explicitly + given. + prototype_desc (str, optional): describes prototype in listings + prototype_locks (str, optional): locks for restricting access to this prototype. Locktypes + supported are 'edit' and 'use'. + prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype + in listings + + parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or + a list of parents, for multiple left-to-right inheritance. + prototype: Deprecated. Same meaning as 'parent'. + typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use + `settings.BASE_OBJECT_TYPECLASS` + key (str or callable, optional): the name of the spawned object. If not given this will set to a + random hash + location (obj, str or callable, optional): location of the object - a valid object or #dbref + home (obj, str or callable, optional): valid object or #dbref + destination (obj, str or callable, optional): only valid for exits (object or #dbref) + + permissions (str, list or callable, optional): which permissions for spawned object to have + locks (str or callable, optional): lock-string for the spawned object + aliases (str, list or callable, optional): Aliases for the spawned object + exec (str or callable, optional): this is a string of python code to execute or a list of such + codes. This can be used e.g. to trigger custom handlers on the object. The execution + namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit + this functionality to Developer/superusers. Usually it's better to use callables or + prototypefuncs instead of this. + tags (str, tuple, list or callable, optional): string or list of strings or tuples + `(tagstr, category)`. Plain strings will be result in tags with no category (default tags). + attrs (tuple, list or callable, optional): tuple or list of tuples of Attributes to add. This + form allows more complex Attributes to be set. Tuples at least specify `(key, value)` + but can also specify up to `(key, value, category, lockstring)`. If you want to specify a + lockstring but not a category, set the category to `None`. + ndb_ (any): value of a nattribute (ndb_ is stripped) + other (any): any other name is interpreted as the key of an Attribute with + its value. Such Attributes have no categories. + +Each value can also be a callable that takes no arguments. It should +return the value to enter into the field and will be called every time +the prototype is used to spawn an object. Note, if you want to store +a callable in an Attribute, embed it in a tuple to the `args` keyword. + +By specifying the "prototype" key, the prototype becomes a child of +that prototype, inheritng all prototype slots it does not explicitly +define itself, while overloading those that it does specify. + +```python +import random + + +GOBLIN_WIZARD = { + "parent": GOBLIN, + "key": "goblin wizard", + "spells": ["fire ball", "lighting bolt"] + } + +GOBLIN_ARCHER = { + "parent": GOBLIN, + "key": "goblin archer", + "attack_skill": (random, (5, 10))" + "attacks": ["short bow"] +} +``` + +One can also have multiple prototypes. These are inherited from the +left, with the ones further to the right taking precedence. + +```python +ARCHWIZARD = { + "attack": ["archwizard staff", "eye of doom"] + +GOBLIN_ARCHWIZARD = { + "key" : "goblin archwizard" + "parent": (GOBLIN_WIZARD, ARCHWIZARD), +} +``` + +The *goblin archwizard* will have some different attacks, but will +otherwise have the same spells as a *goblin wizard* who in turn shares +many traits with a normal *goblin*. + + +Storage 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 __future__ import print_function + +import copy +import hashlib +import time +from ast import literal_eval +from django.conf import settings +from random import randint +import evennia +from evennia.objects.models import ObjectDB +from evennia.utils.utils import ( + make_iter, dbid_to_obj, + is_iter, crop, get_all_typeclasses) + +from evennia.utils.evtable import EvTable + + +_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") +_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") +_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES +_MENU_CROP_WIDTH = 15 +_PROTOTYPE_TAG_CATEGORY = "spawned_by_prototype" + +_MENU_ATTR_LITERAL_EVAL_ERROR = ( + "|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n" + "You also need to use correct Python syntax. Remember especially to put quotes around all " + "strings inside lists and dicts.|n") + + +# Helper functions + +def _to_obj(value, force=True): + return dbid_to_obj(value, ObjectDB) + + +def _to_obj_or_any(value): + obj = dbid_to_obj(value, ObjectDB) + return obj if obj is not None else value + + +def validate_spawn_value(value, validator=None): + """ + Analyze the value and produce a value for use at the point of spawning. + + Args: + value (any): This can be: + callable - will be called as callable() + (callable, (args,)) - will be called as callable(*args) + other - will be assigned depending on the variable type + validator (callable, optional): If given, this will be called with the value to + check and guarantee the outcome is of a given type. + + Returns: + any (any): The (potentially pre-processed value to use for this prototype key) + + """ + value = protfunc_parser(value) + validator = validator if validator else lambda o: o + if callable(value): + return validator(value()) + elif value and is_iter(value) and callable(value[0]): + # a structure (callable, (args, )) + args = value[1:] + return validator(value[0](*make_iter(args))) + else: + return validator(value) + +# Spawner mechanism + + +def validate_prototype(prototype, protkey=None, protparents=None, _visited=None): + """ + Run validation on a prototype, checking for inifinite regress. + + Args: + prototype (dict): Prototype to validate. + protkey (str, optional): The name of the prototype definition. If not given, the prototype + dict needs to have the `prototype_key` field set. + protpartents (dict, optional): The available prototype parent library. If + note given this will be determined from settings/database. + _visited (list, optional): This is an internal work array and should not be set manually. + Raises: + RuntimeError: If prototype has invalid structure. + + """ + if not protparents: + protparents = get_protparent_dict() + if _visited is None: + _visited = [] + + protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) + + assert isinstance(prototype, dict) + + if id(prototype) in _visited: + raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype) + + _visited.append(id(prototype)) + protstrings = prototype.get("prototype") + + if protstrings: + for protstring in make_iter(protstrings): + protstring = protstring.lower() + if protkey is not None and protstring == protkey: + raise RuntimeError("%s tries to prototype itself." % protkey or prototype) + protparent = protparents.get(protstring) + if not protparent: + raise RuntimeError( + "%s's prototype '%s' was not found." % (protkey or prototype, protstring)) + validate_prototype(protparent, protstring, protparents, _visited) + + +def _get_prototype(dic, prot, protparents): + """ + Recursively traverse a prototype dictionary, including multiple + inheritance. Use validate_prototype before this, we don't check + for infinite recursion here. + + """ + if "prototype" in dic: + # move backwards through the inheritance + for prototype in make_iter(dic["prototype"]): + # Build the prot dictionary in reverse order, overloading + new_prot = _get_prototype(protparents.get(prototype.lower(), {}), prot, protparents) + prot.update(new_prot) + prot.update(dic) + prot.pop("prototype", None) # we don't need this anymore + return prot + + +def prototype_diff_from_object(prototype, obj): + """ + 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 + needed. + + Args: + prototype (dict): Prototype. + obj (Object): Object to + + Returns: + diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} + + """ + prot1 = prototype + prot2 = prototype_from_object(obj) + + diff = {} + for key, value in prot1.items(): + diff[key] = "KEEP" + if key in prot2: + if callable(prot2[key]) or value != prot2[key]: + diff[key] = "UPDATE" + elif key not in prot2: + diff[key] = "REMOVE" + + return diff + + +def batch_update_objects_with_prototype(prototype, diff=None, objects=None): + """ + Update existing objects with the latest version of the prototype. + + Args: + prototype (str or dict): Either the `prototype_key` to use or the + prototype dict itself. + diff (dict, optional): This a diff structure that describes how to update the protototype. + 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`. + Returns: + changed (int): The number of objects that had changes applied to them. + + """ + prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] + prototype_obj = search_db_prototype(prototype_key, return_queryset=True) + prototype_obj = prototype_obj[0] if prototype_obj else None + new_prototype = prototype_obj.db.prototype + objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + + if not objs: + return 0 + + if not diff: + diff = prototype_diff_from_object(new_prototype, objs[0]) + + changed = 0 + for obj in objs: + do_save = False + for key, directive in diff.items(): + val = new_prototype[key] + if directive in ('UPDATE', 'REPLACE'): + do_save = True + if key == 'key': + obj.db_key = validate_spawn_value(val, str) + elif key == 'typeclass': + obj.db_typeclass_path = validate_spawn_value(val, str) + elif key == 'location': + obj.db_location = validate_spawn_value(val, _to_obj) + elif key == 'home': + obj.db_home = validate_spawn_value(val, _to_obj) + elif key == 'destination': + obj.db_destination = validate_spawn_value(val, _to_obj) + elif key == 'locks': + if directive == 'REPLACE': + obj.locks.clear() + obj.locks.add(validate_spawn_value(val, str)) + elif key == 'permissions': + if directive == 'REPLACE': + obj.permissions.clear() + obj.permissions.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'aliases': + if directive == 'REPLACE': + obj.aliases.clear() + obj.aliases.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'tags': + if directive == 'REPLACE': + obj.tags.clear() + obj.tags.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'attrs': + if directive == 'REPLACE': + obj.attributes.clear() + obj.attributes.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'exec': + # we don't auto-rerun exec statements, it would be huge security risk! + pass + else: + obj.attributes.add(key, validate_spawn_value(val, _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) + if do_save: + changed += 1 + obj.save() + + return changed + + +def _batch_create_object(*objparams): + """ + This is a cut-down version of the create_object() function, + optimized for speed. It does NOT check and convert various input + so make sure the spawned Typeclass works before using this! + + Args: + objsparams (tuple): Each paremter tuple will create one object instance using the parameters within. + The parameters should be given in the following order: + - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`. + - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`. + - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`. + - `aliases` (list): A list of alias strings for + adding with `new_object.aliases.batch_add(*aliases)`. + - `nattributes` (list): list of tuples `(key, value)` to be loop-added to + add with `new_obj.nattributes.add(*tuple)`. + - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for + adding with `new_obj.attributes.batch_add(*attributes)`. + - `tags` (list): list of tuples `(key, category)` for adding + with `new_obj.tags.batch_add(*tags)`. + - `execs` (list): Code strings to execute together with the creation + of each object. They will be executed with `evennia` and `obj` + (the newly created object) available in the namespace. Execution + will happend after all other properties have been assigned and + is intended for calling custom handlers etc. + + Returns: + objects (list): A list of created objects + + Notes: + The `exec` list will execute arbitrary python code so don't allow this to be available to + unprivileged users! + + """ + + # bulk create all objects in one go + + # unfortunately this doesn't work since bulk_create doesn't creates pks; + # the result would be duplicate objects at the next stage, so we comment + # it out for now: + # dbobjs = _ObjectDB.objects.bulk_create(dbobjs) + + dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] + objs = [] + for iobj, obj in enumerate(dbobjs): + # call all setup hooks on each object + objparam = objparams[iobj] + # setup + obj._createdict = {"permissions": make_iter(objparam[1]), + "locks": objparam[2], + "aliases": make_iter(objparam[3]), + "nattributes": objparam[4], + "attributes": objparam[5], + "tags": make_iter(objparam[6])} + # this triggers all hooks + obj.save() + # run eventual extra code + for code in objparam[7]: + if code: + exec(code, {}, {"evennia": evennia, "obj": obj}) + objs.append(obj) + return objs + + +def spawn(*prototypes, **kwargs): + """ + Spawn a number of prototyped objects. + + Args: + prototypes (dict): Each argument should be a prototype + dictionary. + 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 + the global protparents dictionary accessible by the input + prototypes. If not given, it will instead look for modules + defined by settings.PROTOTYPE_MODULES. + prototype_parents (dict): A dictionary holding a custom + prototype-parent dictionary. Will overload same-named + prototypes from prototype_modules. + return_prototypes (bool): Only return a list of the + prototype-parents (no object creation happens) + + Returns: + object (Object): Spawned object. + + """ + # get available protparents + protparents = get_protparent_dict() + + # overload module's protparents with specifically given protparents + protparents.update(kwargs.get("prototype_parents", {})) + for key, prototype in protparents.items(): + validate_prototype(prototype, key.lower(), protparents) + + if "return_prototypes" in kwargs: + # only return the parents + return copy.deepcopy(protparents) + + objsparams = [] + for prototype in prototypes: + + validate_prototype(prototype, None, protparents) + prot = _get_prototype(prototype, {}, protparents) + if not prot: + continue + + # extract the keyword args we need to create the object itself. If we get a callable, + # call that to get the value (don't catch errors) + create_kwargs = {} + # we must always add a key, so if not given we use a shortened md5 hash. There is a (small) + # chance this is not unique but it should usually not be a problem. + val = prot.pop("key", "Spawned-{}".format( + hashlib.md5(str(time.time())).hexdigest()[:6])) + create_kwargs["db_key"] = validate_spawn_value(val, str) + + val = prot.pop("location", None) + create_kwargs["db_location"] = validate_spawn_value(val, _to_obj) + + val = prot.pop("home", settings.DEFAULT_HOME) + create_kwargs["db_home"] = validate_spawn_value(val, _to_obj) + + val = prot.pop("destination", None) + create_kwargs["db_destination"] = validate_spawn_value(val, _to_obj) + + val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) + create_kwargs["db_typeclass_path"] = validate_spawn_value(val, str) + + # extract calls to handlers + val = prot.pop("permissions", []) + permission_string = validate_spawn_value(val, make_iter) + val = prot.pop("locks", "") + lock_string = validate_spawn_value(val, str) + val = prot.pop("aliases", []) + alias_string = validate_spawn_value(val, make_iter) + + val = prot.pop("tags", []) + tags = validate_spawn_value(val, make_iter) + + 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)) + + val = prot.pop("exec", "") + execs = validate_spawn_value(val, make_iter) + + # extract ndb assignments + nattribute = dict((key.split("_", 1)[1], validate_spawn_value(val, _to_obj)) + for key, val in prot.items() if key.startswith("ndb_")) + + # the rest are attributes + val = prot.pop("attrs", []) + attributes = validate_spawn_value(val, list) + + simple_attributes = [] + for key, value in ((key, value) for key, value in prot.items() + if not (key.startswith("ndb_"))): + if is_iter(value) and len(value) > 1: + # (value, category) + simple_attributes.append((key, + validate_spawn_value(value[0], _to_obj_or_any), + validate_spawn_value(value[1], str))) + else: + simple_attributes.append((key, + validate_spawn_value(value, _to_obj_or_any))) + + attributes = attributes + simple_attributes + attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS] + + # pack for call into _batch_create_object + objsparams.append((create_kwargs, permission_string, lock_string, + alias_string, nattributes, attributes, tags, execs)) + + return _batch_create_object(*objsparams) + + +# Testing + +if __name__ == "__main__": + protparents = { + "NOBODY": {}, + # "INFINITE" : { + # "prototype":"INFINITE" + # }, + "GOBLIN": { + "key": "goblin grunt", + "health": lambda: randint(20, 30), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + }, + "GOBLIN_WIZARD": { + "prototype": "GOBLIN", + "key": "goblin wizard", + "spells": ["fire ball", "lighting bolt"] + }, + "GOBLIN_ARCHER": { + "prototype": "GOBLIN", + "key": "goblin archer", + "attacks": ["short bow"] + }, + "ARCHWIZARD": { + "attacks": ["archwizard staff"], + }, + "GOBLIN_ARCHWIZARD": { + "key": "goblin archwizard", + "prototype": ("GOBLIN_WIZARD", "ARCHWIZARD") + } + } + # test + print([o.key for o in spawn(protparents["GOBLIN"], + protparents["GOBLIN_ARCHWIZARD"], + prototype_parents=protparents)]) diff --git a/evennia/prototypes/utils.py b/evennia/prototypes/utils.py new file mode 100644 index 0000000000..74eaef169f --- /dev/null +++ b/evennia/prototypes/utils.py @@ -0,0 +1,150 @@ +""" + +Prototype utilities + +""" + + +class PermissionError(RuntimeError): + pass + + + + + +def get_protparent_dict(): + """ + Get prototype parents. + + Returns: + parent_dict (dict): A mapping {prototype_key: prototype} for all available prototypes. + + """ + return {prototype['prototype_key']: prototype for prototype in search_prototype()} + + +def list_prototypes(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 prototype 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. + + """ + # this allows us to pass lists of empty strings + tags = [tag for tag in make_iter(tags) if tag] + + # get prototypes for readonly and db-based prototypes + prototypes = search_prototype(key, tags) + + # get use-permissions of readonly attributes (edit is always False) + display_tuples = [] + for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')): + lock_use = caller.locks.check_lockstring( + caller, prototype.get('prototype_locks', ''), access_type='use') + if not show_non_use and not lock_use: + continue + if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES: + lock_edit = False + else: + lock_edit = caller.locks.check_lockstring( + caller, prototype.get('prototype_locks', ''), access_type='edit') + if not show_non_edit and not lock_edit: + continue + ptags = [] + for ptag in prototype.get('prototype_tags', []): + if is_iter(ptag): + if len(ptag) > 1: + ptags.append("{} (category: {}".format(ptag[0], ptag[1])) + else: + ptags.append(ptag[0]) + else: + ptags.append(str(ptag)) + + display_tuples.append( + (prototype.get('prototype_key', ''), + prototype.get('prototype_desc', ''), + "{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'), + ",".join(ptags))) + + if not display_tuples: + return None + + table = [] + width = 78 + for i in range(len(display_tuples[0])): + table.append([str(display_tuple[i]) for display_tuple in display_tuples]) + table = EvTable("Key", "Desc", "Use/Edit", "Tags", table=table, crop=True, width=width) + table.reformat_column(0, width=22) + table.reformat_column(1, width=29) + table.reformat_column(2, width=11, align='c') + table.reformat_column(3, width=16) + return table + + +def prototype_to_str(prototype): + """ + Format a prototype to a nice string representation. + + Args: + prototype (dict): The prototype. + """ + + header = ( + "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" + "|cdesc:|n {} \n|cprototype:|n ".format( + prototype['prototype_key'], + ", ".join(prototype['prototype_tags']), + prototype['prototype_locks'], + prototype['prototype_desc'])) + proto = ("{{\n {} \n}}".format( + "\n ".join( + "{!r}: {!r},".format(key, value) for key, value in + sorted(prototype.items()) if key not in _PROTOTYPE_META_NAMES)).rstrip(",")) + return header + proto + + +def prototype_from_object(obj): + """ + Guess a minimal prototype from an existing object. + + Args: + obj (Object): An object to analyze. + + Returns: + prototype (dict): A prototype estimating the current state of the object. + + """ + # first, check if this object already has a prototype + + prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) + prot = search_prototype(prot) + if not prot or len(prot) > 1: + # no unambiguous prototype found - build new prototype + prot = {} + prot['prototype_key'] = "From-Object-{}-{}".format( + obj.key, hashlib.md5(str(time.time())).hexdigest()[:6]) + prot['prototype_desc'] = "Built from {}".format(str(obj)) + prot['prototype_locks'] = "use:all();edit:all()" + + prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] + prot['location'] = obj.db_location + prot['home'] = obj.db_home + prot['destination'] = obj.db_destination + prot['typeclass'] = obj.db_typeclass_path + prot['locks'] = obj.locks.all() + prot['permissions'] = obj.permissions.get() + prot['aliases'] = obj.aliases.get() + prot['tags'] = [(tag.key, tag.category, tag.data) + for tag in obj.tags.get(return_tagobj=True, return_list=True)] + prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks) + for attr in obj.attributes.get(return_obj=True, return_list=True)] + + return prot diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py deleted file mode 100644 index 3c269ca742..0000000000 --- a/evennia/utils/spawner.py +++ /dev/null @@ -1,1752 +0,0 @@ -""" -Spawner - -The spawner takes input files containing object definitions in -dictionary forms. These use a prototype architecture to define -unique objects without having to make a Typeclass for each. - -The main function is `spawn(*prototype)`, where the `prototype` -is a dictionary like this: - -```python -GOBLIN = { - "typeclass": "types.objects.Monster", - "key": "goblin grunt", - "health": lambda: randint(20,30), - "resists": ["cold", "poison"], - "attacks": ["fists"], - "weaknesses": ["fire", "light"] - "tags": ["mob", "evil", ('greenskin','mob')] - "args": [("weapon", "sword")] - } -``` - -Possible keywords are: - prototype_key (str): name of this prototype. This is used when storing prototypes and should - be unique. This should always be defined but for prototypes defined in modules, the - variable holding the prototype dict will become the prototype_key if it's not explicitly - given. - prototype_desc (str, optional): describes prototype in listings - prototype_locks (str, optional): locks for restricting access to this prototype. Locktypes - supported are 'edit' and 'use'. - prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype - in listings - - prototype (str or callable, optional): bame (prototype_key) of eventual parent prototype - typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use - `settings.BASE_OBJECT_TYPECLASS` - key (str or callable, optional): the name of the spawned object. If not given this will set to a - random hash - location (obj, str or callable, optional): location of the object - a valid object or #dbref - home (obj, str or callable, optional): valid object or #dbref - destination (obj, str or callable, optional): only valid for exits (object or #dbref) - - permissions (str, list or callable, optional): which permissions for spawned object to have - locks (str or callable, optional): lock-string for the spawned object - aliases (str, list or callable, optional): Aliases for the spawned object - exec (str or callable, optional): this is a string of python code to execute or a list of such - codes. This can be used e.g. to trigger custom handlers on the object. The execution - namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit - this functionality to Developer/superusers. Usually it's better to use callables or - prototypefuncs instead of this. - tags (str, tuple, list or callable, optional): string or list of strings or tuples - `(tagstr, category)`. Plain strings will be result in tags with no category (default tags). - attrs (tuple, list or callable, optional): tuple or list of tuples of Attributes to add. This - form allows more complex Attributes to be set. Tuples at least specify `(key, value)` - but can also specify up to `(key, value, category, lockstring)`. If you want to specify a - lockstring but not a category, set the category to `None`. - ndb_ (any): value of a nattribute (ndb_ is stripped) - other (any): any other name is interpreted as the key of an Attribute with - its value. Such Attributes have no categories. - -Each value can also be a callable that takes no arguments. It should -return the value to enter into the field and will be called every time -the prototype is used to spawn an object. Note, if you want to store -a callable in an Attribute, embed it in a tuple to the `args` keyword. - -By specifying the "prototype" key, the prototype becomes a child of -that prototype, inheritng all prototype slots it does not explicitly -define itself, while overloading those that it does specify. - -```python -import random - - -GOBLIN_WIZARD = { - "prototype": GOBLIN, - "key": "goblin wizard", - "spells": ["fire ball", "lighting bolt"] - } - -GOBLIN_ARCHER = { - "prototype": GOBLIN, - "key": "goblin archer", - "attack_skill": (random, (5, 10))" - "attacks": ["short bow"] -} -``` - -One can also have multiple prototypes. These are inherited from the -left, with the ones further to the right taking precedence. - -```python -ARCHWIZARD = { - "attack": ["archwizard staff", "eye of doom"] - -GOBLIN_ARCHWIZARD = { - "key" : "goblin archwizard" - "prototype": (GOBLIN_WIZARD, ARCHWIZARD), -} -``` - -The *goblin archwizard* will have some different attacks, but will -otherwise have the same spells as a *goblin wizard* who in turn shares -many traits with a normal *goblin*. - - -Storage 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 __future__ import print_function - -import copy -import hashlib -import time -from ast import literal_eval -from django.conf import settings -from random import randint -import evennia -from evennia.objects.models import ObjectDB -from evennia.utils.utils import ( - make_iter, all_from_module, callables_from_module, dbid_to_obj, - is_iter, crop, get_all_typeclasses) -from evennia.utils import inlinefuncs - -from evennia.scripts.scripts import DefaultScript -from evennia.utils.create import create_script -from evennia.utils.evtable import EvTable -from evennia.utils.evmenu import EvMenu, list_node -from evennia.utils.ansi import strip_ansi - - -_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") -_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") -_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES -_MODULE_PROTOTYPES = {} -_MODULE_PROTOTYPE_MODULES = {} -_PROTOTYPEFUNCS = {} -_MENU_CROP_WIDTH = 15 -_PROTOTYPE_TAG_CATEGORY = "spawned_by_prototype" - -_MENU_ATTR_LITERAL_EVAL_ERROR = ( - "|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n" - "You also need to use correct Python syntax. Remember especially to put quotes around all " - "strings inside lists and dicts.|n") - - -class PermissionError(RuntimeError): - pass - - -# load resources - - -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 = [(prototype_key, prot) for prototype_key, prot in all_from_module(mod).items() - if prot and isinstance(prot, dict)] - # assign module path to each prototype_key for easy reference - _MODULE_PROTOTYPE_MODULES.update({prototype_key: mod for prototype_key, _ in prots}) - # make sure the prototype contains all meta info - for prototype_key, prot in prots: - actual_prot_key = prot.get('prototype_key', prototype_key).lower() - prot.update({ - "prototype_key": actual_prot_key, - "prototype_desc": prot['prototype_desc'] if 'prototype_desc' in prot else mod, - "prototype_locks": (prot['prototype_locks'] - if 'prototype_locks' in prot else "use:all();edit:false()"), - "prototype_tags": list(set(make_iter(prot.get('prototype_tags', [])) + ["module"]))}) - _MODULE_PROTOTYPES[actual_prot_key] = prot - - -for mod in settings.PROTOTYPEFUNC_MODULES: - try: - _PROTOTYPEFUNCS.update(callables_from_module(mod)) - except ImportError: - pass - - -# Helper functions - - -def protfunc_parser(value, available_functions=None, **kwargs): - """ - This is intended to be used by the in-game olc mechanism. It will parse the prototype - value for function tokens like `$protfunc(arg, arg, ...)`. These functions behave all the - parameters of `inlinefuncs` but they are *not* passed a Session since this is not guaranteed to - be available at the time of spawning. They may also return other structures than strings. - - Available protfuncs are specified as callables in one of the modules of - `settings.PROTOTYPEFUNC_MODULES`, or specified on the command line. - - Args: - value (string): The value to test for a parseable protfunc. - available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. - - Kwargs: - any (any): Passed on to the inlinefunc. - - Returns: - any (any): A structure to replace the string on the prototype level. If this is a - callable or a (callable, (args,)) structure, it will be executed as if one had supplied - it to the prototype directly. - - """ - if not isinstance(value, basestring): - return value - available_functions = _PROTOTYPEFUNCS if available_functions is None else available_functions - return inlinefuncs.parse_inlinefunc(value, _available_funcs=available_functions) - - -def _to_obj(value, force=True): - return dbid_to_obj(value, ObjectDB) - - -def _to_obj_or_any(value): - obj = dbid_to_obj(value, ObjectDB) - return obj if obj is not None else value - - -def validate_spawn_value(value, validator=None): - """ - Analyze the value and produce a value for use at the point of spawning. - - Args: - value (any): This can be: - callable - will be called as callable() - (callable, (args,)) - will be called as callable(*args) - other - will be assigned depending on the variable type - validator (callable, optional): If given, this will be called with the value to - check and guarantee the outcome is of a given type. - - Returns: - any (any): The (potentially pre-processed value to use for this prototype key) - - """ - value = protfunc_parser(value) - validator = validator if validator else lambda o: o - if callable(value): - return validator(value()) - elif value and is_iter(value) and callable(value[0]): - # a structure (callable, (args, )) - args = value[1:] - return validator(value[0](*make_iter(args))) - else: - return validator(value) - - -# Prototype storage mechanisms - - -class DbPrototype(DefaultScript): - """ - This stores a single prototype - """ - def at_script_creation(self): - self.key = "empty prototype" # prototype_key - self.desc = "A prototype" # prototype_desc - - - - - -def save_db_prototype(caller, prototype, key=None, desc=None, 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. - prototype (dict): Prototype dict. - key (str): Name of prototype to store. Will be inserted as `prototype_key` in the prototype. - desc (str, optional): Description of prototype, to use in listing. Will be inserted - as `prototype_desc` in the prototype. - tags (list, optional): Tag-strings to apply to prototype. These are always - applied with the 'db_prototype' category. Will be inserted as `prototype_tags`. - locks (str, optional): Locks to apply to this prototype. Used locks - are 'use' and 'edit'. Will be inserted as `prototype_locks` in the prototype. - 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 or prototype.get('prototype_key', None) - if not key_orig: - caller.msg("This prototype requires a prototype_key.") - return False - key = str(key).lower() - - # we can't edit a prototype defined in a module - if key in _MODULE_PROTOTYPES: - mod = _MODULE_PROTOTYPE_MODULES.get(key, "N/A") - raise PermissionError("{} is a read-only prototype " - "(defined as code in {}).".format(key_orig, mod)) - - prototype['prototype_key'] = key - - if desc: - desc = prototype['prototype_desc'] = desc - else: - desc = prototype.get('prototype_desc', '') - - # set up locks and check they are on a valid form - locks = locks or prototype.get( - "prototype_locks", "use:all();edit:id({}) or perm(Admin)".format(caller.id)) - prototype['prototype_locks'] = locks - - is_valid, err = caller.locks.validate(locks) - if not is_valid: - caller.msg("Lock error: {}".format(err)) - return False - - if tags: - tags = [(tag, "db_prototype") for tag in make_iter(tags)] - else: - tags = prototype.get('prototype_tags', []) - prototype['prototype_tags'] = tags - - stored_prototype = DbPrototype.objects.filter(db_key=key) - - if stored_prototype: - # edit existing 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: - # delete prototype - stored_prototype.delete() - return True - - 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) - elif delete: - # didn't find what to delete - return False - else: - # create a new prototype - stored_prototype = create_script( - DbPrototype, key=key, desc=desc, persistent=True, - locks=locks, tags=tags, attributes=[("prototype", prototype)]) - return stored_prototype - - -def delete_db_prototype(caller, key): - """ - Delete a stored prototype - - Args: - caller (Account or Object): Caller aiming to delete a prototype. - key (str): The persistent prototype to delete. - Returns: - success (bool): If deletion worked or not. - Raises: - PermissionError: If 'edit' lock was not passed. - - """ - return save_db_prototype(caller, key, None, delete=True) - - -def search_db_prototype(key=None, tags=None, return_queryset=False): - """ - 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 'db_protototype' - tag category. - return_queryset (bool, optional): Return the database queryset. - Return: - matches (queryset or list): All found DbPrototypes. If `return_queryset` - is not set, this is a list of prototype dicts. - - Note: - This does not include read-only prototypes defined in modules; use - `search_module_prototype` for those. - - """ - if tags: - # exact match on tag(s) - tags = make_iter(tags) - tag_categories = ["db_prototype" for _ in tags] - matches = DbPrototype.objects.get_by_tag(tags, tag_categories) - else: - matches = DbPrototype.objects.all() - if key: - # exact or partial match on key - matches = matches.filter(db_key=key) or matches.filter(db_key__icontains=key) - if not return_queryset: - # return prototype - matches = [dict(dbprot.attributes.get("prototype", {})) for dbprot in matches] - return matches - - -def search_module_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 prototypes matching the search criterion. - - """ - matches = {} - if tags: - # use tags to limit selection - tagset = set(tags) - matches = {prototype_key: prototype - for prototype_key, prototype in _MODULE_PROTOTYPES.items() - if tagset.intersection(prototype.get("prototype_tags", []))} - else: - matches = _MODULE_PROTOTYPES - - if key: - if key in matches: - # exact match - return [matches[key]] - else: - # fuzzy matching - return [prototype for prototype_key, prototype in matches.items() - if key in prototype_key] - else: - return [match for match in matches.values()] - - -def search_prototype(key=None, tags=None): - """ - Find prototypes based on key and/or tags, or all prototypes. - - 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 'db_protototype' - tag category. - - Return: - matches (list): All found prototype dicts. If no keys - or tags are given, all available prototypes will be returned. - - 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. - - """ - module_prototypes = search_module_prototype(key, tags) - db_prototypes = search_db_prototype(key, tags) - - matches = db_prototypes + module_prototypes - if len(matches) > 1 and key: - key = key.lower() - # avoid duplicates if an exact match exist between the two types - filter_matches = [mta for mta in matches - if mta.get('prototype_key') and mta['prototype_key'] == key] - if filter_matches and len(filter_matches) < len(matches): - matches = filter_matches - - return matches - - -def search_objects_with_prototype(prototype_key): - """ - Retrieve all object instances created by a given prototype. - - Args: - prototype_key (str): The exact (and unique) prototype identifier to query for. - - Returns: - matches (Queryset): All matching objects spawned from this prototype. - - """ - return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY) - - -def get_protparent_dict(): - """ - Get prototype parents. - - Returns: - parent_dict (dict): A mapping {prototype_key: prototype} for all available prototypes. - - """ - return {prototype['prototype_key']: prototype for prototype in search_prototype()} - - -def list_prototypes(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 prototype 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. - - """ - # this allows us to pass lists of empty strings - tags = [tag for tag in make_iter(tags) if tag] - - # get prototypes for readonly and db-based prototypes - prototypes = search_prototype(key, tags) - - # get use-permissions of readonly attributes (edit is always False) - display_tuples = [] - for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')): - lock_use = caller.locks.check_lockstring( - caller, prototype.get('prototype_locks', ''), access_type='use') - if not show_non_use and not lock_use: - continue - if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES: - lock_edit = False - else: - lock_edit = caller.locks.check_lockstring( - caller, prototype.get('prototype_locks', ''), access_type='edit') - if not show_non_edit and not lock_edit: - continue - ptags = [] - for ptag in prototype.get('prototype_tags', []): - if is_iter(ptag): - if len(ptag) > 1: - ptags.append("{} (category: {}".format(ptag[0], ptag[1])) - else: - ptags.append(ptag[0]) - else: - ptags.append(str(ptag)) - - display_tuples.append( - (prototype.get('prototype_key', ''), - prototype.get('prototype_desc', ''), - "{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'), - ",".join(ptags))) - - if not display_tuples: - return None - - table = [] - width = 78 - for i in range(len(display_tuples[0])): - table.append([str(display_tuple[i]) for display_tuple in display_tuples]) - table = EvTable("Key", "Desc", "Use/Edit", "Tags", table=table, crop=True, width=width) - table.reformat_column(0, width=22) - table.reformat_column(1, width=29) - table.reformat_column(2, width=11, align='c') - table.reformat_column(3, width=16) - return table - - -def prototype_to_str(prototype): - """ - Format a prototype to a nice string representation. - - Args: - prototype (dict): The prototype. - """ - - header = ( - "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" - "|cdesc:|n {} \n|cprototype:|n ".format( - prototype['prototype_key'], - ", ".join(prototype['prototype_tags']), - prototype['prototype_locks'], - prototype['prototype_desc'])) - proto = ("{{\n {} \n}}".format( - "\n ".join( - "{!r}: {!r},".format(key, value) for key, value in - sorted(prototype.items()) if key not in _PROTOTYPE_META_NAMES)).rstrip(",")) - return header + proto - - -def prototype_from_object(obj): - """ - Guess a minimal prototype from an existing object. - - Args: - obj (Object): An object to analyze. - - Returns: - prototype (dict): A prototype estimating the current state of the object. - - """ - # first, check if this object already has a prototype - - prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) - prot = search_prototype(prot) - if not prot or len(prot) > 1: - # no unambiguous prototype found - build new prototype - prot = {} - prot['prototype_key'] = "From-Object-{}-{}".format( - obj.key, hashlib.md5(str(time.time())).hexdigest()[:6]) - prot['prototype_desc'] = "Built from {}".format(str(obj)) - prot['prototype_locks'] = "use:all();edit:all()" - - prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] - prot['location'] = obj.db_location - prot['home'] = obj.db_home - prot['destination'] = obj.db_destination - prot['typeclass'] = obj.db_typeclass_path - prot['locks'] = obj.locks.all() - prot['permissions'] = obj.permissions.get() - prot['aliases'] = obj.aliases.get() - prot['tags'] = [(tag.key, tag.category, tag.data) - for tag in obj.tags.get(return_tagobj=True, return_list=True)] - prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks) - for attr in obj.attributes.get(return_obj=True, return_list=True)] - - return prot - -# Spawner mechanism - - -def validate_prototype(prototype, protkey=None, protparents=None, _visited=None): - """ - Run validation on a prototype, checking for inifinite regress. - - Args: - prototype (dict): Prototype to validate. - protkey (str, optional): The name of the prototype definition. If not given, the prototype - dict needs to have the `prototype_key` field set. - protpartents (dict, optional): The available prototype parent library. If - note given this will be determined from settings/database. - _visited (list, optional): This is an internal work array and should not be set manually. - Raises: - RuntimeError: If prototype has invalid structure. - - """ - if not protparents: - protparents = get_protparent_dict() - if _visited is None: - _visited = [] - - protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) - - assert isinstance(prototype, dict) - - if id(prototype) in _visited: - raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype) - - _visited.append(id(prototype)) - protstrings = prototype.get("prototype") - - if protstrings: - for protstring in make_iter(protstrings): - protstring = protstring.lower() - if protkey is not None and protstring == protkey: - raise RuntimeError("%s tries to prototype itself." % protkey or prototype) - protparent = protparents.get(protstring) - if not protparent: - raise RuntimeError( - "%s's prototype '%s' was not found." % (protkey or prototype, protstring)) - validate_prototype(protparent, protstring, protparents, _visited) - - -def _get_prototype(dic, prot, protparents): - """ - Recursively traverse a prototype dictionary, including multiple - inheritance. Use validate_prototype before this, we don't check - for infinite recursion here. - - """ - if "prototype" in dic: - # move backwards through the inheritance - for prototype in make_iter(dic["prototype"]): - # Build the prot dictionary in reverse order, overloading - new_prot = _get_prototype(protparents.get(prototype.lower(), {}), prot, protparents) - prot.update(new_prot) - prot.update(dic) - prot.pop("prototype", None) # we don't need this anymore - return prot - - -def prototype_diff_from_object(prototype, obj): - """ - 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 - needed. - - Args: - prototype (dict): Prototype. - obj (Object): Object to - - Returns: - diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} - - """ - prot1 = prototype - prot2 = prototype_from_object(obj) - - diff = {} - for key, value in prot1.items(): - diff[key] = "KEEP" - if key in prot2: - if callable(prot2[key]) or value != prot2[key]: - diff[key] = "UPDATE" - elif key not in prot2: - diff[key] = "REMOVE" - - return diff - - -def batch_update_objects_with_prototype(prototype, diff=None, objects=None): - """ - Update existing objects with the latest version of the prototype. - - Args: - prototype (str or dict): Either the `prototype_key` to use or the - prototype dict itself. - diff (dict, optional): This a diff structure that describes how to update the protototype. 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`. - Returns: - changed (int): The number of objects that had changes applied to them. - - """ - prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] - prototype_obj = search_db_prototype(prototype_key, return_queryset=True) - prototype_obj = prototype_obj[0] if prototype_obj else None - new_prototype = prototype_obj.db.prototype - objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) - - if not objs: - return 0 - - if not diff: - diff = prototype_diff_from_object(new_prototype, objs[0]) - - changed = 0 - for obj in objs: - do_save = False - for key, directive in diff.items(): - val = new_prototype[key] - if directive in ('UPDATE', 'REPLACE'): - do_save = True - if key == 'key': - obj.db_key = validate_spawn_value(val, str) - elif key == 'typeclass': - obj.db_typeclass_path = validate_spawn_value(val, str) - elif key == 'location': - obj.db_location = validate_spawn_value(val, _to_obj) - elif key == 'home': - obj.db_home = validate_spawn_value(val, _to_obj) - elif key == 'destination': - obj.db_destination = validate_spawn_value(val, _to_obj) - elif key == 'locks': - if directive == 'REPLACE': - obj.locks.clear() - obj.locks.add(validate_spawn_value(val, str)) - elif key == 'permissions': - if directive == 'REPLACE': - obj.permissions.clear() - obj.permissions.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'aliases': - if directive == 'REPLACE': - obj.aliases.clear() - obj.aliases.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'tags': - if directive == 'REPLACE': - obj.tags.clear() - obj.tags.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'attrs': - if directive == 'REPLACE': - obj.attributes.clear() - obj.attributes.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'exec': - # we don't auto-rerun exec statements, it would be huge security risk! - pass - else: - obj.attributes.add(key, validate_spawn_value(val, _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) - if do_save: - changed += 1 - obj.save() - - return changed - - -def _batch_create_object(*objparams): - """ - This is a cut-down version of the create_object() function, - optimized for speed. It does NOT check and convert various input - so make sure the spawned Typeclass works before using this! - - Args: - objsparams (tuple): Each paremter tuple will create one object instance using the parameters within. - The parameters should be given in the following order: - - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`. - - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`. - - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`. - - `aliases` (list): A list of alias strings for - adding with `new_object.aliases.batch_add(*aliases)`. - - `nattributes` (list): list of tuples `(key, value)` to be loop-added to - add with `new_obj.nattributes.add(*tuple)`. - - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for - adding with `new_obj.attributes.batch_add(*attributes)`. - - `tags` (list): list of tuples `(key, category)` for adding - with `new_obj.tags.batch_add(*tags)`. - - `execs` (list): Code strings to execute together with the creation - of each object. They will be executed with `evennia` and `obj` - (the newly created object) available in the namespace. Execution - will happend after all other properties have been assigned and - is intended for calling custom handlers etc. - - Returns: - objects (list): A list of created objects - - Notes: - The `exec` list will execute arbitrary python code so don't allow this to be available to - unprivileged users! - - """ - - # bulk create all objects in one go - - # unfortunately this doesn't work since bulk_create doesn't creates pks; - # the result would be duplicate objects at the next stage, so we comment - # it out for now: - # dbobjs = _ObjectDB.objects.bulk_create(dbobjs) - - dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] - objs = [] - for iobj, obj in enumerate(dbobjs): - # call all setup hooks on each object - objparam = objparams[iobj] - # setup - obj._createdict = {"permissions": make_iter(objparam[1]), - "locks": objparam[2], - "aliases": make_iter(objparam[3]), - "nattributes": objparam[4], - "attributes": objparam[5], - "tags": make_iter(objparam[6])} - # this triggers all hooks - obj.save() - # run eventual extra code - for code in objparam[7]: - if code: - exec(code, {}, {"evennia": evennia, "obj": obj}) - objs.append(obj) - return objs - - -def spawn(*prototypes, **kwargs): - """ - Spawn a number of prototyped objects. - - Args: - prototypes (dict): Each argument should be a prototype - dictionary. - 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 - the global protparents dictionary accessible by the input - prototypes. If not given, it will instead look for modules - defined by settings.PROTOTYPE_MODULES. - prototype_parents (dict): A dictionary holding a custom - prototype-parent dictionary. Will overload same-named - prototypes from prototype_modules. - return_prototypes (bool): Only return a list of the - prototype-parents (no object creation happens) - - Returns: - object (Object): Spawned object. - - """ - # get available protparents - protparents = get_protparent_dict() - - # overload module's protparents with specifically given protparents - protparents.update(kwargs.get("prototype_parents", {})) - for key, prototype in protparents.items(): - validate_prototype(prototype, key.lower(), protparents) - - if "return_prototypes" in kwargs: - # only return the parents - return copy.deepcopy(protparents) - - objsparams = [] - for prototype in prototypes: - - validate_prototype(prototype, None, protparents) - prot = _get_prototype(prototype, {}, protparents) - if not prot: - continue - - # extract the keyword args we need to create the object itself. If we get a callable, - # call that to get the value (don't catch errors) - create_kwargs = {} - # we must always add a key, so if not given we use a shortened md5 hash. There is a (small) - # chance this is not unique but it should usually not be a problem. - val = prot.pop("key", "Spawned-{}".format( - hashlib.md5(str(time.time())).hexdigest()[:6])) - create_kwargs["db_key"] = validate_spawn_value(val, str) - - val = prot.pop("location", None) - create_kwargs["db_location"] = validate_spawn_value(val, _to_obj) - - val = prot.pop("home", settings.DEFAULT_HOME) - create_kwargs["db_home"] = validate_spawn_value(val, _to_obj) - - val = prot.pop("destination", None) - create_kwargs["db_destination"] = validate_spawn_value(val, _to_obj) - - val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) - create_kwargs["db_typeclass_path"] = validate_spawn_value(val, str) - - # extract calls to handlers - val = prot.pop("permissions", []) - permission_string = validate_spawn_value(val, make_iter) - val = prot.pop("locks", "") - lock_string = validate_spawn_value(val, str) - val = prot.pop("aliases", []) - alias_string = validate_spawn_value(val, make_iter) - - val = prot.pop("tags", []) - tags = validate_spawn_value(val, make_iter) - - 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)) - - val = prot.pop("exec", "") - execs = validate_spawn_value(val, make_iter) - - # extract ndb assignments - nattribute = dict((key.split("_", 1)[1], validate_spawn_value(val, _to_obj)) - for key, val in prot.items() if key.startswith("ndb_")) - - # the rest are attributes - val = prot.pop("attrs", []) - attributes = validate_spawn_value(val, list) - - simple_attributes = [] - for key, value in ((key, value) for key, value in prot.items() - if not (key.startswith("ndb_"))): - if is_iter(value) and len(value) > 1: - # (value, category) - simple_attributes.append((key, - validate_spawn_value(value[0], _to_obj_or_any), - validate_spawn_value(value[1], str))) - else: - simple_attributes.append((key, - validate_spawn_value(value, _to_obj_or_any))) - - attributes = attributes + simple_attributes - attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS] - - # pack for call into _batch_create_object - objsparams.append((create_kwargs, permission_string, lock_string, - alias_string, nattributes, attributes, tags, execs)) - - return _batch_create_object(*objsparams) - - -# ------------------------------------------------------------ -# -# OLC Prototype design menu -# -# ------------------------------------------------------------ - -# Helper functions - -def _get_menu_prototype(caller): - - prototype = None - if hasattr(caller.ndb._menutree, "olc_prototype"): - prototype = caller.ndb._menutree.olc_prototype - if not prototype: - caller.ndb._menutree.olc_prototype = prototype = {} - caller.ndb._menutree.olc_new = True - return prototype - - -def _is_new_prototype(caller): - return hasattr(caller.ndb._menutree, "olc_new") - - -def _set_menu_prototype(caller, field, value): - prototype = _get_menu_prototype(caller) - prototype[field] = value - caller.ndb._menutree.olc_prototype = prototype - - -def _format_property(prop, required=False, prototype=None, cropper=None): - - if prototype is not None: - prop = prototype.get(prop, '') - - out = prop - if callable(prop): - if hasattr(prop, '__name__'): - out = "<{}>".format(prop.__name__) - else: - out = repr(prop) - if is_iter(prop): - out = ", ".join(str(pr) for pr in prop) - if not out and required: - out = "|rrequired" - return " ({}|n)".format(cropper(out) if cropper else crop(out, _MENU_CROP_WIDTH)) - - -def _set_property(caller, raw_string, **kwargs): - """ - Update a property. To be called by the 'goto' option variable. - - Args: - caller (Object, Account): The user of the wizard. - raw_string (str): Input from user on given node - the new value to set. - Kwargs: - prop (str): Property name to edit with `raw_string`. - processor (callable): Converts `raw_string` to a form suitable for saving. - next_node (str): Where to redirect to after this has run. - Returns: - next_node (str): Next node to go to. - - """ - prop = kwargs.get("prop", "prototype_key") - processor = kwargs.get("processor", None) - next_node = kwargs.get("next_node", "node_index") - - propname_low = prop.strip().lower() - - if callable(processor): - try: - value = processor(raw_string) - except Exception as err: - caller.msg("Could not set {prop} to {value} ({err})".format( - prop=prop.replace("_", "-").capitalize(), value=raw_string, err=str(err))) - # this means we'll re-run the current node. - return None - else: - value = raw_string - - if not value: - return next_node - - prototype = _get_menu_prototype(caller) - - # typeclass and prototype can't co-exist - if propname_low == "typeclass": - prototype.pop("prototype", None) - if propname_low == "prototype": - prototype.pop("typeclass", None) - - caller.ndb._menutree.olc_prototype = prototype - - caller.msg("Set {prop} to '{value}'.".format(prop, value=str(value))) - - return next_node - - -def _wizard_options(curr_node, prev_node, next_node, color="|W"): - options = [] - if prev_node: - options.append({"key": ("|wb|Wack", "b"), - "desc": "{color}({node})|n".format( - color=color, node=prev_node.replace("_", "-")), - "goto": "node_{}".format(prev_node)}) - if next_node: - options.append({"key": ("|wf|Worward", "f"), - "desc": "{color}({node})|n".format( - color=color, node=next_node.replace("_", "-")), - "goto": "node_{}".format(next_node)}) - - if "index" not in (prev_node, next_node): - options.append({"key": ("|wi|Wndex", "i"), - "goto": "node_index"}) - - if curr_node: - options.append({"key": ("|wv|Walidate prototype", "v"), - "goto": ("node_validate_prototype", {"back": curr_node})}) - - return options - - -def _path_cropper(pythonpath): - "Crop path to only the last component" - return pythonpath.split('.')[-1] - - -# Menu nodes - -def node_index(caller): - prototype = _get_menu_prototype(caller) - - text = ("|c --- Prototype wizard --- |n\n\n" - "Define the |yproperties|n of the prototype. All prototype values can be " - "over-ridden at the time of spawning an instance of the prototype, but some are " - "required.\n\n'|wMeta'-properties|n are not used in the prototype itself but are used " - "to organize and list prototypes. The 'Meta-Key' uniquely identifies the prototype " - "and allows you to edit an existing prototype or save a new one for use by you or " - "others later.\n\n(make choice; q to abort. If unsure, start from 1.)") - - options = [] - options.append( - {"desc": "|WPrototype-Key|n|n{}".format(_format_property("Key", True, prototype, None)), - "goto": "node_prototype_key"}) - for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', - 'Permissions', 'Location', 'Home', 'Destination'): - required = False - cropper = None - if key in ("Prototype", "Typeclass"): - required = "prototype" not in prototype and "typeclass" not in prototype - if key == 'Typeclass': - cropper = _path_cropper - options.append( - {"desc": "|w{}|n{}".format( - key, _format_property(key, required, prototype, cropper=cropper)), - "goto": "node_{}".format(key.lower())}) - required = False - for key in ('Desc', 'Tags', 'Locks'): - options.append( - {"desc": "|WPrototype-{}|n|n{}".format(key, _format_property(key, required, prototype, None)), - "goto": "node_prototype_{}".format(key.lower())}) - - return text, options - - -def node_validate_prototype(caller, raw_string, **kwargs): - prototype = _get_menu_prototype(caller) - - txt = prototype_to_str(prototype) - errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)" - try: - # validate, don't spawn - spawn(prototype, return_prototypes=True) - except RuntimeError as err: - errors = "\n\n|rError: {}|n".format(err) - text = (txt + errors) - - options = _wizard_options(None, kwargs.get("back"), None) - - return text, options - - -def _check_prototype_key(caller, key): - old_prototype = search_prototype(key) - olc_new = _is_new_prototype(caller) - key = key.strip().lower() - if old_prototype: - # we are starting a new prototype that matches an existing - if not caller.locks.check_lockstring( - caller, old_prototype['prototype_locks'], access_type='edit'): - # return to the node_prototype_key to try another key - caller.msg("Prototype '{key}' already exists and you don't " - "have permission to edit it.".format(key=key)) - return "node_prototype_key" - elif olc_new: - # we are selecting an existing prototype to edit. Reset to index. - del caller.ndb._menutree.olc_new - caller.ndb._menutree.olc_prototype = old_prototype - caller.msg("Prototype already exists. Reloading.") - return "node_index" - - return _set_property(caller, key, prop='prototype_key', next_node="node_prototype") - - -def node_prototype_key(caller): - prototype = _get_menu_prototype(caller) - text = ["The prototype name, or |wMeta-Key|n, uniquely identifies the prototype. " - "It is used to find and use the prototype to spawn new entities. " - "It is not case sensitive."] - old_key = prototype.get('prototype_key', None) - if old_key: - text.append("Current key is '|w{key}|n'".format(key=old_key)) - else: - text.append("The key is currently unset.") - text.append("Enter text or make a choice (q for quit)") - text = "\n\n".join(text) - options = _wizard_options("prototype_key", "index", "prototype") - options.append({"key": "_default", - "goto": _check_prototype_key}) - return text, options - - -def _all_prototypes(caller): - return [prototype["prototype_key"] - for prototype in search_prototype() if "prototype_key" in prototype] - - -def _prototype_examine(caller, prototype_name): - prototypes = search_prototype(key=prototype_name) - if prototypes: - caller.msg(prototype_to_str(prototypes[0])) - caller.msg("Prototype not registered.") - return None - - -def _prototype_select(caller, prototype): - ret = _set_property(caller, prototype, prop="prototype", processor=str, next_node="node_key") - caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype)) - return ret - - -@list_node(_all_prototypes, _prototype_select) -def node_prototype(caller): - prototype = _get_menu_prototype(caller) - - prot_parent_key = prototype.get('prototype') - - text = ["Set the prototype's |yParent Prototype|n. If this is unset, Typeclass will be used."] - if prot_parent_key: - prot_parent = search_prototype(prot_parent_key) - if prot_parent: - text.append("Current parent prototype is {}:\n{}".format(prototype_to_str(prot_parent))) - else: - text.append("Current parent prototype |r{prototype}|n " - "does not appear to exist.".format(prot_parent_key)) - else: - text.append("Parent prototype is not set") - text = "\n\n".join(text) - options = _wizard_options("prototype", "prototype_key", "typeclass", color="|W") - options.append({"key": "_default", - "goto": _prototype_examine}) - - return text, options - - -def _all_typeclasses(caller): - return list(sorted(get_all_typeclasses().keys())) - - -def _typeclass_examine(caller, typeclass_path): - if typeclass_path is None: - # this means we are exiting the listing - return "node_key" - - typeclass = get_all_typeclasses().get(typeclass_path) - if typeclass: - docstr = [] - for line in typeclass.__doc__.split("\n"): - if line.strip(): - docstr.append(line) - elif docstr: - break - docstr = '\n'.join(docstr) if docstr else "" - txt = "Typeclass |y{typeclass_path}|n; First paragraph of docstring:\n\n{docstring}".format( - typeclass_path=typeclass_path, docstring=docstr) - else: - txt = "This is typeclass |y{}|n.".format(typeclass) - caller.msg(txt) - return None - - -def _typeclass_select(caller, typeclass): - ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key") - caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass)) - return ret - - -@list_node(_all_typeclasses, _typeclass_select) -def node_typeclass(caller): - prototype = _get_menu_prototype(caller) - typeclass = prototype.get("typeclass") - - text = ["Set the typeclass's parent |yTypeclass|n."] - if typeclass: - text.append("Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass)) - else: - text.append("Using default typeclass {typeclass}.".format( - typeclass=settings.BASE_OBJECT_TYPECLASS)) - text = "\n\n".join(text) - options = _wizard_options("typeclass", "prototype", "key", color="|W") - options.append({"key": "_default", - "goto": _typeclass_examine}) - return text, options - - -def node_key(caller): - prototype = _get_menu_prototype(caller) - key = prototype.get("key") - - text = ["Set the prototype's |yKey|n. This will retain case sensitivity."] - if key: - text.append("Current key value is '|y{key}|n'.".format(key=key)) - else: - text.append("Key is currently unset.") - text = "\n\n".join(text) - options = _wizard_options("key", "typeclass", "aliases") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="key", - processor=lambda s: s.strip(), - next_node="node_aliases"))}) - return text, options - - -def node_aliases(caller): - prototype = _get_menu_prototype(caller) - aliases = prototype.get("aliases") - - text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. " - "ill retain case sensitivity."] - if aliases: - text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases)) - else: - text.append("No aliases are set.") - text = "\n\n".join(text) - options = _wizard_options("aliases", "key", "attrs") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="aliases", - processor=lambda s: [part.strip() for part in s.split(",")], - next_node="node_attrs"))}) - return text, options - - -def _caller_attrs(caller): - prototype = _get_menu_prototype(caller) - attrs = prototype.get("attrs", []) - return attrs - - -def _attrparse(caller, attr_string): - "attr is entering on the form 'attr = value'" - - if '=' in attr_string: - attrname, value = (part.strip() for part in attr_string.split('=', 1)) - attrname = attrname.lower() - if attrname: - try: - value = literal_eval(value) - except SyntaxError: - caller.msg(_MENU_ATTR_LITERAL_EVAL_ERROR) - else: - return attrname, value - else: - return None, None - - -def _add_attr(caller, attr_string, **kwargs): - attrname, value = _attrparse(caller, attr_string) - if attrname: - prot = _get_menu_prototype(caller) - prot['attrs'][attrname] = value - _set_menu_prototype(caller, "prototype", prot) - text = "Added" - else: - text = "Attribute must be given as 'attrname = ' where uses valid Python." - options = {"key": "_default", - "goto": lambda caller: None} - return text, options - - -def _edit_attr(caller, attrname, new_value, **kwargs): - attrname, value = _attrparse("{}={}".format(caller, attrname, new_value)) - if attrname: - prot = _get_menu_prototype(caller) - prot['attrs'][attrname] = value - text = "Edited Attribute {} = {}".format(attrname, value) - else: - text = "Attribute value must be valid Python." - options = {"key": "_default", - "goto": lambda caller: None} - return text, options - - -def _examine_attr(caller, selection): - prot = _get_menu_prototype(caller) - value = prot['attrs'][selection] - return "Attribute {} = {}".format(selection, value) - - -@list_node(_caller_attrs) -def node_attrs(caller): - prot = _get_menu_prototype(caller) - attrs = prot.get("attrs") - - text = ["Set the prototype's |yAttributes|n. Separate multiple attrs with commas. " - "Will retain case sensitivity."] - if attrs: - text.append("Current attrs are '|y{attrs}|n'.".format(attrs=attrs)) - else: - text.append("No attrs are set.") - text = "\n\n".join(text) - options = _wizard_options("attrs", "aliases", "tags") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="attrs", - processor=lambda s: [part.strip() for part in s.split(",")], - next_node="node_tags"))}) - return text, options - - -def _caller_tags(caller): - prototype = _get_menu_prototype(caller) - tags = prototype.get("tags") - return tags - - -def _add_tag(caller, tag, **kwargs): - tag = tag.strip().lower() - prototype = _get_menu_prototype(caller) - tags = prototype.get('tags', []) - if tags: - if tag not in tags: - tags.append(tag) - else: - tags = [tag] - prot['tags'] = tags - _set_menu_prototype(caller, "prototype", prot) - text = kwargs.get("text") - if not text: - text = "Added tag {}. (return to continue)".format(tag) - options = {"key": "_default", - "goto": lambda caller: None} - return text, options - - -def _edit_tag(caller, old_tag, new_tag, **kwargs): - prototype = _get_menu_prototype(caller) - tags = prototype.get('tags', []) - - old_tag = old_tag.strip().lower() - new_tag = new_tag.strip().lower() - tags[tags.index(old_tag)] = new_tag - prototype['tags'] = tags - _set_menu_prototype(caller, 'prototype', prototype) - - text = kwargs.get('text') - if not text: - text = "Changed tag {} to {}.".format(old_tag, new_tag) - options = {"key": "_default", - "goto": lambda caller: None} - return text, options - - -@list_node(_caller_tags) -def node_tags(caller): - text = "Set the prototype's |yTags|n." - options = _wizard_options("tags", "attrs", "locks") - return text, options - - -def node_locks(caller): - prototype = _get_menu_prototype(caller) - locks = prototype.get("locks") - - text = ["Set the prototype's |yLock string|n. Separate multiple locks with semi-colons. " - "Will retain case sensitivity."] - if locks: - text.append("Current locks are '|y{locks}|n'.".format(locks=locks)) - else: - text.append("No locks are set.") - text = "\n\n".join(text) - options = _wizard_options("locks", "tags", "permissions") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="locks", - processor=lambda s: s.strip(), - next_node="node_permissions"))}) - return text, options - - -def node_permissions(caller): - prototype = _get_menu_prototype(caller) - permissions = prototype.get("permissions") - - text = ["Set the prototype's |yPermissions|n. Separate multiple permissions with commas. " - "Will retain case sensitivity."] - if permissions: - text.append("Current permissions are '|y{permissions}|n'.".format(permissions=permissions)) - else: - text.append("No permissions are set.") - text = "\n\n".join(text) - options = _wizard_options("permissions", "destination", "location") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="permissions", - processor=lambda s: [part.strip() for part in s.split(",")], - next_node="node_location"))}) - return text, options - - -def node_location(caller): - prototype = _get_menu_prototype(caller) - location = prototype.get("location") - - text = ["Set the prototype's |yLocation|n"] - if location: - text.append("Current location is |y{location}|n.".format(location=location)) - else: - text.append("Default location is {}'s inventory.".format(caller)) - text = "\n\n".join(text) - options = _wizard_options("location", "permissions", "home") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="location", - processor=lambda s: s.strip(), - next_node="node_home"))}) - return text, options - - -def node_home(caller): - prototype = _get_menu_prototype(caller) - home = prototype.get("home") - - text = ["Set the prototype's |yHome location|n"] - if home: - text.append("Current home location is |y{home}|n.".format(home=home)) - else: - text.append("Default home location (|y{home}|n) used.".format(home=settings.DEFAULT_HOME)) - text = "\n\n".join(text) - options = _wizard_options("home", "aliases", "destination") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="home", - processor=lambda s: s.strip(), - next_node="node_destination"))}) - return text, options - - -def node_destination(caller): - prototype = _get_menu_prototype(caller) - dest = prototype.get("dest") - - text = ["Set the prototype's |yDestination|n. This is usually only used for Exits."] - if dest: - text.append("Current destination is |y{dest}|n.".format(dest=dest)) - else: - text.append("No destination is set (default).") - text = "\n\n".join(text) - options = _wizard_options("destination", "home", "prototype_desc") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="dest", - processor=lambda s: s.strip(), - next_node="node_prototype_desc"))}) - return text, options - - -def node_prototype_desc(caller): - - prototype = _get_menu_prototype(caller) - text = ["The |wMeta-Description|n briefly describes the prototype for viewing in listings."] - desc = prototype.get("prototype_desc", None) - - if desc: - text.append("The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc)) - else: - text.append("Description is currently unset.") - text = "\n\n".join(text) - options = _wizard_options("prototype_desc", "prototype_key", "prototype_tags") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop='prototype_desc', - processor=lambda s: s.strip(), - next_node="node_prototype_tags"))}) - - return text, options - - -def node_prototype_tags(caller): - prototype = _get_menu_prototype(caller) - text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " - "Separate multiple by tags by commas."] - tags = prototype.get('prototype_tags', []) - - if tags: - text.append("The current tags are:\n|w{tags}|n".format(tags=tags)) - else: - text.append("No tags are currently set.") - text = "\n\n".join(text) - options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="prototype_tags", - processor=lambda s: [ - str(part.strip().lower()) for part in s.split(",")], - next_node="node_prototype_locks"))}) - return text, options - - -def node_prototype_locks(caller): - prototype = _get_menu_prototype(caller) - text = ["Set |wMeta-Locks|n on the prototype. There are two valid lock types: " - "'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n" - "(If you are unsure, leave as default.)"] - locks = prototype.get('prototype_locks', '') - if locks: - text.append("Current lock is |w'{lockstring}'|n".format(lockstring=locks)) - else: - text.append("Lock unset - if not changed the default lockstring will be set as\n" - " |w'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) - text = "\n\n".join(text) - options = _wizard_options("prototype_locks", "prototype_tags", "index") - options.append({"key": "_default", - "goto": (_set_property, - dict(prop="prototype_locks", - processor=lambda s: s.strip().lower(), - next_node="node_index"))}) - return text, options - - -class OLCMenu(EvMenu): - """ - A custom EvMenu with a different formatting for the options. - - """ - def options_formatter(self, optionlist): - """ - Split the options into two blocks - olc options and normal options - - """ - olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype") - olc_options = [] - other_options = [] - for key, desc in optionlist: - raw_key = strip_ansi(key) - if raw_key in olc_keys: - desc = " {}".format(desc) if desc else "" - olc_options.append("|lc{}|lt{}|le{}".format(raw_key, key, desc)) - else: - other_options.append((key, desc)) - - olc_options = " | ".join(olc_options) + " | " + "|wq|Wuit" if olc_options else "" - other_options = super(OLCMenu, self).options_formatter(other_options) - sep = "\n\n" if olc_options and other_options else "" - - return "{}{}{}".format(olc_options, sep, other_options) - - -def start_olc(caller, session=None, prototype=None): - """ - Start menu-driven olc system for prototypes. - - Args: - caller (Object or Account): The entity starting the menu. - session (Session, optional): The individual session to get data. - prototype (dict, optional): Given when editing an existing - prototype rather than creating a new one. - - """ - menudata = {"node_index": node_index, - "node_validate_prototype": node_validate_prototype, - "node_prototype_key": node_prototype_key, - "node_prototype": node_prototype, - "node_typeclass": node_typeclass, - "node_key": node_key, - "node_aliases": node_aliases, - "node_attrs": node_attrs, - "node_tags": node_tags, - "node_locks": node_locks, - "node_permissions": node_permissions, - "node_location": node_location, - "node_home": node_home, - "node_destination": node_destination, - "node_prototype_desc": node_prototype_desc, - "node_prototype_tags": node_prototype_tags, - "node_prototype_locks": node_prototype_locks, - } - OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=prototype) - - -# Testing - -if __name__ == "__main__": - protparents = { - "NOBODY": {}, - # "INFINITE" : { - # "prototype":"INFINITE" - # }, - "GOBLIN": { - "key": "goblin grunt", - "health": lambda: randint(20, 30), - "resists": ["cold", "poison"], - "attacks": ["fists"], - "weaknesses": ["fire", "light"] - }, - "GOBLIN_WIZARD": { - "prototype": "GOBLIN", - "key": "goblin wizard", - "spells": ["fire ball", "lighting bolt"] - }, - "GOBLIN_ARCHER": { - "prototype": "GOBLIN", - "key": "goblin archer", - "attacks": ["short bow"] - }, - "ARCHWIZARD": { - "attacks": ["archwizard staff"], - }, - "GOBLIN_ARCHWIZARD": { - "key": "goblin archwizard", - "prototype": ("GOBLIN_WIZARD", "ARCHWIZARD") - } - } - # test - print([o.key for o in spawn(protparents["GOBLIN"], - protparents["GOBLIN_ARCHWIZARD"], - prototype_parents=protparents)]) From fef8b3bf6a7c6626613c2500644470e64d6995ef Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 6 Jun 2018 22:11:32 +0200 Subject: [PATCH 054/103] Continued refactoring --- evennia/prototypes/prototypes.py | 274 +++++++++++++++++++++++++++++++ evennia/prototypes/spawner.py | 240 +-------------------------- evennia/prototypes/utils.py | 128 +++------------ 3 files changed, 295 insertions(+), 347 deletions(-) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 60e194861b..e3d26fd87e 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -61,6 +61,7 @@ class DbPrototype(DefaultScript): # General prototype functions + def check_permission(prototype_key, action, default=True): """ Helper function to check access to actions on given prototype. @@ -278,3 +279,276 @@ def search_objects_with_prototype(prototype_key): """ return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + + +def prototype_from_object(obj): + """ + Guess a minimal prototype from an existing object. + + Args: + obj (Object): An object to analyze. + + Returns: + prototype (dict): A prototype estimating the current state of the object. + + """ + # first, check if this object already has a prototype + + prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) + prot = search_prototype(prot) + if not prot or len(prot) > 1: + # no unambiguous prototype found - build new prototype + prot = {} + prot['prototype_key'] = "From-Object-{}-{}".format( + obj.key, hashlib.md5(str(time.time())).hexdigest()[:6]) + prot['prototype_desc'] = "Built from {}".format(str(obj)) + prot['prototype_locks'] = "spawn:all();edit:all()" + + prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] + prot['location'] = obj.db_location + prot['home'] = obj.db_home + prot['destination'] = obj.db_destination + prot['typeclass'] = obj.db_typeclass_path + prot['locks'] = obj.locks.all() + prot['permissions'] = obj.permissions.get() + prot['aliases'] = obj.aliases.get() + prot['tags'] = [(tag.key, tag.category, tag.data) + for tag in obj.tags.get(return_tagobj=True, return_list=True)] + prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks) + for attr in obj.attributes.get(return_obj=True, return_list=True)] + + return prot + + +def list_prototypes(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 prototype 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. + + """ + # this allows us to pass lists of empty strings + tags = [tag for tag in make_iter(tags) if tag] + + # get prototypes for readonly and db-based prototypes + prototypes = search_prototype(key, tags) + + # get use-permissions of readonly attributes (edit is always False) + display_tuples = [] + for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')): + lock_use = caller.locks.check_lockstring( + caller, prototype.get('prototype_locks', ''), access_type='spawn') + if not show_non_use and not lock_use: + continue + if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES: + lock_edit = False + else: + lock_edit = caller.locks.check_lockstring( + caller, prototype.get('prototype_locks', ''), access_type='edit') + if not show_non_edit and not lock_edit: + continue + ptags = [] + for ptag in prototype.get('prototype_tags', []): + if is_iter(ptag): + if len(ptag) > 1: + ptags.append("{} (category: {}".format(ptag[0], ptag[1])) + else: + ptags.append(ptag[0]) + else: + ptags.append(str(ptag)) + + display_tuples.append( + (prototype.get('prototype_key', ''), + prototype.get('prototype_desc', ''), + "{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'), + ",".join(ptags))) + + if not display_tuples: + return None + + table = [] + width = 78 + for i in range(len(display_tuples[0])): + table.append([str(display_tuple[i]) for display_tuple in display_tuples]) + table = EvTable("Key", "Desc", "Spawn/Edit", "Tags", table=table, crop=True, width=width) + table.reformat_column(0, width=22) + table.reformat_column(1, width=29) + table.reformat_column(2, width=11, align='c') + table.reformat_column(3, width=16) + return table + + + +def batch_update_objects_with_prototype(prototype, diff=None, objects=None): + """ + Update existing objects with the latest version of the prototype. + + Args: + prototype (str or dict): Either the `prototype_key` to use or the + prototype dict itself. + diff (dict, optional): This a diff structure that describes how to update the protototype. + 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`. + Returns: + changed (int): The number of objects that had changes applied to them. + + """ + prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] + prototype_obj = search_db_prototype(prototype_key, return_queryset=True) + prototype_obj = prototype_obj[0] if prototype_obj else None + new_prototype = prototype_obj.db.prototype + objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + + if not objs: + return 0 + + if not diff: + diff = prototype_diff_from_object(new_prototype, objs[0]) + + changed = 0 + for obj in objs: + do_save = False + for key, directive in diff.items(): + val = new_prototype[key] + if directive in ('UPDATE', 'REPLACE'): + do_save = True + if key == 'key': + obj.db_key = validate_spawn_value(val, str) + elif key == 'typeclass': + obj.db_typeclass_path = validate_spawn_value(val, str) + elif key == 'location': + obj.db_location = validate_spawn_value(val, _to_obj) + elif key == 'home': + obj.db_home = validate_spawn_value(val, _to_obj) + elif key == 'destination': + obj.db_destination = validate_spawn_value(val, _to_obj) + elif key == 'locks': + if directive == 'REPLACE': + obj.locks.clear() + obj.locks.add(validate_spawn_value(val, str)) + elif key == 'permissions': + if directive == 'REPLACE': + obj.permissions.clear() + obj.permissions.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'aliases': + if directive == 'REPLACE': + obj.aliases.clear() + obj.aliases.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'tags': + if directive == 'REPLACE': + obj.tags.clear() + obj.tags.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'attrs': + if directive == 'REPLACE': + obj.attributes.clear() + obj.attributes.batch_add(validate_spawn_value(val, make_iter)) + elif key == 'exec': + # we don't auto-rerun exec statements, it would be huge security risk! + pass + else: + obj.attributes.add(key, validate_spawn_value(val, _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) + if do_save: + changed += 1 + obj.save() + + return changed + +def batch_create_object(*objparams): + """ + This is a cut-down version of the create_object() function, + optimized for speed. It does NOT check and convert various input + so make sure the spawned Typeclass works before using this! + + Args: + objsparams (tuple): Each paremter tuple will create one object instance using the parameters within. + The parameters should be given in the following order: + - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`. + - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`. + - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`. + - `aliases` (list): A list of alias strings for + adding with `new_object.aliases.batch_add(*aliases)`. + - `nattributes` (list): list of tuples `(key, value)` to be loop-added to + add with `new_obj.nattributes.add(*tuple)`. + - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for + adding with `new_obj.attributes.batch_add(*attributes)`. + - `tags` (list): list of tuples `(key, category)` for adding + with `new_obj.tags.batch_add(*tags)`. + - `execs` (list): Code strings to execute together with the creation + of each object. They will be executed with `evennia` and `obj` + (the newly created object) available in the namespace. Execution + will happend after all other properties have been assigned and + is intended for calling custom handlers etc. + + Returns: + objects (list): A list of created objects + + Notes: + The `exec` list will execute arbitrary python code so don't allow this to be available to + unprivileged users! + + """ + + # bulk create all objects in one go + + # unfortunately this doesn't work since bulk_create doesn't creates pks; + # the result would be duplicate objects at the next stage, so we comment + # it out for now: + # dbobjs = _ObjectDB.objects.bulk_create(dbobjs) + + dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] + objs = [] + for iobj, obj in enumerate(dbobjs): + # call all setup hooks on each object + objparam = objparams[iobj] + # setup + obj._createdict = {"permissions": make_iter(objparam[1]), + "locks": objparam[2], + "aliases": make_iter(objparam[3]), + "nattributes": objparam[4], + "attributes": objparam[5], + "tags": make_iter(objparam[6])} + # this triggers all hooks + obj.save() + # run eventual extra code + for code in objparam[7]: + if code: + exec(code, {}, {"evennia": evennia, "obj": obj}) + objs.append(obj) + return objs diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 062e15ee92..15ef8afb4d 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -191,48 +191,6 @@ def validate_spawn_value(value, validator=None): # Spawner mechanism -def validate_prototype(prototype, protkey=None, protparents=None, _visited=None): - """ - Run validation on a prototype, checking for inifinite regress. - - Args: - prototype (dict): Prototype to validate. - protkey (str, optional): The name of the prototype definition. If not given, the prototype - dict needs to have the `prototype_key` field set. - protpartents (dict, optional): The available prototype parent library. If - note given this will be determined from settings/database. - _visited (list, optional): This is an internal work array and should not be set manually. - Raises: - RuntimeError: If prototype has invalid structure. - - """ - if not protparents: - protparents = get_protparent_dict() - if _visited is None: - _visited = [] - - protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) - - assert isinstance(prototype, dict) - - if id(prototype) in _visited: - raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype) - - _visited.append(id(prototype)) - protstrings = prototype.get("prototype") - - if protstrings: - for protstring in make_iter(protstrings): - protstring = protstring.lower() - if protkey is not None and protstring == protkey: - raise RuntimeError("%s tries to prototype itself." % protkey or prototype) - protparent = protparents.get(protstring) - if not protparent: - raise RuntimeError( - "%s's prototype '%s' was not found." % (protkey or prototype, protstring)) - validate_prototype(protparent, protstring, protparents, _visited) - - def _get_prototype(dic, prot, protparents): """ Recursively traverse a prototype dictionary, including multiple @@ -251,202 +209,6 @@ def _get_prototype(dic, prot, protparents): return prot -def prototype_diff_from_object(prototype, obj): - """ - 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 - needed. - - Args: - prototype (dict): Prototype. - obj (Object): Object to - - Returns: - diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} - - """ - prot1 = prototype - prot2 = prototype_from_object(obj) - - diff = {} - for key, value in prot1.items(): - diff[key] = "KEEP" - if key in prot2: - if callable(prot2[key]) or value != prot2[key]: - diff[key] = "UPDATE" - elif key not in prot2: - diff[key] = "REMOVE" - - return diff - - -def batch_update_objects_with_prototype(prototype, diff=None, objects=None): - """ - Update existing objects with the latest version of the prototype. - - Args: - prototype (str or dict): Either the `prototype_key` to use or the - prototype dict itself. - diff (dict, optional): This a diff structure that describes how to update the protototype. - 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`. - Returns: - changed (int): The number of objects that had changes applied to them. - - """ - prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] - prototype_obj = search_db_prototype(prototype_key, return_queryset=True) - prototype_obj = prototype_obj[0] if prototype_obj else None - new_prototype = prototype_obj.db.prototype - objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) - - if not objs: - return 0 - - if not diff: - diff = prototype_diff_from_object(new_prototype, objs[0]) - - changed = 0 - for obj in objs: - do_save = False - for key, directive in diff.items(): - val = new_prototype[key] - if directive in ('UPDATE', 'REPLACE'): - do_save = True - if key == 'key': - obj.db_key = validate_spawn_value(val, str) - elif key == 'typeclass': - obj.db_typeclass_path = validate_spawn_value(val, str) - elif key == 'location': - obj.db_location = validate_spawn_value(val, _to_obj) - elif key == 'home': - obj.db_home = validate_spawn_value(val, _to_obj) - elif key == 'destination': - obj.db_destination = validate_spawn_value(val, _to_obj) - elif key == 'locks': - if directive == 'REPLACE': - obj.locks.clear() - obj.locks.add(validate_spawn_value(val, str)) - elif key == 'permissions': - if directive == 'REPLACE': - obj.permissions.clear() - obj.permissions.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'aliases': - if directive == 'REPLACE': - obj.aliases.clear() - obj.aliases.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'tags': - if directive == 'REPLACE': - obj.tags.clear() - obj.tags.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'attrs': - if directive == 'REPLACE': - obj.attributes.clear() - obj.attributes.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'exec': - # we don't auto-rerun exec statements, it would be huge security risk! - pass - else: - obj.attributes.add(key, validate_spawn_value(val, _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) - if do_save: - changed += 1 - obj.save() - - return changed - - -def _batch_create_object(*objparams): - """ - This is a cut-down version of the create_object() function, - optimized for speed. It does NOT check and convert various input - so make sure the spawned Typeclass works before using this! - - Args: - objsparams (tuple): Each paremter tuple will create one object instance using the parameters within. - The parameters should be given in the following order: - - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`. - - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`. - - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`. - - `aliases` (list): A list of alias strings for - adding with `new_object.aliases.batch_add(*aliases)`. - - `nattributes` (list): list of tuples `(key, value)` to be loop-added to - add with `new_obj.nattributes.add(*tuple)`. - - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for - adding with `new_obj.attributes.batch_add(*attributes)`. - - `tags` (list): list of tuples `(key, category)` for adding - with `new_obj.tags.batch_add(*tags)`. - - `execs` (list): Code strings to execute together with the creation - of each object. They will be executed with `evennia` and `obj` - (the newly created object) available in the namespace. Execution - will happend after all other properties have been assigned and - is intended for calling custom handlers etc. - - Returns: - objects (list): A list of created objects - - Notes: - The `exec` list will execute arbitrary python code so don't allow this to be available to - unprivileged users! - - """ - - # bulk create all objects in one go - - # unfortunately this doesn't work since bulk_create doesn't creates pks; - # the result would be duplicate objects at the next stage, so we comment - # it out for now: - # dbobjs = _ObjectDB.objects.bulk_create(dbobjs) - - dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] - objs = [] - for iobj, obj in enumerate(dbobjs): - # call all setup hooks on each object - objparam = objparams[iobj] - # setup - obj._createdict = {"permissions": make_iter(objparam[1]), - "locks": objparam[2], - "aliases": make_iter(objparam[3]), - "nattributes": objparam[4], - "attributes": objparam[5], - "tags": make_iter(objparam[6])} - # this triggers all hooks - obj.save() - # run eventual extra code - for code in objparam[7]: - if code: - exec(code, {}, {"evennia": evennia, "obj": obj}) - objs.append(obj) - return objs - def spawn(*prototypes, **kwargs): """ @@ -472,7 +234,7 @@ def spawn(*prototypes, **kwargs): """ # get available protparents - protparents = get_protparent_dict() + protparents = {prot['prototype_key']: prot for prot in search_prototype()} # overload module's protparents with specifically given protparents protparents.update(kwargs.get("prototype_parents", {})) diff --git a/evennia/prototypes/utils.py b/evennia/prototypes/utils.py index 74eaef169f..6fe87d172c 100644 --- a/evennia/prototypes/utils.py +++ b/evennia/prototypes/utils.py @@ -4,91 +4,13 @@ Prototype utilities """ +_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") + class PermissionError(RuntimeError): pass - - - -def get_protparent_dict(): - """ - Get prototype parents. - - Returns: - parent_dict (dict): A mapping {prototype_key: prototype} for all available prototypes. - - """ - return {prototype['prototype_key']: prototype for prototype in search_prototype()} - - -def list_prototypes(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 prototype 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. - - """ - # this allows us to pass lists of empty strings - tags = [tag for tag in make_iter(tags) if tag] - - # get prototypes for readonly and db-based prototypes - prototypes = search_prototype(key, tags) - - # get use-permissions of readonly attributes (edit is always False) - display_tuples = [] - for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')): - lock_use = caller.locks.check_lockstring( - caller, prototype.get('prototype_locks', ''), access_type='use') - if not show_non_use and not lock_use: - continue - if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES: - lock_edit = False - else: - lock_edit = caller.locks.check_lockstring( - caller, prototype.get('prototype_locks', ''), access_type='edit') - if not show_non_edit and not lock_edit: - continue - ptags = [] - for ptag in prototype.get('prototype_tags', []): - if is_iter(ptag): - if len(ptag) > 1: - ptags.append("{} (category: {}".format(ptag[0], ptag[1])) - else: - ptags.append(ptag[0]) - else: - ptags.append(str(ptag)) - - display_tuples.append( - (prototype.get('prototype_key', ''), - prototype.get('prototype_desc', ''), - "{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'), - ",".join(ptags))) - - if not display_tuples: - return None - - table = [] - width = 78 - for i in range(len(display_tuples[0])): - table.append([str(display_tuple[i]) for display_tuple in display_tuples]) - table = EvTable("Key", "Desc", "Use/Edit", "Tags", table=table, crop=True, width=width) - table.reformat_column(0, width=22) - table.reformat_column(1, width=29) - table.reformat_column(2, width=11, align='c') - table.reformat_column(3, width=16) - return table - - def prototype_to_str(prototype): """ Format a prototype to a nice string representation. @@ -111,40 +33,30 @@ def prototype_to_str(prototype): return header + proto -def prototype_from_object(obj): +def prototype_diff_from_object(prototype, obj): """ - Guess a minimal prototype from an existing object. + 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 + needed. Args: - obj (Object): An object to analyze. + prototype (dict): Prototype. + obj (Object): Object to Returns: - prototype (dict): A prototype estimating the current state of the object. + diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} """ - # first, check if this object already has a prototype + prot1 = prototype + prot2 = prototype_from_object(obj) - prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) - prot = search_prototype(prot) - if not prot or len(prot) > 1: - # no unambiguous prototype found - build new prototype - prot = {} - prot['prototype_key'] = "From-Object-{}-{}".format( - obj.key, hashlib.md5(str(time.time())).hexdigest()[:6]) - prot['prototype_desc'] = "Built from {}".format(str(obj)) - prot['prototype_locks'] = "use:all();edit:all()" + diff = {} + for key, value in prot1.items(): + diff[key] = "KEEP" + if key in prot2: + if callable(prot2[key]) or value != prot2[key]: + diff[key] = "UPDATE" + elif key not in prot2: + diff[key] = "REMOVE" - prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] - prot['location'] = obj.db_location - prot['home'] = obj.db_home - prot['destination'] = obj.db_destination - prot['typeclass'] = obj.db_typeclass_path - prot['locks'] = obj.locks.all() - prot['permissions'] = obj.permissions.get() - prot['aliases'] = obj.aliases.get() - prot['tags'] = [(tag.key, tag.category, tag.data) - for tag in obj.tags.get(return_tagobj=True, return_list=True)] - prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks) - for attr in obj.attributes.get(return_obj=True, return_list=True)] - - return prot + return diff From f864654f5eca43dca7acee89ab4f0475c9f9d1ec Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 7 Jun 2018 22:40:03 +0200 Subject: [PATCH 055/103] Finish refactor prototypes/spawner/menus --- evennia/prototypes/menus.py | 45 ++-- evennia/prototypes/prototypes.py | 346 ++++++++++--------------------- evennia/prototypes/spawner.py | 335 +++++++++++++++++++++++------- evennia/prototypes/utils.py | 62 ------ 4 files changed, 399 insertions(+), 389 deletions(-) delete mode 100644 evennia/prototypes/utils.py diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 85e7f3f574..bebc6d00bd 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -4,8 +4,13 @@ OLC Prototype menu nodes """ +from ast import literal_eval +from django.conf import settings from evennia.utils.evmenu import EvMenu, list_node from evennia.utils.ansi import strip_ansi +from evennia.utils import utils +from evennia.utils.prototypes import prototypes as protlib +from evennia.utils.prototypes import spawner # ------------------------------------------------------------ # @@ -13,6 +18,13 @@ from evennia.utils.ansi import strip_ansi # # ------------------------------------------------------------ +_MENU_CROP_WIDTH = 15 +_MENU_ATTR_LITERAL_EVAL_ERROR = ( + "|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n" + "You also need to use correct Python syntax. Remember especially to put quotes around all " + "strings inside lists and dicts.|n") + + # Helper functions @@ -48,11 +60,11 @@ def _format_property(prop, required=False, prototype=None, cropper=None): out = "<{}>".format(prop.__name__) else: out = repr(prop) - if is_iter(prop): + if utils.is_iter(prop): out = ", ".join(str(pr) for pr in prop) if not out and required: out = "|rrequired" - return " ({}|n)".format(cropper(out) if cropper else crop(out, _MENU_CROP_WIDTH)) + return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH)) def _set_property(caller, raw_string, **kwargs): @@ -166,7 +178,8 @@ def node_index(caller): required = False for key in ('Desc', 'Tags', 'Locks'): options.append( - {"desc": "|WPrototype-{}|n|n{}".format(key, _format_property(key, required, prototype, None)), + {"desc": "|WPrototype-{}|n|n{}".format( + key, _format_property(key, required, prototype, None)), "goto": "node_prototype_{}".format(key.lower())}) return text, options @@ -175,11 +188,11 @@ def node_index(caller): def node_validate_prototype(caller, raw_string, **kwargs): prototype = _get_menu_prototype(caller) - txt = prototype_to_str(prototype) + txt = protlib.prototype_to_str(prototype) errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)" try: # validate, don't spawn - spawn(prototype, return_prototypes=True) + spawner.spawn(prototype, return_prototypes=True) except RuntimeError as err: errors = "\n\n|rError: {}|n".format(err) text = (txt + errors) @@ -190,7 +203,7 @@ def node_validate_prototype(caller, raw_string, **kwargs): def _check_prototype_key(caller, key): - old_prototype = search_prototype(key) + old_prototype = protlib.search_prototype(key) olc_new = _is_new_prototype(caller) key = key.strip().lower() if old_prototype: @@ -231,13 +244,13 @@ def node_prototype_key(caller): def _all_prototypes(caller): return [prototype["prototype_key"] - for prototype in search_prototype() if "prototype_key" in prototype] + for prototype in protlib.search_prototype() if "prototype_key" in prototype] def _prototype_examine(caller, prototype_name): - prototypes = search_prototype(key=prototype_name) + prototypes = protlib.search_prototype(key=prototype_name) if prototypes: - caller.msg(prototype_to_str(prototypes[0])) + caller.msg(protlib.prototype_to_str(prototypes[0])) caller.msg("Prototype not registered.") return None @@ -256,9 +269,10 @@ def node_prototype(caller): text = ["Set the prototype's |yParent Prototype|n. If this is unset, Typeclass will be used."] if prot_parent_key: - prot_parent = search_prototype(prot_parent_key) + prot_parent = protlib.search_prototype(prot_parent_key) if prot_parent: - text.append("Current parent prototype is {}:\n{}".format(prototype_to_str(prot_parent))) + text.append( + "Current parent prototype is {}:\n{}".format(protlib.prototype_to_str(prot_parent))) else: text.append("Current parent prototype |r{prototype}|n " "does not appear to exist.".format(prot_parent_key)) @@ -273,7 +287,7 @@ def node_prototype(caller): def _all_typeclasses(caller): - return list(sorted(get_all_typeclasses().keys())) + return list(sorted(utils.get_all_typeclasses().keys())) def _typeclass_examine(caller, typeclass_path): @@ -281,7 +295,7 @@ def _typeclass_examine(caller, typeclass_path): # this means we are exiting the listing return "node_key" - typeclass = get_all_typeclasses().get(typeclass_path) + typeclass = utils.get_all_typeclasses().get(typeclass_path) if typeclass: docstr = [] for line in typeclass.__doc__.split("\n"): @@ -453,8 +467,8 @@ def _add_tag(caller, tag, **kwargs): tags.append(tag) else: tags = [tag] - prot['tags'] = tags - _set_menu_prototype(caller, "prototype", prot) + prototype['tags'] = tags + _set_menu_prototype(caller, "prototype", prototype) text = kwargs.get("text") if not text: text = "Added tag {}. (return to continue)".format(tag) @@ -706,4 +720,3 @@ def start_olc(caller, session=None, prototype=None): "node_prototype_locks": node_prototype_locks, } OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=prototype) - diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index e3d26fd87e..37fd83f846 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -6,16 +6,26 @@ Handling storage of prototypes, both database-based ones (DBPrototypes) and thos """ from django.conf import settings + from evennia.scripts.scripts import DefaultScript from evennia.objects.models import ObjectDB from evennia.utils.create import create_script -from evennia.utils.utils import all_from_module, make_iter, callables_from_module, is_iter +from evennia.utils.utils import ( + all_from_module, make_iter, is_iter, dbid_to_obj) from evennia.locks.lockhandler import validate_lockstring, check_lockstring from evennia.utils import logger +from evennia.utils.evtable import EvTable +from evennia.utils.prototypes.protfuncs import protfunc_parser _MODULE_PROTOTYPE_MODULES = {} _MODULE_PROTOTYPES = {} +_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") +_PROTOTYPE_TAG_CATEGORY = "spawned_by_prototype" + + +class PermissionError(RuntimeError): + pass class ValidationError(RuntimeError): @@ -25,6 +35,99 @@ class ValidationError(RuntimeError): pass +# helper functions + +def value_to_obj(value, force=True): + return dbid_to_obj(value, ObjectDB) + + +def value_to_obj_or_any(value): + obj = dbid_to_obj(value, ObjectDB) + return obj if obj is not None else value + + +def prototype_to_str(prototype): + """ + Format a prototype to a nice string representation. + + Args: + prototype (dict): The prototype. + """ + + header = ( + "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" + "|cdesc:|n {} \n|cprototype:|n ".format( + prototype['prototype_key'], + ", ".join(prototype['prototype_tags']), + prototype['prototype_locks'], + prototype['prototype_desc'])) + proto = ("{{\n {} \n}}".format( + "\n ".join( + "{!r}: {!r},".format(key, value) for key, value in + sorted(prototype.items()) if key not in _PROTOTYPE_META_NAMES)).rstrip(",")) + return header + proto + + +def check_permission(prototype_key, action, default=True): + """ + Helper function to check access to actions on given prototype. + + Args: + prototype_key (str): The prototype to affect. + action (str): One of "spawn" or "edit". + default (str): If action is unknown or prototype has no locks + + Returns: + passes (bool): If permission for action is granted or not. + + """ + if action == 'edit': + if prototype_key in _MODULE_PROTOTYPES: + mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A") + logger.log_err("{} is a read-only prototype " + "(defined as code in {}).".format(prototype_key, mod)) + return False + + prototype = search_prototype(key=prototype_key) + if not prototype: + logger.log_err("Prototype {} not found.".format(prototype_key)) + return False + + lockstring = prototype.get("prototype_locks") + + if lockstring: + return check_lockstring(None, lockstring, default=default, access_type=action) + return default + + +def init_spawn_value(value, validator=None): + """ + Analyze the prototype value and produce a value useful at the point of spawning. + + Args: + value (any): This can be: + callable - will be called as callable() + (callable, (args,)) - will be called as callable(*args) + other - will be assigned depending on the variable type + validator (callable, optional): If given, this will be called with the value to + check and guarantee the outcome is of a given type. + + Returns: + any (any): The (potentially pre-processed value to use for this prototype key) + + """ + value = protfunc_parser(value) + validator = validator if validator else lambda o: o + if callable(value): + return validator(value()) + elif value and is_iter(value) and callable(value[0]): + # a structure (callable, (args, )) + args = value[1:] + return validator(value[0](*make_iter(args))) + else: + return validator(value) + + # module-based prototypes for mod in settings.PROTOTYPE_MODULES: @@ -59,39 +162,7 @@ class DbPrototype(DefaultScript): self.db.prototype = {} # actual prototype -# General prototype functions - - -def check_permission(prototype_key, action, default=True): - """ - Helper function to check access to actions on given prototype. - - Args: - prototype_key (str): The prototype to affect. - action (str): One of "spawn" or "edit". - default (str): If action is unknown or prototype has no locks - - Returns: - passes (bool): If permission for action is granted or not. - - """ - if action == 'edit': - if prototype_key in _MODULE_PROTOTYPES: - mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A") - logger.log_err("{} is a read-only prototype " - "(defined as code in {}).".format(prototype_key, mod)) - return False - - prototype = search_prototype(key=prototype_key) - if not prototype: - logger.log_err("Prototype {} not found.".format(prototype_key)) - return False - - lockstring = prototype.get("prototype_locks") - - if lockstring: - return check_lockstring(None, lockstring, default=default, access_type=action) - return default +# Prototype manager functions def create_prototype(**kwargs): @@ -281,45 +352,6 @@ def search_objects_with_prototype(prototype_key): return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY) -def prototype_from_object(obj): - """ - Guess a minimal prototype from an existing object. - - Args: - obj (Object): An object to analyze. - - Returns: - prototype (dict): A prototype estimating the current state of the object. - - """ - # first, check if this object already has a prototype - - prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) - prot = search_prototype(prot) - if not prot or len(prot) > 1: - # no unambiguous prototype found - build new prototype - prot = {} - prot['prototype_key'] = "From-Object-{}-{}".format( - obj.key, hashlib.md5(str(time.time())).hexdigest()[:6]) - prot['prototype_desc'] = "Built from {}".format(str(obj)) - prot['prototype_locks'] = "spawn:all();edit:all()" - - prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] - prot['location'] = obj.db_location - prot['home'] = obj.db_home - prot['destination'] = obj.db_destination - prot['typeclass'] = obj.db_typeclass_path - prot['locks'] = obj.locks.all() - prot['permissions'] = obj.permissions.get() - prot['aliases'] = obj.aliases.get() - prot['tags'] = [(tag.key, tag.category, tag.data) - for tag in obj.tags.get(return_tagobj=True, return_list=True)] - prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks) - for attr in obj.attributes.get(return_obj=True, return_list=True)] - - return prot - - def list_prototypes(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. @@ -384,171 +416,3 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed table.reformat_column(2, width=11, align='c') table.reformat_column(3, width=16) return table - - - -def batch_update_objects_with_prototype(prototype, diff=None, objects=None): - """ - Update existing objects with the latest version of the prototype. - - Args: - prototype (str or dict): Either the `prototype_key` to use or the - prototype dict itself. - diff (dict, optional): This a diff structure that describes how to update the protototype. - 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`. - Returns: - changed (int): The number of objects that had changes applied to them. - - """ - prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] - prototype_obj = search_db_prototype(prototype_key, return_queryset=True) - prototype_obj = prototype_obj[0] if prototype_obj else None - new_prototype = prototype_obj.db.prototype - objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) - - if not objs: - return 0 - - if not diff: - diff = prototype_diff_from_object(new_prototype, objs[0]) - - changed = 0 - for obj in objs: - do_save = False - for key, directive in diff.items(): - val = new_prototype[key] - if directive in ('UPDATE', 'REPLACE'): - do_save = True - if key == 'key': - obj.db_key = validate_spawn_value(val, str) - elif key == 'typeclass': - obj.db_typeclass_path = validate_spawn_value(val, str) - elif key == 'location': - obj.db_location = validate_spawn_value(val, _to_obj) - elif key == 'home': - obj.db_home = validate_spawn_value(val, _to_obj) - elif key == 'destination': - obj.db_destination = validate_spawn_value(val, _to_obj) - elif key == 'locks': - if directive == 'REPLACE': - obj.locks.clear() - obj.locks.add(validate_spawn_value(val, str)) - elif key == 'permissions': - if directive == 'REPLACE': - obj.permissions.clear() - obj.permissions.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'aliases': - if directive == 'REPLACE': - obj.aliases.clear() - obj.aliases.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'tags': - if directive == 'REPLACE': - obj.tags.clear() - obj.tags.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'attrs': - if directive == 'REPLACE': - obj.attributes.clear() - obj.attributes.batch_add(validate_spawn_value(val, make_iter)) - elif key == 'exec': - # we don't auto-rerun exec statements, it would be huge security risk! - pass - else: - obj.attributes.add(key, validate_spawn_value(val, _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) - if do_save: - changed += 1 - obj.save() - - return changed - -def batch_create_object(*objparams): - """ - This is a cut-down version of the create_object() function, - optimized for speed. It does NOT check and convert various input - so make sure the spawned Typeclass works before using this! - - Args: - objsparams (tuple): Each paremter tuple will create one object instance using the parameters within. - The parameters should be given in the following order: - - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`. - - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`. - - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`. - - `aliases` (list): A list of alias strings for - adding with `new_object.aliases.batch_add(*aliases)`. - - `nattributes` (list): list of tuples `(key, value)` to be loop-added to - add with `new_obj.nattributes.add(*tuple)`. - - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for - adding with `new_obj.attributes.batch_add(*attributes)`. - - `tags` (list): list of tuples `(key, category)` for adding - with `new_obj.tags.batch_add(*tags)`. - - `execs` (list): Code strings to execute together with the creation - of each object. They will be executed with `evennia` and `obj` - (the newly created object) available in the namespace. Execution - will happend after all other properties have been assigned and - is intended for calling custom handlers etc. - - Returns: - objects (list): A list of created objects - - Notes: - The `exec` list will execute arbitrary python code so don't allow this to be available to - unprivileged users! - - """ - - # bulk create all objects in one go - - # unfortunately this doesn't work since bulk_create doesn't creates pks; - # the result would be duplicate objects at the next stage, so we comment - # it out for now: - # dbobjs = _ObjectDB.objects.bulk_create(dbobjs) - - dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] - objs = [] - for iobj, obj in enumerate(dbobjs): - # call all setup hooks on each object - objparam = objparams[iobj] - # setup - obj._createdict = {"permissions": make_iter(objparam[1]), - "locks": objparam[2], - "aliases": make_iter(objparam[3]), - "nattributes": objparam[4], - "attributes": objparam[5], - "tags": make_iter(objparam[6])} - # this triggers all hooks - obj.save() - # run eventual extra code - for code in objparam[7]: - if code: - exec(code, {}, {"evennia": evennia, "obj": obj}) - objs.append(obj) - return objs diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 15ef8afb4d..995cea6e52 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -126,70 +126,25 @@ from __future__ import print_function import copy import hashlib import time -from ast import literal_eval + from django.conf import settings -from random import randint import evennia +from random import randint from evennia.objects.models import ObjectDB from evennia.utils.utils import ( make_iter, dbid_to_obj, - is_iter, crop, get_all_typeclasses) - -from evennia.utils.evtable import EvTable + is_iter, get_all_typeclasses) +from evennia.prototypes import prototypes as protlib +from evennia.prototypes.prototypes import value_to_obj, value_to_obj_or_any, init_spawn_value _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") _PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") _NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES -_MENU_CROP_WIDTH = 15 _PROTOTYPE_TAG_CATEGORY = "spawned_by_prototype" -_MENU_ATTR_LITERAL_EVAL_ERROR = ( - "|rCritical Python syntax error in your value. Only primitive Python structures are allowed.\n" - "You also need to use correct Python syntax. Remember especially to put quotes around all " - "strings inside lists and dicts.|n") - - -# Helper functions - -def _to_obj(value, force=True): - return dbid_to_obj(value, ObjectDB) - - -def _to_obj_or_any(value): - obj = dbid_to_obj(value, ObjectDB) - return obj if obj is not None else value - - -def validate_spawn_value(value, validator=None): - """ - Analyze the value and produce a value for use at the point of spawning. - - Args: - value (any): This can be: - callable - will be called as callable() - (callable, (args,)) - will be called as callable(*args) - other - will be assigned depending on the variable type - validator (callable, optional): If given, this will be called with the value to - check and guarantee the outcome is of a given type. - - Returns: - any (any): The (potentially pre-processed value to use for this prototype key) - - """ - value = protfunc_parser(value) - validator = validator if validator else lambda o: o - if callable(value): - return validator(value()) - elif value and is_iter(value) and callable(value[0]): - # a structure (callable, (args, )) - args = value[1:] - return validator(value[0](*make_iter(args))) - else: - return validator(value) - -# Spawner mechanism +# Helper def _get_prototype(dic, prot, protparents): """ @@ -209,6 +164,246 @@ def _get_prototype(dic, prot, protparents): return prot +# obj-related prototype functions + +def prototype_from_object(obj): + """ + Guess a minimal prototype from an existing object. + + Args: + obj (Object): An object to analyze. + + Returns: + prototype (dict): A prototype estimating the current state of the object. + + """ + # first, check if this object already has a prototype + + prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) + prot = protlib.search_prototype(prot) + if not prot or len(prot) > 1: + # no unambiguous prototype found - build new prototype + prot = {} + prot['prototype_key'] = "From-Object-{}-{}".format( + obj.key, hashlib.md5(str(time.time())).hexdigest()[:6]) + prot['prototype_desc'] = "Built from {}".format(str(obj)) + prot['prototype_locks'] = "spawn:all();edit:all()" + + prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] + prot['location'] = obj.db_location + prot['home'] = obj.db_home + prot['destination'] = obj.db_destination + prot['typeclass'] = obj.db_typeclass_path + prot['locks'] = obj.locks.all() + prot['permissions'] = obj.permissions.get() + prot['aliases'] = obj.aliases.get() + prot['tags'] = [(tag.key, tag.category, tag.data) + for tag in obj.tags.get(return_tagobj=True, return_list=True)] + prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks) + for attr in obj.attributes.get(return_obj=True, return_list=True)] + + return prot + + +def prototype_diff_from_object(prototype, obj): + """ + 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 + needed. + + Args: + prototype (dict): Prototype. + obj (Object): Object to + + Returns: + diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} + + """ + prot1 = prototype + prot2 = prototype_from_object(obj) + + diff = {} + for key, value in prot1.items(): + diff[key] = "KEEP" + if key in prot2: + if callable(prot2[key]) or value != prot2[key]: + diff[key] = "UPDATE" + elif key not in prot2: + diff[key] = "REMOVE" + + return diff + + +def batch_update_objects_with_prototype(prototype, diff=None, objects=None): + """ + Update existing objects with the latest version of the prototype. + + Args: + prototype (str or dict): Either the `prototype_key` to use or the + prototype dict itself. + diff (dict, optional): This a diff structure that describes how to update the protototype. + 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`. + Returns: + changed (int): The number of objects that had changes applied to them. + + """ + prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] + prototype_obj = protlib.DbPrototype.objects.filter(db_key=prototype_key) + prototype_obj = prototype_obj[0] if prototype_obj else None + new_prototype = prototype_obj.db.prototype + objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + + if not objs: + return 0 + + if not diff: + diff = prototype_diff_from_object(new_prototype, objs[0]) + + changed = 0 + for obj in objs: + do_save = False + for key, directive in diff.items(): + val = new_prototype[key] + if directive in ('UPDATE', 'REPLACE'): + 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(val, make_iter)) + elif key == 'aliases': + if directive == 'REPLACE': + obj.aliases.clear() + obj.aliases.batch_add(init_spawn_value(val, make_iter)) + elif key == 'tags': + if directive == 'REPLACE': + obj.tags.clear() + obj.tags.batch_add(init_spawn_value(val, make_iter)) + elif key == 'attrs': + if directive == 'REPLACE': + obj.attributes.clear() + obj.attributes.batch_add(init_spawn_value(val, make_iter)) + 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) + if do_save: + changed += 1 + obj.save() + + return changed + + +def batch_create_object(*objparams): + """ + This is a cut-down version of the create_object() function, + optimized for speed. It does NOT check and convert various input + so make sure the spawned Typeclass works before using this! + + Args: + objsparams (tuple): Each paremter tuple will create one object instance using the parameters + within. + The parameters should be given in the following order: + - `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`. + - `permissions` (str): Permission string used with `new_obj.batch_add(permission)`. + - `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`. + - `aliases` (list): A list of alias strings for + adding with `new_object.aliases.batch_add(*aliases)`. + - `nattributes` (list): list of tuples `(key, value)` to be loop-added to + add with `new_obj.nattributes.add(*tuple)`. + - `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for + adding with `new_obj.attributes.batch_add(*attributes)`. + - `tags` (list): list of tuples `(key, category)` for adding + with `new_obj.tags.batch_add(*tags)`. + - `execs` (list): Code strings to execute together with the creation + of each object. They will be executed with `evennia` and `obj` + (the newly created object) available in the namespace. Execution + will happend after all other properties have been assigned and + is intended for calling custom handlers etc. + + Returns: + objects (list): A list of created objects + + Notes: + The `exec` list will execute arbitrary python code so don't allow this to be available to + unprivileged users! + + """ + + # bulk create all objects in one go + + # unfortunately this doesn't work since bulk_create doesn't creates pks; + # the result would be duplicate objects at the next stage, so we comment + # it out for now: + # dbobjs = _ObjectDB.objects.bulk_create(dbobjs) + + dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] + objs = [] + for iobj, obj in enumerate(dbobjs): + # call all setup hooks on each object + objparam = objparams[iobj] + # setup + obj._createdict = {"permissions": make_iter(objparam[1]), + "locks": objparam[2], + "aliases": make_iter(objparam[3]), + "nattributes": objparam[4], + "attributes": objparam[5], + "tags": make_iter(objparam[6])} + # this triggers all hooks + obj.save() + # run eventual extra code + for code in objparam[7]: + if code: + exec(code, {}, {"evennia": evennia, "obj": obj}) + objs.append(obj) + return objs + + +# Spawner mechanism def spawn(*prototypes, **kwargs): """ @@ -234,12 +429,12 @@ def spawn(*prototypes, **kwargs): """ # get available protparents - protparents = {prot['prototype_key']: prot for prot in search_prototype()} + protparents = {prot['prototype_key']: prot for prot in protlib.search_prototype()} # overload module's protparents with specifically given protparents protparents.update(kwargs.get("prototype_parents", {})) for key, prototype in protparents.items(): - validate_prototype(prototype, key.lower(), protparents) + protlib.validate_prototype(prototype, key.lower(), protparents) if "return_prototypes" in kwargs: # only return the parents @@ -248,7 +443,7 @@ def spawn(*prototypes, **kwargs): objsparams = [] for prototype in prototypes: - validate_prototype(prototype, None, protparents) + protlib.validate_prototype(prototype, None, protparents) prot = _get_prototype(prototype, {}, protparents) if not prot: continue @@ -260,30 +455,30 @@ def spawn(*prototypes, **kwargs): # chance this is not unique but it should usually not be a problem. val = prot.pop("key", "Spawned-{}".format( hashlib.md5(str(time.time())).hexdigest()[:6])) - create_kwargs["db_key"] = validate_spawn_value(val, str) + create_kwargs["db_key"] = init_spawn_value(val, str) val = prot.pop("location", None) - create_kwargs["db_location"] = validate_spawn_value(val, _to_obj) + create_kwargs["db_location"] = init_spawn_value(val, value_to_obj) val = prot.pop("home", settings.DEFAULT_HOME) - create_kwargs["db_home"] = validate_spawn_value(val, _to_obj) + create_kwargs["db_home"] = init_spawn_value(val, value_to_obj) val = prot.pop("destination", None) - create_kwargs["db_destination"] = validate_spawn_value(val, _to_obj) + create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj) val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) - create_kwargs["db_typeclass_path"] = validate_spawn_value(val, str) + create_kwargs["db_typeclass_path"] = init_spawn_value(val, str) # extract calls to handlers val = prot.pop("permissions", []) - permission_string = validate_spawn_value(val, make_iter) + permission_string = init_spawn_value(val, make_iter) val = prot.pop("locks", "") - lock_string = validate_spawn_value(val, str) + lock_string = init_spawn_value(val, str) val = prot.pop("aliases", []) - alias_string = validate_spawn_value(val, make_iter) + alias_string = init_spawn_value(val, make_iter) val = prot.pop("tags", []) - tags = validate_spawn_value(val, make_iter) + tags = init_spawn_value(val, make_iter) prototype_key = prototype.get('prototype_key', None) if prototype_key: @@ -291,15 +486,15 @@ def spawn(*prototypes, **kwargs): tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY)) val = prot.pop("exec", "") - execs = validate_spawn_value(val, make_iter) + execs = init_spawn_value(val, make_iter) # extract ndb assignments - nattribute = dict((key.split("_", 1)[1], validate_spawn_value(val, _to_obj)) + nattributes = dict((key.split("_", 1)[1], init_spawn_value(val, value_to_obj)) for key, val in prot.items() if key.startswith("ndb_")) # the rest are attributes val = prot.pop("attrs", []) - attributes = validate_spawn_value(val, list) + attributes = init_spawn_value(val, list) simple_attributes = [] for key, value in ((key, value) for key, value in prot.items() @@ -307,11 +502,11 @@ def spawn(*prototypes, **kwargs): if is_iter(value) and len(value) > 1: # (value, category) simple_attributes.append((key, - validate_spawn_value(value[0], _to_obj_or_any), - validate_spawn_value(value[1], str))) + init_spawn_value(value[0], value_to_obj_or_any), + init_spawn_value(value[1], str))) else: simple_attributes.append((key, - validate_spawn_value(value, _to_obj_or_any))) + init_spawn_value(value, value_to_obj_or_any))) attributes = attributes + simple_attributes attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS] @@ -320,7 +515,7 @@ def spawn(*prototypes, **kwargs): objsparams.append((create_kwargs, permission_string, lock_string, alias_string, nattributes, attributes, tags, execs)) - return _batch_create_object(*objsparams) + return batch_create_object(*objsparams) # Testing diff --git a/evennia/prototypes/utils.py b/evennia/prototypes/utils.py deleted file mode 100644 index 6fe87d172c..0000000000 --- a/evennia/prototypes/utils.py +++ /dev/null @@ -1,62 +0,0 @@ -""" - -Prototype utilities - -""" - -_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") - - -class PermissionError(RuntimeError): - pass - - -def prototype_to_str(prototype): - """ - Format a prototype to a nice string representation. - - Args: - prototype (dict): The prototype. - """ - - header = ( - "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" - "|cdesc:|n {} \n|cprototype:|n ".format( - prototype['prototype_key'], - ", ".join(prototype['prototype_tags']), - prototype['prototype_locks'], - prototype['prototype_desc'])) - proto = ("{{\n {} \n}}".format( - "\n ".join( - "{!r}: {!r},".format(key, value) for key, value in - sorted(prototype.items()) if key not in _PROTOTYPE_META_NAMES)).rstrip(",")) - return header + proto - - -def prototype_diff_from_object(prototype, obj): - """ - 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 - needed. - - Args: - prototype (dict): Prototype. - obj (Object): Object to - - Returns: - diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} - - """ - prot1 = prototype - prot2 = prototype_from_object(obj) - - diff = {} - for key, value in prot1.items(): - diff[key] = "KEEP" - if key in prot2: - if callable(prot2[key]) or value != prot2[key]: - diff[key] = "UPDATE" - elif key not in prot2: - diff[key] = "REMOVE" - - return diff From 7bf2cd4c0eef91137fcb3ddf1dc7b56de9f4d5c2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 9 Jun 2018 12:10:32 +0200 Subject: [PATCH 056/103] Move spawner tests into prototypes folder --- evennia/__init__.py | 2 +- evennia/commands/default/building.py | 2 +- evennia/prototypes/prototypes.py | 2 +- evennia/prototypes/spawner.py | 44 +------------------ .../test_spawner.py => prototypes/tests.py} | 41 ++++++++++++++++- 5 files changed, 43 insertions(+), 48 deletions(-) rename evennia/{utils/tests/test_spawner.py => prototypes/tests.py} (74%) diff --git a/evennia/__init__.py b/evennia/__init__.py index 6fdc4aaece..fc916351ad 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -174,7 +174,7 @@ def _init(): from .utils import logger from .utils import gametime from .utils import ansi - from .utils.spawner import spawn + from .prototypes.spawner import spawn from . import contrib from .utils.evmenu import EvMenu from .utils.evtable import EvTable diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 4aabc861b1..692dd2aac6 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, get_all_typeclasses from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore -from evennia.utils import spawner +from evennia.prototypes import spawner from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 37fd83f846..0020f807c1 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -15,7 +15,7 @@ from evennia.utils.utils import ( from evennia.locks.lockhandler import validate_lockstring, check_lockstring from evennia.utils import logger from evennia.utils.evtable import EvTable -from evennia.utils.prototypes.protfuncs import protfunc_parser +from evennia.prototypes.protfuncs import protfunc_parser _MODULE_PROTOTYPE_MODULES = {} diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 995cea6e52..8cadd43656 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -129,11 +129,8 @@ import time from django.conf import settings import evennia -from random import randint from evennia.objects.models import ObjectDB -from evennia.utils.utils import ( - make_iter, dbid_to_obj, - is_iter, get_all_typeclasses) +from evennia.utils.utils import make_iter, is_iter from evennia.prototypes import prototypes as protlib from evennia.prototypes.prototypes import value_to_obj, value_to_obj_or_any, init_spawn_value @@ -516,42 +513,3 @@ def spawn(*prototypes, **kwargs): alias_string, nattributes, attributes, tags, execs)) return batch_create_object(*objsparams) - - -# Testing - -if __name__ == "__main__": - protparents = { - "NOBODY": {}, - # "INFINITE" : { - # "prototype":"INFINITE" - # }, - "GOBLIN": { - "key": "goblin grunt", - "health": lambda: randint(20, 30), - "resists": ["cold", "poison"], - "attacks": ["fists"], - "weaknesses": ["fire", "light"] - }, - "GOBLIN_WIZARD": { - "prototype": "GOBLIN", - "key": "goblin wizard", - "spells": ["fire ball", "lighting bolt"] - }, - "GOBLIN_ARCHER": { - "prototype": "GOBLIN", - "key": "goblin archer", - "attacks": ["short bow"] - }, - "ARCHWIZARD": { - "attacks": ["archwizard staff"], - }, - "GOBLIN_ARCHWIZARD": { - "key": "goblin archwizard", - "prototype": ("GOBLIN_WIZARD", "ARCHWIZARD") - } - } - # test - print([o.key for o in spawn(protparents["GOBLIN"], - protparents["GOBLIN_ARCHWIZARD"], - prototype_parents=protparents)]) diff --git a/evennia/utils/tests/test_spawner.py b/evennia/prototypes/tests.py similarity index 74% rename from evennia/utils/tests/test_spawner.py rename to evennia/prototypes/tests.py index 4d680a9e8a..1b8e340377 100644 --- a/evennia/utils/tests/test_spawner.py +++ b/evennia/prototypes/tests.py @@ -1,10 +1,44 @@ """ -Unit test for the spawner +Unit tests for the prototypes and spawner """ +from random import randint from evennia.utils.test_resources import EvenniaTest -from evennia.utils import spawner +from evennia.prototypes import spawner, prototypes as protlib + + +_PROTPARENTS = { + "NOBODY": {}, + "GOBLIN": { + "key": "goblin grunt", + "health": lambda: randint(1, 1), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + }, + "GOBLIN_WIZARD": { + "prototype": "GOBLIN", + "key": "goblin wizard", + "spells": ["fire ball", "lighting bolt"] + }, + "GOBLIN_ARCHER": { + "prototype": "GOBLIN", + "key": "goblin archer", + "attacks": ["short bow"] + }, + "ARCHWIZARD": { + "attacks": ["archwizard staff"], + }, + "GOBLIN_ARCHWIZARD": { + "key": "goblin archwizard", + "prototype": ("GOBLIN_WIZARD", "ARCHWIZARD") + } +} + + +class TestPrototypes(EvenniaTest): + pass class TestSpawner(EvenniaTest): @@ -17,6 +51,9 @@ class TestSpawner(EvenniaTest): obj1 = spawner.spawn(self.prot1) # check spawned objects have the right tag self.assertEqual(list(spawner.search_objects_with_prototype("testprototype")), obj1) + self.assertEqual([o.key for o in spawner.spawn( + _PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"], + prototype_parents=_PROTPARENTS)], []) class TestPrototypeStorage(EvenniaTest): From d4963ab36b1d4528787f5aaa999b73cd225f320b Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 9 Jun 2018 23:57:46 +0200 Subject: [PATCH 057/103] Start adding unittests for prototypes --- evennia/prototypes/prototypes.py | 42 ++++++++++++++++++++++++++++++++ evennia/prototypes/spawner.py | 6 +++-- evennia/prototypes/tests.py | 4 +-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 0020f807c1..bb917a8dc4 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -416,3 +416,45 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed table.reformat_column(2, width=11, align='c') table.reformat_column(3, width=16) return table + + +def validate_prototype(prototype, protkey=None, protparents=None, _visited=None): + """ + Run validation on a prototype, checking for inifinite regress. + + Args: + prototype (dict): Prototype to validate. + protkey (str, optional): The name of the prototype definition. If not given, the prototype + dict needs to have the `prototype_key` field set. + protpartents (dict, optional): The available prototype parent library. If + note given this will be determined from settings/database. + _visited (list, optional): This is an internal work array and should not be set manually. + Raises: + RuntimeError: If prototype has invalid structure. + + """ + if not protparents: + protparents = {prototype['prototype_key']: prototype for prototype in search_prototype()} + if _visited is None: + _visited = [] + + protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) + + assert isinstance(prototype, dict) + + if id(prototype) in _visited: + raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype) + + _visited.append(id(prototype)) + protstrings = prototype.get("prototype") + + if protstrings: + for protstring in make_iter(protstrings): + protstring = protstring.lower() + if protkey is not None and protstring == protkey: + raise RuntimeError("%s tries to prototype itself." % protkey or prototype) + protparent = protparents.get(protstring) + if not protparent: + raise RuntimeError( + "%s's prototype '%s' was not found." % (protkey or prototype, protstring)) + validate_prototype(protparent, protstring, protparents, _visited) diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 8cadd43656..5a1196513a 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -426,10 +426,12 @@ def spawn(*prototypes, **kwargs): """ # get available protparents - protparents = {prot['prototype_key']: prot for prot in protlib.search_prototype()} + protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} # overload module's protparents with specifically given protparents - protparents.update(kwargs.get("prototype_parents", {})) + protparents.update( + {key.lower(): value for key, value in kwargs.get("prototype_parents", {}).items()}) + for key, prototype in protparents.items(): protlib.validate_prototype(prototype, key.lower(), protparents) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 1b8e340377..e9ef4bce9f 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -50,10 +50,10 @@ class TestSpawner(EvenniaTest): def test_spawn(self): obj1 = spawner.spawn(self.prot1) # check spawned objects have the right tag - self.assertEqual(list(spawner.search_objects_with_prototype("testprototype")), obj1) + self.assertEqual(list(protlib.search_objects_with_prototype("testprototype")), obj1) self.assertEqual([o.key for o in spawner.spawn( _PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"], - prototype_parents=_PROTPARENTS)], []) + prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard']) class TestPrototypeStorage(EvenniaTest): From fe26ffac5f0b967f3be3f205329801b8d6138646 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 10 Jun 2018 14:27:34 +0200 Subject: [PATCH 058/103] Add unittests, fix bugs --- evennia/prototypes/spawner.py | 99 +++++++++++++++++++++++++---------- evennia/prototypes/tests.py | 95 +++++++++++++++++++++++++++++++++ requirements.txt | 4 +- 3 files changed, 168 insertions(+), 30 deletions(-) diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 5a1196513a..f34fe8c854 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -177,27 +177,45 @@ 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 = protlib.search_prototype(prot) + if prot: + prot = protlib.search_prototype(prot[0]) if not prot or len(prot) > 1: # no unambiguous prototype found - build new prototype prot = {} prot['prototype_key'] = "From-Object-{}-{}".format( - obj.key, hashlib.md5(str(time.time())).hexdigest()[:6]) + obj.key, hashlib.md5(str(time.time())).hexdigest()[:7]) prot['prototype_desc'] = "Built from {}".format(str(obj)) prot['prototype_locks'] = "spawn:all();edit:all()" prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] - prot['location'] = obj.db_location - prot['home'] = obj.db_home - prot['destination'] = obj.db_destination prot['typeclass'] = obj.db_typeclass_path - prot['locks'] = obj.locks.all() - prot['permissions'] = obj.permissions.get() - prot['aliases'] = obj.aliases.get() - prot['tags'] = [(tag.key, tag.category, tag.data) - for tag in obj.tags.get(return_tagobj=True, return_list=True)] - prot['attrs'] = [(attr.key, attr.value, attr.category, attr.locks) - for attr in obj.attributes.get(return_obj=True, return_list=True)] + + location = obj.db_location + if location: + prot['location'] = location + home = obj.db_home + if home: + prot['home'] = home + destination = obj.db_destination + if destination: + prot['destination'] = destination + locks = obj.locks.all() + if locks: + prot['locks'] = locks + perms = obj.permissions.get() + if perms: + prot['permissions'] = perms + aliases = obj.aliases.get() + if aliases: + prot['aliases'] = aliases + tags = [(tag.db_key, tag.db_category, tag.db_data) + for tag in obj.tags.get(return_tagobj=True, return_list=True) if tag] + if tags: + prot['tags'] = tags + attrs = [(attr.key, attr.value, attr.category, attr.locks.all()) + for attr in obj.attributes.get(return_obj=True, return_list=True) if attr] + if attrs: + prot['attrs'] = attrs return prot @@ -224,8 +242,14 @@ def prototype_diff_from_object(prototype, obj): diff[key] = "KEEP" if key in prot2: if callable(prot2[key]) or value != prot2[key]: - diff[key] = "UPDATE" + if key in ('attrs', 'tags', 'permissions', 'locks', 'aliases'): + diff[key] = 'REPLACE' + else: + diff[key] = "UPDATE" elif key not in prot2: + diff[key] = "UPDATE" + for key in prot2: + if key not in diff and key not in prot1: diff[key] = "REMOVE" return diff @@ -246,25 +270,42 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): changed (int): The number of objects that had changes applied to them. """ - prototype_key = prototype if isinstance(prototype, basestring) else prototype['prototype_key'] - prototype_obj = protlib.DbPrototype.objects.filter(db_key=prototype_key) - prototype_obj = prototype_obj[0] if prototype_obj else None - new_prototype = prototype_obj.db.prototype - objs = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + if isinstance(prototype, basestring): + new_prototype = protlib.search_prototype(prototype) + else: + new_prototype = prototype - if not objs: + prototype_key = new_prototype['prototype_key'] + + if not objects: + objects = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY) + + if not objects: return 0 if not diff: - diff = prototype_diff_from_object(new_prototype, objs[0]) + diff = prototype_diff_from_object(new_prototype, objects[0]) changed = 0 - for obj in objs: + 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(): - val = new_prototype[key] if directive in ('UPDATE', 'REPLACE'): + + 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': @@ -282,19 +323,19 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): elif key == 'permissions': if directive == 'REPLACE': obj.permissions.clear() - obj.permissions.batch_add(init_spawn_value(val, make_iter)) + obj.permissions.batch_add(*init_spawn_value(val, make_iter)) elif key == 'aliases': if directive == 'REPLACE': obj.aliases.clear() - obj.aliases.batch_add(init_spawn_value(val, make_iter)) + obj.aliases.batch_add(*init_spawn_value(val, make_iter)) elif key == 'tags': if directive == 'REPLACE': obj.tags.clear() - obj.tags.batch_add(init_spawn_value(val, make_iter)) + obj.tags.batch_add(*init_spawn_value(val, make_iter)) elif key == 'attrs': if directive == 'REPLACE': obj.attributes.clear() - obj.attributes.batch_add(init_spawn_value(val, make_iter)) + obj.attributes.batch_add(*init_spawn_value(val, make_iter)) elif key == 'exec': # we don't auto-rerun exec statements, it would be huge security risk! pass @@ -328,9 +369,9 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): pass else: obj.attributes.remove(key) - if do_save: - changed += 1 - obj.save() + if do_save: + changed += 1 + obj.save() return changed diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index e9ef4bce9f..b358043e9d 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -4,6 +4,8 @@ Unit tests for the prototypes and spawner """ from random import randint +import mock +from anything import Anything, Something from evennia.utils.test_resources import EvenniaTest from evennia.prototypes import spawner, prototypes as protlib @@ -56,6 +58,99 @@ class TestSpawner(EvenniaTest): prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard']) +class TestUtils(EvenniaTest): + + def test_prototype_from_object(self): + self.maxDiff = None + self.obj1.attributes.add("test", "testval") + self.obj1.tags.add('foo') + new_prot = spawner.prototype_from_object(self.obj1) + self.assertEqual( + {'attrs': [('test', 'testval', None, [''])], + 'home': Something, + 'key': 'Obj', + 'location': Something, + 'locks': ['call:true()', + 'control:perm(Developer)', + 'delete:perm(Admin)', + 'edit:perm(Admin)', + 'examine:perm(Builder)', + 'get:all()', + 'puppet:pperm(Developer)', + 'tell:perm(Admin)', + 'view:all()'], + 'prototype_desc': 'Built from Obj', + 'prototype_key': Something, + 'prototype_locks': 'spawn:all();edit:all()', + 'tags': [(u'foo', None, None)], + 'typeclass': 'evennia.objects.objects.DefaultObject'}, new_prot) + + def test_update_objects_from_prototypes(self): + + self.maxDiff = None + self.obj1.attributes.add('oldtest', 'to_remove') + + old_prot = spawner.prototype_from_object(self.obj1) + + # modify object away from prototype + self.obj1.attributes.add('test', 'testval') + self.obj1.aliases.add('foo') + self.obj1.key = 'NewObj' + + # modify prototype + old_prot['new'] = 'new_val' + old_prot['test'] = 'testval_changed' + old_prot['permissions'] = 'Builder' + # this will not update, since we don't update the prototype on-disk + old_prot['prototype_desc'] = 'New version of prototype' + + # diff obj/prototype + pdiff = spawner.prototype_diff_from_object(old_prot, self.obj1) + + self.assertEqual( + pdiff, + {'aliases': 'REMOVE', + 'attrs': 'REPLACE', + 'home': 'KEEP', + 'key': 'UPDATE', + 'location': 'KEEP', + 'locks': 'KEEP', + 'new': 'UPDATE', + 'permissions': 'UPDATE', + 'prototype_desc': 'UPDATE', + 'prototype_key': 'UPDATE', + 'prototype_locks': 'KEEP', + 'test': 'UPDATE', + 'typeclass': 'KEEP'}) + + # apply diff + count = spawner.batch_update_objects_with_prototype( + old_prot, diff=pdiff, objects=[self.obj1]) + self.assertEqual(count, 1) + + new_prot = spawner.prototype_from_object(self.obj1) + self.assertEqual({'attrs': [('test', 'testval_changed', None, ['']), + ('new', 'new_val', None, [''])], + 'home': Something, + 'key': 'Obj', + 'location': Something, + 'locks': ['call:true()', + 'control:perm(Developer)', + 'delete:perm(Admin)', + 'edit:perm(Admin)', + 'examine:perm(Builder)', + 'get:all()', + 'puppet:pperm(Developer)', + 'tell:perm(Admin)', + 'view:all()'], + 'permissions': 'builder', + 'prototype_desc': 'Built from Obj', + 'prototype_key': Something, + 'prototype_locks': 'spawn:all();edit:all()', + 'typeclass': 'evennia.objects.objects.DefaultObject'}, + new_prot) + + class TestPrototypeStorage(EvenniaTest): def setUp(self): diff --git a/requirements.txt b/requirements.txt index 7f4b94726f..72df29b9d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,11 @@ django > 1.10, < 2.0 twisted == 16.0.0 -mock >= 1.0.1 pillow == 2.9.0 pytz future >= 0.15.2 django-sekizai inflect + +mock >= 1.0.1 +anything From 2474ab65801fb918d935bfba9434b239973e0bf2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 10 Jun 2018 20:00:35 +0200 Subject: [PATCH 059/103] Fix unittests, resolve bugs --- evennia/locks/lockhandler.py | 2 +- evennia/prototypes/prototypes.py | 44 ++++++++------ evennia/prototypes/spawner.py | 1 + evennia/prototypes/tests.py | 98 +++++++++++++++++++------------- 4 files changed, 87 insertions(+), 58 deletions(-) diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index 4822dde1b6..9e27ca2fad 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -660,7 +660,7 @@ def validate_lockstring(lockstring): if no error was found. """ - return _LOCK_HANDLER.valdate(lockstring) + return _LOCK_HANDLER.validate(lockstring) def _test(): diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index bb917a8dc4..2e96af99c9 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -21,7 +21,8 @@ from evennia.prototypes.protfuncs import protfunc_parser _MODULE_PROTOTYPE_MODULES = {} _MODULE_PROTOTYPES = {} _PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") -_PROTOTYPE_TAG_CATEGORY = "spawned_by_prototype" +_PROTOTYPE_TAG_CATEGORY = "from_prototype" +_PROTOTYPE_TAG_META_CATEGORY = "db_prototype" class PermissionError(RuntimeError): @@ -167,7 +168,7 @@ class DbPrototype(DefaultScript): def create_prototype(**kwargs): """ - Store a prototype persistently. + Create/Store a prototype persistently. Kwargs: prototype_key (str): This is required for any storage. @@ -204,36 +205,45 @@ def create_prototype(**kwargs): raise PermissionError("{} is a read-only prototype " "(defined as code in {}).".format(prototype_key, mod)) - # want to create- or edit - prototype = kwargs - # make sure meta properties are included with defaults - prototype['prototype_desc'] = prototype.get('prototype_desc', '') - locks = prototype.get('prototype_locks', "spawn:all();edit:perm(Admin)") - is_valid, err = validate_lockstring(locks) + stored_prototype = DbPrototype.objects.filter(db_key=prototype_key) + prototype = dict(stored_prototype[0].db.prototype) if stored_prototype else {} + + kwargs['prototype_desc'] = kwargs.get("prototype_desc", prototype.get("prototype_desc", "")) + prototype_locks = kwargs.get( + "prototype_locks", prototype.get('prototype_locks', "spawn:all();edit:perm(Admin)")) + is_valid, err = validate_lockstring(prototype_locks) if not is_valid: raise ValidationError("Lock error: {}".format(err)) - prototype["prototype_locks"] = locks - prototype["prototype_tags"] = [ - _to_batchtuple(tag, "db_prototype") - for tag in make_iter(prototype.get("prototype_tags", []))] + kwargs['prototype_locks'] = prototype_locks - stored_prototype = DbPrototype.objects.filter(db_key=prototype_key) + prototype_tags = [ + _to_batchtuple(tag, _PROTOTYPE_TAG_META_CATEGORY) + for tag in make_iter(kwargs.get("prototype_tags", + prototype.get('prototype_tags', [])))] + kwargs["prototype_tags"] = prototype_tags + + prototype.update(kwargs) if stored_prototype: # edit existing prototype stored_prototype = stored_prototype[0] - stored_prototype.desc = prototype['prototype_desc'] - stored_prototype.tags.batch_add(*prototype['prototype_tags']) + 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) else: # create a new prototype stored_prototype = create_script( DbPrototype, key=prototype_key, desc=prototype['prototype_desc'], persistent=True, - locks=locks, tags=prototype['prototype_tags'], attributes=[("prototype", prototype)]) - return stored_prototype + locks=prototype_locks, tags=prototype['prototype_tags'], + attributes=[("prototype", prototype)]) + return stored_prototype.db.prototype + +# alias +save_prototype = create_prototype def delete_prototype(key, caller=None): diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index f34fe8c854..22add7830a 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -186,6 +186,7 @@ def prototype_from_object(obj): obj.key, hashlib.md5(str(time.time())).hexdigest()[:7]) prot['prototype_desc'] = "Built from {}".format(str(obj)) prot['prototype_locks'] = "spawn:all();edit:all()" + prot['prototype_tags'] = [] prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] prot['typeclass'] = obj.db_typeclass_path diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index b358043e9d..88650caa7b 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -9,6 +9,7 @@ from anything import Anything, Something from evennia.utils.test_resources import EvenniaTest from evennia.prototypes import spawner, prototypes as protlib +from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY _PROTPARENTS = { "NOBODY": {}, @@ -151,63 +152,80 @@ class TestUtils(EvenniaTest): new_prot) +class TestProtLib(EvenniaTest): + + def setUp(self): + super(TestProtLib, self).setUp() + self.obj1.attributes.add("testattr", "testval") + self.prot = spawner.prototype_from_object(self.obj1) + + def test_prototype_to_str(self): + prstr = protlib.prototype_to_str(self.prot) + self.assertTrue(prstr.startswith("|cprototype key:|n")) + + def test_check_permission(self): + pass + class TestPrototypeStorage(EvenniaTest): def setUp(self): super(TestPrototypeStorage, self).setUp() - self.prot1 = {"prototype_key": "testprototype"} - self.prot2 = {"prototype_key": "testprototype2"} - self.prot3 = {"prototype_key": "testprototype3"} + self.maxDiff = None - def _get_metaproto( - self, key='testprototype', desc='testprototype', - locks=['edit:id(6) or perm(Admin)', 'use:all()'], - tags=[], prototype={"key": "testprototype"}): - return spawner.build_metaproto(key, desc, locks, tags, prototype) + self.prot1 = spawner.prototype_from_object(self.obj1) + self.prot1['prototype_key'] = 'testprototype1' + self.prot1['prototype_desc'] = 'testdesc1' + self.prot1['prototype_tags'] = [('foo1', _PROTOTYPE_TAG_META_CATEGORY)] - def _to_metaproto(self, db_prototype): - return spawner.build_metaproto( - db_prototype.key, db_prototype.desc, db_prototype.locks.all(), - db_prototype.tags.get(category="db_prototype", return_list=True), - db_prototype.attributes.get("prototype")) + self.prot2 = self.prot1.copy() + self.prot2['prototype_key'] = 'testprototype2' + self.prot2['prototype_desc'] = 'testdesc2' + self.prot2['prototype_tags'] = [('foo1', _PROTOTYPE_TAG_META_CATEGORY)] + + self.prot3 = self.prot2.copy() + self.prot3['prototype_key'] = 'testprototype3' + self.prot3['prototype_desc'] = 'testdesc3' + self.prot3['prototype_tags'] = [('foo1', _PROTOTYPE_TAG_META_CATEGORY)] def test_prototype_storage(self): - prot = spawner.save_db_prototype(self.char1, self.prot1, "testprot", - desc='testdesc0', tags=["foo"]) + prot1 = protlib.create_prototype(**self.prot1) - self.assertTrue(bool(prot)) - self.assertEqual(prot.db.prototype, self.prot1) - self.assertEqual(prot.desc, "testdesc0") + self.assertTrue(bool(prot1)) + self.assertEqual(prot1, self.prot1) - prot = spawner.save_db_prototype(self.char1, self.prot1, "testprot", - desc='testdesc', tags=["fooB"]) - self.assertEqual(prot.db.prototype, self.prot1) - self.assertEqual(prot.desc, "testdesc") - self.assertTrue(bool(prot.tags.get("fooB", "db_prototype"))) + self.assertEqual(prot1['prototype_desc'], "testdesc1") - self.assertEqual(list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot]) - - prot2 = spawner.save_db_prototype(self.char1, self.prot2, "testprot2", - desc='testdesc2b', tags=["foo"]) + self.assertEqual(prot1['prototype_tags'], [("foo1", _PROTOTYPE_TAG_META_CATEGORY)]) self.assertEqual( - list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot, prot2]) + protlib.DbPrototype.objects.get_by_tag( + "foo1", _PROTOTYPE_TAG_META_CATEGORY)[0].db.prototype, prot1) - prot3 = spawner.save_db_prototype(self.char1, self.prot3, "testprot2", desc='testdesc2') - self.assertEqual(prot2.id, prot3.id) + prot2 = protlib.create_prototype(**self.prot2) self.assertEqual( - list(prot.__class__.objects.get_by_tag("foo", "db_prototype")), [prot, prot2]) + [pobj.db.prototype + for pobj in protlib.DbPrototype.objects.get_by_tag( + "foo1", _PROTOTYPE_TAG_META_CATEGORY)], + [prot1, prot2]) - # returns DBPrototype - self.assertEqual(list(spawner.search_db_prototype("testprot", return_queryset=True)), [prot]) + # add to existing prototype + prot1b = protlib.create_prototype( + prototype_key='testprototype1', foo='bar', prototype_tags=['foo2']) - prot = prot.db.prototype - prot3 = prot3.db.prototype - self.assertEqual(list(spawner.search_prototype("testprot")), [prot]) self.assertEqual( - list(spawner.search_prototype("testprot")), [self.prot1]) + [pobj.db.prototype + for pobj in protlib.DbPrototype.objects.get_by_tag( + "foo2", _PROTOTYPE_TAG_META_CATEGORY)], + [prot1b]) + + self.assertEqual(list(protlib.search_prototype("testprototype2")), [prot2]) + self.assertNotEqual(list(protlib.search_prototype("testprototype1")), [prot1]) + self.assertEqual(list(protlib.search_prototype("testprototype1")), [prot1b]) + + prot3 = protlib.create_prototype(**self.prot3) + # partial match - self.assertEqual(list(spawner.search_prototype("prot")), [prot, prot3]) - self.assertEqual(list(spawner.search_prototype(tags="foo")), [prot, prot3]) + self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3]) + self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3]) - self.assertTrue(str(unicode(spawner.list_prototypes(self.char1)))) + self.assertTrue(str(unicode(protlib.list_prototypes(self.char1)))) From 49622a05342fb6e126a311044dc84a92f2c8062f Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 10 Jun 2018 21:02:09 +0200 Subject: [PATCH 060/103] Fix unittests; still missing protfunc tests and menus --- evennia/commands/default/building.py | 18 +++++++++--------- evennia/commands/default/tests.py | 10 +++++----- evennia/contrib/tutorial_world/objects.py | 2 +- evennia/prototypes/spawner.py | 4 ++-- evennia/prototypes/tests.py | 3 +++ 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 692dd2aac6..301bd03761 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, get_all_typeclasses from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore -from evennia.prototypes import spawner +from evennia.prototypes import spawner, prototypes as protlib from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2887,7 +2887,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): "use the 'exec' prototype key.") return None try: - spawner.validate_prototype(prototype) + protlib.validate_prototype(prototype) except RuntimeError as err: self.caller.msg(str(err)) return @@ -2929,7 +2929,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): 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(spawner.list_prototypes(caller, key=key, tags=tags)), + EvMore(caller, unicode(protlib.list_prototypes(caller, key=key, tags=tags)), exit_on_lastpage=True) return @@ -2947,7 +2947,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if 'list' in self.switches: # for list, all optional arguments are tags - EvMore(caller, unicode(spawner.list_prototypes(caller, + EvMore(caller, unicode(protlib.list_prototypes(caller, tags=self.lhslist)), exit_on_lastpage=True) return @@ -3049,7 +3049,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): return if not self.args: - ncount = len(spawner.search_prototype()) + ncount = len(protlib.search_prototype()) caller.msg("Usage: @spawn or {{key: value, ...}}" "\n ({} existing prototypes. Use /list to inspect)".format(ncount)) return @@ -3065,7 +3065,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): caller.msg("|rDeletion cancelled.|n") return try: - success = spawner.delete_db_prototype(caller, self.args) + success = protlib.delete_db_prototype(caller, self.args) except PermissionError as err: caller.msg("|rError deleting:|R {}|n".format(err)) caller.msg("Deletion {}.".format( @@ -3077,7 +3077,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if 'update' in self.switches: # update existing prototypes key = self.args.strip().lower() - existing_objects = spawner.search_objects_with_prototype(key) + 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 "" @@ -3103,7 +3103,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if isinstance(prototype, basestring): # A prototype key we are looking to apply key = prototype - prototypes = spawner.search_prototype(prototype) + prototypes = protlib.search_prototype(prototype) nprots = len(prototypes) if not prototypes: caller.msg("No prototype named '%s'." % prototype) @@ -3115,7 +3115,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): return # we have a prototype, check access prototype = prototypes[0] - if not caller.locks.check_lockstring(caller, prototype.get('prototype_locks', ''), access_type='use'): + if not caller.locks.check_lockstring(caller, prototype.get('prototype_locks', ''), access_type='spawn'): caller.msg("You don't have access to use this prototype.") return diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index ffb877c3e3..e1688cdb48 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -28,7 +28,7 @@ from evennia.utils import ansi, utils, gametime from evennia.server.sessionhandler import SESSIONS from evennia import search_object from evennia import DefaultObject, DefaultCharacter -from evennia.utils import spawner +from evennia.prototypes import spawner, prototypes as protlib # set up signal here since we are not starting the server @@ -389,16 +389,16 @@ class TestBuilding(CommandTest): spawnLoc = self.room1 self.call(building.CmdSpawn(), - "{'prototype':'GOBLIN', 'key':'goblin', 'location':'%s'}" + "{'prototype_key':'GOBLIN', 'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "Spawned goblin") goblin = getObject(self, "goblin") self.assertEqual(goblin.location, spawnLoc) goblin.delete() - spawner.save_db_prototype(self.char1, {'key': 'Ball', 'prototype': 'GOBLIN'}, 'ball') + protlib.create_prototype(**{'key': 'Ball', 'prototype': 'GOBLIN', 'prototype_key': 'testball'}) # Tests "@spawn " - self.call(building.CmdSpawn(), "ball", "Spawned Ball") + self.call(building.CmdSpawn(), "testball", "Spawned Ball") ball = getObject(self, "Ball") self.assertEqual(ball.location, self.char1.location) self.assertIsInstance(ball, DefaultObject) @@ -414,7 +414,7 @@ class TestBuilding(CommandTest): # Tests "@spawn/noloc ...", but DO specify a location. # Location should be the specified location. self.call(building.CmdSpawn(), - "/noloc {'prototype':'BALL', 'location':'%s'}" + "/noloc {'prototype':'TESTBALL', 'location':'%s'}" % spawnLoc.dbref, "Spawned Ball") ball = getObject(self, "Ball") self.assertEqual(ball.location, spawnLoc) diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index b260770577..807b4d5e09 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -24,7 +24,7 @@ import random from evennia import DefaultObject, DefaultExit, Command, CmdSet from evennia.utils import search, delay -from evennia.utils.spawner import spawn +from evennia.prototypes.spawner import spawn # ------------------------------------------------------------- # diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 22add7830a..da4d69eeb4 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -132,13 +132,13 @@ import evennia from evennia.objects.models import ObjectDB from evennia.utils.utils import make_iter, is_iter from evennia.prototypes import prototypes as protlib -from evennia.prototypes.prototypes import value_to_obj, value_to_obj_or_any, init_spawn_value +from evennia.prototypes.prototypes import ( + value_to_obj, value_to_obj_or_any, init_spawn_value, _PROTOTYPE_TAG_CATEGORY) _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") _PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") _NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES -_PROTOTYPE_TAG_CATEGORY = "spawned_by_prototype" # Helper diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 88650caa7b..94bce1f946 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -83,6 +83,7 @@ class TestUtils(EvenniaTest): 'prototype_desc': 'Built from Obj', 'prototype_key': Something, 'prototype_locks': 'spawn:all();edit:all()', + 'prototype_tags': [], 'tags': [(u'foo', None, None)], 'typeclass': 'evennia.objects.objects.DefaultObject'}, new_prot) @@ -121,6 +122,7 @@ class TestUtils(EvenniaTest): 'prototype_desc': 'UPDATE', 'prototype_key': 'UPDATE', 'prototype_locks': 'KEEP', + 'prototype_tags': 'KEEP', 'test': 'UPDATE', 'typeclass': 'KEEP'}) @@ -148,6 +150,7 @@ class TestUtils(EvenniaTest): 'prototype_desc': 'Built from Obj', 'prototype_key': Something, 'prototype_locks': 'spawn:all();edit:all()', + 'prototype_tags': [], 'typeclass': 'evennia.objects.objects.DefaultObject'}, new_prot) From dad9029c43a826d3ea60526cc7447c0cdfa96f1a Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 12 Jun 2018 00:08:14 +0200 Subject: [PATCH 061/103] Add a selection of default protfuncs --- evennia/prototypes/protfuncs.py | 181 +++++++++++++++++++++++++++++++- 1 file changed, 178 insertions(+), 3 deletions(-) diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index 057f5f770f..01859452b7 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -25,6 +25,9 @@ where *args are the arguments given in the prototype, and **kwargs are inserted - session (Session): The Session of the entity spawning using this prototype. - prototype_key (str): The currently spawning prototype-key. - prototype (dict): The dict this protfunc is a part of. + - testing (bool): This is set if this function is called as part of the prototype validation; if + set, the protfunc should take care not to perform any persistent actions, such as operate on + objects or add things to the database. Any traceback raised by this function will be handled at the time of spawning and abort the spawn before any object is created/updated. It must otherwise return the value to store for the specified @@ -32,9 +35,14 @@ prototype key (this value must be possible to serialize in an Attribute). """ +from ast import literal_eval +from random import randint as base_randint, random as base_random + from django.conf import settings from evennia.utils import inlinefuncs from evennia.utils.utils import callables_from_module +from evennia.utils.utils import justify as base_justify, is_iter +from evennia.prototypes.prototypes import value_to_obj_or_any _PROTOTYPEFUNCS = {} @@ -57,7 +65,8 @@ def protfunc_parser(value, available_functions=None, **kwargs): `settings.PROTOTYPEFUNC_MODULES`, or specified on the command line. Args: - value (string): The value to test for a parseable protfunc. + value (any): The value to test for a parseable protfunc. Only strings will be parsed for + protfuncs, all other types are returned as-is. available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. Kwargs: @@ -66,13 +75,179 @@ def protfunc_parser(value, available_functions=None, **kwargs): Returns: any (any): A structure to replace the string on the prototype level. If this is a callable or a (callable, (args,)) structure, it will be executed as if one had supplied - it to the prototype directly. + it to the prototype directly. This structure is also passed through literal_eval so one + can get actual Python primitives out of it (not just strings). It will also identify + eventual object #dbrefs in the output from the protfunc. + """ if not isinstance(value, basestring): return value available_functions = _PROTOTYPEFUNCS if available_functions is None else available_functions - return inlinefuncs.parse_inlinefunc(value, _available_funcs=available_functions, **kwargs) + result = inlinefuncs.parse_inlinefunc(value, _available_funcs=available_functions, **kwargs) + result = value_to_obj_or_any(result) + try: + return literal_eval(result) + except ValueError: + return result + # default protfuncs + +def random(*args, **kwargs): + """ + Usage: $random() + Returns a random value in the interval [0, 1) + + """ + return base_random() + + +def randint(*args, **kwargs): + """ + Usage: $randint(start, end) + Returns random integer in interval [start, end] + + """ + if len(args) != 2: + raise TypeError("$randint needs two arguments - start and end.") + start, end = int(args[0]), int(args[1]) + return base_randint(start, end) + + +def left_justify(*args, **kwargs): + """ + Usage: $left_justify() + Returns left-justified. + + """ + if args: + return base_justify(args[0], align='l') + return "" + + +def right_justify(*args, **kwargs): + """ + Usage: $right_justify() + Returns right-justified across screen width. + + """ + if args: + return base_justify(args[0], align='r') + return "" + + +def center_justify(*args, **kwargs): + + """ + Usage: $center_justify() + Returns centered in screen width. + + """ + if args: + return base_justify(args[0], align='c') + return "" + + +def full_justify(*args, **kwargs): + + """ + Usage: $full_justify() + Returns filling up screen width by adding extra space. + + """ + if args: + return base_justify(args[0], align='f') + return "" + + +def protkey(*args, **kwargs): + """ + Usage: $protkey() + Returns the value of another key in this prototoype. Will raise an error if + the key is not found in this prototype. + + """ + if args: + prototype = kwargs['prototype'] + return prototype[args[0]] + + +def add(*args, **kwargs): + """ + Usage: $add(val1, val2) + Returns the result of val1 + val2. Values must be + valid simple Python structures possible to add, + such as numbers, lists etc. + + """ + if len(args) > 1: + val1, val2 = args[0], args[1] + return literal_eval(val1) + literal_eval(val2) + raise ValueError("$add requires two arguments.") + + +def sub(*args, **kwargs): + """ + Usage: $del(val1, val2) + Returns the value of val1 - val2. Values must be + valid simple Python structures possible to + subtract. + + """ + if len(args) > 1: + val1, val2 = args[0], args[1] + return literal_eval(val1) - literal_eval(val2) + raise ValueError("$sub requires two arguments.") + + +def mul(*args, **kwargs): + """ + Usage: $mul(val1, val2) + Returns the value of val1 * val2. The values must be + valid simple Python structures possible to + multiply, like strings and/or numbers. + + """ + if len(args) > 1: + val1, val2 = args[0], args[1] + return literal_eval(val1) * literal_eval(val2) + raise ValueError("$mul requires two arguments.") + + +def div(*args, **kwargs): + """ + Usage: $div(val1, val2) + Returns the value of val1 / val2. Values must be numbers and + the result is always a float. + + """ + if len(args) > 1: + val1, val2 = args[0], args[1] + return literal_eval(val1) / float(literal_eval(val2)) + raise ValueError("$mult requires two arguments.") + + +def eval(*args, **kwargs): + """ + Usage $eval() + Returns evaluation of a simple Python expression. The string may *only* consist of the following + Python literal structures: strings, numbers, tuples, lists, dicts, booleans, + and None. The strings can also contain #dbrefs. Escape embedded protfuncs as $$protfunc(..) + - those will then be evaluated *after* $eval. + + """ + string = args[0] if args else '' + struct = literal_eval(string) + + def _recursive_parse(val): + # an extra round of recursive parsing, to catch any escaped $$profuncs + if is_iter(val): + stype = type(val) + if stype == dict: + return {_recursive_parse(key): _recursive_parse(v) for key, v in val.items()} + return stype((_recursive_parse(v) for v in val)) + return protfunc_parser(val) + + return _recursive_parse(struct) From 4034de21bb8ff12e111fc8642460388bbc24d076 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 12 Jun 2018 20:10:20 +0200 Subject: [PATCH 062/103] Start protfunc tests (unworking) --- evennia/prototypes/protfuncs.py | 24 +++++++++++++----------- evennia/prototypes/tests.py | 24 +++++++++++++++++++----- evennia/settings_default.py | 3 +++ 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index 01859452b7..853634d9f6 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -15,7 +15,7 @@ In the prototype dict, the protfunc is specified as a string inside the prototyp and multiple functions can be nested (no keyword args are supported). The result will be used as the value for that prototype key for that individual spawn. -Available protfuncs are callables in one of the modules of `settings.PROTOTYPEFUNC_MODULES`. They +Available protfuncs are callables in one of the modules of `settings.PROT_FUNC_MODULES`. They are specified as functions def funcname (*args, **kwargs) @@ -42,17 +42,16 @@ from django.conf import settings from evennia.utils import inlinefuncs from evennia.utils.utils import callables_from_module from evennia.utils.utils import justify as base_justify, is_iter -from evennia.prototypes.prototypes import value_to_obj_or_any +_PROTLIB = None +_PROT_FUNCS = {} -_PROTOTYPEFUNCS = {} - -for mod in settings.PROTOTYPEFUNC_MODULES: +for mod in settings.PROT_FUNC_MODULES: try: callables = callables_from_module(mod) if mod == __name__: - callables.pop("protfunc_parser") - _PROTOTYPEFUNCS.update(callables) + callables.pop("protfunc_parser", None) + _PROT_FUNCS.update(callables) except ImportError: pass @@ -62,7 +61,7 @@ def protfunc_parser(value, available_functions=None, **kwargs): Parse a prototype value string for a protfunc and process it. Available protfuncs are specified as callables in one of the modules of - `settings.PROTOTYPEFUNC_MODULES`, or specified on the command line. + `settings.PROTFUNC_MODULES`, or specified on the command line. Args: value (any): The value to test for a parseable protfunc. Only strings will be parsed for @@ -81,18 +80,21 @@ def protfunc_parser(value, available_functions=None, **kwargs): """ + global _PROTLIB + if not _PROTLIB: + from evennia.prototypes import prototypes as _PROTLIB + if not isinstance(value, basestring): return value - available_functions = _PROTOTYPEFUNCS if available_functions is None else available_functions + available_functions = _PROT_FUNCS if available_functions is None else available_functions result = inlinefuncs.parse_inlinefunc(value, _available_funcs=available_functions, **kwargs) - result = value_to_obj_or_any(result) + result = _PROTLIB.value_to_obj_or_any(result) try: return literal_eval(result) except ValueError: return result - # default protfuncs def random(*args, **kwargs): diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 94bce1f946..fa7eeca246 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -6,8 +6,9 @@ Unit tests for the prototypes and spawner from random import randint import mock from anything import Anything, Something +from django.test.utils import override_settings from evennia.utils.test_resources import EvenniaTest -from evennia.prototypes import spawner, prototypes as protlib +from evennia.prototypes import spawner, prototypes as protlib, protfuncs from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY @@ -40,10 +41,6 @@ _PROTPARENTS = { } -class TestPrototypes(EvenniaTest): - pass - - class TestSpawner(EvenniaTest): def setUp(self): @@ -169,6 +166,23 @@ class TestProtLib(EvenniaTest): def test_check_permission(self): pass + +@override_settings(PROT_FUNC_MODULES=['evennia.prototypes.protfuncs']) +class TestProtFuncs(EvenniaTest): + + def setUp(self): + super(TestProtFuncs, self).setUp() + self.prot = {"prototype_key": "test_prototype", + "prototype_desc": "testing prot", + "key": "ExampleObj"} + + @mock.patch("random.random", new=mock.MagicMock(return_value=0.5)) + @mock.patch("random.randint", new=mock.MagicMock(return_value=5)) + def test_protfuncs(self): + self.assertEqual(protfuncs.protfunc_parser("$random()", 0.5)) + self.assertEqual(protfuncs.protfunc_parser("$randint(1, 10)", 5)) + + class TestPrototypeStorage(EvenniaTest): def setUp(self): diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 1d7adb4375..172fee8922 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -354,6 +354,9 @@ LOCK_FUNC_MODULES = ("evennia.locks.lockfuncs", "server.conf.lockfuncs",) INPUT_FUNC_MODULES = ["evennia.server.inputfuncs", "server.conf.inputfuncs"] # Modules that contain prototypes for use with the spawner mechanism. PROTOTYPE_MODULES = ["world.prototypes"] +# Modules containining Prototype functions able to be embedded in prototype +# definitions from in-game. +PROT_FUNC_MODULES = ["evennia.prototypes.protfuncs"] # Module holding settings/actions for the dummyrunner program (see the # dummyrunner for more information) DUMMYRUNNER_SETTINGS_MODULE = "evennia.server.profiling.dummyrunner_settings" From 590ffb646591b7951a0b32346bf6df409cfbad29 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 15 Jun 2018 23:45:55 +0200 Subject: [PATCH 063/103] Work on resolving inlinefunc errors #1498 --- evennia/prototypes/protfuncs.py | 9 ++++++++- evennia/utils/inlinefuncs.py | 32 +++++++++++++++++--------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index 853634d9f6..6e9c7e5679 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -87,7 +87,14 @@ def protfunc_parser(value, available_functions=None, **kwargs): if not isinstance(value, basestring): return value available_functions = _PROT_FUNCS if available_functions is None else available_functions - result = inlinefuncs.parse_inlinefunc(value, _available_funcs=available_functions, **kwargs) + result = inlinefuncs.parse_inlinefunc(value, available_funcs=available_functions, **kwargs) + # at this point we have a string where all procfuncs were parsed + try: + result = literal_eval(result) + except ValueError: + # this is due to the string not being valid for literal_eval - keep it a string + pass + result = _PROTLIB.value_to_obj_or_any(result) try: return literal_eval(result) diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index 2646fb3991..575baf281f 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -157,6 +157,9 @@ def clr(*args, **kwargs): return text +def null(*args, **kwargs): + return args[0] if args else '' + # we specify a default nomatch function to use if no matching func was # found. This will be overloaded by any nomatch function defined in # the imported modules. @@ -177,10 +180,6 @@ for module in utils.make_iter(settings.INLINEFUNC_MODULES): raise -# remove the core function if we include examples in this module itself -#_INLINE_FUNCS.pop("inline_func_parse", None) - - # The stack size is a security measure. Set to <=0 to disable. try: _STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE @@ -198,7 +197,7 @@ _RE_TOKEN = re.compile(r""" (?P(?(?\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text - (?P[\w\s.-\/#!%\^&\*;:=\-_`~\|\(}{\[\]]+|\"{1}|\'{1}) # everything else should also be included""", + (?P[\w\s.-\/#@$\>\ 0 and _STACK_MAXSIZE < len(stack): # if stack is larger than limit, throw away parsing return string + gdict["stackfull"](*args, **kwargs) - else: - # cache the stack + elif usecache: + # cache the stack - we do this also if we don't check the cache above _PARSING_CACHE[string] = stack # run the stack recursively @@ -368,8 +370,8 @@ def parse_inlinefunc(string, strip=False, _available_funcs=None, **kwargs): retval = "" if strip else func(*args, **kwargs) return utils.to_str(retval, force_string=True) - # execute the stack from the cache - return "".join(_run_stack(item) for item in _PARSING_CACHE[string]) + # execute the stack + return "".join(_run_stack(item) for item in stack) # # Nick templating From a9e0ee35400d7ca43afee4d045ae1f2a268f30eb Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 16 Jun 2018 22:14:28 +0200 Subject: [PATCH 064/103] Handle missing characters in inlinefunc as per #1498 --- evennia/utils/inlinefuncs.py | 942 +++++++++++++++++------------------ 1 file changed, 471 insertions(+), 471 deletions(-) diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index 575baf281f..de03e13c2d 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -1,471 +1,471 @@ -""" -Inline functions (nested form). - -This parser accepts nested inlinefunctions on the form - -``` -$funcname(arg, arg, ...) -``` - -embedded in any text where any arg can be another $funcname{} call. -This functionality is turned off by default - to activate, -`settings.INLINEFUNC_ENABLED` must be set to `True`. - -Each token starts with "$funcname(" where there must be no space -between the $funcname and (. It ends with a matched ending parentesis. -")". - -Inside the inlinefunc definition, one can use `\` to escape. This is -mainly needed for escaping commas in flowing text (which would -otherwise be interpreted as an argument separator), or to escape `}` -when not intended to close the function block. Enclosing text in -matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will -also escape *everything* within without needing to escape individual -characters. - -The available inlinefuncs are defined as global-level functions in -modules defined by `settings.INLINEFUNC_MODULES`. They are identified -by their function name (and ignored if this name starts with `_`). They -should be on the following form: - -```python -def funcname (*args, **kwargs): - # ... -``` - -Here, the arguments given to `$funcname(arg1,arg2)` will appear as the -`*args` tuple. This will be populated by the arguments given to the -inlinefunc in-game - the only part that will be available from -in-game. `**kwargs` are not supported from in-game but are only used -internally by Evennia to make details about the caller available to -the function. The kwarg passed to all functions is `session`, the -Sessionobject for the object seeing the string. This may be `None` if -the string is sent to a non-puppetable object. The inlinefunc should -never raise an exception. - -There are two reserved function names: -- "nomatch": This is called if the user uses a functionname that is - not registered. The nomatch function will get the name of the - not-found function as its first argument followed by the normal - arguments to the given function. If not defined the default effect is - to print `` to replace the unknown function. -- "stackfull": This is called when the maximum nested function stack is reached. - When this happens, the original parsed string is returned and the result of - the `stackfull` inlinefunc is appended to the end. By default this is an - error message. - -Error handling: - Syntax errors, notably not completely closing all inlinefunc - blocks, will lead to the entire string remaining unparsed. - -""" - -import re -from django.conf import settings -from evennia.utils import utils - - -# example/testing inline functions - -def pad(*args, **kwargs): - """ - Inlinefunc. Pads text to given width. - - Args: - text (str, optional): Text to pad. - width (str, optional): Will be converted to integer. Width - of padding. - align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'. - fillchar (str, optional): Character used for padding. Defaults to a space. - - Kwargs: - session (Session): Session performing the pad. - - Example: - `$pad(text, width, align, fillchar)` - - """ - text, width, align, fillchar = "", 78, 'c', ' ' - nargs = len(args) - if nargs > 0: - text = args[0] - if nargs > 1: - width = int(args[1]) if args[1].strip().isdigit() else 78 - if nargs > 2: - align = args[2] if args[2] in ('c', 'l', 'r') else 'c' - if nargs > 3: - fillchar = args[3] - return utils.pad(text, width=width, align=align, fillchar=fillchar) - - -def crop(*args, **kwargs): - """ - Inlinefunc. Crops ingoing text to given widths. - - Args: - text (str, optional): Text to crop. - width (str, optional): Will be converted to an integer. Width of - crop in characters. - suffix (str, optional): End string to mark the fact that a part - of the string was cropped. Defaults to `[...]`. - Kwargs: - session (Session): Session performing the crop. - - Example: - `$crop(text, width=78, suffix='[...]')` - - """ - text, width, suffix = "", 78, "[...]" - nargs = len(args) - if nargs > 0: - text = args[0] - if nargs > 1: - width = int(args[1]) if args[1].strip().isdigit() else 78 - if nargs > 2: - suffix = args[2] - return utils.crop(text, width=width, suffix=suffix) - - -def clr(*args, **kwargs): - """ - Inlinefunc. Colorizes nested text. - - Args: - startclr (str, optional): An ANSI color abbreviation without the - prefix `|`, such as `r` (red foreground) or `[r` (red background). - text (str, optional): Text - endclr (str, optional): The color to use at the end of the string. Defaults - to `|n` (reset-color). - Kwargs: - session (Session): Session object triggering inlinefunc. - - Example: - `$clr(startclr, text, endclr)` - - """ - text = "" - nargs = len(args) - if nargs > 0: - color = args[0].strip() - if nargs > 1: - text = args[1] - text = "|" + color + text - if nargs > 2: - text += "|" + args[2].strip() - else: - text += "|n" - return text - - -def null(*args, **kwargs): - return args[0] if args else '' - -# we specify a default nomatch function to use if no matching func was -# found. This will be overloaded by any nomatch function defined in -# the imported modules. -_INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "", - "stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"} - - -# load custom inline func modules. -for module in utils.make_iter(settings.INLINEFUNC_MODULES): - try: - _INLINE_FUNCS.update(utils.callables_from_module(module)) - except ImportError as err: - if module == "server.conf.inlinefuncs": - # a temporary warning since the default module changed name - raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should " - "be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err) - else: - raise - - -# The stack size is a security measure. Set to <=0 to disable. -try: - _STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE -except AttributeError: - _STACK_MAXSIZE = 20 - -# regex definitions - -_RE_STARTTOKEN = re.compile(r"(?.*?)(?.*?)(?(?(?(?\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text - (?P[\w\s.-\/#@$\>\ 0: - # commas outside strings and inside a callable are - # used to mark argument separation - we use None - # in the stack to indicate such a separation. - stack.append(None) - else: - # no callable active - just a string - stack.append(",") - else: - # the rest - stack.append(gdict["rest"]) - - if ncallable > 0: - # this means not all inlinefuncs were complete - return string - - if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack): - # if stack is larger than limit, throw away parsing - return string + gdict["stackfull"](*args, **kwargs) - elif usecache: - # cache the stack - we do this also if we don't check the cache above - _PARSING_CACHE[string] = stack - - # run the stack recursively - def _run_stack(item, depth=0): - retval = item - if isinstance(item, tuple): - if strip: - return "" - else: - func, arglist = item - args = [""] - for arg in arglist: - if arg is None: - # an argument-separating comma - start a new arg - args.append("") - else: - # all other args should merge into one string - args[-1] += _run_stack(arg, depth=depth + 1) - # execute the inlinefunc at this point or strip it. - kwargs["inlinefunc_stack_depth"] = depth - retval = "" if strip else func(*args, **kwargs) - return utils.to_str(retval, force_string=True) - - # execute the stack - return "".join(_run_stack(item) for item in stack) - -# -# Nick templating -# - - -""" -This supports the use of replacement templates in nicks: - -This happens in two steps: - -1) The user supplies a template that is converted to a regex according - to the unix-like templating language. -2) This regex is tested against nicks depending on which nick replacement - strategy is considered (most commonly inputline). -3) If there is a template match and there are templating markers, - these are replaced with the arguments actually given. - -@desc $1 $2 $3 - -This will be converted to the following regex: - -\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+) - -Supported template markers (through fnmatch) - * matches anything (non-greedy) -> .*? - ? matches any single character -> - [seq] matches any entry in sequence - [!seq] matches entries not in sequence -Custom arg markers - $N argument position (1-99) - -""" -import fnmatch -_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)") -_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)") -_RE_NICK_SPACE = re.compile(r"\\ ") - - -class NickTemplateInvalid(ValueError): - pass - - -def initialize_nick_templates(in_template, out_template): - """ - Initialize the nick templates for matching and remapping a string. - - Args: - in_template (str): The template to be used for nick recognition. - out_template (str): The template to be used to replace the string - matched by the in_template. - - Returns: - regex (regex): Regex to match against strings - template (str): Template with markers {arg1}, {arg2}, etc for - replacement using the standard .format method. - - Raises: - NickTemplateInvalid: If the in/out template does not have a matching - number of $args. - - """ - # create the regex for in_template - regex_string = fnmatch.translate(in_template) - n_inargs = len(_RE_NICK_ARG.findall(regex_string)) - regex_string = _RE_NICK_SPACE.sub("\s+", regex_string) - regex_string = _RE_NICK_ARG.sub(lambda m: "(?P.+?)" % m.group(2), regex_string) - - # create the out_template - template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template) - - # validate the tempaltes - they should at least have the same number of args - n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template)) - if n_inargs != n_outargs: - print n_inargs, n_outargs - raise NickTemplateInvalid - - return re.compile(regex_string), template_string - - -def parse_nick_template(string, template_regex, outtemplate): - """ - Parse a text using a template and map it to another template - - Args: - string (str): The input string to processj - template_regex (regex): A template regex created with - initialize_nick_template. - outtemplate (str): The template to which to map the matches - produced by the template_regex. This should have $1, $2, - etc to match the regex. - - """ - match = template_regex.match(string) - if match: - return outtemplate.format(**match.groupdict()) - return string +""" +Inline functions (nested form). + +This parser accepts nested inlinefunctions on the form + +``` +$funcname(arg, arg, ...) +``` + +embedded in any text where any arg can be another $funcname{} call. +This functionality is turned off by default - to activate, +`settings.INLINEFUNC_ENABLED` must be set to `True`. + +Each token starts with "$funcname(" where there must be no space +between the $funcname and (. It ends with a matched ending parentesis. +")". + +Inside the inlinefunc definition, one can use `\` to escape. This is +mainly needed for escaping commas in flowing text (which would +otherwise be interpreted as an argument separator), or to escape `}` +when not intended to close the function block. Enclosing text in +matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will +also escape *everything* within without needing to escape individual +characters. + +The available inlinefuncs are defined as global-level functions in +modules defined by `settings.INLINEFUNC_MODULES`. They are identified +by their function name (and ignored if this name starts with `_`). They +should be on the following form: + +```python +def funcname (*args, **kwargs): + # ... +``` + +Here, the arguments given to `$funcname(arg1,arg2)` will appear as the +`*args` tuple. This will be populated by the arguments given to the +inlinefunc in-game - the only part that will be available from +in-game. `**kwargs` are not supported from in-game but are only used +internally by Evennia to make details about the caller available to +the function. The kwarg passed to all functions is `session`, the +Sessionobject for the object seeing the string. This may be `None` if +the string is sent to a non-puppetable object. The inlinefunc should +never raise an exception. + +There are two reserved function names: +- "nomatch": This is called if the user uses a functionname that is + not registered. The nomatch function will get the name of the + not-found function as its first argument followed by the normal + arguments to the given function. If not defined the default effect is + to print `` to replace the unknown function. +- "stackfull": This is called when the maximum nested function stack is reached. + When this happens, the original parsed string is returned and the result of + the `stackfull` inlinefunc is appended to the end. By default this is an + error message. + +Error handling: + Syntax errors, notably not completely closing all inlinefunc + blocks, will lead to the entire string remaining unparsed. + +""" + +import re +from django.conf import settings +from evennia.utils import utils + + +# example/testing inline functions + +def pad(*args, **kwargs): + """ + Inlinefunc. Pads text to given width. + + Args: + text (str, optional): Text to pad. + width (str, optional): Will be converted to integer. Width + of padding. + align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'. + fillchar (str, optional): Character used for padding. Defaults to a space. + + Kwargs: + session (Session): Session performing the pad. + + Example: + `$pad(text, width, align, fillchar)` + + """ + text, width, align, fillchar = "", 78, 'c', ' ' + nargs = len(args) + if nargs > 0: + text = args[0] + if nargs > 1: + width = int(args[1]) if args[1].strip().isdigit() else 78 + if nargs > 2: + align = args[2] if args[2] in ('c', 'l', 'r') else 'c' + if nargs > 3: + fillchar = args[3] + return utils.pad(text, width=width, align=align, fillchar=fillchar) + + +def crop(*args, **kwargs): + """ + Inlinefunc. Crops ingoing text to given widths. + + Args: + text (str, optional): Text to crop. + width (str, optional): Will be converted to an integer. Width of + crop in characters. + suffix (str, optional): End string to mark the fact that a part + of the string was cropped. Defaults to `[...]`. + Kwargs: + session (Session): Session performing the crop. + + Example: + `$crop(text, width=78, suffix='[...]')` + + """ + text, width, suffix = "", 78, "[...]" + nargs = len(args) + if nargs > 0: + text = args[0] + if nargs > 1: + width = int(args[1]) if args[1].strip().isdigit() else 78 + if nargs > 2: + suffix = args[2] + return utils.crop(text, width=width, suffix=suffix) + + +def clr(*args, **kwargs): + """ + Inlinefunc. Colorizes nested text. + + Args: + startclr (str, optional): An ANSI color abbreviation without the + prefix `|`, such as `r` (red foreground) or `[r` (red background). + text (str, optional): Text + endclr (str, optional): The color to use at the end of the string. Defaults + to `|n` (reset-color). + Kwargs: + session (Session): Session object triggering inlinefunc. + + Example: + `$clr(startclr, text, endclr)` + + """ + text = "" + nargs = len(args) + if nargs > 0: + color = args[0].strip() + if nargs > 1: + text = args[1] + text = "|" + color + text + if nargs > 2: + text += "|" + args[2].strip() + else: + text += "|n" + return text + + +def null(*args, **kwargs): + return args[0] if args else '' + +# we specify a default nomatch function to use if no matching func was +# found. This will be overloaded by any nomatch function defined in +# the imported modules. +_INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "", + "stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"} + + +# load custom inline func modules. +for module in utils.make_iter(settings.INLINEFUNC_MODULES): + try: + _INLINE_FUNCS.update(utils.callables_from_module(module)) + except ImportError as err: + if module == "server.conf.inlinefuncs": + # a temporary warning since the default module changed name + raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should " + "be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err) + else: + raise + + +# The stack size is a security measure. Set to <=0 to disable. +try: + _STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE +except AttributeError: + _STACK_MAXSIZE = 20 + +# regex definitions + +_RE_STARTTOKEN = re.compile(r"(?.*?)(?.*?)(?(?(?(?\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text + (?P[\w\s.-\/#!%\^&\*;:=\-_`~\|\(}{\[\]@\$\\\+\<\>?]+|\"{1}|\'{1}) # everything else """, + re.UNICODE + re.IGNORECASE + re.VERBOSE + re.DOTALL) + +# Cache for function lookups. +_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000) + + +class ParseStack(list): + """ + Custom stack that always concatenates strings together when the + strings are added next to one another. Tuples are stored + separately and None is used to mark that a string should be broken + up into a new chunk. Below is the resulting stack after separately + appending 3 strings, None, 2 strings, a tuple and finally 2 + strings: + + [string + string + string, + None + string + string, + tuple, + string + string] + + """ + + def __init__(self, *args, **kwargs): + super(ParseStack, self).__init__(*args, **kwargs) + # always start stack with the empty string + list.append(self, "") + # indicates if the top of the stack is a string or not + self._string_last = True + + def __eq__(self, other): + return (super(ParseStack).__eq__(other) and + hasattr(other, "_string_last") and self._string_last == other._string_last) + + def __ne__(self, other): + return not self.__eq__(other) + + def append(self, item): + """ + The stack will merge strings, add other things as normal + """ + if isinstance(item, basestring): + if self._string_last: + self[-1] += item + else: + list.append(self, item) + self._string_last = True + else: + # everything else is added as normal + list.append(self, item) + self._string_last = False + + +class InlinefuncError(RuntimeError): + pass + + +def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): + """ + Parse the incoming string. + + Args: + string (str): The incoming string to parse. + strip (bool, optional): Whether to strip function calls rather than + execute them. + available_funcs (dict, optional): Define an alternative source of functions to parse for. + If unset, use the functions found through `settings.INLINEFUNC_MODULES`. + Kwargs: + session (Session): This is sent to this function by Evennia when triggering + it. It is passed to the inlinefunc. + kwargs (any): All other kwargs are also passed on to the inlinefunc. + + + """ + global _PARSING_CACHE + + usecache = False + if not available_funcs: + available_funcs = _INLINE_FUNCS + usecache = True + + if usecache and string in _PARSING_CACHE: + # stack is already cached + stack = _PARSING_CACHE[string] + elif not _RE_STARTTOKEN.search(string): + # if there are no unescaped start tokens at all, return immediately. + return string + else: + # no cached stack; build a new stack and continue + stack = ParseStack() + + # process string on stack + ncallable = 0 + for match in _RE_TOKEN.finditer(string): + gdict = match.groupdict() + if gdict["singlequote"]: + stack.append(gdict["singlequote"]) + elif gdict["doublequote"]: + stack.append(gdict["doublequote"]) + elif gdict["end"]: + if ncallable <= 0: + stack.append(")") + continue + args = [] + while stack: + operation = stack.pop() + if callable(operation): + if not strip: + stack.append((operation, [arg for arg in reversed(args)])) + ncallable -= 1 + break + else: + args.append(operation) + elif gdict["start"]: + funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1) + try: + # try to fetch the matching inlinefunc from storage + stack.append(available_funcs[funcname]) + except KeyError: + stack.append(available_funcs["nomatch"]) + stack.append(funcname) + ncallable += 1 + elif gdict["escaped"]: + # escaped tokens + token = gdict["escaped"].lstrip("\\") + stack.append(token) + elif gdict["comma"]: + if ncallable > 0: + # commas outside strings and inside a callable are + # used to mark argument separation - we use None + # in the stack to indicate such a separation. + stack.append(None) + else: + # no callable active - just a string + stack.append(",") + else: + # the rest + stack.append(gdict["rest"]) + + if ncallable > 0: + # this means not all inlinefuncs were complete + return string + + if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack): + # if stack is larger than limit, throw away parsing + return string + gdict["stackfull"](*args, **kwargs) + elif usecache: + # cache the stack - we do this also if we don't check the cache above + _PARSING_CACHE[string] = stack + + # run the stack recursively + def _run_stack(item, depth=0): + retval = item + if isinstance(item, tuple): + if strip: + return "" + else: + func, arglist = item + args = [""] + for arg in arglist: + if arg is None: + # an argument-separating comma - start a new arg + args.append("") + else: + # all other args should merge into one string + args[-1] += _run_stack(arg, depth=depth + 1) + # execute the inlinefunc at this point or strip it. + kwargs["inlinefunc_stack_depth"] = depth + retval = "" if strip else func(*args, **kwargs) + return utils.to_str(retval, force_string=True) + + print("STACK:\n{}".format(stack)) + # execute the stack + return "".join(_run_stack(item) for item in stack) + +# +# Nick templating +# + + +""" +This supports the use of replacement templates in nicks: + +This happens in two steps: + +1) The user supplies a template that is converted to a regex according + to the unix-like templating language. +2) This regex is tested against nicks depending on which nick replacement + strategy is considered (most commonly inputline). +3) If there is a template match and there are templating markers, + these are replaced with the arguments actually given. + +@desc $1 $2 $3 + +This will be converted to the following regex: + +\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+) + +Supported template markers (through fnmatch) + * matches anything (non-greedy) -> .*? + ? matches any single character -> + [seq] matches any entry in sequence + [!seq] matches entries not in sequence +Custom arg markers + $N argument position (1-99) + +""" + +import fnmatch +_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)") +_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)") +_RE_NICK_SPACE = re.compile(r"\\ ") + + +class NickTemplateInvalid(ValueError): + pass + + +def initialize_nick_templates(in_template, out_template): + """ + Initialize the nick templates for matching and remapping a string. + + Args: + in_template (str): The template to be used for nick recognition. + out_template (str): The template to be used to replace the string + matched by the in_template. + + Returns: + regex (regex): Regex to match against strings + template (str): Template with markers {arg1}, {arg2}, etc for + replacement using the standard .format method. + + Raises: + NickTemplateInvalid: If the in/out template does not have a matching + number of $args. + + """ + # create the regex for in_template + regex_string = fnmatch.translate(in_template) + n_inargs = len(_RE_NICK_ARG.findall(regex_string)) + regex_string = _RE_NICK_SPACE.sub("\s+", regex_string) + regex_string = _RE_NICK_ARG.sub(lambda m: "(?P.+?)" % m.group(2), regex_string) + + # create the out_template + template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template) + + # validate the tempaltes - they should at least have the same number of args + n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template)) + if n_inargs != n_outargs: + raise NickTemplateInvalid + + return re.compile(regex_string), template_string + + +def parse_nick_template(string, template_regex, outtemplate): + """ + Parse a text using a template and map it to another template + + Args: + string (str): The input string to processj + template_regex (regex): A template regex created with + initialize_nick_template. + outtemplate (str): The template to which to map the matches + produced by the template_regex. This should have $1, $2, + etc to match the regex. + + """ + match = template_regex.match(string) + if match: + return outtemplate.format(**match.groupdict()) + return string From 266971567b8bc6ab1097030fed4d5bc13775c0e2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 17 Jun 2018 01:22:24 +0200 Subject: [PATCH 065/103] Much improved inlinefunc regex; resolving #1498 --- evennia/utils/inlinefuncs.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index de03e13c2d..a0ec65e001 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -188,17 +188,20 @@ except AttributeError: # regex definitions -_RE_STARTTOKEN = re.compile(r"(?.*?)(?.*?)(?(?(?(?\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text - (?P[\w\s.-\/#!%\^&\*;:=\-_`~\|\(}{\[\]@\$\\\+\<\>?]+|\"{1}|\'{1}) # everything else """, - re.UNICODE + re.IGNORECASE + re.VERBOSE + re.DOTALL) + (?.*?)(?.*?)(?(?(?(? # escaped tokens to re-insert sans backslash + \\\'|\\\"|\\\)|\\\$\w+\()| + (?P # everything else to re-insert verbatim + \$(?!\w+\()|\'{1}|\"{1}|\\{1}|[^),$\'\"\\]+)""", + re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL) # Cache for function lookups. _PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000) @@ -293,6 +296,7 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): ncallable = 0 for match in _RE_TOKEN.finditer(string): gdict = match.groupdict() + print("match: {}".format({key: val for key, val in gdict.items() if val})) if gdict["singlequote"]: stack.append(gdict["singlequote"]) elif gdict["doublequote"]: From 274b02c598c2bc75b6a17817965f4a8d46e02da1 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 17 Jun 2018 08:37:29 +0200 Subject: [PATCH 066/103] Handle lone left-parents within inlinefunc --- evennia/utils/inlinefuncs.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index a0ec65e001..4becfb7b01 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -61,6 +61,7 @@ Error handling: """ import re +import fnmatch from django.conf import settings from evennia.utils import utils @@ -164,7 +165,8 @@ def null(*args, **kwargs): # found. This will be overloaded by any nomatch function defined in # the imported modules. _INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "", - "stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"} + "stackfull": lambda *args, **kwargs: "\n (not parsed: " + "inlinefunc stack size exceeded.)"} # load custom inline func modules. @@ -175,7 +177,8 @@ for module in utils.make_iter(settings.INLINEFUNC_MODULES): if module == "server.conf.inlinefuncs": # a temporary warning since the default module changed name raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should " - "be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err) + "be renamed to mygame/server/conf/inlinefuncs.py (note " + "the S at the end)." % err) else: raise @@ -190,17 +193,18 @@ except AttributeError: _RE_STARTTOKEN = re.compile(r"(?.*?)(?.*?)(?(?(?(?(?(? # escaped tokens to re-insert sans backslash - \\\'|\\\"|\\\)|\\\$\w+\()| + \\\'|\\\"|\\\)|\\\$\w+\(|\\\()| (?P # everything else to re-insert verbatim - \$(?!\w+\()|\'{1}|\"{1}|\\{1}|[^),$\'\"\\]+)""", + \$(?!\w+\()|\'|\"|\\|[^),$\'\"\\\(]+)""", re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL) # Cache for function lookups. @@ -294,14 +298,24 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): # process string on stack ncallable = 0 + nlparens = 0 for match in _RE_TOKEN.finditer(string): gdict = match.groupdict() - print("match: {}".format({key: val for key, val in gdict.items() if val})) + # print("match: {}".format({key: val for key, val in gdict.items() if val})) if gdict["singlequote"]: stack.append(gdict["singlequote"]) elif gdict["doublequote"]: stack.append(gdict["doublequote"]) + elif gdict["leftparens"]: + # we have a left-parens inside a callable + if ncallable: + nlparens += 1 + stack.append("(") elif gdict["end"]: + if nlparens > 0: + nlparens -= 1 + stack.append(")") + continue if ncallable <= 0: stack.append(")") continue @@ -373,7 +387,7 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): retval = "" if strip else func(*args, **kwargs) return utils.to_str(retval, force_string=True) - print("STACK:\n{}".format(stack)) + # print("STACK:\n{}".format(stack)) # execute the stack return "".join(_run_stack(item) for item in stack) @@ -410,7 +424,6 @@ Custom arg markers """ -import fnmatch _RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)") _RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)") _RE_NICK_SPACE = re.compile(r"\\ ") From 5ce7af39498a21a3b7405e2b8fcb6b9bf38ba31d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 17 Jun 2018 23:42:53 +0200 Subject: [PATCH 067/103] Many more tests, debugging of protfuncs/inlinefuncs --- evennia/prototypes/protfuncs.py | 195 ++++++++++++++++++++----------- evennia/prototypes/prototypes.py | 69 ++++++++++- evennia/prototypes/tests.py | 54 ++++++++- evennia/utils/inlinefuncs.py | 26 +++-- evennia/utils/utils.py | 15 +-- 5 files changed, 267 insertions(+), 92 deletions(-) diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index 6e9c7e5679..5ecb4b5e7d 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -23,8 +23,8 @@ are specified as functions where *args are the arguments given in the prototype, and **kwargs are inserted by Evennia: - session (Session): The Session of the entity spawning using this prototype. - - prototype_key (str): The currently spawning prototype-key. - prototype (dict): The dict this protfunc is a part of. + - current_key (str): The active key this value belongs to in the prototype. - testing (bool): This is set if this function is called as part of the prototype validation; if set, the protfunc should take care not to perform any persistent actions, such as operate on objects or add things to the database. @@ -38,68 +38,10 @@ prototype key (this value must be possible to serialize in an Attribute). from ast import literal_eval from random import randint as base_randint, random as base_random -from django.conf import settings -from evennia.utils import inlinefuncs -from evennia.utils.utils import callables_from_module -from evennia.utils.utils import justify as base_justify, is_iter +from evennia.utils import search +from evennia.utils.utils import justify as base_justify, is_iter, to_str _PROTLIB = None -_PROT_FUNCS = {} - -for mod in settings.PROT_FUNC_MODULES: - try: - callables = callables_from_module(mod) - if mod == __name__: - callables.pop("protfunc_parser", None) - _PROT_FUNCS.update(callables) - except ImportError: - pass - - -def protfunc_parser(value, available_functions=None, **kwargs): - """ - Parse a prototype value string for a protfunc and process it. - - Available protfuncs are specified as callables in one of the modules of - `settings.PROTFUNC_MODULES`, or specified on the command line. - - Args: - value (any): The value to test for a parseable protfunc. Only strings will be parsed for - protfuncs, all other types are returned as-is. - available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. - - Kwargs: - any (any): Passed on to the inlinefunc. - - Returns: - any (any): A structure to replace the string on the prototype level. If this is a - callable or a (callable, (args,)) structure, it will be executed as if one had supplied - it to the prototype directly. This structure is also passed through literal_eval so one - can get actual Python primitives out of it (not just strings). It will also identify - eventual object #dbrefs in the output from the protfunc. - - - """ - global _PROTLIB - if not _PROTLIB: - from evennia.prototypes import prototypes as _PROTLIB - - if not isinstance(value, basestring): - return value - available_functions = _PROT_FUNCS if available_functions is None else available_functions - result = inlinefuncs.parse_inlinefunc(value, available_funcs=available_functions, **kwargs) - # at this point we have a string where all procfuncs were parsed - try: - result = literal_eval(result) - except ValueError: - # this is due to the string not being valid for literal_eval - keep it a string - pass - - result = _PROTLIB.value_to_obj_or_any(result) - try: - return literal_eval(result) - except ValueError: - return result # default protfuncs @@ -180,7 +122,7 @@ def protkey(*args, **kwargs): """ if args: prototype = kwargs['prototype'] - return prototype[args[0]] + return prototype[args[0].strip()] def add(*args, **kwargs): @@ -193,7 +135,16 @@ def add(*args, **kwargs): """ if len(args) > 1: val1, val2 = args[0], args[1] - return literal_eval(val1) + literal_eval(val2) + # try to convert to python structures, otherwise, keep as strings + try: + val1 = literal_eval(val1.strip()) + except Exception: + pass + try: + val2 = literal_eval(val2.strip()) + except Exception: + pass + return val1 + val2 raise ValueError("$add requires two arguments.") @@ -207,11 +158,20 @@ def sub(*args, **kwargs): """ if len(args) > 1: val1, val2 = args[0], args[1] - return literal_eval(val1) - literal_eval(val2) + # try to convert to python structures, otherwise, keep as strings + try: + val1 = literal_eval(val1.strip()) + except Exception: + pass + try: + val2 = literal_eval(val2.strip()) + except Exception: + pass + return val1 - val2 raise ValueError("$sub requires two arguments.") -def mul(*args, **kwargs): +def mult(*args, **kwargs): """ Usage: $mul(val1, val2) Returns the value of val1 * val2. The values must be @@ -221,7 +181,16 @@ def mul(*args, **kwargs): """ if len(args) > 1: val1, val2 = args[0], args[1] - return literal_eval(val1) * literal_eval(val2) + # try to convert to python structures, otherwise, keep as strings + try: + val1 = literal_eval(val1.strip()) + except Exception: + pass + try: + val2 = literal_eval(val2.strip()) + except Exception: + pass + return val1 * val2 raise ValueError("$mul requires two arguments.") @@ -234,10 +203,33 @@ def div(*args, **kwargs): """ if len(args) > 1: val1, val2 = args[0], args[1] - return literal_eval(val1) / float(literal_eval(val2)) + # try to convert to python structures, otherwise, keep as strings + try: + val1 = literal_eval(val1.strip()) + except Exception: + pass + try: + val2 = literal_eval(val2.strip()) + except Exception: + pass + return val1 / float(val2) raise ValueError("$mult requires two arguments.") +def toint(*args, **kwargs): + """ + Usage: $toint() + Returns as an integer. + """ + if args: + val = args[0] + try: + return int(literal_eval(val.strip())) + except ValueError: + return val + raise ValueError("$toint requires one argument.") + + def eval(*args, **kwargs): """ Usage $eval() @@ -247,16 +239,79 @@ def eval(*args, **kwargs): - those will then be evaluated *after* $eval. """ - string = args[0] if args else '' + global _PROTLIB + if not _PROTLIB: + from evennia.prototypes import prototypes as _PROTLIB + + string = ",".join(args) struct = literal_eval(string) + if isinstance(struct, basestring): + # we must shield the string, otherwise it will be merged as a string and future + # literal_evals will pick up e.g. '2' as something that should be converted to a number + struct = '"{}"'.format(struct) + def _recursive_parse(val): - # an extra round of recursive parsing, to catch any escaped $$profuncs + # an extra round of recursive parsing after literal_eval, to catch any + # escaped $$profuncs. This is commonly useful for object references. if is_iter(val): stype = type(val) if stype == dict: return {_recursive_parse(key): _recursive_parse(v) for key, v in val.items()} return stype((_recursive_parse(v) for v in val)) - return protfunc_parser(val) + return _PROTLIB.protfunc_parser(val) return _recursive_parse(struct) + + +def _obj_search(return_list=False, *args, **kwargs): + "Helper function to search for an object" + + query = "".join(args) + session = kwargs.get("session", None) + + if not session: + raise ValueError("$obj called by Evennia without Session. This is not supported.") + account = session.account + if not account: + raise ValueError("$obj requires a logged-in account session.") + targets = search.search_object(query) + + if return_list: + retlist = [] + for target in targets: + if target.access(account, target, 'control'): + retlist.append(target) + return retlist + else: + # single-match + if not targets: + raise ValueError("$obj: Query '{}' gave no matches.".format(query)) + if targets.count() > 1: + raise ValueError("$obj: Query '{query}' gave {nmatches} matches. Limit your " + "query or use $objlist instead.".format( + query=query, nmatches=targets.count())) + target = target[0] + if not target.access(account, target, 'control'): + raise ValueError("$obj: Obj {target}(#{dbref} cannot be added - " + "Account {account} does not have 'control' access.".format( + target=target.key, dbref=target.id, account=account)) + return target + + +def obj(*args, **kwargs): + """ + Usage $obj() + Returns one Object searched globally by key, alias or #dbref. Error if more than one. + + """ + return _obj_search(*args, **kwargs) + + +def objlist(*args, **kwargs): + """ + Usage $objlist() + Returns list with one or more Objects searched globally by key, alias or #dbref. + + """ + return _obj_search(return_list=True, *args, **kwargs) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 2e96af99c9..86230354b9 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -5,17 +5,17 @@ Handling storage of prototypes, both database-based ones (DBPrototypes) and thos """ +from ast import literal_eval from django.conf import settings - from evennia.scripts.scripts import DefaultScript from evennia.objects.models import ObjectDB from evennia.utils.create import create_script from evennia.utils.utils import ( - all_from_module, make_iter, is_iter, dbid_to_obj) + all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module) from evennia.locks.lockhandler import validate_lockstring, check_lockstring from evennia.utils import logger +from evennia.utils import inlinefuncs from evennia.utils.evtable import EvTable -from evennia.prototypes.protfuncs import protfunc_parser _MODULE_PROTOTYPE_MODULES = {} @@ -23,6 +23,7 @@ _MODULE_PROTOTYPES = {} _PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") _PROTOTYPE_TAG_CATEGORY = "from_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" +_PROT_FUNCS = {} class PermissionError(RuntimeError): @@ -36,6 +37,68 @@ class ValidationError(RuntimeError): pass +# Protfunc parsing + +for mod in settings.PROT_FUNC_MODULES: + try: + callables = callables_from_module(mod) + _PROT_FUNCS.update(callables) + except ImportError: + logger.log_trace() + raise + + +def protfunc_parser(value, available_functions=None, testing=False, **kwargs): + """ + Parse a prototype value string for a protfunc and process it. + + Available protfuncs are specified as callables in one of the modules of + `settings.PROTFUNC_MODULES`, or specified on the command line. + + Args: + value (any): The value to test for a parseable protfunc. Only strings will be parsed for + protfuncs, all other types are returned as-is. + available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. + testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may + behave differently. + + Kwargs: + session (Session): Passed to protfunc. Session of the entity spawning the prototype. + protototype (dict): Passed to protfunc. The dict this protfunc is a part of. + current_key(str): Passed to protfunc. The key in the prototype that will hold this value. + any (any): Passed on to the protfunc. + + Returns: + testresult (tuple): If `testing` is set, returns a tuple (error, result) where error is + either None or a string detailing the error from protfunc_parser or seen when trying to + run `literal_eval` on the parsed string. + any (any): A structure to replace the string on the prototype level. If this is a + callable or a (callable, (args,)) structure, it will be executed as if one had supplied + it to the prototype directly. This structure is also passed through literal_eval so one + can get actual Python primitives out of it (not just strings). It will also identify + eventual object #dbrefs in the output from the protfunc. + + """ + if not isinstance(value, basestring): + return value + available_functions = _PROT_FUNCS if available_functions is None else available_functions + result = inlinefuncs.parse_inlinefunc( + value, available_funcs=available_functions, testing=testing, **kwargs) + # at this point we have a string where all procfuncs were parsed + # print("parse_inlinefuncs(\"{}\", available_funcs={}) => {}".format(value, available_functions, result)) + result = value_to_obj_or_any(result) + err = None + try: + result = literal_eval(result) + except ValueError: + pass + except Exception as err: + err = str(err) + if testing: + return err, result + return result + + # helper functions def value_to_obj(value, force=True): diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index fa7eeca246..36be5f4c6b 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -167,7 +167,7 @@ class TestProtLib(EvenniaTest): pass -@override_settings(PROT_FUNC_MODULES=['evennia.prototypes.protfuncs']) +@override_settings(PROT_FUNC_MODULES=['evennia.prototypes.protfuncs'], CLIENT_DEFAULT_WIDTH=20) class TestProtFuncs(EvenniaTest): def setUp(self): @@ -176,11 +176,55 @@ class TestProtFuncs(EvenniaTest): "prototype_desc": "testing prot", "key": "ExampleObj"} - @mock.patch("random.random", new=mock.MagicMock(return_value=0.5)) - @mock.patch("random.randint", new=mock.MagicMock(return_value=5)) + @mock.patch("evennia.prototypes.protfuncs.base_random", new=mock.MagicMock(return_value=0.5)) + @mock.patch("evennia.prototypes.protfuncs.base_randint", new=mock.MagicMock(return_value=5)) def test_protfuncs(self): - self.assertEqual(protfuncs.protfunc_parser("$random()", 0.5)) - self.assertEqual(protfuncs.protfunc_parser("$randint(1, 10)", 5)) + self.assertEqual(protlib.protfunc_parser("$random()"), 0.5) + self.assertEqual(protlib.protfunc_parser("$randint(1, 10)"), 5) + self.assertEqual(protlib.protfunc_parser("$left_justify( foo )"), "foo ") + self.assertEqual(protlib.protfunc_parser("$right_justify( foo )"), " foo") + self.assertEqual(protlib.protfunc_parser("$center_justify(foo )"), " foo ") + self.assertEqual(protlib.protfunc_parser( + "$full_justify(foo bar moo too)"), 'foo bar moo too') + self.assertEqual( + protlib.protfunc_parser("$right_justify( foo )", testing=True), + ('unexpected indent (, line 1)', ' foo')) + + test_prot = {"key1": "value1", + "key2": 2} + + self.assertEqual(protlib.protfunc_parser( + "$protkey(key1)", testing=True, prototype=test_prot), (None, "value1")) + self.assertEqual(protlib.protfunc_parser( + "$protkey(key2)", testing=True, prototype=test_prot), (None, 2)) + + self.assertEqual(protlib.protfunc_parser("$add(1, 2)"), 3) + self.assertEqual(protlib.protfunc_parser("$add(10, 25)"), 35) + self.assertEqual(protlib.protfunc_parser( + "$add('''[1,2,3]''', '''[4,5,6]''')"), [1, 2, 3, 4, 5, 6]) + self.assertEqual(protlib.protfunc_parser("$add(foo, bar)"), "foo bar") + + self.assertEqual(protlib.protfunc_parser("$sub(5, 2)"), 3) + self.assertRaises(TypeError, protlib.protfunc_parser, "$sub(5, test)") + + self.assertEqual(protlib.protfunc_parser("$mult(5, 2)"), 10) + self.assertEqual(protlib.protfunc_parser("$mult( 5 , 10)"), 50) + self.assertEqual(protlib.protfunc_parser("$mult('foo',3)"), "foofoofoo") + self.assertEqual(protlib.protfunc_parser("$mult(foo,3)"), "foofoofoo") + self.assertRaises(TypeError, protlib.protfunc_parser, "$mult(foo, foo)") + + self.assertEqual(protlib.protfunc_parser("$toint(5.3)"), 5) + + self.assertEqual(protlib.protfunc_parser("$div(5, 2)"), 2.5) + self.assertEqual(protlib.protfunc_parser("$toint($div(5, 2))"), 2) + self.assertEqual(protlib.protfunc_parser("$sub($add(5, 3), $add(10, 2))"), -4) + + self.assertEqual(protlib.protfunc_parser("$eval('2')"), '2') + + self.assertEqual(protlib.protfunc_parser( + "$eval(['test', 1, '2', 3.5, \"foo\"])"), ['test', 1, '2', 3.5, 'foo']) + self.assertEqual(protlib.protfunc_parser( + "$eval({'test': '1', 2:3, 3: $toint(3.5)})"), {'test': '1', 2: 3, 3: 3}) class TestPrototypeStorage(EvenniaTest): diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index 4becfb7b01..f60f9f0d8a 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -161,13 +161,15 @@ def clr(*args, **kwargs): def null(*args, **kwargs): return args[0] if args else '' +_INLINE_FUNCS = {} + # we specify a default nomatch function to use if no matching func was # found. This will be overloaded by any nomatch function defined in # the imported modules. -_INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "", - "stackfull": lambda *args, **kwargs: "\n (not parsed: " - "inlinefunc stack size exceeded.)"} +_DEFAULT_FUNCS = {"nomatch": lambda *args, **kwargs: "", + "stackfull": lambda *args, **kwargs: "\n (not parsed: "} +_INLINE_FUNCS.update(_DEFAULT_FUNCS) # load custom inline func modules. for module in utils.make_iter(settings.INLINEFUNC_MODULES): @@ -285,6 +287,11 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): if not available_funcs: available_funcs = _INLINE_FUNCS usecache = True + else: + # make sure the default keys are available, but also allow overriding + tmp = _DEFAULT_FUNCS.copy() + tmp.update(available_funcs) + available_funcs = tmp if usecache and string in _PARSING_CACHE: # stack is already cached @@ -299,9 +306,14 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): # process string on stack ncallable = 0 nlparens = 0 + + # print("STRING: {} =>".format(string)) + for match in _RE_TOKEN.finditer(string): gdict = match.groupdict() - # print("match: {}".format({key: val for key, val in gdict.items() if val})) + + # print(" MATCH: {}".format({key: val for key, val in gdict.items() if val})) + if gdict["singlequote"]: stack.append(gdict["singlequote"]) elif gdict["doublequote"]: @@ -386,10 +398,10 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): kwargs["inlinefunc_stack_depth"] = depth retval = "" if strip else func(*args, **kwargs) return utils.to_str(retval, force_string=True) - - # print("STACK:\n{}".format(stack)) + retval = "".join(_run_stack(item) for item in stack) + # print("STACK: \n{} => {}\n".format(stack, retval)) # execute the stack - return "".join(_run_stack(item) for item in stack) + return retval # # Nick templating diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 22d59a165f..3d07a82e9a 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -43,8 +43,6 @@ _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ -_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH - def is_iter(iterable): """ @@ -80,7 +78,7 @@ def make_iter(obj): return not hasattr(obj, '__iter__') and [obj] or obj -def wrap(text, width=_DEFAULT_WIDTH, indent=0): +def wrap(text, width=None, indent=0): """ Safely wrap text to a certain number of characters. @@ -93,6 +91,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0): text (str): Properly wrapped text. """ + width = width if width else settings.CLIENT_DEFAULT_WIDTH if not text: return "" text = to_unicode(text) @@ -104,7 +103,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0): fill = wrap -def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "): +def pad(text, width=None, align="c", fillchar=" "): """ Pads to a given width. @@ -119,6 +118,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "): text (str): The padded text. """ + width = width if width else settings.CLIENT_DEFAULT_WIDTH align = align if align in ('c', 'l', 'r') else 'c' fillchar = fillchar[0] if fillchar else " " if align == 'l': @@ -129,7 +129,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "): return text.center(width, fillchar) -def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"): +def crop(text, width=None, suffix="[...]"): """ Crop text to a certain width, throwing away text from too-long lines. @@ -147,7 +147,7 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"): text (str): The cropped text. """ - + width = width if width else settings.CLIENT_DEFAULT_WIDTH utext = to_unicode(text) ltext = len(utext) if ltext <= width: @@ -179,7 +179,7 @@ def dedent(text): return textwrap.dedent(text) -def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0): +def justify(text, width=None, align="f", indent=0): """ Fully justify a text so that it fits inside `width`. When using full justification (default) this will be done by padding between @@ -198,6 +198,7 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0): justified (str): The justified and indented block of text. """ + width = width if width else settings.CLIENT_DEFAULT_WIDTH def _process_line(line): """ From 0100a7597773d3254b2681d45df4ac8a74414cad Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 19 Jun 2018 21:44:20 +0200 Subject: [PATCH 068/103] Testing obj conversion in profuncs --- evennia/prototypes/protfuncs.py | 33 ++++++++++++++++---------------- evennia/prototypes/prototypes.py | 23 ++++++++++++++++++++++ evennia/prototypes/tests.py | 2 ++ 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index 5ecb4b5e7d..4c9d9a4a5f 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -248,27 +248,21 @@ def eval(*args, **kwargs): if isinstance(struct, basestring): # we must shield the string, otherwise it will be merged as a string and future - # literal_evals will pick up e.g. '2' as something that should be converted to a number + # literal_evas will pick up e.g. '2' as something that should be converted to a number struct = '"{}"'.format(struct) - def _recursive_parse(val): - # an extra round of recursive parsing after literal_eval, to catch any - # escaped $$profuncs. This is commonly useful for object references. - if is_iter(val): - stype = type(val) - if stype == dict: - return {_recursive_parse(key): _recursive_parse(v) for key, v in val.items()} - return stype((_recursive_parse(v) for v in val)) - return _PROTLIB.protfunc_parser(val) + # convert any #dbrefs to objects (also in nested structures) + struct = _PROTLIB.value_to_obj_or_any(struct) - return _recursive_parse(struct) + return struct -def _obj_search(return_list=False, *args, **kwargs): +def _obj_search(*args, **kwargs): "Helper function to search for an object" query = "".join(args) session = kwargs.get("session", None) + return_list = kwargs.pop("return_list", False) if not session: raise ValueError("$obj called by Evennia without Session. This is not supported.") @@ -277,6 +271,8 @@ def _obj_search(return_list=False, *args, **kwargs): raise ValueError("$obj requires a logged-in account session.") targets = search.search_object(query) + print("targets: {}".format(targets)) + if return_list: retlist = [] for target in targets: @@ -287,11 +283,11 @@ def _obj_search(return_list=False, *args, **kwargs): # single-match if not targets: raise ValueError("$obj: Query '{}' gave no matches.".format(query)) - if targets.count() > 1: + if len(targets) > 1: raise ValueError("$obj: Query '{query}' gave {nmatches} matches. Limit your " "query or use $objlist instead.".format( - query=query, nmatches=targets.count())) - target = target[0] + query=query, nmatches=len(targets))) + target = targets[0] if not target.access(account, target, 'control'): raise ValueError("$obj: Obj {target}(#{dbref} cannot be added - " "Account {account} does not have 'control' access.".format( @@ -305,7 +301,10 @@ def obj(*args, **kwargs): Returns one Object searched globally by key, alias or #dbref. Error if more than one. """ - return _obj_search(*args, **kwargs) + obj = _obj_search(return_list=False, *args, **kwargs) + if obj: + return "#{}".format(obj.id) + return "".join(args) def objlist(*args, **kwargs): @@ -314,4 +313,4 @@ def objlist(*args, **kwargs): Returns list with one or more Objects searched globally by key, alias or #dbref. """ - return _obj_search(return_list=True, *args, **kwargs) + return ["#{}".format(obj.id) for obj in _obj_search(return_list=True, *args, **kwargs)] diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 86230354b9..81bb4188a2 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -5,6 +5,7 @@ Handling storage of prototypes, both database-based ones (DBPrototypes) and thos """ +import re from ast import literal_eval from django.conf import settings from evennia.scripts.scripts import DefaultScript @@ -26,6 +27,9 @@ _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" _PROT_FUNCS = {} +_RE_DBREF = re.compile(r"(? {}".format(value, available_functions, result)) result = value_to_obj_or_any(result) @@ -102,10 +111,24 @@ def protfunc_parser(value, available_functions=None, testing=False, **kwargs): # helper functions def value_to_obj(value, force=True): + "Always convert value(s) to Object, or None" + stype = type(value) + if is_iter(value): + if stype == dict: + return {value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.iter()} + else: + return stype([value_to_obj_or_any(val) for val in value]) return dbid_to_obj(value, ObjectDB) def value_to_obj_or_any(value): + "Convert value(s) to Object if possible, otherwise keep original value" + stype = type(value) + if is_iter(value): + if stype == dict: + return {value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.items()} + else: + return stype([value_to_obj_or_any(val) for val in value]) obj = dbid_to_obj(value, ObjectDB) return obj if obj is not None else value diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 36be5f4c6b..c49292bbf5 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -226,6 +226,8 @@ class TestProtFuncs(EvenniaTest): self.assertEqual(protlib.protfunc_parser( "$eval({'test': '1', 2:3, 3: $toint(3.5)})"), {'test': '1', 2: 3, 3: 3}) + self.assertEqual(protlib.protfunc_parser("$obj(#1)", session=self.session), '#1') + class TestPrototypeStorage(EvenniaTest): From 2fcf6d164026e5cfa803db85857e04d5af58b212 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 21 Jun 2018 22:42:50 +0200 Subject: [PATCH 069/103] Unittests pass for all protfuncs --- evennia/prototypes/protfuncs.py | 31 ++++++++++++++++--------------- evennia/prototypes/prototypes.py | 11 +++++------ evennia/prototypes/tests.py | 16 ++++++++++++++-- evennia/utils/inlinefuncs.py | 22 +++++++++++++++++----- 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index 4c9d9a4a5f..6dff62ef96 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -263,21 +263,21 @@ def _obj_search(*args, **kwargs): query = "".join(args) session = kwargs.get("session", None) return_list = kwargs.pop("return_list", False) + account = None + + if session: + account = session.account - if not session: - raise ValueError("$obj called by Evennia without Session. This is not supported.") - account = session.account - if not account: - raise ValueError("$obj requires a logged-in account session.") targets = search.search_object(query) - print("targets: {}".format(targets)) - if return_list: retlist = [] - for target in targets: - if target.access(account, target, 'control'): - retlist.append(target) + if account: + for target in targets: + if target.access(account, target, 'control'): + retlist.append(target) + else: + retlist = targets return retlist else: # single-match @@ -288,11 +288,12 @@ def _obj_search(*args, **kwargs): "query or use $objlist instead.".format( query=query, nmatches=len(targets))) target = targets[0] - if not target.access(account, target, 'control'): - raise ValueError("$obj: Obj {target}(#{dbref} cannot be added - " - "Account {account} does not have 'control' access.".format( - target=target.key, dbref=target.id, account=account)) - return target + if account: + if not target.access(account, target, 'control'): + raise ValueError("$obj: Obj {target}(#{dbref} cannot be added - " + "Account {account} does not have 'control' access.".format( + target=target.key, dbref=target.id, account=account)) + return target def obj(*args, **kwargs): diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 81bb4188a2..ac343b3ec6 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -27,7 +27,7 @@ _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" _PROT_FUNCS = {} -_RE_DBREF = re.compile(r"(? {}".format(value, available_functions, result)) - result = value_to_obj_or_any(result) err = None try: result = literal_eval(result) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index c49292bbf5..0eeb236fb2 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -5,10 +5,10 @@ Unit tests for the prototypes and spawner from random import randint import mock -from anything import Anything, Something +from anything import Something from django.test.utils import override_settings from evennia.utils.test_resources import EvenniaTest -from evennia.prototypes import spawner, prototypes as protlib, protfuncs +from evennia.prototypes import spawner, prototypes as protlib from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY @@ -227,6 +227,18 @@ class TestProtFuncs(EvenniaTest): "$eval({'test': '1', 2:3, 3: $toint(3.5)})"), {'test': '1', 2: 3, 3: 3}) self.assertEqual(protlib.protfunc_parser("$obj(#1)", session=self.session), '#1') + self.assertEqual(protlib.protfunc_parser("#1", session=self.session), '#1') + self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6') + self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6') + self.assertEqual(protlib.protfunc_parser("$objlist(#1)", session=self.session), ['#1']) + + self.assertEqual(protlib.value_to_obj( + protlib.protfunc_parser("#6", session=self.session)), self.char1) + self.assertEqual(protlib.value_to_obj_or_any( + protlib.protfunc_parser("#6", session=self.session)), self.char1) + self.assertEqual(protlib.value_to_obj_or_any( + protlib.protfunc_parser("[1,2,3,'#6',5]", session=self.session)), + [1, 2, 3, self.char1, 5]) class TestPrototypeStorage(EvenniaTest): diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index f60f9f0d8a..d62493c786 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -63,7 +63,8 @@ Error handling: import re import fnmatch from django.conf import settings -from evennia.utils import utils + +from evennia.utils import utils, logger # example/testing inline functions @@ -264,7 +265,7 @@ class InlinefuncError(RuntimeError): pass -def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): +def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False, **kwargs): """ Parse the incoming string. @@ -274,6 +275,7 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): execute them. available_funcs (dict, optional): Define an alternative source of functions to parse for. If unset, use the functions found through `settings.INLINEFUNC_MODULES`. + stacktrace (bool, optional): If set, print the stacktrace to log. Kwargs: session (Session): This is sent to this function by Evennia when triggering it. It is passed to the inlinefunc. @@ -307,12 +309,18 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): ncallable = 0 nlparens = 0 - # print("STRING: {} =>".format(string)) + if stacktrace: + out = "STRING: {} =>".format(string) + print(out) + logger.log_info(out) for match in _RE_TOKEN.finditer(string): gdict = match.groupdict() - # print(" MATCH: {}".format({key: val for key, val in gdict.items() if val})) + if stacktrace: + out = " MATCH: {}".format({key: val for key, val in gdict.items() if val}) + print(out) + logger.log_info(out) if gdict["singlequote"]: stack.append(gdict["singlequote"]) @@ -399,7 +407,11 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): retval = "" if strip else func(*args, **kwargs) return utils.to_str(retval, force_string=True) retval = "".join(_run_stack(item) for item in stack) - # print("STACK: \n{} => {}\n".format(stack, retval)) + if stacktrace: + out = "STACK: \n{} => {}\n".format(stack, retval) + print(out) + logger.log_info(out) + # execute the stack return retval From 19c9687f010e85427289c710c74ed309aa11ce7a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 24 Jun 2018 09:50:03 +0200 Subject: [PATCH 070/103] Rename prototype to prototype_parent, fixing olc menu --- evennia/commands/default/building.py | 4 +- evennia/prototypes/menus.py | 52 ++++++++------- evennia/prototypes/prototypes.py | 94 +++++++++++++++++++++------- evennia/prototypes/spawner.py | 25 ++++---- evennia/prototypes/tests.py | 36 +++++++++++ evennia/utils/tests/test_evmenu.py | 3 +- 6 files changed, 155 insertions(+), 59 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 301bd03761..5c96ad1cf6 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, get_all_typeclasses from evennia.utils.eveditor import EvEditor from evennia.utils.evmore import EvMore -from evennia.prototypes import spawner, prototypes as protlib +from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus from evennia.utils.ansi import raw COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -2917,7 +2917,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): elif prototype: # one match prototype = prototype[0] - spawner.start_olc(caller, session=self.session, prototype=prototype) + olc_menus.start_olc(caller, session=self.session, prototype=prototype) return if 'search' in self.switches: diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index bebc6d00bd..ead299abc7 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -9,8 +9,8 @@ from django.conf import settings from evennia.utils.evmenu import EvMenu, list_node from evennia.utils.ansi import strip_ansi from evennia.utils import utils -from evennia.utils.prototypes import prototypes as protlib -from evennia.utils.prototypes import spawner +from evennia.prototypes import prototypes as protlib +from evennia.prototypes import spawner # ------------------------------------------------------------ # @@ -43,12 +43,6 @@ def _is_new_prototype(caller): return hasattr(caller.ndb._menutree, "olc_new") -def _set_menu_prototype(caller, field, value): - prototype = _get_menu_prototype(caller) - prototype[field] = value - caller.ndb._menutree.olc_prototype = prototype - - def _format_property(prop, required=False, prototype=None, cropper=None): if prototype is not None: @@ -67,6 +61,13 @@ def _format_property(prop, required=False, prototype=None, cropper=None): return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH)) +def _set_prototype_value(caller, field, value): + prototype = _get_menu_prototype(caller) + prototype[field] = value + caller.ndb._menutree.olc_prototype = prototype + return prototype + + def _set_property(caller, raw_string, **kwargs): """ Update a property. To be called by the 'goto' option variable. @@ -102,22 +103,26 @@ def _set_property(caller, raw_string, **kwargs): if not value: return next_node - prototype = _get_menu_prototype(caller) + prototype = _set_prototype_value(caller, "prototype_key", value) - # typeclass and prototype can't co-exist + # typeclass and prototype_parent can't co-exist if propname_low == "typeclass": - prototype.pop("prototype", None) - if propname_low == "prototype": + prototype.pop("prototype_parent", None) + if propname_low == "prototype_parent": prototype.pop("typeclass", None) caller.ndb._menutree.olc_prototype = prototype - caller.msg("Set {prop} to '{value}'.".format(prop, value=str(value))) + caller.msg("Set {prop} to '{value}'.".format(prop=prop, value=str(value))) return next_node def _wizard_options(curr_node, prev_node, next_node, color="|W"): + """ + Creates default navigation options available in the wizard. + + """ options = [] if prev_node: options.append({"key": ("|wb|Wack", "b"), @@ -154,8 +159,8 @@ def node_index(caller): text = ("|c --- Prototype wizard --- |n\n\n" "Define the |yproperties|n of the prototype. All prototype values can be " "over-ridden at the time of spawning an instance of the prototype, but some are " - "required.\n\n'|wMeta'-properties|n are not used in the prototype itself but are used " - "to organize and list prototypes. The 'Meta-Key' uniquely identifies the prototype " + "required.\n\n'|wprototype-'-properties|n are not used in the prototype itself but are used " + "to organize and list prototypes. The 'prototype-key' uniquely identifies the prototype " "and allows you to edit an existing prototype or save a new one for use by you or " "others later.\n\n(make choice; q to abort. If unsure, start from 1.)") @@ -192,9 +197,12 @@ def node_validate_prototype(caller, raw_string, **kwargs): errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)" try: # validate, don't spawn - spawner.spawn(prototype, return_prototypes=True) + spawner.spawn(prototype, only_validate=True) except RuntimeError as err: - errors = "\n\n|rError: {}|n".format(err) + errors = "\n\n|r{}|n".format(err) + except RuntimeWarning as err: + errors = "\n\n|y{}|n".format(err) + text = (txt + errors) options = _wizard_options(None, kwargs.get("back"), None) @@ -287,7 +295,9 @@ def node_prototype(caller): def _all_typeclasses(caller): - return list(sorted(utils.get_all_typeclasses().keys())) + return list(name for name in + sorted(utils.get_all_typeclasses("evennia.objects.models.ObjectDB").keys()) + if name != "evennia.objects.models.ObjectDB") def _typeclass_examine(caller, typeclass_path): @@ -403,7 +413,7 @@ def _add_attr(caller, attr_string, **kwargs): if attrname: prot = _get_menu_prototype(caller) prot['attrs'][attrname] = value - _set_menu_prototype(caller, "prototype", prot) + _set_prototype_value(caller, "prototype", prot) text = "Added" else: text = "Attribute must be given as 'attrname = ' where uses valid Python." @@ -468,7 +478,7 @@ def _add_tag(caller, tag, **kwargs): else: tags = [tag] prototype['tags'] = tags - _set_menu_prototype(caller, "prototype", prototype) + _set_prototype_value(caller, "prototype", prototype) text = kwargs.get("text") if not text: text = "Added tag {}. (return to continue)".format(tag) @@ -485,7 +495,7 @@ def _edit_tag(caller, old_tag, new_tag, **kwargs): new_tag = new_tag.strip().lower() tags[tags.index(old_tag)] = new_tag prototype['tags'] = tags - _set_menu_prototype(caller, 'prototype', prototype) + _set_prototype_value(caller, 'prototype', prototype) text = kwargs.get('text') if not text: diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index ac343b3ec6..18516681b2 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -12,7 +12,8 @@ from evennia.scripts.scripts import DefaultScript from evennia.objects.models import ObjectDB from evennia.utils.create import create_script from evennia.utils.utils import ( - all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module) + all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module, + get_all_typeclasses) from evennia.locks.lockhandler import validate_lockstring, check_lockstring from evennia.utils import logger from evennia.utils import inlinefuncs @@ -143,10 +144,10 @@ def prototype_to_str(prototype): header = ( "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" "|cdesc:|n {} \n|cprototype:|n ".format( - prototype['prototype_key'], - ", ".join(prototype['prototype_tags']), - prototype['prototype_locks'], - prototype['prototype_desc'])) + prototype.get('prototype_key', None), + ", ".join(prototype.get('prototype_tags', ['None'])), + prototype.get('prototype_locks', None), + prototype.get('prototype_desc', None))) proto = ("{{\n {} \n}}".format( "\n ".join( "{!r}: {!r},".format(key, value) for key, value in @@ -513,7 +514,8 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed return table -def validate_prototype(prototype, protkey=None, protparents=None, _visited=None): +def validate_prototype(prototype, protkey=None, protparents=None, + is_prototype_base=True, _flags=None): """ Run validation on a prototype, checking for inifinite regress. @@ -523,33 +525,77 @@ def validate_prototype(prototype, protkey=None, protparents=None, _visited=None) dict needs to have the `prototype_key` field set. protpartents (dict, optional): The available prototype parent library. If note given this will be determined from settings/database. - _visited (list, optional): This is an internal work array and should not be set manually. + is_prototype_base (bool, optional): We are trying to create a new object *based on this + object*. This means we can't allow 'mixin'-style prototypes without typeclass/parent + etc. + _flags (dict, optional): Internal work dict that should not be set externally. Raises: RuntimeError: If prototype has invalid structure. + RuntimeWarning: If prototype has issues that would make it unsuitable to build an object + with (it may still be useful as a mix-in prototype). """ + assert isinstance(prototype, dict) + + if _flags is None: + _flags = {"visited": [], "depth": 0, "typeclass": False, "errors": [], "warnings": []} + if not protparents: protparents = {prototype['prototype_key']: prototype for prototype in search_prototype()} - if _visited is None: - _visited = [] protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) - assert isinstance(prototype, dict) + if not bool(protkey): + _flags['errors'].append("Prototype lacks a `prototype_key`.") + protkey = "[UNSET]" - if id(prototype) in _visited: - raise RuntimeError("%s has infinite nesting of prototypes." % protkey or prototype) + typeclass = prototype.get('typeclass') + prototype_parent = prototype.get('prototype_parent', []) - _visited.append(id(prototype)) - protstrings = prototype.get("prototype") + if not (typeclass or prototype_parent): + if is_prototype_base: + _flags['errors'].append("Prototype {} requires `typeclass` " + "or 'prototype_parent'.".format(protkey)) + else: + _flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks " + "a typeclass or a prototype_parent.".format(protkey)) - if protstrings: - for protstring in make_iter(protstrings): - protstring = protstring.lower() - if protkey is not None and protstring == protkey: - raise RuntimeError("%s tries to prototype itself." % protkey or prototype) - protparent = protparents.get(protstring) - if not protparent: - raise RuntimeError( - "%s's prototype '%s' was not found." % (protkey or prototype, protstring)) - validate_prototype(protparent, protstring, protparents, _visited) + if typeclass and typeclass not in get_all_typeclasses("evennia.objects.models.ObjectDB"): + _flags['errors'].append( + "Prototype {} is based on typeclass {} which could not be imported!".format( + protkey, typeclass)) + + # recursively traverese prototype_parent chain + + if id(prototype) in _flags['visited']: + _flags['errors'].append( + "{} has infinite nesting of prototypes.".format(protkey or prototype)) + + _flags['visited'].append(id(prototype)) + + for protstring in make_iter(prototype_parent): + protstring = protstring.lower() + if protkey is not None and protstring == protkey: + _flags['errors'].append("Protototype {} tries to parent itself.".format(protkey)) + protparent = protparents.get(protstring) + if not protparent: + _flags['errors'].append("Prototype {}'s prototype_parent '{}' was not found.".format( + (protkey, protstring))) + _flags['depth'] += 1 + validate_prototype(protparent, protstring, protparents, _flags) + _flags['depth'] -= 1 + + if typeclass and not _flags['typeclass']: + _flags['typeclass'] = typeclass + + # if we get back to the current level without a typeclass it's an error. + if is_prototype_base and _flags['depth'] <= 0 and not _flags['typeclass']: + _flags['errors'].append("Prototype {} has no `typeclass` defined anywhere in its parent " + "chain. Add `typeclass`, or a `prototype_parent` pointing to a " + "prototype with a typeclass.".format(protkey)) + + if _flags['depth'] <= 0: + if _flags['errors']: + raise RuntimeError("Error: " + "\nError: ".join(_flags['errors'])) + if _flags['warnings']: + raise RuntimeWarning("Warning: " + "\nWarning: ".join(_flags['warnings'])) diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index da4d69eeb4..df07e3b155 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -32,7 +32,7 @@ Possible keywords are: prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype in listings - parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or + prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or a list of parents, for multiple left-to-right inheritance. prototype: Deprecated. Same meaning as 'parent'. typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use @@ -75,13 +75,13 @@ import random GOBLIN_WIZARD = { - "parent": GOBLIN, + "prototype_parent": GOBLIN, "key": "goblin wizard", "spells": ["fire ball", "lighting bolt"] } GOBLIN_ARCHER = { - "parent": GOBLIN, + "prototype_parent": GOBLIN, "key": "goblin archer", "attack_skill": (random, (5, 10))" "attacks": ["short bow"] @@ -97,7 +97,7 @@ ARCHWIZARD = { GOBLIN_ARCHWIZARD = { "key" : "goblin archwizard" - "parent": (GOBLIN_WIZARD, ARCHWIZARD), + "prototype_parent": (GOBLIN_WIZARD, ARCHWIZARD), } ``` @@ -460,11 +460,15 @@ def spawn(*prototypes, **kwargs): prototype_parents (dict): A dictionary holding a custom prototype-parent dictionary. Will overload same-named prototypes from prototype_modules. - return_prototypes (bool): Only return a list of the + return_parents (bool): Only return a dict of the prototype-parents (no object creation happens) + only_validate (bool): Only run validation of prototype/parents + (no object creation) and return the create-kwargs. Returns: - object (Object): Spawned object. + object (Object, dict or list): Spawned object. If `only_validate` is given, return + a list of the creation kwargs to build the object(s) without actually creating it. If + `return_parents` is set, return dict of prototype parents. """ # get available protparents @@ -474,17 +478,14 @@ def spawn(*prototypes, **kwargs): protparents.update( {key.lower(): value for key, value in kwargs.get("prototype_parents", {}).items()}) - for key, prototype in protparents.items(): - protlib.validate_prototype(prototype, key.lower(), protparents) - - if "return_prototypes" in kwargs: + if "return_parents" in kwargs: # only return the parents return copy.deepcopy(protparents) objsparams = [] for prototype in prototypes: - protlib.validate_prototype(prototype, None, protparents) + protlib.validate_prototype(prototype, None, protparents, is_prototype_base=True) prot = _get_prototype(prototype, {}, protparents) if not prot: continue @@ -556,4 +557,6 @@ def spawn(*prototypes, **kwargs): objsparams.append((create_kwargs, permission_string, lock_string, alias_string, nattributes, attributes, tags, execs)) + if kwargs.get("only_validate"): + return objsparams return batch_create_object(*objsparams) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 0eeb236fb2..0f48c3780a 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -8,7 +8,9 @@ import mock from anything import Something from django.test.utils import override_settings 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.prototypes import _PROTOTYPE_TAG_META_CATEGORY @@ -304,3 +306,37 @@ class TestPrototypeStorage(EvenniaTest): self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3]) self.assertTrue(str(unicode(protlib.list_prototypes(self.char1)))) + + +@mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock( + return_value=[{"prototype_key": "TestPrototype", + "typeclass": "TypeClassTest", "key": "TestObj"}])) +@mock.patch("evennia.utils.utils.get_all_typeclasses", new=mock.MagicMock( + return_value={"TypeclassTest": None})) +class TestOLCMenu(TestEvMenu): + + maxDiff = None + menutree = "evennia.prototypes.menus" + startnode = "node_index" + + debug_output = True + + expected_node_texts = { + "node_index": "|c --- Prototype wizard --- |n" + } + + expected_tree = \ + ['node_index', + ['node_prototype_key', + 'node_typeclass', + 'node_aliases', + 'node_attrs', + 'node_tags', + 'node_locks', + 'node_permissions', + 'node_location', + 'node_home', + 'node_destination', + 'node_prototype_desc', + 'node_prototype_tags', + 'node_prototype_locks']] diff --git a/evennia/utils/tests/test_evmenu.py b/evennia/utils/tests/test_evmenu.py index 04310c90ed..d3ee14a74f 100644 --- a/evennia/utils/tests/test_evmenu.py +++ b/evennia/utils/tests/test_evmenu.py @@ -58,7 +58,7 @@ class TestEvMenu(TestCase): def _debug_output(self, indent, msg): if self.debug_output: - print(" " * indent + msg) + print(" " * indent + ansi.strip_ansi(msg)) def _test_menutree(self, menu): """ @@ -168,6 +168,7 @@ class TestEvMenu(TestCase): self.caller2.msg = MagicMock() self.session = MagicMock() self.session2 = MagicMock() + self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode, cmdset_mergetype=self.cmdset_mergetype, cmdset_priority=self.cmdset_priority, From 952a5a1ee3bac67216c8001d38758aa1fe48216f Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 24 Jun 2018 16:03:48 +0200 Subject: [PATCH 071/103] Unit testing/debugging olc menu --- evennia/prototypes/menus.py | 94 ++++++++++++++++++++++---------- evennia/prototypes/prototypes.py | 6 +- evennia/prototypes/tests.py | 86 +++++++++++++++++++++++++++++ evennia/utils/inlinefuncs.py | 16 +++++- 4 files changed, 171 insertions(+), 31 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index ead299abc7..ff38c3448e 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -29,7 +29,7 @@ _MENU_ATTR_LITERAL_EVAL_ERROR = ( def _get_menu_prototype(caller): - + """Return currently active menu prototype.""" prototype = None if hasattr(caller.ndb._menutree, "olc_prototype"): prototype = caller.ndb._menutree.olc_prototype @@ -40,11 +40,23 @@ def _get_menu_prototype(caller): def _is_new_prototype(caller): + """Check if prototype is marked as new or was loaded from a saved one.""" return hasattr(caller.ndb._menutree, "olc_new") -def _format_property(prop, required=False, prototype=None, cropper=None): +def _format_option_value(prop, required=False, prototype=None, cropper=None): + """ + Format wizard option values. + Args: + prop (str): Name or value to format. + required (bool, optional): The option is required. + prototype (dict, optional): If given, `prop` will be considered a key in this prototype. + cropper (callable, optional): A function to crop the value to a certain width. + + Returns: + value (str): The formatted value. + """ if prototype is not None: prop = prototype.get(prop, '') @@ -61,7 +73,8 @@ def _format_property(prop, required=False, prototype=None, cropper=None): return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH)) -def _set_prototype_value(caller, field, value): +def _set_prototype_value(caller, field, value, parse=True): + """Set prototype's field in a safe way.""" prototype = _get_menu_prototype(caller) prototype[field] = value caller.ndb._menutree.olc_prototype = prototype @@ -70,15 +83,21 @@ def _set_prototype_value(caller, field, value): def _set_property(caller, raw_string, **kwargs): """ - Update a property. To be called by the 'goto' option variable. + Add or update a property. To be called by the 'goto' option variable. Args: caller (Object, Account): The user of the wizard. raw_string (str): Input from user on given node - the new value to set. + Kwargs: + test_parse (bool): If set (default True), parse raw_string for protfuncs and obj-refs and + try to run result through literal_eval. The parser will be run in 'testing' mode and any + parsing errors will shown to the user. Note that this is just for testing, the original + given string will be what is inserted. prop (str): Property name to edit with `raw_string`. processor (callable): Converts `raw_string` to a form suitable for saving. next_node (str): Where to redirect to after this has run. + Returns: next_node (str): Next node to go to. @@ -103,7 +122,7 @@ def _set_property(caller, raw_string, **kwargs): if not value: return next_node - prototype = _set_prototype_value(caller, "prototype_key", value) + prototype = _set_prototype_value(caller, prop, value) # typeclass and prototype_parent can't co-exist if propname_low == "typeclass": @@ -113,16 +132,26 @@ def _set_property(caller, raw_string, **kwargs): caller.ndb._menutree.olc_prototype = prototype - caller.msg("Set {prop} to '{value}'.".format(prop=prop, value=str(value))) + out = [" Set {prop} to {value} ({typ}).".format(prop=prop, value=value, typ=type(value))] + + if kwargs.get("test_parse", True): + out.append(" Simulating parsing ...") + err, parsed_value = protlib.protfunc_parser(value, testing=True) + if err: + out.append(" |yPython `literal_eval` warning: {}|n".format(err)) + if parsed_value != value: + out.append(" |g(Example-)value when parsed ({}):|n {}".format( + type(parsed_value), parsed_value)) + else: + out.append(" |gNo change.") + + caller.msg("\n".join(out)) return next_node def _wizard_options(curr_node, prev_node, next_node, color="|W"): - """ - Creates default navigation options available in the wizard. - - """ + """Creates default navigation options available in the wizard.""" options = [] if prev_node: options.append({"key": ("|wb|Wack", "b"), @@ -166,7 +195,7 @@ def node_index(caller): options = [] options.append( - {"desc": "|WPrototype-Key|n|n{}".format(_format_property("Key", True, prototype, None)), + {"desc": "|WPrototype-Key|n|n{}".format(_format_option_value("Key", True, prototype, None)), "goto": "node_prototype_key"}) for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', 'Permissions', 'Location', 'Home', 'Destination'): @@ -178,13 +207,13 @@ def node_index(caller): cropper = _path_cropper options.append( {"desc": "|w{}|n{}".format( - key, _format_property(key, required, prototype, cropper=cropper)), + key, _format_option_value(key, required, prototype, cropper=cropper)), "goto": "node_{}".format(key.lower())}) required = False for key in ('Desc', 'Tags', 'Locks'): options.append( {"desc": "|WPrototype-{}|n|n{}".format( - key, _format_property(key, required, prototype, None)), + key, _format_option_value(key, required, prototype, None)), "goto": "node_prototype_{}".format(key.lower())}) return text, options @@ -215,6 +244,7 @@ def _check_prototype_key(caller, key): olc_new = _is_new_prototype(caller) key = key.strip().lower() if old_prototype: + old_prototype = old_prototype[0] # we are starting a new prototype that matches an existing if not caller.locks.check_lockstring( caller, old_prototype['prototype_locks'], access_type='edit'): @@ -229,7 +259,7 @@ def _check_prototype_key(caller, key): caller.msg("Prototype already exists. Reloading.") return "node_index" - return _set_property(caller, key, prop='prototype_key', next_node="node_prototype") + return _set_property(caller, key, prop='prototype_key', next_node="node_prototype_parent") def node_prototype_key(caller): @@ -250,27 +280,32 @@ def node_prototype_key(caller): return text, options -def _all_prototypes(caller): +def _all_prototype_parents(caller): + """Return prototype_key of all available prototypes for listing in menu""" return [prototype["prototype_key"] for prototype in protlib.search_prototype() if "prototype_key" in prototype] -def _prototype_examine(caller, prototype_name): +def _prototype_parent_examine(caller, prototype_name): + """Convert prototype to a string representation for closer inspection""" prototypes = protlib.search_prototype(key=prototype_name) if prototypes: - caller.msg(protlib.prototype_to_str(prototypes[0])) - caller.msg("Prototype not registered.") - return None + ret = protlib.prototype_to_str(prototypes[0]) + caller.msg(ret) + return ret + else: + caller.msg("Prototype not registered.") -def _prototype_select(caller, prototype): - ret = _set_property(caller, prototype, prop="prototype", processor=str, next_node="node_key") +def _prototype_parent_select(caller, prototype): + ret = _set_property(caller, prototype['prototype_key'], + prop="prototype_parent", processor=str, next_node="node_key") caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype)) return ret -@list_node(_all_prototypes, _prototype_select) -def node_prototype(caller): +@list_node(_all_prototype_parents, _prototype_parent_select) +def node_prototype_parent(caller): prototype = _get_menu_prototype(caller) prot_parent_key = prototype.get('prototype') @@ -289,18 +324,20 @@ def node_prototype(caller): text = "\n\n".join(text) options = _wizard_options("prototype", "prototype_key", "typeclass", color="|W") options.append({"key": "_default", - "goto": _prototype_examine}) + "goto": _prototype_parent_examine}) return text, options def _all_typeclasses(caller): + """Get name of available typeclasses.""" return list(name for name in sorted(utils.get_all_typeclasses("evennia.objects.models.ObjectDB").keys()) if name != "evennia.objects.models.ObjectDB") def _typeclass_examine(caller, typeclass_path): + """Show info (docstring) about given typeclass.""" if typeclass_path is None: # this means we are exiting the listing return "node_key" @@ -319,10 +356,11 @@ def _typeclass_examine(caller, typeclass_path): else: txt = "This is typeclass |y{}|n.".format(typeclass) caller.msg(txt) - return None + return txt def _typeclass_select(caller, typeclass): + """Select typeclass from list and add it to prototype. Return next node to go to.""" ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key") caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass)) return ret @@ -350,7 +388,7 @@ def node_key(caller): prototype = _get_menu_prototype(caller) key = prototype.get("key") - text = ["Set the prototype's |yKey|n. This will retain case sensitivity."] + text = ["Set the prototype's name (|yKey|n.) This will retain case sensitivity."] if key: text.append("Current key value is '|y{key}|n'.".format(key=key)) else: @@ -370,7 +408,7 @@ def node_aliases(caller): aliases = prototype.get("aliases") text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. " - "ill retain case sensitivity."] + "they'll retain case sensitivity."] if aliases: text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases)) else: @@ -714,7 +752,7 @@ def start_olc(caller, session=None, prototype=None): menudata = {"node_index": node_index, "node_validate_prototype": node_validate_prototype, "node_prototype_key": node_prototype_key, - "node_prototype": node_prototype, + "node_prototype_parent": node_prototype_parent, "node_typeclass": node_typeclass, "node_key": node_key, "node_aliases": node_aliases, diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 18516681b2..2ab3416afe 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -13,7 +13,7 @@ from evennia.objects.models import ObjectDB from evennia.utils.create import create_script from evennia.utils.utils import ( all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module, - get_all_typeclasses) + get_all_typeclasses, to_str) from evennia.locks.lockhandler import validate_lockstring, check_lockstring from evennia.utils import logger from evennia.utils import inlinefuncs @@ -64,6 +64,7 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F value (any): The value to test for a parseable protfunc. Only strings will be parsed for protfuncs, all other types are returned as-is. available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. + If not set, use default sources. testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may behave differently. stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser. @@ -86,7 +87,8 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F """ if not isinstance(value, basestring): - return value + value = to_str(value, force_string=True) + available_functions = _PROT_FUNCS if available_functions is None else available_functions # insert $obj(#dbref) for #dbref diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 0f48c3780a..49624905c7 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -308,6 +308,91 @@ class TestPrototypeStorage(EvenniaTest): self.assertTrue(str(unicode(protlib.list_prototypes(self.char1)))) +class _MockMenu(object): + pass + + +class TestMenuModule(EvenniaTest): + + def setUp(self): + super(TestMenuModule, self).setUp() + + # set up fake store + self.caller = self.char1 + menutree = _MockMenu() + self.caller.ndb._menutree = menutree + + self.test_prot = {"prototype_key": "test_prot", + "prototype_locks": "edit:all();spawn:all()"} + + def test_helpers(self): + + caller = self.caller + + # general helpers + + self.assertEqual(olc_menus._get_menu_prototype(caller), {}) + self.assertEqual(olc_menus._is_new_prototype(caller), True) + + self.assertEqual( + olc_menus._set_prototype_value(caller, "key", "TestKey"), {"key": "TestKey"}) + self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "TestKey"}) + + self.assertEqual(olc_menus._format_option_value( + "key", required=True, prototype=olc_menus._get_menu_prototype(caller)), " (TestKey|n)") + self.assertEqual(olc_menus._format_option_value( + [1, 2, 3, "foo"], required=True), ' (1, 2, 3, foo|n)') + + self.assertEqual(olc_menus._set_property( + caller, "ChangedKey", prop="key", processor=str, next_node="foo"), "foo") + self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "ChangedKey"}) + + self.assertEqual(olc_menus._wizard_options( + "ThisNode", "PrevNode", "NextNode"), + [{'goto': 'node_PrevNode', 'key': ('|wb|Wack', 'b'), 'desc': '|W(PrevNode)|n'}, + {'goto': 'node_NextNode', 'key': ('|wf|Worward', 'f'), 'desc': '|W(NextNode)|n'}, + {'goto': 'node_index', 'key': ('|wi|Wndex', 'i')}, + {'goto': ('node_validate_prototype', {'back': 'ThisNode'}), + 'key': ('|wv|Walidate prototype', 'v')}]) + + def test_node_helpers(self): + + caller = self.caller + + with mock.patch("evennia.prototypes.menus.protlib.search_prototype", + new=mock.MagicMock(return_value=[self.test_prot])): + self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), + "node_prototype_parent") + caller.ndb._menutree.olc_new = True + self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), + "node_index") + + self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot']) + self.assertEqual(olc_menus._prototype_parent_examine( + caller, 'test_prot'), + '|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() ' + '\n|cdesc:|n None \n|cprototype:|n {\n \n}') + self.assertEqual(olc_menus._prototype_parent_select(caller, self.test_prot), "node_key") + self.assertEqual(olc_menus._get_menu_prototype(caller), + {'prototype_key': 'test_prot', + 'prototype_locks': 'edit:all();spawn:all()', + 'prototype_parent': "test_prot"}) + + with mock.patch("evennia.utils.utils.get_all_typeclasses", + new=mock.MagicMock(return_value={"foo": None, "bar": None})): + self.assertEqual(olc_menus._all_typeclasses(caller), ["bar", "foo"]) + self.assertTrue(olc_menus._typeclass_examine( + caller, "evennia.objects.objects.DefaultObject").startswith("Typeclass |y")) + + self.assertEqual(olc_menus._typeclass_select( + caller, "evennia.objects.objects.DefaultObject"), "node_key") + # prototype_parent should be popped off here + self.assertEqual(olc_menus._get_menu_prototype(caller), + {'prototype_key': 'test_prot', + 'prototype_locks': 'edit:all();spawn:all()', + 'typeclass': 'evennia.objects.objects.DefaultObject'}) + + @mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock( return_value=[{"prototype_key": "TestPrototype", "typeclass": "TypeClassTest", "key": "TestObj"}])) @@ -320,6 +405,7 @@ class TestOLCMenu(TestEvMenu): startnode = "node_index" debug_output = True + expect_all_nodes = True expected_node_texts = { "node_index": "|c --- Prototype wizard --- |n" diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index d62493c786..85ceeadc8a 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -162,6 +162,20 @@ def clr(*args, **kwargs): def null(*args, **kwargs): return args[0] if args else '' + +def nomatch(name, *args, **kwargs): + """ + Default implementation of nomatch returns the function as-is as a string. + + """ + kwargs.pop("inlinefunc_stack_depth", None) + kwargs.pop("session") + + return "${name}({args}{kwargs})".format( + name=name, + args=",".join(args), + kwargs=",".join("{}={}".format(key, val) for key, val in kwargs.items())) + _INLINE_FUNCS = {} # we specify a default nomatch function to use if no matching func was @@ -284,7 +298,6 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False """ global _PARSING_CACHE - usecache = False if not available_funcs: available_funcs = _INLINE_FUNCS @@ -357,6 +370,7 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False except KeyError: stack.append(available_funcs["nomatch"]) stack.append(funcname) + stack.append(None) ncallable += 1 elif gdict["escaped"]: # escaped tokens From a0d34c72230a5240a8915568cfb4db49845ead28 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 27 Jun 2018 00:13:19 +0200 Subject: [PATCH 072/103] Start with final load/save/spawn nodes of menu --- evennia/prototypes/menus.py | 250 +++++++++++++++++++++---------- evennia/prototypes/prototypes.py | 6 +- evennia/prototypes/spawner.py | 2 +- 3 files changed, 179 insertions(+), 79 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index ff38c3448e..80e34e4c21 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -4,6 +4,7 @@ OLC Prototype menu nodes """ +import json from ast import literal_eval from django.conf import settings from evennia.utils.evmenu import EvMenu, list_node @@ -132,10 +133,16 @@ def _set_property(caller, raw_string, **kwargs): caller.ndb._menutree.olc_prototype = prototype - out = [" Set {prop} to {value} ({typ}).".format(prop=prop, value=value, typ=type(value))] + try: + # TODO simple way to get rid of the u'' markers in list reprs, remove this when on py3. + repr_value = json.dumps(value) + except Exception: + repr_value = value + + out = [" Set {prop} to {value} ({typ}).".format(prop=prop, value=repr_value, typ=type(value))] if kwargs.get("test_parse", True): - out.append(" Simulating parsing ...") + out.append(" Simulating prototype-func parsing ...") err, parsed_value = protlib.protfunc_parser(value, testing=True) if err: out.append(" |yPython `literal_eval` warning: {}|n".format(err)) @@ -143,7 +150,7 @@ def _set_property(caller, raw_string, **kwargs): out.append(" |g(Example-)value when parsed ({}):|n {}".format( type(parsed_value), parsed_value)) else: - out.append(" |gNo change.") + out.append(" |gNo change when parsed.") caller.msg("\n".join(out)) @@ -185,23 +192,24 @@ def _path_cropper(pythonpath): def node_index(caller): prototype = _get_menu_prototype(caller) - text = ("|c --- Prototype wizard --- |n\n\n" - "Define the |yproperties|n of the prototype. All prototype values can be " - "over-ridden at the time of spawning an instance of the prototype, but some are " - "required.\n\n'|wprototype-'-properties|n are not used in the prototype itself but are used " - "to organize and list prototypes. The 'prototype-key' uniquely identifies the prototype " - "and allows you to edit an existing prototype or save a new one for use by you or " - "others later.\n\n(make choice; q to abort. If unsure, start from 1.)") + text = ( + "|c --- Prototype wizard --- |n\n\n" + "Define the |yproperties|n of the prototype. All prototype values can be " + "over-ridden at the time of spawning an instance of the prototype, but some are " + "required.\n\n'|wprototype-'-properties|n are not used in the prototype itself but are used " + "to organize and list prototypes. The 'prototype-key' uniquely identifies the prototype " + "and allows you to edit an existing prototype or save a new one for use by you or " + "others later.\n\n(make choice; q to abort. If unsure, start from 1.)") options = [] options.append( {"desc": "|WPrototype-Key|n|n{}".format(_format_option_value("Key", True, prototype, None)), "goto": "node_prototype_key"}) - for key in ('Prototype', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', + for key in ('Typeclass', 'Prototype-parent', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', 'Permissions', 'Location', 'Home', 'Destination'): required = False cropper = None - if key in ("Prototype", "Typeclass"): + if key in ("Prototype-parent", "Typeclass"): required = "prototype" not in prototype and "typeclass" not in prototype if key == 'Typeclass': cropper = _path_cropper @@ -215,6 +223,12 @@ def node_index(caller): {"desc": "|WPrototype-{}|n|n{}".format( key, _format_option_value(key, required, prototype, None)), "goto": "node_prototype_{}".format(key.lower())}) + for key in ("Load", "Save", "Spawn"): + options.append( + {"key": ("|w{}|W{}".format(key[0], key[1:]), key[0]), + "desc": "|W{}|n".format( + key, _format_option_value(key, required, prototype, None)), + "goto": "node_prototype_{}".format(key.lower())}) return text, options @@ -429,54 +443,82 @@ def _caller_attrs(caller): return attrs -def _attrparse(caller, attr_string): - "attr is entering on the form 'attr = value'" +def _display_attribute(attr_tuple): + """Pretty-print attribute tuple""" + attrkey, value, category, locks, default_access = attr_tuple + value = protlib.protfunc_parser(value) + typ = type(value) + out = ("Attribute key: '{attrkey}' (category: {category}, " + "locks: {locks})\n" + "Value (parsed to {typ}): {value}").format( + attrkey=attrkey, + category=category, locks=locks, + typ=typ, value=value) + return out + + +def _add_attr(caller, attr_string, **kwargs): + """ + Add new attrubute, parsing input. + attr is entered on these forms + attr = value + attr;category = value + attr;category;lockstring = value + + """ + attrname = '' + category = None + locks = '' if '=' in attr_string: attrname, value = (part.strip() for part in attr_string.split('=', 1)) attrname = attrname.lower() - if attrname: - try: - value = literal_eval(value) - except SyntaxError: - caller.msg(_MENU_ATTR_LITERAL_EVAL_ERROR) - else: - return attrname, value - else: - return None, None + nameparts = attrname.split(";", 2) + nparts = len(nameparts) + if nparts == 2: + attrname, category = nameparts + elif nparts > 2: + attrname, category, locks = nameparts + attr_tuple = (attrname, category, locks) - -def _add_attr(caller, attr_string, **kwargs): - attrname, value = _attrparse(caller, attr_string) if attrname: prot = _get_menu_prototype(caller) - prot['attrs'][attrname] = value - _set_prototype_value(caller, "prototype", prot) - text = "Added" + attrs = prot.get('attrs', []) + + try: + # replace existing attribute with the same name in the prototype + ind = [tup[0] for tup in attrs].index(attrname) + attrs[ind] = attr_tuple + except IndexError: + attrs.append(attr_tuple) + + _set_prototype_value(caller, "attrs", attrs) + + text = kwargs.get('text') + if not text: + if 'edit' in kwargs: + text = "Edited " + _display_attribute(attr_tuple) + else: + text = "Added " + _display_attribute(attr_tuple) else: - text = "Attribute must be given as 'attrname = ' where uses valid Python." + text = "Attribute must be given as 'attrname[;category;locks] = '." + options = {"key": "_default", "goto": lambda caller: None} return text, options def _edit_attr(caller, attrname, new_value, **kwargs): - attrname, value = _attrparse("{}={}".format(caller, attrname, new_value)) - if attrname: - prot = _get_menu_prototype(caller) - prot['attrs'][attrname] = value - text = "Edited Attribute {} = {}".format(attrname, value) - else: - text = "Attribute value must be valid Python." - options = {"key": "_default", - "goto": lambda caller: None} - return text, options + + attr_string = "{}={}".format(attrname, new_value) + + return _add_attr(caller, attr_string, edit=True) def _examine_attr(caller, selection): prot = _get_menu_prototype(caller) - value = prot['attrs'][selection] - return "Attribute {} = {}".format(selection, value) + attr_tuple = prot['attrs'][selection] + return _display_attribute(attr_tuple) @list_node(_caller_attrs) @@ -484,8 +526,12 @@ def node_attrs(caller): prot = _get_menu_prototype(caller) attrs = prot.get("attrs") - text = ["Set the prototype's |yAttributes|n. Separate multiple attrs with commas. " - "Will retain case sensitivity."] + text = ["Set the prototype's |yAttributes|n. Enter attributes on one of these forms:\n" + " attrname=value\n attrname;category=value\n attrname;category;lockstring=value\n" + "To give an attribute without a category but with a lockstring, leave that spot empty " + "(attrname;;lockstring=value)." + "Separate multiple attrs with commas. Use quotes to escape inputs with commas and " + "semi-colon."] if attrs: text.append("Current attrs are '|y{attrs}|n'.".format(attrs=attrs)) else: @@ -506,46 +552,78 @@ def _caller_tags(caller): return tags +def _display_tag(tag_tuple): + """Pretty-print attribute tuple""" + tagkey, category, data = tag_tuple + out = ("Tag: '{tagkey}' (category: {category}{})".format( + tagkey=tagkey, category=category, data=", data: {}".format(data) if data else "")) + return out + + def _add_tag(caller, tag, **kwargs): + """ + Add tags to the system, parsing this syntax: + tagname + tagname;category + tagname;category;data + + """ + tag = tag.strip().lower() - prototype = _get_menu_prototype(caller) - tags = prototype.get('tags', []) - if tags: - if tag not in tags: - tags.append(tag) + category = None + data = "" + + tagtuple = tag.split(";", 2) + ntuple = len(tagtuple) + + if ntuple == 2: + tag, category = tagtuple + elif ntuple > 2: + tag, category, data = tagtuple + + tag_tuple = (tag, category, data) + + if tag: + prot = _get_menu_prototype(caller) + tags = prot.get('tags', []) + + old_tag = kwargs.get("edit", None) + + if old_tag: + # editing a tag means removing the old and replacing with new + try: + ind = [tup[0] for tup in tags].index(old_tag) + del tags[ind] + except IndexError: + pass + + tags.append(tag_tuple) + + _set_prototype_value(caller, "tags", tags) + + text = kwargs.get('text') + if not text: + if 'edit' in kwargs: + text = "Edited " + _display_tag(tag_tuple) + else: + text = "Added " + _display_tag(tag_tuple) else: - tags = [tag] - prototype['tags'] = tags - _set_prototype_value(caller, "prototype", prototype) - text = kwargs.get("text") - if not text: - text = "Added tag {}. (return to continue)".format(tag) + text = "Tag must be given as 'tag[;category;data]." + options = {"key": "_default", "goto": lambda caller: None} return text, options def _edit_tag(caller, old_tag, new_tag, **kwargs): - prototype = _get_menu_prototype(caller) - tags = prototype.get('tags', []) - - old_tag = old_tag.strip().lower() - new_tag = new_tag.strip().lower() - tags[tags.index(old_tag)] = new_tag - prototype['tags'] = tags - _set_prototype_value(caller, 'prototype', prototype) - - text = kwargs.get('text') - if not text: - text = "Changed tag {} to {}.".format(old_tag, new_tag) - options = {"key": "_default", - "goto": lambda caller: None} - return text, options + return _add_tag(caller, new_tag, edit=old_tag) @list_node(_caller_tags) def node_tags(caller): - text = "Set the prototype's |yTags|n." + text = ("Set the prototype's |yTags|n. Enter tags on one of the following forms:\n" + " tag\n tag;category\n tag;category;data\n" + "Note that 'data' is not commonly used.") options = _wizard_options("tags", "attrs", "locks") return text, options @@ -650,7 +728,7 @@ def node_destination(caller): def node_prototype_desc(caller): prototype = _get_menu_prototype(caller) - text = ["The |wMeta-Description|n briefly describes the prototype for viewing in listings."] + text = ["The |wPrototype-Description|n briefly describes the prototype for viewing in listings."] desc = prototype.get("prototype_desc", None) if desc: @@ -670,7 +748,7 @@ def node_prototype_desc(caller): def node_prototype_tags(caller): prototype = _get_menu_prototype(caller) - text = ["|wMeta-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " + text = ["|wPrototype-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " "Separate multiple by tags by commas."] tags = prototype.get('prototype_tags', []) @@ -691,15 +769,15 @@ def node_prototype_tags(caller): def node_prototype_locks(caller): prototype = _get_menu_prototype(caller) - text = ["Set |wMeta-Locks|n on the prototype. There are two valid lock types: " - "'edit' (who can edit the prototype) and 'use' (who can apply the prototype)\n" - "(If you are unsure, leave as default.)"] + text = ["Set |wPrototype-Locks|n on the prototype. There are two valid lock types: " + "'edit' (who can edit the prototype) and 'spawn' (who can spawn new objects with this " + "prototype)\n(If you are unsure, leave as default.)"] locks = prototype.get('prototype_locks', '') if locks: text.append("Current lock is |w'{lockstring}'|n".format(lockstring=locks)) else: text.append("Lock unset - if not changed the default lockstring will be set as\n" - " |w'use:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) + " |w'spawn:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) text = "\n\n".join(text) options = _wizard_options("prototype_locks", "prototype_tags", "index") options.append({"key": "_default", @@ -710,6 +788,21 @@ def node_prototype_locks(caller): return text, options +def node_prototype_load(caller): + # load prototype from storage + pass + + +def node_prototype_save(caller): + # save current prototype to disk + pass + + +def node_prototype_spawn(caller): + # spawn an instance of this prototype + pass + + class OLCMenu(EvMenu): """ A custom EvMenu with a different formatting for the options. @@ -766,5 +859,8 @@ def start_olc(caller, session=None, prototype=None): "node_prototype_desc": node_prototype_desc, "node_prototype_tags": node_prototype_tags, "node_prototype_locks": node_prototype_locks, + "node_prototype_load": node_prototype_load, + "node_prototype_save": node_prototype_save, + "node_prototype_spawn": node_prototype_spawn } OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=prototype) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 2ab3416afe..6f155fdac9 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -22,7 +22,11 @@ from evennia.utils.evtable import EvTable _MODULE_PROTOTYPE_MODULES = {} _MODULE_PROTOTYPES = {} -_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") +_PROTOTYPE_META_NAMES = ( + "prototype_key", "prototype_desc", "prototype_tags", "prototype_locks", "prototype_parent") +_PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + ( + "key", "aliases", "typeclass", "location", "home", "destination", + "permissions", "locks", "exec", "tags", "attrs") _PROTOTYPE_TAG_CATEGORY = "from_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" _PROT_FUNCS = {} diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index df07e3b155..71aecfd61e 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -31,10 +31,10 @@ Possible keywords are: supported are 'edit' and 'use'. prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype in listings - prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or a list of parents, for multiple left-to-right inheritance. prototype: Deprecated. Same meaning as 'parent'. + typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use `settings.BASE_OBJECT_TYPECLASS` key (str or callable, optional): the name of the spawned object. If not given this will set to a From 55ad8adb73307a232c2538f818795fc29768050d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 30 Jun 2018 16:37:30 +0200 Subject: [PATCH 073/103] Complete design of olc menu, not tested yet --- evennia/prototypes/menus.py | 207 ++++++++++++++++++++++++++----- evennia/prototypes/prototypes.py | 4 +- 2 files changed, 180 insertions(+), 31 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 80e34e4c21..faeae88fca 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -5,7 +5,6 @@ OLC Prototype menu nodes """ import json -from ast import literal_eval from django.conf import settings from evennia.utils.evmenu import EvMenu, list_node from evennia.utils.ansi import strip_ansi @@ -40,6 +39,13 @@ def _get_menu_prototype(caller): return prototype +def _set_menu_prototype(caller, prototype): + """Set the prototype with existing one""" + caller.ndb._menutree.olc_prototype = prototype + caller.ndb._menutree.olc_new = False + return prototype + + def _is_new_prototype(caller): """Check if prototype is marked as new or was loaded from a saved one.""" return hasattr(caller.ndb._menutree, "olc_new") @@ -177,7 +183,7 @@ def _wizard_options(curr_node, prev_node, next_node, color="|W"): if curr_node: options.append({"key": ("|wv|Walidate prototype", "v"), - "goto": ("node_validate_prototype", {"back": curr_node})}) + "goto": ("node_view_prototype", {"back": curr_node})}) return options @@ -187,6 +193,26 @@ def _path_cropper(pythonpath): return pythonpath.split('.')[-1] +def _validate_prototype(prototype): + """Run validation on prototype""" + + txt = protlib.prototype_to_str(prototype) + errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)" + err = False + try: + # validate, don't spawn + spawner.spawn(prototype, only_validate=True) + except RuntimeError as err: + errors = "\n\n|r{}|n".format(err) + err = True + except RuntimeWarning as err: + errors = "\n\n|y{}|n".format(err) + err = True + + text = (txt + errors) + return err, text + + # Menu nodes def node_index(caller): @@ -223,7 +249,7 @@ def node_index(caller): {"desc": "|WPrototype-{}|n|n{}".format( key, _format_option_value(key, required, prototype, None)), "goto": "node_prototype_{}".format(key.lower())}) - for key in ("Load", "Save", "Spawn"): + for key in ("Save", "Spawn", "Load"): options.append( {"key": ("|w{}|W{}".format(key[0], key[1:]), key[0]), "desc": "|W{}|n".format( @@ -233,22 +259,18 @@ def node_index(caller): return text, options -def node_validate_prototype(caller, raw_string, **kwargs): - prototype = _get_menu_prototype(caller) +def node_view_prototype(caller, raw_string, **kwargs): + """General node to view and validate a protototype""" + prototype = kwargs.get('prototype', _get_menu_prototype(caller)) + validate = kwargs.get("validate", True) + prev_node = kwargs.get("back", "node_index") - txt = protlib.prototype_to_str(prototype) - errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)" - try: - # validate, don't spawn - spawner.spawn(prototype, only_validate=True) - except RuntimeError as err: - errors = "\n\n|r{}|n".format(err) - except RuntimeWarning as err: - errors = "\n\n|y{}|n".format(err) + if validate: + _, text = _validate_prototype(prototype) + else: + text = protlib.prototype_to_str(prototype) - text = (txt + errors) - - options = _wizard_options(None, kwargs.get("back"), None) + options = _wizard_options(None, prev_node, None) return text, options @@ -728,7 +750,8 @@ def node_destination(caller): def node_prototype_desc(caller): prototype = _get_menu_prototype(caller) - text = ["The |wPrototype-Description|n briefly describes the prototype for viewing in listings."] + text = ["The |wPrototype-Description|n briefly describes the prototype for " + "viewing in listings."] desc = prototype.get("prototype_desc", None) if desc: @@ -748,7 +771,8 @@ def node_prototype_desc(caller): def node_prototype_tags(caller): prototype = _get_menu_prototype(caller) - text = ["|wPrototype-Tags|n can be used to classify and find prototypes. Tags are case-insensitive. " + text = ["|wPrototype-Tags|n can be used to classify and find prototypes. " + "Tags are case-insensitive. " "Separate multiple by tags by commas."] tags = prototype.get('prototype_tags', []) @@ -788,19 +812,144 @@ def node_prototype_locks(caller): return text, options -def node_prototype_load(caller): - # load prototype from storage - pass +def node_prototype_save(caller, **kwargs): + """Save prototype to disk """ + # these are only set if we selected 'yes' to save on a previous pass + accept_save = kwargs.get("accept", False) + prototype = kwargs.get("prototype", None) + + if accept_save and prototype: + # we already validated and accepted the save, so this node acts as a goto callback and + # should now only return the next node + protlib.save_prototype(**prototype) + caller.msg("|gPrototype saved.|n") + return "node_spawn" + + # not validated yet + prototype = _get_menu_prototype(caller) + error, text = _validate_prototype(prototype) + + text = [text] + + if error: + # abort save + text.append( + "Validation errors were found. They need to be corrected before this prototype " + "can be saved (or used to spawn).") + options = _wizard_options("prototype_save", "prototype_locks", "index") + return "\n".join(text), options + + prototype_key = prototype['prototype_key'] + if protlib.search_prototype(prototype_key): + text.append("Do you want to save/overwrite the existing prototype '{name}'?".format( + name=prototype_key)) + else: + text.append("Do you want to save the prototype as '{name}'?".format(prototype_key)) + + options = ( + {"key": ("[|wY|Wes|n]", "yes", "y"), + "goto": lambda caller: + node_prototype_save(caller, + {"accept": True, "prototype": prototype})}, + {"key": ("|wN|Wo|n", "n"), + "goto": "node_spawn"}, + {"key": "_default", + "goto": lambda caller: + node_prototype_save(caller, + {"accept": True, "prototype": prototype})}) + + return "\n".join(text), options -def node_prototype_save(caller): - # save current prototype to disk - pass +def _spawn(caller, **kwargs): + """Spawn prototype""" + prototype = kwargs["prototype"].copy() + new_location = kwargs.get('location', None) + if new_location: + prototype['location'] = new_location + obj = spawner.spawn(prototype) + if obj: + caller.msg("|gNew instance|n {key} ({dbref}) |gspawned.|n".format( + key=obj.key, dbref=obj.dbref)) + else: + caller.msg("|rError: Spawner did not return a new instance.|n") -def node_prototype_spawn(caller): - # spawn an instance of this prototype - pass +def _update_spawned(caller, **kwargs): + """update existing objects""" + prototype = kwargs['prototype'] + objects = kwargs['objects'] + num_changed = spawner.batch_update_objects_with_prototype(prototype, objects=objects) + caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed)) + + +def node_prototype_spawn(caller, **kwargs): + """Submenu for spawning the prototype""" + + prototype = _get_menu_prototype(caller) + error, text = _validate_prototype(prototype) + + text = [text] + + if error: + text.append("|rPrototype validation failed. Correct the errors before spawning.|n") + options = _wizard_options("prototype_spawn", "prototype_locks", "index") + return "\n".join(text), options + + # show spawn submenu options + options = [] + prototype_key = prototype['prototype_key'] + location = prototype.get('location', None) + + if location: + options.append( + {"desc": "Spawn in prototype's defined location ({loc})".format(loc=location), + "goto": (_spawn, + dict(prototype=prototype))}) + caller_loc = caller.location + if location != caller_loc: + options.append( + {"desc": "Spawn in {caller}'s location ({loc})".format( + caller=caller, loc=caller_loc), + "goto": (_spawn, + dict(prototype=prototype, location=caller_loc))}) + if location != caller_loc != caller: + options.append( + {"desc": "Spawn in {caller}'s inventory".format(caller=caller), + "goto": (_spawn, + dict(prototype=prototype, location=caller))}) + + spawned_objects = protlib.search_objects_with_prototype(prototype_key) + nspawned = spawned_objects.count() + if spawned_objects: + options.append( + {"desc": "Update {num} existing objects with this prototype".format(num=nspawned), + "goto": (_update_spawned, + dict(prototype=prototype, + opjects=spawned_objects))}) + options.extend(_wizard_options("prototype_spawn", "prototype_save", "index")) + return text, options + + +def _prototype_load_select(caller, prototype_key): + matches = protlib.search_prototype(key=prototype_key) + if matches: + prototype = matches[0] + _set_menu_prototype(caller, prototype) + caller.msg("|gLoaded prototype '{}'.".format(prototype_key)) + return "node_index" + else: + caller.msg("|rFailed to load prototype '{}'.".format(prototype_key)) + return None + + +@list_node(_all_prototype_parents, _prototype_load_select) +def node_prototype_load(caller, **kwargs): + text = ["Select a prototype to load. This will replace any currently edited prototype."] + options = _wizard_options("load", "save", "index") + options.append({"key": "_default", + "goto": _prototype_parent_examine}) + return "\n".join(text), options class OLCMenu(EvMenu): @@ -843,7 +992,7 @@ def start_olc(caller, session=None, prototype=None): """ menudata = {"node_index": node_index, - "node_validate_prototype": node_validate_prototype, + "node_view_prototype": node_view_prototype, "node_prototype_key": node_prototype_key, "node_prototype_parent": node_prototype_parent, "node_typeclass": node_typeclass, diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 6f155fdac9..c02041d8bc 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -258,7 +258,7 @@ class DbPrototype(DefaultScript): # Prototype manager functions -def create_prototype(**kwargs): +def save_prototype(**kwargs): """ Create/Store a prototype persistently. @@ -335,7 +335,7 @@ def create_prototype(**kwargs): return stored_prototype.db.prototype # alias -save_prototype = create_prototype +create_prototype = save_prototype def delete_prototype(key, caller=None): From 644b6906adebecaf0dc4ef82f3f82642a49ab17d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 1 Jul 2018 00:17:43 +0200 Subject: [PATCH 074/103] Start debugging olc menu structure --- evennia/prototypes/menus.py | 67 +++++++++++++++++++------------------ evennia/prototypes/tests.py | 2 ++ 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index faeae88fca..f978aae566 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -167,23 +167,23 @@ def _wizard_options(curr_node, prev_node, next_node, color="|W"): """Creates default navigation options available in the wizard.""" options = [] if prev_node: - options.append({"key": ("|wb|Wack", "b"), + options.append({"key": ("|wB|Wack", "b"), "desc": "{color}({node})|n".format( color=color, node=prev_node.replace("_", "-")), "goto": "node_{}".format(prev_node)}) if next_node: - options.append({"key": ("|wf|Worward", "f"), + options.append({"key": ("|wF|Worward", "f"), "desc": "{color}({node})|n".format( color=color, node=next_node.replace("_", "-")), "goto": "node_{}".format(next_node)}) if "index" not in (prev_node, next_node): - options.append({"key": ("|wi|Wndex", "i"), + options.append({"key": ("|wI|Wndex", "i"), "goto": "node_index"}) if curr_node: - options.append({"key": ("|wv|Walidate prototype", "v"), - "goto": ("node_view_prototype", {"back": curr_node})}) + options.append({"key": ("|wV|Walidate prototype", "validate", "v"), + "goto": ("node_validate_prototype", {"back": curr_node})}) return options @@ -229,19 +229,21 @@ def node_index(caller): options = [] options.append( - {"desc": "|WPrototype-Key|n|n{}".format(_format_option_value("Key", True, prototype, None)), + {"desc": "|WPrototype-Key|n|n{}".format( + _format_option_value("Key", "prototype_key" not in prototype, prototype, None)), "goto": "node_prototype_key"}) - for key in ('Typeclass', 'Prototype-parent', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', + for key in ('Typeclass', 'Prototype_parent', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', 'Permissions', 'Location', 'Home', 'Destination'): required = False cropper = None if key in ("Prototype-parent", "Typeclass"): - required = "prototype" not in prototype and "typeclass" not in prototype + required = "prototype_parent" not in prototype and "typeclass" not in prototype if key == 'Typeclass': cropper = _path_cropper options.append( {"desc": "|w{}|n{}".format( - key, _format_option_value(key, required, prototype, cropper=cropper)), + key.replace("_", "-"), + _format_option_value(key, required, prototype, cropper=cropper)), "goto": "node_{}".format(key.lower())}) required = False for key in ('Desc', 'Tags', 'Locks'): @@ -249,26 +251,26 @@ def node_index(caller): {"desc": "|WPrototype-{}|n|n{}".format( key, _format_option_value(key, required, prototype, None)), "goto": "node_prototype_{}".format(key.lower())}) - for key in ("Save", "Spawn", "Load"): - options.append( - {"key": ("|w{}|W{}".format(key[0], key[1:]), key[0]), - "desc": "|W{}|n".format( - key, _format_option_value(key, required, prototype, None)), - "goto": "node_prototype_{}".format(key.lower())}) + + options.extend(( + {"key": ("|wV|Walidate prototype", "validate", "v"), + "goto": "node_validate_prototype"}, + {"key": ("|wS|Wave prototype", "save", "s"), + "goto": "node_prototype_save"}, + {"key": ("|wSP|Wawn prototype", "spawn", "sp"), + "goto": "node_prototype_spawn"}, + {"key": ("|wL|Woad prototype", "load", "l"), + "goto": "node_prototype_load"})) return text, options -def node_view_prototype(caller, raw_string, **kwargs): +def node_validate_prototype(caller, raw_string, **kwargs): """General node to view and validate a protototype""" - prototype = kwargs.get('prototype', _get_menu_prototype(caller)) - validate = kwargs.get("validate", True) - prev_node = kwargs.get("back", "node_index") + prototype = _get_menu_prototype(caller) + prev_node = kwargs.get("back", "index") - if validate: - _, text = _validate_prototype(prototype) - else: - text = protlib.prototype_to_str(prototype) + _, text = _validate_prototype(prototype) options = _wizard_options(None, prev_node, None) @@ -310,7 +312,7 @@ def node_prototype_key(caller): text.append("The key is currently unset.") text.append("Enter text or make a choice (q for quit)") text = "\n\n".join(text) - options = _wizard_options("prototype_key", "index", "prototype") + options = _wizard_options("prototype_key", "index", "prototype_parent") options.append({"key": "_default", "goto": _check_prototype_key}) return text, options @@ -334,7 +336,7 @@ def _prototype_parent_examine(caller, prototype_name): def _prototype_parent_select(caller, prototype): - ret = _set_property(caller, prototype['prototype_key'], + ret = _set_property(caller, "", prop="prototype_parent", processor=str, next_node="node_key") caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype)) return ret @@ -358,7 +360,7 @@ def node_prototype_parent(caller): else: text.append("Parent prototype is not set") text = "\n\n".join(text) - options = _wizard_options("prototype", "prototype_key", "typeclass", color="|W") + options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W") options.append({"key": "_default", "goto": _prototype_parent_examine}) @@ -414,7 +416,7 @@ def node_typeclass(caller): text.append("Using default typeclass {typeclass}.".format( typeclass=settings.BASE_OBJECT_TYPECLASS)) text = "\n\n".join(text) - options = _wizard_options("typeclass", "prototype", "key", color="|W") + options = _wizard_options("typeclass", "prototype_parent", "key", color="|W") options.append({"key": "_default", "goto": _typeclass_examine}) return text, options @@ -923,7 +925,7 @@ def node_prototype_spawn(caller, **kwargs): nspawned = spawned_objects.count() if spawned_objects: options.append( - {"desc": "Update {num} existing objects with this prototype".format(num=nspawned), + {"desc": "Update {num} existing objects with this prototype".format(num=nspawned), "goto": (_update_spawned, dict(prototype=prototype, opjects=spawned_objects))}) @@ -962,18 +964,19 @@ class OLCMenu(EvMenu): Split the options into two blocks - olc options and normal options """ - olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype") + olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype", + "save prototype", "load prototype", "spawn prototype") olc_options = [] other_options = [] for key, desc in optionlist: - raw_key = strip_ansi(key) + raw_key = strip_ansi(key).lower() if raw_key in olc_keys: desc = " {}".format(desc) if desc else "" olc_options.append("|lc{}|lt{}|le{}".format(raw_key, key, desc)) else: other_options.append((key, desc)) - olc_options = " | ".join(olc_options) + " | " + "|wq|Wuit" if olc_options else "" + olc_options = " | ".join(olc_options) + " | " + "|wQ|Wuit" if olc_options else "" other_options = super(OLCMenu, self).options_formatter(other_options) sep = "\n\n" if olc_options and other_options else "" @@ -992,7 +995,7 @@ def start_olc(caller, session=None, prototype=None): """ menudata = {"node_index": node_index, - "node_view_prototype": node_view_prototype, + "node_validate_prototype": node_validate_prototype, "node_prototype_key": node_prototype_key, "node_prototype_parent": node_prototype_parent, "node_typeclass": node_typeclass, diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 49624905c7..0d5e247378 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -355,6 +355,8 @@ class TestMenuModule(EvenniaTest): {'goto': ('node_validate_prototype', {'back': 'ThisNode'}), 'key': ('|wv|Walidate prototype', 'v')}]) + self.assertEqual(olc_menus._validate_prototype(self.test_prot, (False, Something))) + def test_node_helpers(self): caller = self.caller From 649cb44ba1826059c61b8292542856ae3c8c9786 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 3 Jul 2018 23:56:55 +0200 Subject: [PATCH 075/103] Add functionality for object-update menu node, untested --- CHANGELOG.md | 40 ++++++-- evennia/prototypes/menus.py | 166 +++++++++++++++++++++++++------ evennia/prototypes/prototypes.py | 20 ++-- evennia/prototypes/spawner.py | 18 +++- evennia/prototypes/tests.py | 85 +++++++++++++--- 5 files changed, 265 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a05d65fdc0..3c1c4cf787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,29 @@ -# Evennia Changelog +# Changelog -# Sept 2017: -Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to +## Evennia 0.8 (2018) + +### Prototype changes + +- A new form of prototype - database-stored prototypes, editable from in-game. The old, + module-created prototypes remain as read-only prototypes. +- All prototypes must have a key `prototype_key` identifying the prototype in listings. This is + checked to be server-unique. Prototypes created in a module will use the global variable name they + are assigned to if no `prototype_key` is given. +- Prototype field `prototype` was renamed to `prototype_parent` to avoid mixing terms. +- All prototypes must either have `typeclass` or `prototype_parent` defined. If using + `prototype_parent`, `typeclass` must be defined somewhere in the inheritance chain. This is a + change from Evennia 0.7 which allowed 'mixin' prototypes without `typeclass`/`prototype_key`. To + make a mixin now, give it a default typeclass, like `evennia.objects.objects.DefaultObject` and just + override in the child as needed. +- The spawn command was extended to accept a full prototype on one line. +- The spawn command got the /save switch to save the defined prototype and its key. +- The command spawn/menu will now start an OLC (OnLine Creation) menu to load/save/edit/spawn prototypes. + + +# Overviews + +## Sept 2017: +Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to 'Account', rework the website template and a slew of other updates. Info on what changed and how to migrate is found here: https://groups.google.com/forum/#!msg/evennia/0JYYNGY-NfE/cDFaIwmPBAAJ @@ -14,9 +36,9 @@ Lots of bugfixes and considerable uptick in contributors. Unittest coverage and PEP8 adoption and refactoring. ## May 2016: -Evennia 0.6 with completely reworked Out-of-band system, making +Evennia 0.6 with completely reworked Out-of-band system, making the message path completely flexible and built around input/outputfuncs. -A completely new webclient, split into the evennia.js library and a +A completely new webclient, split into the evennia.js library and a gui library, making it easier to customize. ## Feb 2016: @@ -33,15 +55,15 @@ library format with a stand-alone launcher, in preparation for making an 'evennia' pypy package and using versioning. The version we will merge with will likely be 0.5. There is also work with an expanded testing structure and the use of threading for saves. We also now -use Travis for automatic build checking. +use Travis for automatic build checking. ## Sept 2014: Updated to Django 1.7+ which means South dependency was dropped and minimum Python version upped to 2.7. MULTISESSION_MODE=3 was added -and the web customization system was overhauled using the latest -functionality of django. Otherwise, mostly bug-fixes and +and the web customization system was overhauled using the latest +functionality of django. Otherwise, mostly bug-fixes and implementation of various smaller feature requests as we got used -to github. Many new users have appeared. +to github. Many new users have appeared. ## Jan 2014: Moved Evennia project from Google Code to github.com/evennia/evennia. diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index f978aae566..af670b743a 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -5,6 +5,7 @@ OLC Prototype menu nodes """ import json +from random import choice from django.conf import settings from evennia.utils.evmenu import EvMenu, list_node from evennia.utils.ansi import strip_ansi @@ -469,7 +470,7 @@ def _caller_attrs(caller): def _display_attribute(attr_tuple): """Pretty-print attribute tuple""" - attrkey, value, category, locks, default_access = attr_tuple + attrkey, value, category, locks = attr_tuple value = protlib.protfunc_parser(value) typ = type(value) out = ("Attribute key: '{attrkey}' (category: {category}, " @@ -503,7 +504,7 @@ def _add_attr(caller, attr_string, **kwargs): attrname, category = nameparts elif nparts > 2: attrname, category, locks = nameparts - attr_tuple = (attrname, category, locks) + attr_tuple = (attrname, value, category, locks) if attrname: prot = _get_menu_prototype(caller) @@ -513,7 +514,7 @@ def _add_attr(caller, attr_string, **kwargs): # replace existing attribute with the same name in the prototype ind = [tup[0] for tup in attrs].index(attrname) attrs[ind] = attr_tuple - except IndexError: + except ValueError: attrs.append(attr_tuple) _set_prototype_value(caller, "attrs", attrs) @@ -541,7 +542,8 @@ def _edit_attr(caller, attrname, new_value, **kwargs): def _examine_attr(caller, selection): prot = _get_menu_prototype(caller) - attr_tuple = prot['attrs'][selection] + ind = [part[0] for part in prot['attrs']].index(selection) + attr_tuple = prot['attrs'][ind] return _display_attribute(attr_tuple) @@ -572,15 +574,15 @@ def node_attrs(caller): def _caller_tags(caller): prototype = _get_menu_prototype(caller) - tags = prototype.get("tags") + tags = prototype.get("tags", []) return tags def _display_tag(tag_tuple): """Pretty-print attribute tuple""" tagkey, category, data = tag_tuple - out = ("Tag: '{tagkey}' (category: {category}{})".format( - tagkey=tagkey, category=category, data=", data: {}".format(data) if data else "")) + out = ("Tag: '{tagkey}' (category: {category}{dat})".format( + tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else "")) return out @@ -613,16 +615,21 @@ def _add_tag(caller, tag, **kwargs): old_tag = kwargs.get("edit", None) - if old_tag: - # editing a tag means removing the old and replacing with new + if not old_tag: + # a fresh, new tag + tags.append(tag_tuple) + else: + # old tag exists; editing a tag means removing the old and replacing with new try: ind = [tup[0] for tup in tags].index(old_tag) del tags[ind] + if tags: + tags.insert(ind, tag_tuple) + else: + tags = [tag_tuple] except IndexError: pass - tags.append(tag_tuple) - _set_prototype_value(caller, "tags", tags) text = kwargs.get('text') @@ -814,18 +821,121 @@ def node_prototype_locks(caller): return text, options +def _update_spawned(caller, **kwargs): + """update existing objects""" + prototype = kwargs['prototype'] + objects = kwargs['objects'] + back_node = kwargs['back_key'] + num_changed = spawner.batch_update_objects_with_prototype(prototype, objects=objects) + caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed)) + return back_key + + +def _keep_diff(caller, **kwargs): + key = kwargs['key'] + diff = kwargs['diff'] + diff[key] = "KEEP" + + +def node_update_objects(caller, **kwargs): + """Offer options for updating objects""" + + def _keep_option(keyname, prototype, obj, obj_prototype, diff, objects, back_node): + """helper returning an option dict""" + options = {"desc": "Keep {} as-is".format(keyname), + "goto": (_keep_diff, + {"key": keyname, "prototype": prototype, + "obj": obj, "obj_prototype": obj_prototype, + "diff": diff, "objects": objects, "back_node": back_node})} + return options + + prototype = kwargs.get("prototype", None) + update_objects = kwargs.get("objects", None) + back_node = kwargs.get("back_node", "node_index") + obj_prototype = kwargs.get("obj_prototype", None) + diff = kwargs.get("diff", None) + + if not update_objects: + text = "There are no existing objects to update." + options = {"key": "_default", + "goto": back_node} + return text, options + + if not diff: + # use one random object as a reference to calculate a diff + obj = choice(update_objects) + diff, obj_prototype = spawner.prototype_diff_from_object(prototype, obj) + + text = ["Suggested changes to {} objects".format(len(update_objects)), + "Showing random example obj to change: {name} (#{dbref}))\n".format(obj.key, obj.dbref)] + options = [] + io = 0 + for (key, inst) in sorted(((key, val) for key, val in diff.items()), key=lambda tup: tup[0]): + line = "{iopt} |w{key}|n: {old}{sep}{new} {change}" + old_val = utils.crop(str(obj_prototype[key]), width=20) + + if inst == "KEEP": + text.append(line.format(iopt='', key=key, old=old_val, sep=" ", new='', change=inst)) + continue + + new_val = utils.crop(str(spawner.init_spawn_value(prototype[key])), width=20) + io += 1 + if inst in ("UPDATE", "REPLACE"): + text.append(line.format(iopt=io, key=key, old=old_val, + sep=" |y->|n ", new=new_val, change=inst)) + options.append(_keep_option(key, prototype, + obj, obj_prototype, diff, objects, back_node)) + elif inst == "REMOVE": + text.append(line.format(iopt=io, key=key, old=old_val, + sep=" |r->|n ", new='', change=inst)) + options.append(_keep_option(key, prototype, + obj, obj_prototype, diff, objects, back_node)) + options.extend( + [{"key": ("|wu|r update {} objects".format(len(update_objects)), "update", "u"), + "goto": (_update_spawned, {"prototype": prototype, "objects": objects, + "back_node": back_node, "diff": diff})}, + {"key": ("|wr|neset changes", "reset", "r"), + "goto": ("node_update_objects", {"prototype": prototype, "back_node": back_node, + "objects": update_objects})}, + {"key": "|wb|rack ({})".format(back_node[5:], 'b'), + "goto": back_node}]) + + return text, options + + def node_prototype_save(caller, **kwargs): """Save prototype to disk """ # these are only set if we selected 'yes' to save on a previous pass - accept_save = kwargs.get("accept", False) prototype = kwargs.get("prototype", None) + accept_save = kwargs.get("accept_save", False) if accept_save and prototype: # we already validated and accepted the save, so this node acts as a goto callback and # should now only return the next node + prototype_key = prototype.get("prototype_key") protlib.save_prototype(**prototype) - caller.msg("|gPrototype saved.|n") - return "node_spawn" + + spawned_objects = protlib.search_objects_with_prototype(prototype_key) + nspawned = spawned_objects.count() + + if nspawned: + text = ("Do you want to update {} object(s) " + "already using this prototype?".format(nspawned)) + options = ( + {"key": ("|wY|Wes|n", "yes", "y"), + "goto": ("node_update_objects", + {"accept_update": True, "objects": spawned_objects, + "prototype": prototype, "back_node": "node_prototype_save"})}, + {"key": ("[|wN|Wo|n]", "n"), + "goto": "node_spawn"}, + {"key": "_default", + "goto": "node_spawn"}) + else: + text = "|gPrototype saved.|n" + options = {"key": "_default", + "goto": "node_spawn"} + + return text, options # not validated yet prototype = _get_menu_prototype(caller) @@ -850,15 +960,13 @@ def node_prototype_save(caller, **kwargs): options = ( {"key": ("[|wY|Wes|n]", "yes", "y"), - "goto": lambda caller: - node_prototype_save(caller, - {"accept": True, "prototype": prototype})}, + "goto": ("node_prototype_save", + {"accept": True, "prototype": prototype})}, {"key": ("|wN|Wo|n", "n"), "goto": "node_spawn"}, {"key": "_default", - "goto": lambda caller: - node_prototype_save(caller, - {"accept": True, "prototype": prototype})}) + "goto": ("node_prototype_save", + {"accept": True, "prototype": prototype})}) return "\n".join(text), options @@ -869,20 +977,15 @@ def _spawn(caller, **kwargs): new_location = kwargs.get('location', None) if new_location: prototype['location'] = new_location + obj = spawner.spawn(prototype) if obj: + obj = obj[0] caller.msg("|gNew instance|n {key} ({dbref}) |gspawned.|n".format( key=obj.key, dbref=obj.dbref)) else: caller.msg("|rError: Spawner did not return a new instance.|n") - - -def _update_spawned(caller, **kwargs): - """update existing objects""" - prototype = kwargs['prototype'] - objects = kwargs['objects'] - num_changed = spawner.batch_update_objects_with_prototype(prototype, objects=objects) - caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed)) + return obj def node_prototype_spawn(caller, **kwargs): @@ -926,9 +1029,9 @@ def node_prototype_spawn(caller, **kwargs): if spawned_objects: options.append( {"desc": "Update {num} existing objects with this prototype".format(num=nspawned), - "goto": (_update_spawned, - dict(prototype=prototype, - opjects=spawned_objects))}) + "goto": ("node_update_objects", + dict(prototype=prototype, opjects=spawned_objects, + back_node="node_prototype_spawn"))}) options.extend(_wizard_options("prototype_spawn", "prototype_save", "index")) return text, options @@ -1008,6 +1111,7 @@ def start_olc(caller, session=None, prototype=None): "node_location": node_location, "node_home": node_home, "node_destination": node_destination, + "node_update_objects": node_o "node_prototype_desc": node_prototype_desc, "node_prototype_tags": node_prototype_tags, "node_prototype_locks": node_prototype_locks, diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index c02041d8bc..2457f86994 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -547,7 +547,8 @@ def validate_prototype(prototype, protkey=None, protparents=None, _flags = {"visited": [], "depth": 0, "typeclass": False, "errors": [], "warnings": []} if not protparents: - protparents = {prototype['prototype_key']: prototype for prototype in search_prototype()} + protparents = {prototype.get('prototype_key', "").lower(): prototype + for prototype in search_prototype()} protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) @@ -568,17 +569,11 @@ def validate_prototype(prototype, protkey=None, protparents=None, if typeclass and typeclass not in get_all_typeclasses("evennia.objects.models.ObjectDB"): _flags['errors'].append( - "Prototype {} is based on typeclass {} which could not be imported!".format( + "Prototype {} is based on typeclass {}, which could not be imported!".format( protkey, typeclass)) # recursively traverese prototype_parent chain - if id(prototype) in _flags['visited']: - _flags['errors'].append( - "{} has infinite nesting of prototypes.".format(protkey or prototype)) - - _flags['visited'].append(id(prototype)) - for protstring in make_iter(prototype_parent): protstring = protstring.lower() if protkey is not None and protstring == protkey: @@ -587,8 +582,15 @@ def validate_prototype(prototype, protkey=None, protparents=None, if not protparent: _flags['errors'].append("Prototype {}'s prototype_parent '{}' was not found.".format( (protkey, protstring))) + if id(prototype) in _flags['visited']: + _flags['errors'].append( + "{} has infinite nesting of prototypes.".format(protkey or prototype)) + + _flags['visited'].append(id(prototype)) _flags['depth'] += 1 - validate_prototype(protparent, protstring, protparents, _flags) + validate_prototype(protparent, protstring, protparents, + is_prototype_base=is_prototype_base, _flags=_flags) + _flags['visited'].pop() _flags['depth'] -= 1 if typeclass and not _flags['typeclass']: diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 71aecfd61e..d826317fec 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -179,6 +179,7 @@ def prototype_from_object(obj): prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True) if prot: prot = protlib.search_prototype(prot[0]) + if not prot or len(prot) > 1: # no unambiguous prototype found - build new prototype prot = {} @@ -187,6 +188,8 @@ def prototype_from_object(obj): prot['prototype_desc'] = "Built from {}".format(str(obj)) prot['prototype_locks'] = "spawn:all();edit:all()" prot['prototype_tags'] = [] + else: + prot = prot[0] prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6] prot['typeclass'] = obj.db_typeclass_path @@ -233,6 +236,8 @@ def prototype_diff_from_object(prototype, obj): Returns: diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...} + other_prototype (dict): The prototype for the given object. The diff is a how to convert + this prototype into the new prototype. """ prot1 = prototype @@ -253,7 +258,7 @@ def prototype_diff_from_object(prototype, obj): if key not in diff and key not in prot1: diff[key] = "REMOVE" - return diff + return diff, prot2 def batch_update_objects_with_prototype(prototype, diff=None, objects=None): @@ -475,8 +480,12 @@ def spawn(*prototypes, **kwargs): protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} # overload module's protparents with specifically given protparents - protparents.update( - {key.lower(): value for key, value in kwargs.get("prototype_parents", {}).items()}) + # we allow prototype_key to be the key of the protparent dict, to allow for module-level + # prototype imports. We need to insert prototype_key in this case + for key, protparent in kwargs.get("prototype_parents", {}).items(): + key = str(key).lower() + protparent['prototype_key'] = str(protparent.get("prototype_key", key)).lower() + protparents[key] = protparent if "return_parents" in kwargs: # only return the parents @@ -541,6 +550,9 @@ def spawn(*prototypes, **kwargs): simple_attributes = [] for key, value in ((key, value) for key, value in prot.items() if not (key.startswith("ndb_"))): + if key in _PROTOTYPE_META_NAMES: + continue + if is_iter(value) and len(value) > 1: # (value, category) simple_attributes.append((key, diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 0d5e247378..69eb495dd5 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -17,6 +17,8 @@ from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY _PROTPARENTS = { "NOBODY": {}, "GOBLIN": { + "prototype_key": "GOBLIN", + "typeclass": "evennia.objects.objects.DefaultObject", "key": "goblin grunt", "health": lambda: randint(1, 1), "resists": ["cold", "poison"], @@ -24,21 +26,22 @@ _PROTPARENTS = { "weaknesses": ["fire", "light"] }, "GOBLIN_WIZARD": { - "prototype": "GOBLIN", + "prototype_parent": "GOBLIN", "key": "goblin wizard", "spells": ["fire ball", "lighting bolt"] }, "GOBLIN_ARCHER": { - "prototype": "GOBLIN", + "prototype_parent": "GOBLIN", "key": "goblin archer", "attacks": ["short bow"] }, "ARCHWIZARD": { + "prototype_parent": "GOBLIN", "attacks": ["archwizard staff"], }, "GOBLIN_ARCHWIZARD": { "key": "goblin archwizard", - "prototype": ("GOBLIN_WIZARD", "ARCHWIZARD") + "prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD") } } @@ -47,7 +50,8 @@ class TestSpawner(EvenniaTest): def setUp(self): super(TestSpawner, self).setUp() - self.prot1 = {"prototype_key": "testprototype"} + self.prot1 = {"prototype_key": "testprototype", + "typeclass": "evennia.objects.objects.DefaultObject"} def test_spawn(self): obj1 = spawner.spawn(self.prot1) @@ -323,6 +327,7 @@ class TestMenuModule(EvenniaTest): self.caller.ndb._menutree = menutree self.test_prot = {"prototype_key": "test_prot", + "typeclass": "evennia.objects.objects.DefaultObject", "prototype_locks": "edit:all();spawn:all()"} def test_helpers(self): @@ -334,6 +339,8 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._get_menu_prototype(caller), {}) self.assertEqual(olc_menus._is_new_prototype(caller), True) + self.assertEqual(olc_menus._set_menu_prototype(caller, {}), {}) + self.assertEqual( olc_menus._set_prototype_value(caller, "key", "TestKey"), {"key": "TestKey"}) self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "TestKey"}) @@ -349,13 +356,16 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._wizard_options( "ThisNode", "PrevNode", "NextNode"), - [{'goto': 'node_PrevNode', 'key': ('|wb|Wack', 'b'), 'desc': '|W(PrevNode)|n'}, - {'goto': 'node_NextNode', 'key': ('|wf|Worward', 'f'), 'desc': '|W(NextNode)|n'}, - {'goto': 'node_index', 'key': ('|wi|Wndex', 'i')}, + [{'goto': 'node_PrevNode', 'key': ('|wB|Wack', 'b'), 'desc': '|W(PrevNode)|n'}, + {'goto': 'node_NextNode', 'key': ('|wF|Worward', 'f'), 'desc': '|W(NextNode)|n'}, + {'goto': 'node_index', 'key': ('|wI|Wndex', 'i')}, {'goto': ('node_validate_prototype', {'back': 'ThisNode'}), - 'key': ('|wv|Walidate prototype', 'v')}]) + 'key': ('|wV|Walidate prototype', 'validate', 'v')}]) - self.assertEqual(olc_menus._validate_prototype(self.test_prot, (False, Something))) + self.assertEqual(olc_menus._validate_prototype(self.test_prot), (False, Something)) + self.assertEqual(olc_menus._validate_prototype( + {"prototype_key": "testthing", "key": "mytest"}), + (True, Something)) def test_node_helpers(self): @@ -363,23 +373,27 @@ class TestMenuModule(EvenniaTest): with mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(return_value=[self.test_prot])): + # prototype_key helpers self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), "node_prototype_parent") caller.ndb._menutree.olc_new = True self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), "node_index") + # prototype_parent helpers self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot']) self.assertEqual(olc_menus._prototype_parent_examine( caller, 'test_prot'), - '|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() ' - '\n|cdesc:|n None \n|cprototype:|n {\n \n}') + "|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() " + "\n|cdesc:|n None \n|cprototype:|n " + "{\n 'typeclass': 'evennia.objects.objects.DefaultObject', \n}") self.assertEqual(olc_menus._prototype_parent_select(caller, self.test_prot), "node_key") self.assertEqual(olc_menus._get_menu_prototype(caller), {'prototype_key': 'test_prot', 'prototype_locks': 'edit:all();spawn:all()', - 'prototype_parent': "test_prot"}) + 'typeclass': 'evennia.objects.objects.DefaultObject'}) + # typeclass helpers with mock.patch("evennia.utils.utils.get_all_typeclasses", new=mock.MagicMock(return_value={"foo": None, "bar": None})): self.assertEqual(olc_menus._all_typeclasses(caller), ["bar", "foo"]) @@ -394,6 +408,53 @@ class TestMenuModule(EvenniaTest): 'prototype_locks': 'edit:all();spawn:all()', 'typeclass': 'evennia.objects.objects.DefaultObject'}) + # attr helpers + self.assertEqual(olc_menus._caller_attrs(caller), []) + self.assertEqual(olc_menus._add_attr(caller, "test1=foo1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_attr(caller, "test2;cat1=foo2"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_attr(caller, "test3;cat2;edit:false()=foo3"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_attr(caller, "test4;cat3;set:true();edit:false()=foo4"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_attr(caller, "test5;cat4;set:true();edit:false()=123"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._caller_attrs( + caller), + [("test1", "foo1", None, ''), + ("test2", "foo2", "cat1", ''), + ("test3", "foo3", "cat2", "edit:false()"), + ("test4", "foo4", "cat3", "set:true();edit:false()"), + ("test5", '123', "cat4", "set:true();edit:false()")]) + self.assertEqual(olc_menus._edit_attr(caller, "test1", "1;cat5;edit:all()"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._examine_attr(caller, "test1"), Something) + + # tag helpers + self.assertEqual(olc_menus._caller_tags(caller), []) + self.assertEqual(olc_menus._add_tag(caller, "foo1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_tag(caller, "foo2;cat1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._add_tag(caller, "foo3;cat2;dat1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._caller_tags( + caller), + [('foo1', None, ""), + ('foo2', 'cat1', ""), + ('foo3', 'cat2', "dat1")]) + self.assertEqual(olc_menus._edit_tag(caller, "foo1", "bar1;cat1"), (Something, {"key": "_default", "goto": Something})) + self.assertEqual(olc_menus._display_tag(olc_menus._caller_tags(caller)[0]), Something) + self.assertEqual(olc_menus._caller_tags(caller)[0], ("bar1", "cat1", "")) + + protlib.save_prototype(**self.test_prot) + + # spawn helpers + obj = olc_menus._spawn(caller, prototype=self.test_prot) + + self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject") + self.assertEqual(obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot['prototype_key']) + self.assertEqual(olc_menus._update_spawned(caller, prototype=self.test_prot, objects=[obj]), 0) # no changes to apply + self.test_prot['key'] = "updated key" # change prototype + self.assertEqual(self._update_spawned(caller, prototype=self.test_prot, objects=[obj]), 1) # apply change to the one obj + + + # load helpers + + + @mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock( return_value=[{"prototype_key": "TestPrototype", From 178a2c8c8492fe46266eeaf07de6d9c887ebbc20 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 4 Jul 2018 19:25:44 +0200 Subject: [PATCH 076/103] Add unit tests to all menu helpers --- evennia/prototypes/menus.py | 15 ++++++++------- evennia/prototypes/spawner.py | 2 +- evennia/prototypes/tests.py | 11 +++++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index af670b743a..1f2eb26a4f 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -825,10 +825,11 @@ def _update_spawned(caller, **kwargs): """update existing objects""" prototype = kwargs['prototype'] objects = kwargs['objects'] - back_node = kwargs['back_key'] - num_changed = spawner.batch_update_objects_with_prototype(prototype, objects=objects) + back_node = kwargs['back_node'] + diff = kwargs.get('diff', None) + num_changed = spawner.batch_update_objects_with_prototype(prototype, diff=diff, objects=objects) caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed)) - return back_key + return back_node def _keep_diff(caller, **kwargs): @@ -884,15 +885,15 @@ def node_update_objects(caller, **kwargs): text.append(line.format(iopt=io, key=key, old=old_val, sep=" |y->|n ", new=new_val, change=inst)) options.append(_keep_option(key, prototype, - obj, obj_prototype, diff, objects, back_node)) + obj, obj_prototype, diff, update_objects, back_node)) elif inst == "REMOVE": text.append(line.format(iopt=io, key=key, old=old_val, sep=" |r->|n ", new='', change=inst)) options.append(_keep_option(key, prototype, - obj, obj_prototype, diff, objects, back_node)) + obj, obj_prototype, diff, update_objects, back_node)) options.extend( [{"key": ("|wu|r update {} objects".format(len(update_objects)), "update", "u"), - "goto": (_update_spawned, {"prototype": prototype, "objects": objects, + "goto": (_update_spawned, {"prototype": prototype, "objects": update_objects, "back_node": back_node, "diff": diff})}, {"key": ("|wr|neset changes", "reset", "r"), "goto": ("node_update_objects", {"prototype": prototype, "back_node": back_node, @@ -1111,7 +1112,7 @@ def start_olc(caller, session=None, prototype=None): "node_location": node_location, "node_home": node_home, "node_destination": node_destination, - "node_update_objects": node_o + "node_update_objects": node_update_objects, "node_prototype_desc": node_prototype_desc, "node_prototype_tags": node_prototype_tags, "node_prototype_locks": node_prototype_locks, diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index d826317fec..c09a192819 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -290,7 +290,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None): return 0 if not diff: - diff = prototype_diff_from_object(new_prototype, objects[0]) + diff, _ = prototype_diff_from_object(new_prototype, objects[0]) changed = 0 for obj in objects: diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 69eb495dd5..8932b368c1 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -446,13 +446,16 @@ 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']) - self.assertEqual(olc_menus._update_spawned(caller, prototype=self.test_prot, objects=[obj]), 0) # no changes to apply - self.test_prot['key'] = "updated key" # change prototype - self.assertEqual(self._update_spawned(caller, prototype=self.test_prot, objects=[obj]), 1) # apply change to the one obj + # update helpers + self.assertEqual(olc_menus._update_spawned( + caller, prototype=self.test_prot, back_node="foo", objects=[obj]), 'foo') # no changes to apply + self.test_prot['key'] = "updated key" # change prototype + self.assertEqual(olc_menus._update_spawned( + caller, prototype=self.test_prot, objects=[obj], back_node='foo'), 'foo') # apply change to the one obj # load helpers - + self.assertEqual(olc_menus._prototype_load_select(caller, self.test_prot['prototype_key']), "node_index") From 7ac113a3e12bf8b5738c9b155a537fc728629a24 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 12 Jul 2018 10:18:04 +0200 Subject: [PATCH 077/103] More work on unittests, still issues --- evennia/commands/default/building.py | 86 +++++++++------------------- evennia/commands/default/tests.py | 26 +++++---- evennia/prototypes/prototypes.py | 13 ++++- 3 files changed, 56 insertions(+), 69 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 5c96ad1cf6..a589b5131e 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2795,17 +2795,17 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): spawn objects from prototype Usage: - @spawn[/noloc] + @spawn[/noloc] @spawn[/noloc] - @spawn/search [key][;tag[,tag]] - @spawn/list [tag, tag] - @spawn/show [] - @spawn/update + @spawn/search [prototype_keykey][;tag[,tag]] + @spawn/list [tag, tag, ...] + @spawn/show [] + @spawn/update - @spawn/save [;desc[;tag,tag[,...][;lockstring]]] = - @spawn/menu [] - @olc - equivalent to @spawn/menu + @spawn/save + @spawn/edit [] + @olc - equivalent to @spawn/edit Switches: noloc - allow location to be None if not specified explicitly. Otherwise, @@ -2819,7 +2819,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. - menu, olc - create/manipulate prototype in a menu interface. + edit, olc - create/manipulate prototype in a menu interface. Example: @spawn GOBLIN @@ -2827,10 +2827,11 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): @spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all() Dictionary keys: - |wprototype |n - name of parent prototype to use. Can be a list for - multiple inheritance (inherits left to right) + |wprototype_parent |n - name of parent prototype to use. Required if typeclass is + not set. Can be a path or a list for multiple inheritance (inherits + left to right). If set one of the parents must have a typeclass. + |wtypeclass |n - string. Required if prototype_parent is not set. |wkey |n - string, the main object identifier - |wtypeclass |n - string, if not set, will use settings.BASE_OBJECT_TYPECLASS |wlocation |n - this should be a valid object or #dbref |whome |n - valid object or #dbref |wdestination|n - only valid for exits (object or dbref) @@ -2875,7 +2876,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): 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".format(err)) + "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) @@ -2896,9 +2898,9 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): def _search_show_prototype(query, prototypes=None): # prototype detail if not prototypes: - prototypes = spawner.search_prototype(key=query) + prototypes = protlib.search_prototype(key=query) if prototypes: - return "\n".join(spawner.prototype_to_str(prot) for prot in prototypes) + return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes) else: return False @@ -2947,64 +2949,36 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): if 'list' in self.switches: # for list, all optional arguments are tags + # import pudb; pudb.set_trace() + EvMore(caller, unicode(protlib.list_prototypes(caller, tags=self.lhslist)), exit_on_lastpage=True) return if 'save' in self.switches: # store a prototype to the database store - if not self.args or not self.rhs: + if not self.args: caller.msg( "Usage: @spawn/save [;desc[;tag,tag[,...][;lockstring]]] = ") return - # handle lhs - parts = self.lhs.split(";", 3) - nparts = len(parts) - if nparts == 1: - key = parts[0].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(",") if tag] - else: - # lockstrings can itself contain ; - key, desc, tags, lockstring = (part.strip() for part in parts) - tags = [tag.strip().lower() for tag in tags.split(",") if tag] - if not key: - caller.msg("The prototype must have a key.") - return - if not desc: - desc = "User-created prototype" - if not tags: - tags = ["user"] - if not lockstring: - lockstring = "edit:id({}) or perm(Admin); use:all()".format(caller.id) - - is_valid, err = caller.locks.validate(lockstring) - if not is_valid: - caller.msg("|rLock error|n: {}".format(err)) - return - # handle rhs: - prototype = _parse_prototype(self.rhs) + prototype = _parse_prototype(self.lhs.strip()) if not prototype: return - # inject the prototype_* keys into the prototype to save - prototype['prototype_key'] = prototype.get('prototype_key', key) - prototype['prototype_desc'] = prototype.get('prototype_desc', desc) - prototype['prototype_tags'] = prototype.get('prototype_tags', tags) - prototype['prototype_locks'] = prototype.get('prototype_locks', lockstring) - # 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" + 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 + # check for existing prototype, - old_matchstring = _search_show_prototype(key) + old_matchstring = _search_show_prototype(prototype_key) if old_matchstring: string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring) @@ -3017,14 +2991,10 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # all seems ok. Try to save. try: - prot = spawner.save_db_prototype( - caller, key, prototype, desc=desc, tags=tags, locks=lockstring) + prot = protlib.save_prototype(**prototype) if not prot: caller.msg("|rError saving:|R {}.|n".format(key)) return - prot.locks.append("edit", "perm(Admin)") - if not prot.locks.get("use"): - prot.locks.add("use:all()") except PermissionError as err: caller.msg("|rError saving:|R {}|n".format(err)) return diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index e1688cdb48..f047b3a458 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -360,6 +360,7 @@ class TestBuilding(CommandTest): # check that it exists in the process. query = search_object(objKeyStr) commandTest.assertIsNotNone(query) + commandTest.assertTrue(bool(query)) obj = query[0] commandTest.assertIsNotNone(obj) return obj @@ -368,18 +369,20 @@ class TestBuilding(CommandTest): self.call(building.CmdSpawn(), " ", "Usage: @spawn") # Tests "@spawn " without specifying location. - self.call(building.CmdSpawn(), - "{'prototype_key': 'testprot', 'key':'goblin', " - "'typeclass':'evennia.DefaultCharacter'}", "Spawned goblin") - goblin = getObject(self, "goblin") + with mock.patch('evennia.commands.default.func', return_value=iter(['y'])) as mock_iter: + self.call(building.CmdSpawn(), + "/save {'prototype_key': 'testprot', 'key':'Test Char', " + "'typeclass':'evennia.objects.objects.DefaultCharacter'}", "") + mock_iter.assert_called() - # Tests that the spawned object's type is a DefaultCharacter. - self.assertIsInstance(goblin, DefaultCharacter) + self.call(building.CmdSpawn(), "/list", "foo") + self.call(building.CmdSpawn(), 'testprot', "Spawned Test Char") # Tests that the spawned object's location is the same as the caharacter's location, since # we did not specify it. - self.assertEqual(goblin.location, self.char1.location) - goblin.delete() + testchar = getObject(self, "Test Char") + self.assertEqual(testchar.location, self.char1.location) + testchar.delete() # Test "@spawn " with a location other than the character's. spawnLoc = self.room2 @@ -389,10 +392,13 @@ class TestBuilding(CommandTest): spawnLoc = self.room1 self.call(building.CmdSpawn(), - "{'prototype_key':'GOBLIN', 'key':'goblin', 'location':'%s'}" - % spawnLoc.dbref, "Spawned goblin") + "{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', " + "'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "Spawned goblin") goblin = getObject(self, "goblin") + # Tests that the spawned object's type is a DefaultCharacter. + self.assertIsInstance(goblin, DefaultCharacter) self.assertEqual(goblin.location, spawnLoc) + goblin.delete() protlib.create_prototype(**{'key': 'Ball', 'prototype': 'GOBLIN', 'prototype_key': 'testball'}) diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 2457f86994..57087b133f 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -506,7 +506,7 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed ",".join(ptags))) if not display_tuples: - return None + return "" table = [] width = 78 @@ -607,3 +607,14 @@ def validate_prototype(prototype, protkey=None, protparents=None, raise RuntimeError("Error: " + "\nError: ".join(_flags['errors'])) if _flags['warnings']: raise RuntimeWarning("Warning: " + "\nWarning: ".join(_flags['warnings'])) + + # make sure prototype_locks are set to defaults + prototype_locks = [lstring.split(":", 1) + for lstring in prototype.get("prototype_locks", "").split(';')] + locktypes = [tup[0].strip() for tup in prototype_locks] + if "spawn" not in locktypes: + prototype_locks.append(("spawn", "all()")) + if "edit" not in locktypes: + prototype_locks.append(("edit", "all()")) + prototype_locks = ";".join(":".join(tup) for tup in prototype_locks) + prototype['prototype_locks'] = prototype_locks From 5db7c1dfbb2f81dea93a2e2e43d888db7a41d3be Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 21 Jul 2018 13:40:46 +0200 Subject: [PATCH 078/103] Fix unittests --- evennia/commands/default/building.py | 12 +-- evennia/commands/default/tests.py | 49 +++++++++--- evennia/contrib/tutorial_world/objects.py | 24 +++--- evennia/prototypes/prototypes.py | 10 ++- evennia/prototypes/tests.py | 93 ++++++++++++++++------- 5 files changed, 128 insertions(+), 60 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index a589b5131e..cdcbadc103 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2993,22 +2993,22 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): try: prot = protlib.save_prototype(**prototype) if not prot: - caller.msg("|rError saving:|R {}.|n".format(key)) + caller.msg("|rError saving:|R {}.|n".format(prototype_key)) return - except PermissionError as err: + except protlib.PermissionError as err: caller.msg("|rError saving:|R {}|n".format(err)) return - caller.msg("|gSaved prototype:|n {}".format(key)) + caller.msg("|gSaved prototype:|n {}".format(prototype_key)) # check if we want to update existing objects - existing_objects = spawner.search_objects_with_prototype(key) + 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, key, slow)) + n_existing, prototype_key, slow)) answer = yield(string) if answer.lower() in ["n", "no"]: caller.msg("|rNo update was done of existing objects. " @@ -3036,7 +3036,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): return try: success = protlib.delete_db_prototype(caller, self.args) - except PermissionError as err: + 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?)')) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index f047b3a458..709e7154ba 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -28,7 +28,7 @@ from evennia.utils import ansi, utils, gametime from evennia.server.sessionhandler import SESSIONS from evennia import search_object from evennia import DefaultObject, DefaultCharacter -from evennia.prototypes import spawner, prototypes as protlib +from evennia.prototypes import prototypes as protlib # set up signal here since we are not starting the server @@ -46,7 +46,7 @@ class CommandTest(EvenniaTest): Tests a command """ def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None, - receiver=None, cmdstring=None, obj=None): + receiver=None, cmdstring=None, obj=None, inputs=None): """ Test a command by assigning all the needed properties to cmdobj and running @@ -75,14 +75,31 @@ class CommandTest(EvenniaTest): cmdobj.obj = obj or (caller if caller else self.char1) # test old_msg = receiver.msg + inputs = inputs or [] + try: receiver.msg = Mock() if cmdobj.at_pre_cmd(): return cmdobj.parse() ret = cmdobj.func() + + # handle func's with yield in them (generators) if isinstance(ret, types.GeneratorType): - ret.next() + while True: + try: + inp = inputs.pop() if inputs else None + if inp: + try: + ret.send(inp) + except TypeError: + ret.next() + ret = ret.send(inp) + else: + ret.next() + except StopIteration: + break + cmdobj.at_post_cmd() except StopIteration: pass @@ -95,7 +112,7 @@ class CommandTest(EvenniaTest): # Get the first element of a tuple if msg received a tuple instead of a string stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg] if msg is not None: - returned_msg = "||".join(_RE.sub("", mess) for mess in stored_msg) + returned_msg = "||".join(_RE.sub("", str(mess)) for mess in stored_msg) returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip() if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()): sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n" @@ -369,13 +386,13 @@ class TestBuilding(CommandTest): self.call(building.CmdSpawn(), " ", "Usage: @spawn") # Tests "@spawn " without specifying location. - with mock.patch('evennia.commands.default.func', return_value=iter(['y'])) as mock_iter: - self.call(building.CmdSpawn(), - "/save {'prototype_key': 'testprot', 'key':'Test Char', " - "'typeclass':'evennia.objects.objects.DefaultCharacter'}", "") - mock_iter.assert_called() - self.call(building.CmdSpawn(), "/list", "foo") + self.call(building.CmdSpawn(), + "/save {'prototype_key': 'testprot', 'key':'Test Char', " + "'typeclass':'evennia.objects.objects.DefaultCharacter'}", + "Saved prototype: testprot", inputs=['y']) + + self.call(building.CmdSpawn(), "/list", "| Key ") self.call(building.CmdSpawn(), 'testprot', "Spawned Test Char") # Tests that the spawned object's location is the same as the caharacter's location, since @@ -401,10 +418,14 @@ class TestBuilding(CommandTest): goblin.delete() - protlib.create_prototype(**{'key': 'Ball', 'prototype': 'GOBLIN', 'prototype_key': 'testball'}) + # create prototype + protlib.create_prototype(**{'key': 'Ball', + 'typeclass': 'evennia.objects.objects.DefaultCharacter', + 'prototype_key': 'testball'}) # Tests "@spawn " self.call(building.CmdSpawn(), "testball", "Spawned Ball") + ball = getObject(self, "Ball") self.assertEqual(ball.location, self.char1.location) self.assertIsInstance(ball, DefaultObject) @@ -417,10 +438,14 @@ class TestBuilding(CommandTest): self.assertIsNone(ball.location) ball.delete() + self.call(building.CmdSpawn(), + "/noloc {'prototype_parent':'TESTBALL', 'prototype_key': 'testball', 'location':'%s'}" + % spawnLoc.dbref, "Error: Prototype testball tries to parent itself.") + # Tests "@spawn/noloc ...", but DO specify a location. # Location should be the specified location. self.call(building.CmdSpawn(), - "/noloc {'prototype':'TESTBALL', 'location':'%s'}" + "/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo', 'location':'%s'}" % spawnLoc.dbref, "Spawned Ball") ball = getObject(self, "Ball") self.assertEqual(ball.location, spawnLoc) diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index 807b4d5e09..1e088aa8d0 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -905,19 +905,19 @@ WEAPON_PROTOTYPES = { "magic": False, "desc": "A generic blade."}, "knife": { - "prototype": "weapon", + "prototype_parent": "weapon", "aliases": "sword", "key": "Kitchen knife", "desc": "A rusty kitchen knife. Better than nothing.", "damage": 3}, "dagger": { - "prototype": "knife", + "prototype_parent": "knife", "key": "Rusty dagger", "aliases": ["knife", "dagger"], "desc": "A double-edged dagger with a nicked edge and a wooden handle.", "hit": 0.25}, "sword": { - "prototype": "weapon", + "prototype_parent": "weapon", "key": "Rusty sword", "aliases": ["sword"], "desc": "A rusty shortsword. It has a leather-wrapped handle covered i food grease.", @@ -925,28 +925,28 @@ WEAPON_PROTOTYPES = { "damage": 5, "parry": 0.5}, "club": { - "prototype": "weapon", + "prototype_parent": "weapon", "key": "Club", "desc": "A heavy wooden club, little more than a heavy branch.", "hit": 0.4, "damage": 6, "parry": 0.2}, "axe": { - "prototype": "weapon", + "prototype_parent": "weapon", "key": "Axe", "desc": "A woodcutter's axe with a keen edge.", "hit": 0.4, "damage": 6, "parry": 0.2}, "ornate longsword": { - "prototype": "sword", + "prototype_parent": "sword", "key": "Ornate longsword", "desc": "A fine longsword with some swirling patterns on the handle.", "hit": 0.5, "magic": True, "damage": 5}, "warhammer": { - "prototype": "club", + "prototype_parent": "club", "key": "Silver Warhammer", "aliases": ["hammer", "warhammer", "war"], "desc": "A heavy war hammer with silver ornaments. This huge weapon causes massive damage - if you can hit.", @@ -954,21 +954,21 @@ WEAPON_PROTOTYPES = { "magic": True, "damage": 8}, "rune axe": { - "prototype": "axe", + "prototype_parent": "axe", "key": "Runeaxe", "aliases": ["axe"], "hit": 0.4, "magic": True, "damage": 6}, "thruning": { - "prototype": "ornate longsword", + "prototype_parent": "ornate longsword", "key": "Broadsword named Thruning", "desc": "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands.", "hit": 0.6, "parry": 0.6, "damage": 7}, "slayer waraxe": { - "prototype": "rune axe", + "prototype_parent": "rune axe", "key": "Slayer waraxe", "aliases": ["waraxe", "war", "slayer"], "desc": "A huge double-bladed axe marked with the runes for 'Slayer'." @@ -976,7 +976,7 @@ WEAPON_PROTOTYPES = { "hit": 0.7, "damage": 8}, "ghostblade": { - "prototype": "ornate longsword", + "prototype_parent": "ornate longsword", "key": "The Ghostblade", "aliases": ["blade", "ghost"], "desc": "This massive sword is large as you are tall, yet seems to weigh almost nothing." @@ -985,7 +985,7 @@ WEAPON_PROTOTYPES = { "parry": 0.8, "damage": 10}, "hawkblade": { - "prototype": "ghostblade", + "prototype_parent": "ghostblade", "key": "The Hawkblade", "aliases": ["hawk", "blade"], "desc": "The weapon of a long-dead heroine and a more civilized age," diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 57087b133f..df9674b4e7 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -13,7 +13,7 @@ from evennia.objects.models import ObjectDB from evennia.utils.create import create_script from evennia.utils.utils import ( all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module, - get_all_typeclasses, to_str) + get_all_typeclasses, to_str, dbref) from evennia.locks.lockhandler import validate_lockstring, check_lockstring from evennia.utils import logger from evennia.utils import inlinefuncs @@ -91,6 +91,10 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F """ if not isinstance(value, basestring): + try: + value = value.dbref + except AttributeError: + pass value = to_str(value, force_string=True) available_functions = _PROT_FUNCS if available_functions is None else available_functions @@ -577,7 +581,7 @@ def validate_prototype(prototype, protkey=None, protparents=None, for protstring in make_iter(prototype_parent): protstring = protstring.lower() if protkey is not None and protstring == protkey: - _flags['errors'].append("Protototype {} tries to parent itself.".format(protkey)) + _flags['errors'].append("Prototype {} tries to parent itself.".format(protkey)) protparent = protparents.get(protstring) if not protparent: _flags['errors'].append("Prototype {}'s prototype_parent '{}' was not found.".format( @@ -610,7 +614,7 @@ def validate_prototype(prototype, protkey=None, protparents=None, # make sure prototype_locks are set to defaults prototype_locks = [lstring.split(":", 1) - for lstring in prototype.get("prototype_locks", "").split(';')] + for lstring in prototype.get("prototype_locks", "").split(';') if ":" in lstring] locktypes = [tup[0].strip() for tup in prototype_locks] if "spawn" not in locktypes: prototype_locks.append(("spawn", "all()")) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 8932b368c1..221200672d 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -114,24 +114,41 @@ class TestUtils(EvenniaTest): self.assertEqual( pdiff, - {'aliases': 'REMOVE', - 'attrs': 'REPLACE', - 'home': 'KEEP', - 'key': 'UPDATE', - 'location': 'KEEP', - 'locks': 'KEEP', - 'new': 'UPDATE', - 'permissions': 'UPDATE', - 'prototype_desc': 'UPDATE', - 'prototype_key': 'UPDATE', - 'prototype_locks': 'KEEP', - 'prototype_tags': 'KEEP', - 'test': 'UPDATE', - 'typeclass': 'KEEP'}) + ({'aliases': 'REMOVE', + 'attrs': 'REPLACE', + 'home': 'KEEP', + 'key': 'UPDATE', + 'location': 'KEEP', + 'locks': 'KEEP', + 'new': 'UPDATE', + 'permissions': 'UPDATE', + 'prototype_desc': 'UPDATE', + 'prototype_key': 'UPDATE', + 'prototype_locks': 'KEEP', + 'prototype_tags': 'KEEP', + 'test': 'UPDATE', + 'typeclass': 'KEEP'}, + {'attrs': [('oldtest', 'to_remove', None, ['']), + ('test', 'testval', None, [''])], + 'prototype_locks': 'spawn:all();edit:all()', + 'prototype_key': Something, + 'locks': ['call:true()', 'control:perm(Developer)', + 'delete:perm(Admin)', 'edit:perm(Admin)', + 'examine:perm(Builder)', 'get:all()', + 'puppet:pperm(Developer)', 'tell:perm(Admin)', + 'view:all()'], + 'prototype_tags': [], + 'location': self.room1, + 'key': 'NewObj', + 'home': self.room1, + 'typeclass': 'evennia.objects.objects.DefaultObject', + 'prototype_desc': 'Built from NewObj', + 'aliases': 'foo'}) + ) # apply diff count = spawner.batch_update_objects_with_prototype( - old_prot, diff=pdiff, objects=[self.obj1]) + old_prot, diff=pdiff[0], objects=[self.obj1]) self.assertEqual(count, 1) new_prot = spawner.prototype_from_object(self.obj1) @@ -470,7 +487,7 @@ class TestOLCMenu(TestEvMenu): menutree = "evennia.prototypes.menus" startnode = "node_index" - debug_output = True + # debug_output = True expect_all_nodes = True expected_node_texts = { @@ -480,15 +497,37 @@ class TestOLCMenu(TestEvMenu): expected_tree = \ ['node_index', ['node_prototype_key', + ['node_index', + 'node_index', + 'node_validate_prototype', + ['node_index'], + 'node_index'], 'node_typeclass', - 'node_aliases', - 'node_attrs', - 'node_tags', - 'node_locks', - 'node_permissions', - 'node_location', - 'node_home', - 'node_destination', - 'node_prototype_desc', - 'node_prototype_tags', - 'node_prototype_locks']] + ['node_key', + ['node_typeclass', + 'node_key', + 'node_index', + 'node_validate_prototype', + 'node_validate_prototype'], + 'node_index', + 'node_index', + 'node_index', + 'node_validate_prototype', + 'node_validate_prototype'], + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype', + 'node_validate_prototype']] From 47eb1896d1e6f95cbbb3e4721799597b2e62660e Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 21 Jul 2018 13:51:13 +0200 Subject: [PATCH 079/103] Remove old olc/ folder --- evennia/utils/olc/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 evennia/utils/olc/__init__.py diff --git a/evennia/utils/olc/__init__.py b/evennia/utils/olc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 0e13f272b351d177c535029ff7db668fd042036c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 21 Jul 2018 19:06:15 +0200 Subject: [PATCH 080/103] Start improve OLC menu docs and help texts --- CHANGELOG.md | 9 ++ evennia/prototypes/menus.py | 180 +++++++++++++++++++++++++++---- evennia/prototypes/prototypes.py | 22 +++- evennia/utils/evmenu.py | 15 ++- evennia/utils/inlinefuncs.py | 6 +- 5 files changed, 203 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1c4cf787..5ae990b85b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,15 @@ - The spawn command got the /save switch to save the defined prototype and its key. - The command spawn/menu will now start an OLC (OnLine Creation) menu to load/save/edit/spawn prototypes. +### EvMenu + +- Added `EvMenu.helptext_formatter(helptext)` to allow custom formatting of per-node help. +- Added `evennia.utils.evmenu.list_node` decorator for turning an EvMenu node into a multi-page listing. +- A `goto` option callable returning None (rather than the name of the next node) will now rerun the + current node instead of failing. +- Better error handling of in-node syntax errors. + + # Overviews diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 1f2eb26a4f..1e775fbd77 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -8,6 +8,7 @@ import json from random import choice from django.conf import settings from evennia.utils.evmenu import EvMenu, list_node +from evennia.utils import evmore from evennia.utils.ansi import strip_ansi from evennia.utils import utils from evennia.prototypes import prototypes as protlib @@ -78,7 +79,9 @@ def _format_option_value(prop, required=False, prototype=None, cropper=None): out = ", ".join(str(pr) for pr in prop) if not out and required: out = "|rrequired" - return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH)) + if out: + return " ({}|n)".format(cropper(out) if cropper else utils.crop(out, _MENU_CROP_WIDTH)) + return "" def _set_prototype_value(caller, field, value, parse=True): @@ -214,31 +217,75 @@ def _validate_prototype(prototype): return err, text -# Menu nodes +def _format_protfuncs(): + out = [] + sorted_funcs = [(key, func) for key, func in + sorted(protlib.PROT_FUNCS.items(), key=lambda tup: tup[0])] + for protfunc_name, protfunc in sorted_funcs: + out.append("- |c${name}|n - |W{docs}".format( + name=protfunc_name, + docs=utils.justify(protfunc.__doc__.strip(), align='l', indent=10).strip())) + return "\n ".join(out) + + +# Menu nodes ------------------------------ + + +# main index (start page) node + def node_index(caller): prototype = _get_menu_prototype(caller) - text = ( - "|c --- Prototype wizard --- |n\n\n" - "Define the |yproperties|n of the prototype. All prototype values can be " - "over-ridden at the time of spawning an instance of the prototype, but some are " - "required.\n\n'|wprototype-'-properties|n are not used in the prototype itself but are used " - "to organize and list prototypes. The 'prototype-key' uniquely identifies the prototype " - "and allows you to edit an existing prototype or save a new one for use by you or " - "others later.\n\n(make choice; q to abort. If unsure, start from 1.)") + text = """ + |c --- Prototype wizard --- |n + + A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype + can be hard-coded or scripted using |w$protfuncs|n - for example to randomize the value + every time the prototype is used to spawn a new entity. + + The prototype fields named 'prototype_*' are not used to create the entity itself but for + organizing the template when saving it for you (and maybe others) to use later. + + Select prototype field to edit. If you are unsure, start from [|w1|n]. At any time you can + [|wV|n]alidate that the prototype works correctly and use it to [|wSP|n]awn a new entity. You + can also [|wSA|n]ve|n your work or [|wLO|n]oad an existing prototype to use as a base. Use + [|wL|n]ook to re-show a menu node. [|wQ|n]uit will always exit the menu and [|wH|n]elp will + show context-sensitive help. + """ + + helptxt = """ + |c- prototypes |n + + A prototype is really just a Python dictionary. When spawning, this dictionary is essentially + passed into `|wevennia.utils.create.create_object(**prototype)|n` to create a new object. By + using different prototypes you can customize instances of objects without having to do code + changes to their typeclass (something which requires code access). The classical example is + to spawn goblins with different names, looks, equipment and skill, each based on the same + `Goblin` typeclass. + + |c- $protfuncs |n + + Prototype-functions (protfuncs) allow for limited scripting within a prototype. These are + entered as a string $funcname(arg, arg, ...) and are evaluated |wat the time of spawning|n only. + They can also be nested for combined effects. + + {pfuncs} + """.format(pfuncs=_format_protfuncs()) + + text = (text, helptxt) options = [] options.append( {"desc": "|WPrototype-Key|n|n{}".format( _format_option_value("Key", "prototype_key" not in prototype, prototype, None)), "goto": "node_prototype_key"}) - for key in ('Typeclass', 'Prototype_parent', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', + for key in ('Typeclass', 'Prototype-parent', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', 'Permissions', 'Location', 'Home', 'Destination'): required = False cropper = None if key in ("Prototype-parent", "Typeclass"): - required = "prototype_parent" not in prototype and "typeclass" not in prototype + required = ("prototype_parent" not in prototype) and ("typeclass" not in prototype) if key == 'Typeclass': cropper = _path_cropper options.append( @@ -256,16 +303,18 @@ def node_index(caller): options.extend(( {"key": ("|wV|Walidate prototype", "validate", "v"), "goto": "node_validate_prototype"}, - {"key": ("|wS|Wave prototype", "save", "s"), + {"key": ("|wSA|Wve prototype", "save", "sa"), "goto": "node_prototype_save"}, {"key": ("|wSP|Wawn prototype", "spawn", "sp"), "goto": "node_prototype_spawn"}, - {"key": ("|wL|Woad prototype", "load", "l"), + {"key": ("|wLO|Wad prototype", "load", "lo"), "goto": "node_prototype_load"})) return text, options +# validate prototype (available as option from all nodes) + def node_validate_prototype(caller, raw_string, **kwargs): """General node to view and validate a protototype""" prototype = _get_menu_prototype(caller) @@ -273,11 +322,22 @@ def node_validate_prototype(caller, raw_string, **kwargs): _, text = _validate_prototype(prototype) + helptext = """ + The validator checks if the prototype's various values are on the expected form. It also test + any $protfuncs. + + """ + + text = (text, helptext) + options = _wizard_options(None, prev_node, None) return text, options +# prototype_key node + + def _check_prototype_key(caller, key): old_prototype = protlib.search_prototype(key) olc_new = _is_new_prototype(caller) @@ -303,22 +363,36 @@ def _check_prototype_key(caller, key): def node_prototype_key(caller): prototype = _get_menu_prototype(caller) - text = ["The prototype name, or |wMeta-Key|n, uniquely identifies the prototype. " - "It is used to find and use the prototype to spawn new entities. " - "It is not case sensitive."] + text = """ + The |cPrototype-Key|n uniquely identifies the prototype. It must be specified. It is used to + find and use the prototype to spawn new entities. It is not case sensitive. + + {current}""" + + helptext = """ + The prototype-key is not itself used to spawn the new object, but is only used for managing, + storing and loading the prototype. It must be globally unique, so existing keys will be + checked before a new key is accepted. If an existing key is picked, the existing prototype + will be loaded. + """ + old_key = prototype.get('prototype_key', None) if old_key: - text.append("Current key is '|w{key}|n'".format(key=old_key)) + text = text.format(current="Currently set to '|w{key}|n'".format(key=old_key)) else: - text.append("The key is currently unset.") - text.append("Enter text or make a choice (q for quit)") - text = "\n\n".join(text) + text = text.format(current="Currently |runset|n (required).") + options = _wizard_options("prototype_key", "index", "prototype_parent") options.append({"key": "_default", "goto": _check_prototype_key}) + + text = (text, helptext) return text, options +# prototype_parents node + + def _all_prototype_parents(caller): """Return prototype_key of all available prototypes for listing in menu""" return [prototype["prototype_key"] @@ -368,6 +442,8 @@ def node_prototype_parent(caller): return text, options +# typeclasses node + def _all_typeclasses(caller): """Get name of available typeclasses.""" return list(name for name in @@ -423,6 +499,9 @@ def node_typeclass(caller): return text, options +# key node + + def node_key(caller): prototype = _get_menu_prototype(caller) key = prototype.get("key") @@ -442,6 +521,9 @@ def node_key(caller): return text, options +# aliases node + + def node_aliases(caller): prototype = _get_menu_prototype(caller) aliases = prototype.get("aliases") @@ -462,6 +544,9 @@ def node_aliases(caller): return text, options +# attributes node + + def _caller_attrs(caller): prototype = _get_menu_prototype(caller) attrs = prototype.get("attrs", []) @@ -572,6 +657,9 @@ def node_attrs(caller): return text, options +# tags node + + def _caller_tags(caller): prototype = _get_menu_prototype(caller) tags = prototype.get("tags", []) @@ -659,6 +747,9 @@ def node_tags(caller): return text, options +# locks node + + def node_locks(caller): prototype = _get_menu_prototype(caller) locks = prototype.get("locks") @@ -679,6 +770,9 @@ def node_locks(caller): return text, options +# permissions node + + def node_permissions(caller): prototype = _get_menu_prototype(caller) permissions = prototype.get("permissions") @@ -699,6 +793,9 @@ def node_permissions(caller): return text, options +# location node + + def node_location(caller): prototype = _get_menu_prototype(caller) location = prototype.get("location") @@ -718,6 +815,9 @@ def node_location(caller): return text, options +# home node + + def node_home(caller): prototype = _get_menu_prototype(caller) home = prototype.get("home") @@ -737,6 +837,9 @@ def node_home(caller): return text, options +# destination node + + def node_destination(caller): prototype = _get_menu_prototype(caller) dest = prototype.get("dest") @@ -756,6 +859,9 @@ def node_destination(caller): return text, options +# prototype_desc node + + def node_prototype_desc(caller): prototype = _get_menu_prototype(caller) @@ -778,6 +884,9 @@ def node_prototype_desc(caller): return text, options +# prototype_tags node + + def node_prototype_tags(caller): prototype = _get_menu_prototype(caller) text = ["|wPrototype-Tags|n can be used to classify and find prototypes. " @@ -800,6 +909,9 @@ def node_prototype_tags(caller): return text, options +# prototype_locks node + + def node_prototype_locks(caller): prototype = _get_menu_prototype(caller) text = ["Set |wPrototype-Locks|n on the prototype. There are two valid lock types: " @@ -821,6 +933,9 @@ def node_prototype_locks(caller): return text, options +# update existing objects node + + def _update_spawned(caller, **kwargs): """update existing objects""" prototype = kwargs['prototype'] @@ -904,6 +1019,9 @@ def node_update_objects(caller, **kwargs): return text, options +# prototype save node + + def node_prototype_save(caller, **kwargs): """Save prototype to disk """ # these are only set if we selected 'yes' to save on a previous pass @@ -972,6 +1090,9 @@ def node_prototype_save(caller, **kwargs): return "\n".join(text), options +# spawning node + + def _spawn(caller, **kwargs): """Spawn prototype""" prototype = kwargs["prototype"].copy() @@ -1037,6 +1158,9 @@ def node_prototype_spawn(caller, **kwargs): return text, options +# prototype load node + + def _prototype_load_select(caller, prototype_key): matches = protlib.search_prototype(key=prototype_key) if matches: @@ -1052,12 +1176,15 @@ def _prototype_load_select(caller, prototype_key): @list_node(_all_prototype_parents, _prototype_load_select) def node_prototype_load(caller, **kwargs): text = ["Select a prototype to load. This will replace any currently edited prototype."] - options = _wizard_options("load", "save", "index") + options = _wizard_options("prototype_load", "prototype_save", "index") options.append({"key": "_default", "goto": _prototype_parent_examine}) return "\n".join(text), options +# EvMenu definition, formatting and access functions + + class OLCMenu(EvMenu): """ A custom EvMenu with a different formatting for the options. @@ -1086,6 +1213,15 @@ class OLCMenu(EvMenu): return "{}{}{}".format(olc_options, sep, other_options) + def helptext_formatter(self, helptext): + """ + Show help text + """ + return "|c --- Help ---|n\n" + helptext + + def display_helptext(self): + evmore.msg(self.caller, self.helptext, session=self._session) + def start_olc(caller, session=None, prototype=None): """ diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index df9674b4e7..011445b039 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -13,7 +13,7 @@ from evennia.objects.models import ObjectDB from evennia.utils.create import create_script from evennia.utils.utils import ( all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module, - get_all_typeclasses, to_str, dbref) + get_all_typeclasses, to_str, dbref, justify) from evennia.locks.lockhandler import validate_lockstring, check_lockstring from evennia.utils import logger from evennia.utils import inlinefuncs @@ -29,7 +29,7 @@ _PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + ( "permissions", "locks", "exec", "tags", "attrs") _PROTOTYPE_TAG_CATEGORY = "from_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" -_PROT_FUNCS = {} +PROT_FUNCS = {} _RE_DBREF = re.compile(r"(?".format(string) @@ -367,6 +368,7 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False try: # try to fetch the matching inlinefunc from storage stack.append(available_funcs[funcname]) + nvalid += 1 except KeyError: stack.append(available_funcs["nomatch"]) stack.append(funcname) @@ -393,9 +395,9 @@ def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False # this means not all inlinefuncs were complete return string - if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack): + if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid: # if stack is larger than limit, throw away parsing - return string + gdict["stackfull"](*args, **kwargs) + return string + available_funcs["stackfull"](*args, **kwargs) elif usecache: # cache the stack - we do this also if we don't check the cache above _PARSING_CACHE[string] = stack From 058f35650a189eef6794237f4c52e3994e1f62ba Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 23 Jul 2018 23:12:11 +0200 Subject: [PATCH 081/103] Add more in-menu docs --- evennia/prototypes/menus.py | 133 +++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 38 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 1e775fbd77..e70d5d87ae 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -117,8 +117,6 @@ def _set_property(caller, raw_string, **kwargs): processor = kwargs.get("processor", None) next_node = kwargs.get("next_node", "node_index") - propname_low = prop.strip().lower() - if callable(processor): try: value = processor(raw_string) @@ -134,13 +132,6 @@ def _set_property(caller, raw_string, **kwargs): return next_node prototype = _set_prototype_value(caller, prop, value) - - # typeclass and prototype_parent can't co-exist - if propname_low == "typeclass": - prototype.pop("prototype_parent", None) - if propname_low == "prototype_parent": - prototype.pop("typeclass", None) - caller.ndb._menutree.olc_prototype = prototype try: @@ -253,7 +244,6 @@ def node_index(caller): [|wL|n]ook to re-show a menu node. [|wQ|n]uit will always exit the menu and [|wH|n]elp will show context-sensitive help. """ - helptxt = """ |c- prototypes |n @@ -323,7 +313,7 @@ def node_validate_prototype(caller, raw_string, **kwargs): _, text = _validate_prototype(prototype) helptext = """ - The validator checks if the prototype's various values are on the expected form. It also test + The validator checks if the prototype's various values are on the expected form. It also tests any $protfuncs. """ @@ -364,16 +354,15 @@ def _check_prototype_key(caller, key): def node_prototype_key(caller): prototype = _get_menu_prototype(caller) text = """ - The |cPrototype-Key|n uniquely identifies the prototype. It must be specified. It is used to + The |cPrototype-Key|n uniquely identifies the prototype and is |wmandatory|n. It is used to find and use the prototype to spawn new entities. It is not case sensitive. {current}""" - helptext = """ - The prototype-key is not itself used to spawn the new object, but is only used for managing, - storing and loading the prototype. It must be globally unique, so existing keys will be - checked before a new key is accepted. If an existing key is picked, the existing prototype - will be loaded. + The prototype-key is not itself used when spawnng the new object, but is only used for + managing, storing and loading the prototype. It must be globally unique, so existing keys + will be checked before a new key is accepted. If an existing key is picked, the existing + prototype will be loaded. """ old_key = prototype.get('prototype_key', None) @@ -423,18 +412,36 @@ def node_prototype_parent(caller): prot_parent_key = prototype.get('prototype') - text = ["Set the prototype's |yParent Prototype|n. If this is unset, Typeclass will be used."] + text = """ + The |cPrototype Parent|n allows you to |winherit|n prototype values from another named + prototype (given as that prototype's |wprototype_key|). If not changing these values in the + current prototype, the parent's value will be used. Pick the available prototypes below. + + Note that somewhere in the prototype's parentage, a |ctypeclass|n must be specified. If no + parent is given, this prototype must define the typeclass (next menu node). + + {current} + """ + helptext = """ + Prototypes can inherit from one another. Changes in the child replace any values set in a + parent. The |wtypeclass|n key must exist |wsomewhere|n in the parent chain for the + prototype to be valid. + """ + if prot_parent_key: prot_parent = protlib.search_prototype(prot_parent_key) if prot_parent: - text.append( - "Current parent prototype is {}:\n{}".format(protlib.prototype_to_str(prot_parent))) + text.format( + current="Current parent prototype is {}:\n{}".format( + protlib.prototype_to_str(prot_parent))) else: - text.append("Current parent prototype |r{prototype}|n " + text.format( + current="Current parent prototype |r{prototype}|n " "does not appear to exist.".format(prot_parent_key)) else: - text.append("Parent prototype is not set") - text = "\n\n".join(text) + text.format(current="Parent prototype is not set") + text = (text, helptext) + options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W") options.append({"key": "_default", "goto": _prototype_parent_examine}) @@ -477,7 +484,7 @@ def _typeclass_examine(caller, typeclass_path): def _typeclass_select(caller, typeclass): """Select typeclass from list and add it to prototype. Return next node to go to.""" ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key") - caller.msg("Selected typeclass |y{}|n. Removed any set prototype parent.".format(typeclass)) + caller.msg("Selected typeclass |y{}|n.".format(typeclass)) return ret @@ -486,13 +493,32 @@ def node_typeclass(caller): prototype = _get_menu_prototype(caller) typeclass = prototype.get("typeclass") - text = ["Set the typeclass's parent |yTypeclass|n."] + text = """ + The |cTypeclass|n defines what 'type' of object this is - the actual working code to use. + + All spawned objects must have a typeclass. If not given here, the typeclass must be set in + one of the prototype's |cparents|n. + + {current} + """ + helptext = """ + A |nTypeclass|n is specified by the actual python-path to the class definition in the + Evennia code structure. + + Which |cAttributes|n, |cLocks|n and other properties have special + effects or expects certain values depend greatly on the code in play. + """ + if typeclass: - text.append("Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass)) + text.format( + current="Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass)) else: - text.append("Using default typeclass {typeclass}.".format( - typeclass=settings.BASE_OBJECT_TYPECLASS)) - text = "\n\n".join(text) + text.format( + current="Using default typeclass {typeclass}.".format( + typeclass=settings.BASE_OBJECT_TYPECLASS)) + + text = (text, helptext) + options = _wizard_options("typeclass", "prototype_parent", "key", color="|W") options.append({"key": "_default", "goto": _typeclass_examine}) @@ -506,12 +532,27 @@ def node_key(caller): prototype = _get_menu_prototype(caller) key = prototype.get("key") - text = ["Set the prototype's name (|yKey|n.) This will retain case sensitivity."] + text = """ + The |cKey|n is the given name of the object to spawn. This will retain the given case. + + {current} + """ + helptext = """ + The key should often not be identical for every spawned object. Using a randomising + $protfunc can be used, for example |c$choice(Alan, Tom, John)|n will give one of the three + names every time an object of this prototype is spawned. + + |c$protfuncs|n + {pfuncs} + """.format(pfuncs=_format_protfuncs()) + if key: - text.append("Current key value is '|y{key}|n'.".format(key=key)) + text.format(current="Current key is '{key}'.".format(key=key)) else: - text.append("Key is currently unset.") - text = "\n\n".join(text) + text.format(current="The key is currently unset.") + + text = (text, helptext) + options = _wizard_options("key", "typeclass", "aliases") options.append({"key": "_default", "goto": (_set_property, @@ -528,13 +569,29 @@ def node_aliases(caller): prototype = _get_menu_prototype(caller) aliases = prototype.get("aliases") - text = ["Set the prototype's |yAliases|n. Separate multiple aliases with commas. " - "they'll retain case sensitivity."] + text = """ + |cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not + case sensitive. + + Add multiple aliases separating with commas. + + {current} + """ + helptext = """ + Aliases are fixed alternative identifiers and are stored with the new object. + + |c$protfuncs|n + + {pfuncs} + """.format(pfuncs=_format_protfuncs()) + if aliases: - text.append("Current aliases are '|y{aliases}|n'.".format(aliases=aliases)) + text.format(current="Current aliases are '|c{aliases}|n'.".format(aliases=aliases)) else: - text.append("No aliases are set.") - text = "\n\n".join(text) + text.format(current="No aliases are set.") + + text = (text, helptext) + options = _wizard_options("aliases", "key", "attrs") options.append({"key": "_default", "goto": (_set_property, From 89ffa84c01585d018441d88dd30a160db5097892 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 24 Jul 2018 20:48:54 +0200 Subject: [PATCH 082/103] List lockfuncs in menu, more elaborate doc strings --- evennia/locks/lockhandler.py | 13 ++ evennia/prototypes/menus.py | 327 +++++++++++++++++++++++++++++------ 2 files changed, 284 insertions(+), 56 deletions(-) diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index 9e27ca2fad..19bfbec707 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -663,6 +663,19 @@ def validate_lockstring(lockstring): return _LOCK_HANDLER.validate(lockstring) +def get_all_lockfuncs(): + """ + Get a dict of available lock funcs. + + Returns: + lockfuncs (dict): Mapping {lockfuncname:func}. + + """ + if not _LOCKFUNCS: + _cache_lockfuncs() + return _LOCKFUNCS + + def _test(): # testing diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index e70d5d87ae..182a8b63f4 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -11,6 +11,7 @@ from evennia.utils.evmenu import EvMenu, list_node from evennia.utils import evmore from evennia.utils.ansi import strip_ansi from evennia.utils import utils +from evennia.locks.lockhandler import get_all_lockfuncs from evennia.prototypes import prototypes as protlib from evennia.prototypes import spawner @@ -219,6 +220,16 @@ def _format_protfuncs(): return "\n ".join(out) +def _format_lockfuncs(): + out = [] + sorted_funcs = [(key, func) for key, func in + sorted(get_all_lockfuncs(), key=lambda tup: tup[0])] + for lockfunc_name, lockfunc in sorted_funcs: + out.append("- |c${name}|n - |W{docs}".format( + name=lockfunc_name, + docs=utils.justify(lockfunc.__doc__.strip(), align='l', indent=10).strip())) + + # Menu nodes ------------------------------ @@ -694,17 +705,37 @@ def node_attrs(caller): prot = _get_menu_prototype(caller) attrs = prot.get("attrs") - text = ["Set the prototype's |yAttributes|n. Enter attributes on one of these forms:\n" - " attrname=value\n attrname;category=value\n attrname;category;lockstring=value\n" - "To give an attribute without a category but with a lockstring, leave that spot empty " - "(attrname;;lockstring=value)." - "Separate multiple attrs with commas. Use quotes to escape inputs with commas and " - "semi-colon."] + text = """ + |cAttributes|n are custom properties of the object. Enter attributes on one of these forms: + + attrname=value + attrname;category=value + attrname;category;lockstring=value + + To give an attribute without a category but with a lockstring, leave that spot empty + (attrname;;lockstring=value). Attribute values can have embedded $protfuncs. + + {current} + """ + helptext = """ + Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types + 'attredit', 'attrread' are used to limiting editing and viewing of the Attribute. Putting + the lock-type `attrcreate` in the |clocks|n prototype key can be used to restrict builders + to add new Attributes. + + |c$protfuncs + + {pfuncs} + """.format(pfuncs=_format_protfuncs()) + if attrs: - text.append("Current attrs are '|y{attrs}|n'.".format(attrs=attrs)) + text.format(current="Current attrs {attrs}.".format( + attrs=attrs)) else: - text.append("No attrs are set.") - text = "\n\n".join(text) + text.format(current="No attrs are set.") + + text = (text, helptext) + options = _wizard_options("attrs", "aliases", "tags") options.append({"key": "_default", "goto": (_set_property, @@ -797,9 +828,24 @@ def _edit_tag(caller, old_tag, new_tag, **kwargs): @list_node(_caller_tags) def node_tags(caller): - text = ("Set the prototype's |yTags|n. Enter tags on one of the following forms:\n" - " tag\n tag;category\n tag;category;data\n" - "Note that 'data' is not commonly used.") + text = """ + |cTags|n are used to group objects so they can quickly be found later. Enter tags on one of + the following forms: + tagname + tagname;category + tagname;category;data + """ + helptext = """ + Tags are shared between all objects with that tag. So the 'data' field (which is not + commonly used) can only hold eventual info about the Tag itself, not about the individual + object on which it sits. + + All objects created with this prototype will automatically get assigned a tag named the same + 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(protlib._PROTOTYPE_TAG_CATEGORY) + + text = (text, helptext) options = _wizard_options("tags", "attrs", "locks") return text, options @@ -811,13 +857,39 @@ def node_locks(caller): prototype = _get_menu_prototype(caller) locks = prototype.get("locks") - text = ["Set the prototype's |yLock string|n. Separate multiple locks with semi-colons. " - "Will retain case sensitivity."] + text = """ + The |cLock string|n defines limitations for accessing various properties of the object once + it's spawned. The string should be on one of the following forms: + + locktype:[NOT] lockfunc(args) + locktype: [NOT] lockfunc(args) [AND|OR|NOT] lockfunc(args) [AND|OR|NOT] ... + + Separate multiple lockstrings by semicolons (;). + + {current} + """ + helptext = """ + Here is an example of a lock string constisting of two locks: + + edit:false();call:tag(Foo) OR perm(Builder) + + Above locks limit two things, 'edit' and 'call'. Which lock types are actually checked + depend on the typeclass of the object being spawned. Here 'edit' is never allowed by anyone + while 'call' is allowed to all accessors with a |ctag|n 'Foo' OR which has the + |cPermission|n 'Builder'. + + |c$lockfuncs|n + + {lfuncs} + """.format(lfuncs=_format_lockfuncs()) + if locks: - text.append("Current locks are '|y{locks}|n'.".format(locks=locks)) + text.format(current="Current locks are '|y{locks}|n'.".format(locks=locks)) else: - text.append("No locks are set.") - text = "\n\n".join(text) + text.format(current="No locks are set.") + + text = (text, helptext) + options = _wizard_options("locks", "tags", "permissions") options.append({"key": "_default", "goto": (_set_property, @@ -834,13 +906,32 @@ def node_permissions(caller): prototype = _get_menu_prototype(caller) permissions = prototype.get("permissions") - text = ["Set the prototype's |yPermissions|n. Separate multiple permissions with commas. " - "Will retain case sensitivity."] + text = """ + |cPermissions|n are simple strings used to grant access to this object. A permission is used + when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions. + + {current} + """ + helptext = """ + Any string can act as a permission as long as a lock is set to look for it. Depending on the + lock, having a permission could even be negative (i.e. the lock is only passed if you + |wdon't|n have the 'permission'). The most common permissions are the hierarchical + permissions: + + {permissions}. + + For example, a |clock|n string like "edit:perm(Builder)" will grant access to accessors + having the |cpermission|n "Builder" or higher. + """.format(settings.PERMISSION_HIERARCHY) + if permissions: - text.append("Current permissions are '|y{permissions}|n'.".format(permissions=permissions)) + text.format(current="Current permissions are {permissions}.".format( + permissions=permissions)) else: - text.append("No permissions are set.") - text = "\n\n".join(text) + text.format(current="No permissions are set.") + + text = (text, helptext) + options = _wizard_options("permissions", "destination", "location") options.append({"key": "_default", "goto": (_set_property, @@ -857,12 +948,28 @@ def node_location(caller): prototype = _get_menu_prototype(caller) location = prototype.get("location") - text = ["Set the prototype's |yLocation|n"] + text = """ + The |cLocation|n of this object in the world. If not given, the object will spawn + in the inventory of |c{caller}|n instead. + + {current} + """.format(caller=caller.key) + helptext = """ + You get the most control by not specifying the location - you can then teleport the spawned + objects as needed later. Setting the location may be useful for quickly populating a given + location. One could also consider randomizing the location using a $protfunc. + + |c$protfuncs|n + {pfuncs} + """.format(pfuncs=_format_protfuncs) + if location: - text.append("Current location is |y{location}|n.".format(location=location)) + text.format(current="Current location is {location}.".format(location=location)) else: - text.append("Default location is {}'s inventory.".format(caller)) - text = "\n\n".join(text) + text.format(current="Default location is {}'s inventory.".format(caller)) + + text = (text, helptext) + options = _wizard_options("location", "permissions", "home") options.append({"key": "_default", "goto": (_set_property, @@ -879,12 +986,28 @@ def node_home(caller): prototype = _get_menu_prototype(caller) home = prototype.get("home") - text = ["Set the prototype's |yHome location|n"] + text = """ + The |cHome|n location of an object is often only used as a backup - this is where the object + will be moved to if its location is deleted. The home location can also be used as an actual + home for characters to quickly move back to. If unset, the global home default will be used. + + {current} + """ + helptext = """ + The location can be specified as as #dbref but can also be explicitly searched for using + $obj(name). + + The home location is often not used except as a backup. It should never be unset. + """ + if home: - text.append("Current home location is |y{home}|n.".format(home=home)) + text.format(current="Current home location is {home}.".format(home=home)) else: - text.append("Default home location (|y{home}|n) used.".format(home=settings.DEFAULT_HOME)) - text = "\n\n".join(text) + text.format( + current="Default home location ({home}) used.".format(home=settings.DEFAULT_HOME)) + + text = (text, helptext) + options = _wizard_options("home", "aliases", "destination") options.append({"key": "_default", "goto": (_set_property, @@ -901,12 +1024,24 @@ def node_destination(caller): prototype = _get_menu_prototype(caller) dest = prototype.get("dest") - text = ["Set the prototype's |yDestination|n. This is usually only used for Exits."] + text = """ + The object's |cDestination|n is usually only set for Exit-like objects and designates where + the exit 'leads to'. It's usually unset for all other types of objects. + + {current} + """ + helptext = """ + The destination can be given as a #dbref but can also be explicitly searched for using + $obj(name). + """ + if dest: - text.append("Current destination is |y{dest}|n.".format(dest=dest)) + text.format(current="Current destination is {dest}.".format(dest=dest)) else: - text.append("No destination is set (default).") - text = "\n\n".join(text) + text.format("No destination is set (default).") + + text = (text, helptext) + options = _wizard_options("destination", "home", "prototype_desc") options.append({"key": "_default", "goto": (_set_property, @@ -922,15 +1057,25 @@ def node_destination(caller): def node_prototype_desc(caller): prototype = _get_menu_prototype(caller) - text = ["The |wPrototype-Description|n briefly describes the prototype for " - "viewing in listings."] desc = prototype.get("prototype_desc", None) + text = """ + The |cPrototype-Description|n optionally briefly describes the prototype when it's viewed in + listings. + + {current} + """ + helptext = """ + Giving a brief description helps you and others to locate the prototype for use later. + """ + if desc: - text.append("The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc)) + text.format(current="The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc)) else: - text.append("Description is currently unset.") - text = "\n\n".join(text) + text.format(current="Prototype-Description is currently unset.") + + text = (text, helptext) + options = _wizard_options("prototype_desc", "prototype_key", "prototype_tags") options.append({"key": "_default", "goto": (_set_property, @@ -946,16 +1091,25 @@ def node_prototype_desc(caller): def node_prototype_tags(caller): prototype = _get_menu_prototype(caller) - text = ["|wPrototype-Tags|n can be used to classify and find prototypes. " - "Tags are case-insensitive. " - "Separate multiple by tags by commas."] + text = """ + |cPrototype-Tags|n can be used to classify and find prototypes in listings Tag names are not + case-sensitive and can have not have a custom category. Separate multiple tags by commas. + """ + helptext = """ + Using prototype-tags is a good way to organize and group large numbers of prototypes by + genre, type etc. Under the hood, prototypes' tags will all be stored with the category + '{tagmetacategory}'. + """.format(tagmetacategory=protlib._PROTOTYPE_TAG_META_CATEGORY) + tags = prototype.get('prototype_tags', []) if tags: - text.append("The current tags are:\n|w{tags}|n".format(tags=tags)) + text.format(current="The current tags are:\n|w{tags}|n".format(tags=tags)) else: - text.append("No tags are currently set.") - text = "\n\n".join(text) + text.format(current="No tags are currently set.") + + text = (text, helptext) + options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks") options.append({"key": "_default", "goto": (_set_property, @@ -971,16 +1125,35 @@ def node_prototype_tags(caller): def node_prototype_locks(caller): prototype = _get_menu_prototype(caller) - text = ["Set |wPrototype-Locks|n on the prototype. There are two valid lock types: " - "'edit' (who can edit the prototype) and 'spawn' (who can spawn new objects with this " - "prototype)\n(If you are unsure, leave as default.)"] locks = prototype.get('prototype_locks', '') + + text = """ + |cPrototype-Locks|n are used to limit access to this prototype when someone else is trying + to access it. By default any prototype can be edited only by the creator and by Admins while + they can be used by anyone with access to the spawn command. There are two valid lock types + the prototype access tools look for: + + - 'edit': Who can edit the prototype. + - 'spawn': Who can spawn new objects with this prototype. + + If unsure, leave as default. + + {current} + """ + helptext = """ + Prototype locks can be used when there are different tiers of builders or for developers to + produce 'base prototypes' only meant for builders to inherit and expand on rather than + change. + """ + if locks: - text.append("Current lock is |w'{lockstring}'|n".format(lockstring=locks)) + text.format(current="Current lock is |w'{lockstring}'|n".format(lockstring=locks)) else: - text.append("Lock unset - if not changed the default lockstring will be set as\n" - " |w'spawn:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) - text = "\n\n".join(text) + text.format( + current="Default lock set: |w'spawn:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) + + text = (text, helptext) + options = _wizard_options("prototype_locks", "prototype_tags", "index") options.append({"key": "_default", "goto": (_set_property, @@ -1039,7 +1212,7 @@ def node_update_objects(caller, **kwargs): obj = choice(update_objects) diff, obj_prototype = spawner.prototype_diff_from_object(prototype, obj) - text = ["Suggested changes to {} objects".format(len(update_objects)), + text = ["Suggested changes to {} objects. ".format(len(update_objects)), "Showing random example obj to change: {name} (#{dbref}))\n".format(obj.key, obj.dbref)] options = [] io = 0 @@ -1073,6 +1246,17 @@ def node_update_objects(caller, **kwargs): {"key": "|wb|rack ({})".format(back_node[5:], 'b'), "goto": back_node}]) + helptext = """ + Be careful with this operation! The upgrade mechanism will try to automatically estimate + what changes need to be applied. But the estimate is |wonly based on the analysis of one + randomly selected object|n among all objects spawned by this prototype. If that object + happens to be unusual in some way the estimate will be off and may lead to unexpected + results for other objects. Always test your objects carefully after an upgrade and + consider being conservative (switch to KEEP) or even do the update manually if you are + unsure that the results will be acceptable. """ + + text = (text, helptext) + return text, options @@ -1144,7 +1328,17 @@ def node_prototype_save(caller, **kwargs): "goto": ("node_prototype_save", {"accept": True, "prototype": prototype})}) - return "\n".join(text), options + helptext = """ + Saving the prototype makes it available for use later. It can also be used to inherit from, + by name. Depending on |cprototype-locks|n it also makes the prototype usable and/or + editable by others. Consider setting good |cPrototype-tags|n and to give a useful, brief + |cPrototype-desc|n to make the prototype easy to find later. + + """ + + text = (text, helptext) + + return text, options # spawning node @@ -1212,6 +1406,16 @@ def node_prototype_spawn(caller, **kwargs): dict(prototype=prototype, opjects=spawned_objects, back_node="node_prototype_spawn"))}) options.extend(_wizard_options("prototype_spawn", "prototype_save", "index")) + + helptext = """ + Spawning is the act of instantiating a prototype into an actual object. As a new object is + spawned, every $protfunc in the prototype is called anew. Since this is a common thing to + do, you may also temporarily change the |clocation|n of this prototype to bypass whatever + value is set in the prototype. + + """ + text = (text, helptext) + return text, options @@ -1232,11 +1436,22 @@ def _prototype_load_select(caller, prototype_key): @list_node(_all_prototype_parents, _prototype_load_select) def node_prototype_load(caller, **kwargs): - text = ["Select a prototype to load. This will replace any currently edited prototype."] + """Load prototype""" + + text = """ + Select a prototype to load. This will replace any prototype currently being edited! + """ + helptext = """ + Loading a prototype will load it and return you to the main index. It can be a good idea to + examine the prototype before loading it. + """ + + text = (text, helptext) + options = _wizard_options("prototype_load", "prototype_save", "index") options.append({"key": "_default", "goto": _prototype_parent_examine}) - return "\n".join(text), options + return text, options # EvMenu definition, formatting and access functions From 423023419b54512aa5656058a63f2fabb8f77942 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 24 Jul 2018 20:54:21 +0200 Subject: [PATCH 083/103] Fix unit tests --- evennia/utils/tests/test_evmenu.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evennia/utils/tests/test_evmenu.py b/evennia/utils/tests/test_evmenu.py index d3ee14a74f..a6959c0509 100644 --- a/evennia/utils/tests/test_evmenu.py +++ b/evennia/utils/tests/test_evmenu.py @@ -82,6 +82,8 @@ class TestEvMenu(TestCase): self.assertIsNotNone( bool(node_text), "node: {}: node-text is None, which was not expected.".format(nodename)) + if isinstance(node_text, tuple): + node_text, helptext = node_text node_text = ansi.strip_ansi(node_text.strip()) self.assertTrue( node_text.startswith(compare_text), From c82eabf6edbc552b5f030137b468eb8c5a3b9ba4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 24 Jul 2018 21:47:54 +0200 Subject: [PATCH 084/103] Prepare for flattening prototype display --- evennia/commands/default/building.py | 2 +- evennia/prototypes/menus.py | 63 +++++++++++++++------------- evennia/prototypes/spawner.py | 23 ++++++++-- 3 files changed, 54 insertions(+), 34 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index cdcbadc103..bd4fb5e188 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2855,7 +2855,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): """ key = "@spawn" - aliases = ["@olc"] + aliases = ["olc"] switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu", "olc", "update") locks = "cmd:perm(spawn) or perm(Builder)" help_category = "Building" diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 182a8b63f4..0589a8e65c 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -281,7 +281,7 @@ def node_index(caller): {"desc": "|WPrototype-Key|n|n{}".format( _format_option_value("Key", "prototype_key" not in prototype, prototype, None)), "goto": "node_prototype_key"}) - for key in ('Typeclass', 'Prototype-parent', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', + for key in ('Prototype-parent', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', 'Permissions', 'Location', 'Home', 'Destination'): required = False cropper = None @@ -412,8 +412,9 @@ def _prototype_parent_examine(caller, prototype_name): def _prototype_parent_select(caller, prototype): ret = _set_property(caller, "", - prop="prototype_parent", processor=str, next_node="node_key") - caller.msg("Selected prototype |y{}|n. Removed any set typeclass parent.".format(prototype)) + prop="prototype_parent", processor=str, next_node="node_typeclass") + caller.msg("Selected prototype |y{}|n.".format(prototype)) + return ret @@ -442,15 +443,15 @@ def node_prototype_parent(caller): if prot_parent_key: prot_parent = protlib.search_prototype(prot_parent_key) if prot_parent: - text.format( + text = text.format( current="Current parent prototype is {}:\n{}".format( protlib.prototype_to_str(prot_parent))) else: - text.format( + text = text.format( current="Current parent prototype |r{prototype}|n " "does not appear to exist.".format(prot_parent_key)) else: - text.format(current="Parent prototype is not set") + text = text.format(current="Parent prototype is not set") text = (text, helptext) options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W") @@ -521,10 +522,10 @@ def node_typeclass(caller): """ if typeclass: - text.format( + text = text.format( current="Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass)) else: - text.format( + text = text.format( current="Using default typeclass {typeclass}.".format( typeclass=settings.BASE_OBJECT_TYPECLASS)) @@ -558,9 +559,9 @@ def node_key(caller): """.format(pfuncs=_format_protfuncs()) if key: - text.format(current="Current key is '{key}'.".format(key=key)) + text = text.format(current="Current key is '{key}'.".format(key=key)) else: - text.format(current="The key is currently unset.") + text = text.format(current="The key is currently unset.") text = (text, helptext) @@ -597,9 +598,9 @@ def node_aliases(caller): """.format(pfuncs=_format_protfuncs()) if aliases: - text.format(current="Current aliases are '|c{aliases}|n'.".format(aliases=aliases)) + text = text.format(current="Current aliases are '|c{aliases}|n'.".format(aliases=aliases)) else: - text.format(current="No aliases are set.") + text = text.format(current="No aliases are set.") text = (text, helptext) @@ -729,10 +730,10 @@ def node_attrs(caller): """.format(pfuncs=_format_protfuncs()) if attrs: - text.format(current="Current attrs {attrs}.".format( + text = text.format(current="Current attrs {attrs}.".format( attrs=attrs)) else: - text.format(current="No attrs are set.") + text = text.format(current="No attrs are set.") text = (text, helptext) @@ -884,9 +885,9 @@ def node_locks(caller): """.format(lfuncs=_format_lockfuncs()) if locks: - text.format(current="Current locks are '|y{locks}|n'.".format(locks=locks)) + text = text.format(current="Current locks are '|y{locks}|n'.".format(locks=locks)) else: - text.format(current="No locks are set.") + text = text.format(current="No locks are set.") text = (text, helptext) @@ -925,10 +926,10 @@ def node_permissions(caller): """.format(settings.PERMISSION_HIERARCHY) if permissions: - text.format(current="Current permissions are {permissions}.".format( + text = text.format(current="Current permissions are {permissions}.".format( permissions=permissions)) else: - text.format(current="No permissions are set.") + text = text.format(current="No permissions are set.") text = (text, helptext) @@ -964,9 +965,9 @@ def node_location(caller): """.format(pfuncs=_format_protfuncs) if location: - text.format(current="Current location is {location}.".format(location=location)) + text = text.format(current="Current location is {location}.".format(location=location)) else: - text.format(current="Default location is {}'s inventory.".format(caller)) + text = text.format(current="Default location is {}'s inventory.".format(caller)) text = (text, helptext) @@ -1001,9 +1002,9 @@ def node_home(caller): """ if home: - text.format(current="Current home location is {home}.".format(home=home)) + text = text.format(current="Current home location is {home}.".format(home=home)) else: - text.format( + text = text.format( current="Default home location ({home}) used.".format(home=settings.DEFAULT_HOME)) text = (text, helptext) @@ -1036,9 +1037,9 @@ def node_destination(caller): """ if dest: - text.format(current="Current destination is {dest}.".format(dest=dest)) + text = text.format(current="Current destination is {dest}.".format(dest=dest)) else: - text.format("No destination is set (default).") + text = text.format("No destination is set (default).") text = (text, helptext) @@ -1070,9 +1071,9 @@ def node_prototype_desc(caller): """ if desc: - text.format(current="The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc)) + text = text.format(current="The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc)) else: - text.format(current="Prototype-Description is currently unset.") + text = text.format(current="Prototype-Description is currently unset.") text = (text, helptext) @@ -1094,6 +1095,8 @@ def node_prototype_tags(caller): text = """ |cPrototype-Tags|n can be used to classify and find prototypes in listings Tag names are not case-sensitive and can have not have a custom category. Separate multiple tags by commas. + + {current} """ helptext = """ Using prototype-tags is a good way to organize and group large numbers of prototypes by @@ -1104,9 +1107,9 @@ def node_prototype_tags(caller): tags = prototype.get('prototype_tags', []) if tags: - text.format(current="The current tags are:\n|w{tags}|n".format(tags=tags)) + text = text.format(current="The current tags are:\n|w{tags}|n".format(tags=tags)) else: - text.format(current="No tags are currently set.") + text = text.format(current="No tags are currently set.") text = (text, helptext) @@ -1147,9 +1150,9 @@ def node_prototype_locks(caller): """ if locks: - text.format(current="Current lock is |w'{lockstring}'|n".format(lockstring=locks)) + text = text.format(current="Current lock is |w'{lockstring}'|n".format(lockstring=locks)) else: - text.format( + text = text.format( current="Default lock set: |w'spawn:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) text = (text, helptext) diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index c09a192819..494837b5fb 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -150,17 +150,34 @@ def _get_prototype(dic, prot, protparents): for infinite recursion here. """ - if "prototype" in dic: + if "prototype_parent" in dic: # move backwards through the inheritance - for prototype in make_iter(dic["prototype"]): + for prototype in make_iter(dic["prototype_parent"]): # Build the prot dictionary in reverse order, overloading new_prot = _get_prototype(protparents.get(prototype.lower(), {}), prot, protparents) prot.update(new_prot) prot.update(dic) - prot.pop("prototype", None) # we don't need this anymore + prot.pop("prototype_parent", None) # we don't need this anymore return prot +def flatten_prototype(prototype): + """ + Produce a 'flattened' prototype, where all prototype parents in the inheritance tree have been + merged into a final prototype. + + Args: + prototype (dict): Prototype to flatten. Its `prototype_parent` field will be parsed. + + Returns: + flattened (dict): The final, flattened prototype. + + """ + protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} + protlib.validate_prototype(prototype, None, protparents, is_prototype_base=True) + return _get_prototype(prototype, {}, protparents) + + # obj-related prototype functions def prototype_from_object(obj): From abed588f5e405337ad1c2164e65c76a0e249faf6 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 25 Jul 2018 14:11:44 +0200 Subject: [PATCH 085/103] Show flattened current values in menu --- evennia/prototypes/menus.py | 170 +++++++++++----------------------- evennia/prototypes/spawner.py | 8 +- 2 files changed, 59 insertions(+), 119 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 0589a8e65c..e63acb98c2 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -42,6 +42,17 @@ def _get_menu_prototype(caller): return prototype +def _get_flat_menu_prototype(caller, refresh=False): + """Return prototype where parent values are included""" + flat_prototype = None + if not refresh and hasattr(caller.ndb._menutree, "olc_flat_prototype"): + flat_prototype = caller.ndb._menutree.olc_flat_prototype + if not flat_prototype: + prot = _get_menu_prototype(caller) + caller.ndb._menutree.olc_flat_prototype = flat_prototype = spawner.flatten_prototype(prot) + return flat_prototype + + def _set_menu_prototype(caller, prototype): """Set the prototype with existing one""" caller.ndb._menutree.olc_prototype = prototype @@ -230,6 +241,19 @@ def _format_lockfuncs(): docs=utils.justify(lockfunc.__doc__.strip(), align='l', indent=10).strip())) +def _get_current_value(caller, keyname, formatter=str): + "Return current value, marking if value comes from parent or set in this prototype" + prot = _get_menu_prototype(caller) + if keyname in prot: + # value in current prot + return "Current {}: {}".format(keyname, formatter(prot[keyname])) + flat_prot = _get_flat_menu_prototype(caller) + if keyname in flat_prot: + # value in flattened prot + return "Current {} (|binherited|n): {}".format(keyname, formatter(flat_prot[keyname])) + return "[No {} set]".format(keyname) + + # Menu nodes ------------------------------ @@ -363,12 +387,13 @@ def _check_prototype_key(caller, key): def node_prototype_key(caller): - prototype = _get_menu_prototype(caller) + text = """ The |cPrototype-Key|n uniquely identifies the prototype and is |wmandatory|n. It is used to find and use the prototype to spawn new entities. It is not case sensitive. - {current}""" + {current}""".format(current=_get_current_value(caller, "prototype_key")) + helptext = """ The prototype-key is not itself used when spawnng the new object, but is only used for managing, storing and loading the prototype. It must be globally unique, so existing keys @@ -376,12 +401,6 @@ def node_prototype_key(caller): prototype will be loaded. """ - old_key = prototype.get('prototype_key', None) - if old_key: - text = text.format(current="Currently set to '|w{key}|n'".format(key=old_key)) - else: - text = text.format(current="Currently |runset|n (required).") - options = _wizard_options("prototype_key", "index", "prototype_parent") options.append({"key": "_default", "goto": _check_prototype_key}) @@ -502,9 +521,6 @@ def _typeclass_select(caller, typeclass): @list_node(_all_typeclasses, _typeclass_select) def node_typeclass(caller): - prototype = _get_menu_prototype(caller) - typeclass = prototype.get("typeclass") - text = """ The |cTypeclass|n defines what 'type' of object this is - the actual working code to use. @@ -512,7 +528,8 @@ def node_typeclass(caller): one of the prototype's |cparents|n. {current} - """ + """.format(current=_get_current_value(caller, "typeclass")) + helptext = """ A |nTypeclass|n is specified by the actual python-path to the class definition in the Evennia code structure. @@ -521,14 +538,6 @@ def node_typeclass(caller): effects or expects certain values depend greatly on the code in play. """ - if typeclass: - text = text.format( - current="Current typeclass is |y{typeclass}|n.".format(typeclass=typeclass)) - else: - text = text.format( - current="Using default typeclass {typeclass}.".format( - typeclass=settings.BASE_OBJECT_TYPECLASS)) - text = (text, helptext) options = _wizard_options("typeclass", "prototype_parent", "key", color="|W") @@ -541,14 +550,12 @@ def node_typeclass(caller): def node_key(caller): - prototype = _get_menu_prototype(caller) - key = prototype.get("key") - text = """ The |cKey|n is the given name of the object to spawn. This will retain the given case. {current} - """ + """.format(current=_get_current_value(caller, "key")) + helptext = """ The key should often not be identical for every spawned object. Using a randomising $protfunc can be used, for example |c$choice(Alan, Tom, John)|n will give one of the three @@ -558,11 +565,6 @@ def node_key(caller): {pfuncs} """.format(pfuncs=_format_protfuncs()) - if key: - text = text.format(current="Current key is '{key}'.".format(key=key)) - else: - text = text.format(current="The key is currently unset.") - text = (text, helptext) options = _wizard_options("key", "typeclass", "aliases") @@ -578,8 +580,6 @@ def node_key(caller): def node_aliases(caller): - prototype = _get_menu_prototype(caller) - aliases = prototype.get("aliases") text = """ |cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not @@ -588,7 +588,8 @@ def node_aliases(caller): Add multiple aliases separating with commas. {current} - """ + """.format(current=_get_current_value(caller, "aliases")) + helptext = """ Aliases are fixed alternative identifiers and are stored with the new object. @@ -597,11 +598,6 @@ def node_aliases(caller): {pfuncs} """.format(pfuncs=_format_protfuncs()) - if aliases: - text = text.format(current="Current aliases are '|c{aliases}|n'.".format(aliases=aliases)) - else: - text = text.format(current="No aliases are set.") - text = (text, helptext) options = _wizard_options("aliases", "key", "attrs") @@ -703,8 +699,6 @@ def _examine_attr(caller, selection): @list_node(_caller_attrs) def node_attrs(caller): - prot = _get_menu_prototype(caller) - attrs = prot.get("attrs") text = """ |cAttributes|n are custom properties of the object. Enter attributes on one of these forms: @@ -717,7 +711,8 @@ def node_attrs(caller): (attrname;;lockstring=value). Attribute values can have embedded $protfuncs. {current} - """ + """.format(current=_get_current_value(caller, "attrs")) + helptext = """ Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types 'attredit', 'attrread' are used to limiting editing and viewing of the Attribute. Putting @@ -729,12 +724,6 @@ def node_attrs(caller): {pfuncs} """.format(pfuncs=_format_protfuncs()) - if attrs: - text = text.format(current="Current attrs {attrs}.".format( - attrs=attrs)) - else: - text = text.format(current="No attrs are set.") - text = (text, helptext) options = _wizard_options("attrs", "aliases", "tags") @@ -835,7 +824,10 @@ def node_tags(caller): tagname tagname;category tagname;category;data - """ + + {current} + """.format(current=_get_current_value(caller, 'tags')) + helptext = """ Tags are shared between all objects with that tag. So the 'data' field (which is not commonly used) can only hold eventual info about the Tag itself, not about the individual @@ -855,8 +847,6 @@ def node_tags(caller): def node_locks(caller): - prototype = _get_menu_prototype(caller) - locks = prototype.get("locks") text = """ The |cLock string|n defines limitations for accessing various properties of the object once @@ -868,7 +858,8 @@ def node_locks(caller): Separate multiple lockstrings by semicolons (;). {current} - """ + """.format(current=_get_current_value(caller, 'locks')) + helptext = """ Here is an example of a lock string constisting of two locks: @@ -884,11 +875,6 @@ def node_locks(caller): {lfuncs} """.format(lfuncs=_format_lockfuncs()) - if locks: - text = text.format(current="Current locks are '|y{locks}|n'.".format(locks=locks)) - else: - text = text.format(current="No locks are set.") - text = (text, helptext) options = _wizard_options("locks", "tags", "permissions") @@ -904,15 +890,14 @@ def node_locks(caller): def node_permissions(caller): - prototype = _get_menu_prototype(caller) - permissions = prototype.get("permissions") text = """ |cPermissions|n are simple strings used to grant access to this object. A permission is used when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions. {current} - """ + """.format(current=_get_current_value(caller, "permissions")) + helptext = """ Any string can act as a permission as long as a lock is set to look for it. Depending on the lock, having a permission could even be negative (i.e. the lock is only passed if you @@ -925,12 +910,6 @@ def node_permissions(caller): having the |cpermission|n "Builder" or higher. """.format(settings.PERMISSION_HIERARCHY) - if permissions: - text = text.format(current="Current permissions are {permissions}.".format( - permissions=permissions)) - else: - text = text.format(current="No permissions are set.") - text = (text, helptext) options = _wizard_options("permissions", "destination", "location") @@ -946,15 +925,14 @@ def node_permissions(caller): def node_location(caller): - prototype = _get_menu_prototype(caller) - location = prototype.get("location") text = """ The |cLocation|n of this object in the world. If not given, the object will spawn in the inventory of |c{caller}|n instead. {current} - """.format(caller=caller.key) + """.format(caller=caller.key, current=_get_current_value(caller, "location")) + helptext = """ You get the most control by not specifying the location - you can then teleport the spawned objects as needed later. Setting the location may be useful for quickly populating a given @@ -964,11 +942,6 @@ def node_location(caller): {pfuncs} """.format(pfuncs=_format_protfuncs) - if location: - text = text.format(current="Current location is {location}.".format(location=location)) - else: - text = text.format(current="Default location is {}'s inventory.".format(caller)) - text = (text, helptext) options = _wizard_options("location", "permissions", "home") @@ -984,8 +957,6 @@ def node_location(caller): def node_home(caller): - prototype = _get_menu_prototype(caller) - home = prototype.get("home") text = """ The |cHome|n location of an object is often only used as a backup - this is where the object @@ -993,7 +964,7 @@ def node_home(caller): home for characters to quickly move back to. If unset, the global home default will be used. {current} - """ + """.format(current=_get_current_value(caller, "home")) helptext = """ The location can be specified as as #dbref but can also be explicitly searched for using $obj(name). @@ -1001,12 +972,6 @@ def node_home(caller): The home location is often not used except as a backup. It should never be unset. """ - if home: - text = text.format(current="Current home location is {home}.".format(home=home)) - else: - text = text.format( - current="Default home location ({home}) used.".format(home=settings.DEFAULT_HOME)) - text = (text, helptext) options = _wizard_options("home", "aliases", "destination") @@ -1022,25 +987,19 @@ def node_home(caller): def node_destination(caller): - prototype = _get_menu_prototype(caller) - dest = prototype.get("dest") text = """ The object's |cDestination|n is usually only set for Exit-like objects and designates where the exit 'leads to'. It's usually unset for all other types of objects. {current} - """ + """.format(current=_get_current_node(caller, "destination")) + helptext = """ The destination can be given as a #dbref but can also be explicitly searched for using $obj(name). """ - if dest: - text = text.format(current="Current destination is {dest}.".format(dest=dest)) - else: - text = text.format("No destination is set (default).") - text = (text, helptext) options = _wizard_options("destination", "home", "prototype_desc") @@ -1057,24 +1016,17 @@ def node_destination(caller): def node_prototype_desc(caller): - prototype = _get_menu_prototype(caller) - desc = prototype.get("prototype_desc", None) - text = """ The |cPrototype-Description|n optionally briefly describes the prototype when it's viewed in listings. {current} - """ + """.format(current=_get_current_value(caller, "prototype_desc")) + helptext = """ Giving a brief description helps you and others to locate the prototype for use later. """ - if desc: - text = text.format(current="The current meta desc is:\n\"|w{desc}|n\"".format(desc=desc)) - else: - text = text.format(current="Prototype-Description is currently unset.") - text = (text, helptext) options = _wizard_options("prototype_desc", "prototype_key", "prototype_tags") @@ -1091,26 +1043,19 @@ def node_prototype_desc(caller): def node_prototype_tags(caller): - prototype = _get_menu_prototype(caller) + text = """ |cPrototype-Tags|n can be used to classify and find prototypes in listings Tag names are not case-sensitive and can have not have a custom category. Separate multiple tags by commas. {current} - """ + """.format(current=_get_current_value(caller, "prototype_tags")) helptext = """ Using prototype-tags is a good way to organize and group large numbers of prototypes by genre, type etc. Under the hood, prototypes' tags will all be stored with the category '{tagmetacategory}'. """.format(tagmetacategory=protlib._PROTOTYPE_TAG_META_CATEGORY) - tags = prototype.get('prototype_tags', []) - - if tags: - text = text.format(current="The current tags are:\n|w{tags}|n".format(tags=tags)) - else: - text = text.format(current="No tags are currently set.") - text = (text, helptext) options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks") @@ -1127,8 +1072,6 @@ def node_prototype_tags(caller): def node_prototype_locks(caller): - prototype = _get_menu_prototype(caller) - locks = prototype.get('prototype_locks', '') text = """ |cPrototype-Locks|n are used to limit access to this prototype when someone else is trying @@ -1142,19 +1085,14 @@ def node_prototype_locks(caller): If unsure, leave as default. {current} - """ + """.format(current=_get_current_value(caller, "prototype_locks")) + helptext = """ Prototype locks can be used when there are different tiers of builders or for developers to produce 'base prototypes' only meant for builders to inherit and expand on rather than change. """ - if locks: - text = text.format(current="Current lock is |w'{lockstring}'|n".format(lockstring=locks)) - else: - text = text.format( - current="Default lock set: |w'spawn:all(); edit:id({dbref}) or perm(Admin)'|n".format(dbref=caller.id)) - text = (text, helptext) options = _wizard_options("prototype_locks", "prototype_tags", "index") diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 494837b5fb..31a77ce303 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -173,9 +173,11 @@ def flatten_prototype(prototype): flattened (dict): The final, flattened prototype. """ - protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} - protlib.validate_prototype(prototype, None, protparents, is_prototype_base=True) - return _get_prototype(prototype, {}, protparents) + if prototype: + protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} + protlib.validate_prototype(prototype, None, protparents, is_prototype_base=True) + return _get_prototype(prototype, {}, protparents) + return {} # obj-related prototype functions From cc5d9ffd4d939148ffc58402ecdea208c99f1dd4 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 25 Jul 2018 19:51:48 +0200 Subject: [PATCH 086/103] Validate prototype parent before chosing it --- evennia/prototypes/menus.py | 67 ++++++++++++++++++++------------ evennia/prototypes/prototypes.py | 12 +++--- evennia/prototypes/spawner.py | 6 ++- 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index e63acb98c2..34f8eaf648 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -42,14 +42,15 @@ def _get_menu_prototype(caller): return prototype -def _get_flat_menu_prototype(caller, refresh=False): +def _get_flat_menu_prototype(caller, refresh=False, validate=False): """Return prototype where parent values are included""" flat_prototype = None if not refresh and hasattr(caller.ndb._menutree, "olc_flat_prototype"): flat_prototype = caller.ndb._menutree.olc_flat_prototype if not flat_prototype: prot = _get_menu_prototype(caller) - caller.ndb._menutree.olc_flat_prototype = flat_prototype = spawner.flatten_prototype(prot) + caller.ndb._menutree.olc_flat_prototype = \ + flat_prototype = spawner.flatten_prototype(prot, validate=validate) return flat_prototype @@ -305,11 +306,11 @@ def node_index(caller): {"desc": "|WPrototype-Key|n|n{}".format( _format_option_value("Key", "prototype_key" not in prototype, prototype, None)), "goto": "node_prototype_key"}) - for key in ('Prototype-parent', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', + for key in ('Prototype_parent', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', 'Permissions', 'Location', 'Home', 'Destination'): required = False cropper = None - if key in ("Prototype-parent", "Typeclass"): + if key in ("Prototype_parent", "Typeclass"): required = ("prototype_parent" not in prototype) and ("typeclass" not in prototype) if key == 'Typeclass': cropper = _path_cropper @@ -429,11 +430,24 @@ def _prototype_parent_examine(caller, prototype_name): caller.msg("Prototype not registered.") -def _prototype_parent_select(caller, prototype): - ret = _set_property(caller, "", - prop="prototype_parent", processor=str, next_node="node_typeclass") - caller.msg("Selected prototype |y{}|n.".format(prototype)) +def _prototype_parent_select(caller, new_parent): + ret = None + prototype_parent = protlib.search_prototype(new_parent) + try: + if prototype_parent: + spawner.flatten_prototype(prototype_parent[0], validate=True) + else: + raise RuntimeError("Not found.") + except RuntimeError as err: + caller.msg("Selected prototype parent {} " + "caused Error(s):\n|r{}|n".format(new_parent, err)) + else: + ret = _set_property(caller, new_parent, + prop="prototype_parent", + processor=str, next_node="node_prototype_parent") + _get_flat_menu_prototype(caller, refresh=True) + caller.msg("Selected prototype parent |c{}|n.".format(new_parent)) return ret @@ -441,12 +455,12 @@ def _prototype_parent_select(caller, prototype): def node_prototype_parent(caller): prototype = _get_menu_prototype(caller) - prot_parent_key = prototype.get('prototype') + prot_parent_keys = prototype.get('prototype_parent') text = """ The |cPrototype Parent|n allows you to |winherit|n prototype values from another named - prototype (given as that prototype's |wprototype_key|). If not changing these values in the - current prototype, the parent's value will be used. Pick the available prototypes below. + prototype (given as that prototype's |wprototype_key|n). If not changing these values in + the current prototype, the parent's value will be used. Pick the available prototypes below. Note that somewhere in the prototype's parentage, a |ctypeclass|n must be specified. If no parent is given, this prototype must define the typeclass (next menu node). @@ -459,18 +473,23 @@ def node_prototype_parent(caller): prototype to be valid. """ - if prot_parent_key: - prot_parent = protlib.search_prototype(prot_parent_key) - if prot_parent: - text = text.format( - current="Current parent prototype is {}:\n{}".format( - protlib.prototype_to_str(prot_parent))) - else: - text = text.format( - current="Current parent prototype |r{prototype}|n " - "does not appear to exist.".format(prot_parent_key)) - else: - text = text.format(current="Parent prototype is not set") + ptexts = [] + if prot_parent_keys: + for pkey in utils.make_iter(prot_parent_keys): + prot_parent = protlib.search_prototype(pkey) + if prot_parent: + prot_parent = prot_parent[0] + ptexts.append("|c -- {pkey} -- |n\n{prot}".format( + pkey=pkey, + prot=protlib.prototype_to_str(prot_parent))) + else: + ptexts.append("Prototype parent |r{pkey} was not found.".format(pkey=pkey)) + + if not ptexts: + ptexts.append("[No prototype_parent set]") + + text = text.format(current="\n\n".join(ptexts)) + text = (text, helptext) options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W") @@ -993,7 +1012,7 @@ def node_destination(caller): the exit 'leads to'. It's usually unset for all other types of objects. {current} - """.format(current=_get_current_node(caller, "destination")) + """.format(current=_get_current_value(caller, "destination")) helptext = """ The destination can be given as a #dbref but can also be explicitly searched for using diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 011445b039..767919a7a9 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -539,7 +539,7 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed def validate_prototype(prototype, protkey=None, protparents=None, - is_prototype_base=True, _flags=None): + is_prototype_base=True, strict=True, _flags=None): """ Run validation on a prototype, checking for inifinite regress. @@ -552,6 +552,8 @@ def validate_prototype(prototype, protkey=None, protparents=None, is_prototype_base (bool, optional): We are trying to create a new object *based on this object*. This means we can't allow 'mixin'-style prototypes without typeclass/parent etc. + strict (bool, optional): If unset, don't require needed keys, only check against infinite + recursion etc. _flags (dict, optional): Internal work dict that should not be set externally. Raises: RuntimeError: If prototype has invalid structure. @@ -570,14 +572,14 @@ def validate_prototype(prototype, protkey=None, protparents=None, protkey = protkey and protkey.lower() or prototype.get('prototype_key', None) - if not bool(protkey): + if strict and not bool(protkey): _flags['errors'].append("Prototype lacks a `prototype_key`.") protkey = "[UNSET]" typeclass = prototype.get('typeclass') prototype_parent = prototype.get('prototype_parent', []) - if not (typeclass or prototype_parent): + if strict and not (typeclass or prototype_parent): if is_prototype_base: _flags['errors'].append("Prototype {} requires `typeclass` " "or 'prototype_parent'.".format(protkey)) @@ -585,7 +587,7 @@ def validate_prototype(prototype, protkey=None, protparents=None, _flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks " "a typeclass or a prototype_parent.".format(protkey)) - if typeclass and typeclass not in get_all_typeclasses("evennia.objects.models.ObjectDB"): + if strict and typeclass and typeclass not in get_all_typeclasses("evennia.objects.models.ObjectDB"): _flags['errors'].append( "Prototype {} is based on typeclass {}, which could not be imported!".format( protkey, typeclass)) @@ -615,7 +617,7 @@ def validate_prototype(prototype, protkey=None, protparents=None, _flags['typeclass'] = typeclass # if we get back to the current level without a typeclass it's an error. - if is_prototype_base and _flags['depth'] <= 0 and not _flags['typeclass']: + if strict and is_prototype_base and _flags['depth'] <= 0 and not _flags['typeclass']: _flags['errors'].append("Prototype {} has no `typeclass` defined anywhere in its parent " "chain. Add `typeclass`, or a `prototype_parent` pointing to a " "prototype with a typeclass.".format(protkey)) diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 31a77ce303..3dd8e11d67 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -161,13 +161,14 @@ def _get_prototype(dic, prot, protparents): return prot -def flatten_prototype(prototype): +def flatten_prototype(prototype, validate=False): """ Produce a 'flattened' prototype, where all prototype parents in the inheritance tree have been merged into a final prototype. Args: prototype (dict): Prototype to flatten. Its `prototype_parent` field will be parsed. + validate (bool, optional): Validate for valid keys etc. Returns: flattened (dict): The final, flattened prototype. @@ -175,7 +176,8 @@ def flatten_prototype(prototype): """ if prototype: protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} - protlib.validate_prototype(prototype, None, protparents, is_prototype_base=True) + protlib.validate_prototype(prototype, None, protparents, + is_prototype_base=validate, strict=validate) return _get_prototype(prototype, {}, protparents) return {} From ef131f6f5bc12b366fd2fe56f324e550daf95e44 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 26 Jul 2018 23:41:00 +0200 Subject: [PATCH 087/103] Refactor menu up until attrs --- evennia/prototypes/menus.py | 376 +++++++++++++++++++++++++------ evennia/prototypes/prototypes.py | 4 +- evennia/prototypes/tests.py | 31 ++- evennia/utils/evmenu.py | 2 +- 4 files changed, 329 insertions(+), 84 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 34f8eaf648..54ec054340 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -5,6 +5,7 @@ OLC Prototype menu nodes """ import json +import re from random import choice from django.conf import settings from evennia.utils.evmenu import EvMenu, list_node @@ -242,6 +243,25 @@ def _format_lockfuncs(): docs=utils.justify(lockfunc.__doc__.strip(), align='l', indent=10).strip())) +def _format_list_actions(*args, **kwargs): + """Create footer text for nodes with extra list actions + + Args: + actions (str): Available actions. The first letter of the action name will be assumed + to be a shortcut. + Kwargs: + prefix (str): Default prefix to use. + Returns: + string (str): Formatted footer for adding to the node text. + + """ + actions = [] + prefix = kwargs.get('prefix', "|WSelect with |w|W. Other actions:|n ") + for action in args: + actions.append("|w{}|n|W{} |w|n".format(action[0], action[1:])) + return prefix + "|W,|n ".join(actions) + + def _get_current_value(caller, keyname, formatter=str): "Return current value, marking if value comes from parent or set in this prototype" prot = _get_menu_prototype(caller) @@ -255,6 +275,32 @@ def _get_current_value(caller, keyname, formatter=str): return "[No {} set]".format(keyname) +def _default_parse(raw_inp, choices, *args): + """ + Helper to parse default input to a node decorated with the node_list decorator on + the form l1, l 2, look 1, etc. Spaces are ignored, as is case. + + Args: + raw_inp (str): Input from the user. + choices (list): List of available options on the node listing (list of strings). + args (tuples): The available actions, each specifed as a tuple (name, alias, ...) + Returns: + choice (str): A choice among the choices, or None if no match was found. + action (str): The action operating on the choice, or None. + + """ + raw_inp = raw_inp.lower().strip() + mapping = {t.lower(): tup[0] for tup in args for t in tup} + match = re.match(r"(%s)\s*?(\d+)$" % "|".join(mapping.keys()), raw_inp) + if match: + action = mapping.get(match.group(1), None) + num = int(match.group(2)) - 1 + num = num if 0 <= num < len(choices) else None + if action is not None and num is not None: + return choices[num], action + return None, None + + # Menu nodes ------------------------------ @@ -357,6 +403,26 @@ def node_validate_prototype(caller, raw_string, **kwargs): text = (text, helptext) options = _wizard_options(None, prev_node, None) + options.append({"key": "_default", + "goto": "node_" + prev_node}) + + return text, options + + +def node_examine_entity(caller, raw_string, **kwargs): + """ + General node to view a text and then return to previous node. Kwargs should contain "text" for + the text to show and 'back" pointing to the node to return to. + """ + text = kwargs.get("text", "Nothing was found here.") + helptext = "Use |wback|n to return to the previous node." + prev_node = kwargs.get('back', 'index') + + text = (text, helptext) + + options = _wizard_options(None, prev_node, None) + options.append({"key": "_default", + "goto": "node_" + prev_node}) return text, options @@ -419,15 +485,64 @@ def _all_prototype_parents(caller): for prototype in protlib.search_prototype() if "prototype_key" in prototype] -def _prototype_parent_examine(caller, prototype_name): - """Convert prototype to a string representation for closer inspection""" - prototypes = protlib.search_prototype(key=prototype_name) - if prototypes: - ret = protlib.prototype_to_str(prototypes[0]) - caller.msg(ret) - return ret - else: - caller.msg("Prototype not registered.") +def _prototype_parent_actions(caller, raw_inp, **kwargs): + """Parse the default Convert prototype to a string representation for closer inspection""" + choices = kwargs.get("available_choices", []) + prototype_parent, action = _default_parse( + raw_inp, choices, ("examine", "e", "l"), ("add", "a"), ("remove", "r", 'delete', 'd')) + + if prototype_parent: + # a selection of parent was made + prototype_parent = protlib.search_prototype(key=prototype_parent)[0] + prototype_parent_key = prototype_parent['prototype_key'] + + # which action to apply on the selection + if action == 'examine': + # examine the prototype + txt = protlib.prototype_to_str(prototype_parent) + kwargs['text'] = txt + kwargs['back'] = 'prototype_parent' + return "node_examine_entity", kwargs + elif action == 'add': + # add/append parent + prot = _get_menu_prototype(caller) + current_prot_parent = prot.get('prototype_parent', None) + if current_prot_parent: + current_prot_parent = utils.make_iter(current_prot_parent) + if prototype_parent_key in current_prot_parent: + caller.msg("Prototype_parent {} is already used.".format(prototype_parent_key)) + return "node_prototype_parent" + else: + current_prot_parent.append(prototype_parent_key) + caller.msg("Add prototype parent for multi-inheritance.") + else: + current_prot_parent = prototype_parent_key + try: + if prototype_parent: + spawner.flatten_prototype(prototype_parent, validate=True) + else: + raise RuntimeError("Not found.") + except RuntimeError as err: + caller.msg("Selected prototype-parent {} " + "caused Error(s):\n|r{}|n".format(prototype_parent, err)) + return "node_prototype_parent" + _set_prototype_value(caller, "prototype_parent", current_prot_parent) + _get_flat_menu_prototype(caller, refresh=True) + elif action == "remove": + # remove prototype parent + prot = _get_menu_prototype(caller) + current_prot_parent = prot.get('prototype_parent', None) + if current_prot_parent: + current_prot_parent = utils.make_iter(current_prot_parent) + try: + current_prot_parent.remove(prototype_parent_key) + _set_prototype_value(caller, 'prototype_parent', current_prot_parent) + _get_flat_menu_prototype(caller, refresh=True) + caller.msg("Removed prototype parent {}.".format(prototype_parent_key)) + except ValueError: + caller.msg("|rPrototype-parent {} could not be removed.".format( + prototype_parent_key)) + return 'node_prototype_parent' def _prototype_parent_select(caller, new_parent): @@ -440,7 +555,7 @@ def _prototype_parent_select(caller, new_parent): else: raise RuntimeError("Not found.") except RuntimeError as err: - caller.msg("Selected prototype parent {} " + caller.msg("Selected prototype-parent {} " "caused Error(s):\n|r{}|n".format(new_parent, err)) else: ret = _set_property(caller, new_parent, @@ -466,6 +581,8 @@ def node_prototype_parent(caller): parent is given, this prototype must define the typeclass (next menu node). {current} + + {actions} """ helptext = """ Prototypes can inherit from one another. Changes in the child replace any values set in a @@ -488,13 +605,14 @@ def node_prototype_parent(caller): if not ptexts: ptexts.append("[No prototype_parent set]") - text = text.format(current="\n\n".join(ptexts)) + text = text.format(current="\n\n".join(ptexts), + actions=_format_list_actions("examine", "add", "remove")) text = (text, helptext) options = _wizard_options("prototype_parent", "prototype_key", "typeclass", color="|W") options.append({"key": "_default", - "goto": _prototype_parent_examine}) + "goto": _prototype_parent_actions}) return text, options @@ -508,33 +626,45 @@ def _all_typeclasses(caller): if name != "evennia.objects.models.ObjectDB") -def _typeclass_examine(caller, typeclass_path): - """Show info (docstring) about given typeclass.""" - if typeclass_path is None: - # this means we are exiting the listing - return "node_key" +def _typeclass_actions(caller, raw_inp, **kwargs): + """Parse actions for typeclass listing""" - typeclass = utils.get_all_typeclasses().get(typeclass_path) - if typeclass: - docstr = [] - for line in typeclass.__doc__.split("\n"): - if line.strip(): - docstr.append(line) - elif docstr: - break - docstr = '\n'.join(docstr) if docstr else "" - txt = "Typeclass |y{typeclass_path}|n; First paragraph of docstring:\n\n{docstring}".format( - typeclass_path=typeclass_path, docstring=docstr) - else: - txt = "This is typeclass |y{}|n.".format(typeclass) - caller.msg(txt) - return txt + choices = kwargs.get("available_choices", []) + typeclass_path, action = _default_parse( + raw_inp, choices, ("examine", "e", "l"), ("remove", "r", "delete", "d")) + + if typeclass_path: + if action == 'examine': + typeclass = utils.get_all_typeclasses().get(typeclass_path) + if typeclass: + docstr = [] + for line in typeclass.__doc__.split("\n"): + if line.strip(): + docstr.append(line) + elif docstr: + break + docstr = '\n'.join(docstr) if docstr else "" + txt = "Typeclass |c{typeclass_path}|n; " \ + "First paragraph of docstring:\n\n{docstring}".format( + typeclass_path=typeclass_path, docstring=docstr) + else: + txt = "This is typeclass |y{}|n.".format(typeclass) + return "node_examine_entity", {"text": txt, "back": "typeclass"} + elif action == 'remove': + prototype = _get_menu_prototype(caller) + old_typeclass = prototype.pop('typeclass', None) + if old_typeclass: + _set_menu_prototype(caller, prototype) + caller.msg("Cleared typeclass {}.".format(old_typeclass)) + else: + caller.msg("No typeclass to remove.") + return "node_typeclass" def _typeclass_select(caller, typeclass): """Select typeclass from list and add it to prototype. Return next node to go to.""" ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key") - caller.msg("Selected typeclass |y{}|n.".format(typeclass)) + caller.msg("Selected typeclass |c{}|n.".format(typeclass)) return ret @@ -547,7 +677,10 @@ def node_typeclass(caller): one of the prototype's |cparents|n. {current} - """.format(current=_get_current_value(caller, "typeclass")) + + {actions} + """.format(current=_get_current_value(caller, "typeclass"), + actions=_format_list_actions("examine", "remove")) helptext = """ A |nTypeclass|n is specified by the actual python-path to the class definition in the @@ -561,7 +694,7 @@ def node_typeclass(caller): options = _wizard_options("typeclass", "prototype_parent", "key", color="|W") options.append({"key": "_default", - "goto": _typeclass_examine}) + "goto": _typeclass_actions}) return text, options @@ -598,16 +731,62 @@ def node_key(caller): # aliases node +def _all_aliases(caller): + "Get aliases in prototype" + prototype = _get_menu_prototype(caller) + return prototype.get("aliases", []) + + +def _aliases_select(caller, alias): + "Add numbers as aliases" + aliases = _all_aliases(caller) + try: + ind = str(aliases.index(alias) + 1) + if ind not in aliases: + aliases.append(ind) + _set_prototype_value(caller, "aliases", aliases) + caller.msg("Added alias '{}'.".format(ind)) + except (IndexError, ValueError) as err: + caller.msg("Error: {}".format(err)) + + return "node_aliases" + + +def _aliases_actions(caller, raw_inp, **kwargs): + """Parse actions for aliases listing""" + choices = kwargs.get("available_choices", []) + alias, action = _default_parse( + raw_inp, choices, ("remove", "r", "delete", "d")) + + aliases = _all_aliases(caller) + if alias and action == 'remove': + try: + aliases.remove(alias) + _set_prototype_value(caller, "aliases", aliases) + caller.msg("Removed alias '{}'.".format(alias)) + except ValueError: + caller.msg("No matching alias found to remove.") + else: + # if not a valid remove, add as a new alias + alias = raw_inp.lower().strip() + if alias not in aliases: + aliases.append(alias) + _set_prototype_value(caller, "aliases", aliases) + caller.msg("Added alias '{}'.".format(alias)) + else: + caller.msg("Alias '{}' was already set.".format(alias)) + return "node_aliases" + + +@list_node(_all_aliases, _aliases_select) def node_aliases(caller): text = """ |cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not case sensitive. - Add multiple aliases separating with commas. - - {current} - """.format(current=_get_current_value(caller, "aliases")) + {actions} + """.format(_format_list_actions("remove", prefix="|w|W to add new alias. Other action: ")) helptext = """ Aliases are fixed alternative identifiers and are stored with the new object. @@ -621,10 +800,7 @@ def node_aliases(caller): options = _wizard_options("aliases", "key", "attrs") options.append({"key": "_default", - "goto": (_set_property, - dict(prop="aliases", - processor=lambda s: [part.strip() for part in s.split(",")], - next_node="node_attrs"))}) + "goto": _aliases_actions}) return text, options @@ -633,38 +809,62 @@ def node_aliases(caller): def _caller_attrs(caller): prototype = _get_menu_prototype(caller) - attrs = prototype.get("attrs", []) + attrs = ["{}={}".format(tup[0], utils.crop(utils.to_str(tup[1]), width=10)) + for tup in prototype.get("attrs", [])] return attrs +def _get_tup_by_attrname(caller, attrname): + prototype = _get_menu_prototype(caller) + attrs = prototype.get("attrs", []) + try: + inp = [tup[0] for tup in attrs].index(attrname) + return attrs[inp] + except ValueError: + return None + + def _display_attribute(attr_tuple): """Pretty-print attribute tuple""" attrkey, value, category, locks = attr_tuple value = protlib.protfunc_parser(value) typ = type(value) - out = ("Attribute key: '{attrkey}' (category: {category}, " - "locks: {locks})\n" - "Value (parsed to {typ}): {value}").format( + out = ("|cAttribute key:|n '{attrkey}' " + "(|ccategory:|n {category}, " + "|clocks:|n {locks})\n" + "|cValue|n |W(parsed to {typ})|n:\n{value}").format( attrkey=attrkey, - category=category, locks=locks, + category=category if category else "|wNone|n", + locks=locks if locks else "|wNone|n", typ=typ, value=value) return out def _add_attr(caller, attr_string, **kwargs): """ - Add new attrubute, parsing input. - attr is entered on these forms - attr = value - attr;category = value - attr;category;lockstring = value + Add new attribute, parsing input. + Args: + caller (Object): Caller of menu. + attr_string (str): Input from user + attr is entered on these forms + attr = value + attr;category = value + attr;category;lockstring = value + Kwargs: + delete (str): If this is set, attr_string is + considered the name of the attribute to delete and + no further parsing happens. + Returns: + result (str): Result string of action. """ attrname = '' category = None locks = '' - if '=' in attr_string: + if 'delete' in kwargs: + attrname = attr_string + elif '=' in attr_string: attrname, value = (part.strip() for part in attr_string.split('=', 1)) attrname = attrname.lower() nameparts = attrname.split(";", 2) @@ -679,6 +879,15 @@ def _add_attr(caller, attr_string, **kwargs): prot = _get_menu_prototype(caller) attrs = prot.get('attrs', []) + if 'delete' in kwargs: + try: + ind = [tup[0] for tup in attrs].index(attrname) + del attrs[ind] + _set_prototype_value(caller, "attrs", attrs) + return "Removed Attribute '{}'".format(attrname) + except IndexError: + return "Attribute to delete not found." + try: # replace existing attribute with the same name in the prototype ind = [tup[0] for tup in attrs].index(attrname) @@ -697,26 +906,47 @@ def _add_attr(caller, attr_string, **kwargs): else: text = "Attribute must be given as 'attrname[;category;locks] = '." - options = {"key": "_default", - "goto": lambda caller: None} - return text, options + return text -def _edit_attr(caller, attrname, new_value, **kwargs): +def _attr_select(caller, attrstr): + attrname, _ = attrstr.split("=", 1) + attrname = attrname.strip() - attr_string = "{}={}".format(attrname, new_value) - - return _add_attr(caller, attr_string, edit=True) + attr_tup = _get_tup_by_attrname(caller, attrname) + if attr_tup: + return "node_examine_entity", \ + {"text": _display_attribute(attr_tup), "back": "attrs"} + else: + caller.msg("Attribute not found.") + return "node_attrs" -def _examine_attr(caller, selection): - prot = _get_menu_prototype(caller) - ind = [part[0] for part in prot['attrs']].index(selection) - attr_tuple = prot['attrs'][ind] - return _display_attribute(attr_tuple) +def _attrs_actions(caller, raw_inp, **kwargs): + """Parse actions for attribute listing""" + choices = kwargs.get("available_choices", []) + attrstr, action = _default_parse( + raw_inp, choices, ('examine', 'e'), ('remove', 'r', 'delete', 'd')) + if attrstr is None: + attrstr = raw_inp + attrname, _ = attrstr.split("=", 1) + attrname = attrname.strip() + attr_tup = _get_tup_by_attrname(caller, attrname) + + if attr_tup: + if action == 'examine': + return "node_examine_entity", \ + {"text": _display_attribute(attr_tup), "back": "attrs"} + elif action == 'remove': + res = _add_attr(caller, attr_tup, delete=True) + caller.msg(res) + else: + res = _add_attr(caller, raw_inp) + caller.msg(res) + return "node_attrs" -@list_node(_caller_attrs) +@list_node(_caller_attrs, _attr_select) def node_attrs(caller): text = """ @@ -729,8 +959,8 @@ def node_attrs(caller): To give an attribute without a category but with a lockstring, leave that spot empty (attrname;;lockstring=value). Attribute values can have embedded $protfuncs. - {current} - """.format(current=_get_current_value(caller, "attrs")) + {actions} + """.format(actions=_format_list_actions("examine", "remove", prefix="Actions: ")) helptext = """ Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types @@ -747,10 +977,7 @@ def node_attrs(caller): options = _wizard_options("attrs", "aliases", "tags") options.append({"key": "_default", - "goto": (_set_property, - dict(prop="attrs", - processor=lambda s: [part.strip() for part in s.split(",")], - next_node="node_tags"))}) + "goto": _attrs_actions}) return text, options @@ -1410,7 +1637,7 @@ def node_prototype_load(caller, **kwargs): options = _wizard_options("prototype_load", "prototype_save", "index") options.append({"key": "_default", - "goto": _prototype_parent_examine}) + "goto": _prototype_parent_actions}) return text, options @@ -1468,6 +1695,7 @@ def start_olc(caller, session=None, prototype=None): """ menudata = {"node_index": node_index, "node_validate_prototype": node_validate_prototype, + "node_examine_entity": node_examine_entity, "node_prototype_key": node_prototype_key, "node_prototype_parent": node_prototype_parent, "node_typeclass": node_typeclass, diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 767919a7a9..4c0a2d3186 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -606,6 +606,8 @@ def validate_prototype(prototype, protkey=None, protparents=None, _flags['errors'].append( "{} has infinite nesting of prototypes.".format(protkey or prototype)) + if _flags['errors']: + raise RuntimeError("Error: " + "\nError: ".join(_flags['errors'])) _flags['visited'].append(id(prototype)) _flags['depth'] += 1 validate_prototype(protparent, protstring, protparents, @@ -618,7 +620,7 @@ def validate_prototype(prototype, protkey=None, protparents=None, # if we get back to the current level without a typeclass it's an error. if strict and is_prototype_base and _flags['depth'] <= 0 and not _flags['typeclass']: - _flags['errors'].append("Prototype {} has no `typeclass` defined anywhere in its parent " + _flags['errors'].append("Prototype {} has no `typeclass` defined anywhere in its parent\n " "chain. Add `typeclass`, or a `prototype_parent` pointing to a " "prototype with a typeclass.".format(protkey)) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 221200672d..4b16ad9ab2 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -384,6 +384,14 @@ class TestMenuModule(EvenniaTest): {"prototype_key": "testthing", "key": "mytest"}), (True, Something)) + choices = ["test1", "test2", "test3", "test4"] + actions = (("examine", "e", "l"), ("add", "a"), ("foo", "f")) + self.assertEqual(olc_menus._default_parse("l4", choices, *actions), ('test4', 'examine')) + self.assertEqual(olc_menus._default_parse("add 2", choices, *actions), ('test2', 'add')) + self.assertEqual(olc_menus._default_parse("foo3", choices, *actions), ('test3', 'foo')) + self.assertEqual(olc_menus._default_parse("f3", choices, *actions), ('test3', 'foo')) + self.assertEqual(olc_menus._default_parse("f5", choices, *actions), (None, None)) + def test_node_helpers(self): caller = self.caller @@ -399,15 +407,20 @@ class TestMenuModule(EvenniaTest): # prototype_parent helpers self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot']) - self.assertEqual(olc_menus._prototype_parent_examine( - caller, 'test_prot'), - "|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() " - "\n|cdesc:|n None \n|cprototype:|n " - "{\n 'typeclass': 'evennia.objects.objects.DefaultObject', \n}") - self.assertEqual(olc_menus._prototype_parent_select(caller, self.test_prot), "node_key") + # self.assertEqual(olc_menus._prototype_parent_parse( + # caller, 'test_prot'), + # "|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() " + # "\n|cdesc:|n None \n|cprototype:|n " + # "{\n 'typeclass': 'evennia.objects.objects.DefaultObject', \n}") + + with mock.patch("evennia.prototypes.menus.protlib.search_prototype", + new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])): + self.assertEqual(olc_menus._prototype_parent_select(caller, "goblin"), "node_prototype_parent") + self.assertEqual(olc_menus._get_menu_prototype(caller), {'prototype_key': 'test_prot', 'prototype_locks': 'edit:all();spawn:all()', + 'prototype_parent': 'goblin', 'typeclass': 'evennia.objects.objects.DefaultObject'}) # typeclass helpers @@ -423,6 +436,7 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._get_menu_prototype(caller), {'prototype_key': 'test_prot', 'prototype_locks': 'edit:all();spawn:all()', + 'prototype_parent': 'goblin', 'typeclass': 'evennia.objects.objects.DefaultObject'}) # attr helpers @@ -459,7 +473,9 @@ class TestMenuModule(EvenniaTest): protlib.save_prototype(**self.test_prot) # spawn helpers - obj = olc_menus._spawn(caller, prototype=self.test_prot) + with mock.patch("evennia.prototypes.menus.protlib.search_prototype", + new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])): + obj = olc_menus._spawn(caller, prototype=self.test_prot) self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject") self.assertEqual(obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot['prototype_key']) @@ -475,7 +491,6 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._prototype_load_select(caller, self.test_prot['prototype_key']), "node_index") - @mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock( return_value=[{"prototype_key": "TestPrototype", "typeclass": "TypeClassTest", "key": "TestObj"}])) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 2f1b7d64fa..d21aec2c56 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -938,7 +938,7 @@ class EvMenu(object): for key, desc in optionlist: if not (key or desc): continue - desc_string = ": %s" % desc if desc else "" + desc_string = ": %s" % (desc if desc else "") table_width_max = max(table_width_max, max(m_len(p) for p in key.split("\n")) + max(m_len(p) for p in desc_string.split("\n")) + colsep) From 0c53088e515f450c7a8881b7311ca3f42c57fecf Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 27 Jul 2018 13:34:20 +0200 Subject: [PATCH 088/103] Add tag handling in old menu --- evennia/prototypes/menus.py | 163 +++++++++++++++++++++++------------- evennia/prototypes/tests.py | 37 ++++---- 2 files changed, 123 insertions(+), 77 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 54ec054340..e5ff1179a2 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -863,7 +863,7 @@ def _add_attr(caller, attr_string, **kwargs): locks = '' if 'delete' in kwargs: - attrname = attr_string + attrname = attr_string.lower().strip() elif '=' in attr_string: attrname, value = (part.strip() for part in attr_string.split('=', 1)) attrname = attrname.lower() @@ -892,17 +892,12 @@ def _add_attr(caller, attr_string, **kwargs): # replace existing attribute with the same name in the prototype ind = [tup[0] for tup in attrs].index(attrname) attrs[ind] = attr_tuple + text = "Edited Attribute '{}'.".format(attrname) except ValueError: attrs.append(attr_tuple) + text = "Added Attribute " + _display_attribute(attr_tuple) _set_prototype_value(caller, "attrs", attrs) - - text = kwargs.get('text') - if not text: - if 'edit' in kwargs: - text = "Edited " + _display_attribute(attr_tuple) - else: - text = "Added " + _display_attribute(attr_tuple) else: text = "Attribute must be given as 'attrname[;category;locks] = '." @@ -929,7 +924,12 @@ def _attrs_actions(caller, raw_inp, **kwargs): raw_inp, choices, ('examine', 'e'), ('remove', 'r', 'delete', 'd')) if attrstr is None: attrstr = raw_inp - attrname, _ = attrstr.split("=", 1) + try: + attrname, _ = attrstr.split("=", 1) + except ValueError: + caller.msg("|rNeed to enter the attribute on the form attrname=value.|n") + return "node_attrs" + attrname = attrname.strip() attr_tup = _get_tup_by_attrname(caller, attrname) @@ -938,7 +938,7 @@ def _attrs_actions(caller, raw_inp, **kwargs): return "node_examine_entity", \ {"text": _display_attribute(attr_tup), "back": "attrs"} elif action == 'remove': - res = _add_attr(caller, attr_tup, delete=True) + res = _add_attr(caller, attrname, delete=True) caller.msg(res) else: res = _add_attr(caller, raw_inp) @@ -964,9 +964,9 @@ def node_attrs(caller): helptext = """ Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types - 'attredit', 'attrread' are used to limiting editing and viewing of the Attribute. Putting + 'attredit' and 'attrread' are used to limit editing and viewing of the Attribute. Putting the lock-type `attrcreate` in the |clocks|n prototype key can be used to restrict builders - to add new Attributes. + from adding new Attributes. |c$protfuncs @@ -986,83 +986,128 @@ def node_attrs(caller): def _caller_tags(caller): prototype = _get_menu_prototype(caller) - tags = prototype.get("tags", []) + tags = [tup[0] for tup in prototype.get("tags", [])] return tags +def _get_tup_by_tagname(caller, tagname): + prototype = _get_menu_prototype(caller) + tags = prototype.get("tags", []) + try: + inp = [tup[0] for tup in tags].index(tagname) + return tags[inp] + except ValueError: + return None + + def _display_tag(tag_tuple): - """Pretty-print attribute tuple""" + """Pretty-print tag tuple""" tagkey, category, data = tag_tuple out = ("Tag: '{tagkey}' (category: {category}{dat})".format( tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else "")) return out -def _add_tag(caller, tag, **kwargs): +def _add_tag(caller, tag_string, **kwargs): """ - Add tags to the system, parsing this syntax: - tagname - tagname;category - tagname;category;data + Add tags to the system, parsing input + + Args: + caller (Object): Caller of menu. + tag_string (str): Input from user on one of these forms + tagname + tagname;category + tagname;category;data + + Kwargs: + delete (str): If this is set, tag_string is considered + the name of the tag to delete. + + Returns: + result (str): Result string of action. """ - - tag = tag.strip().lower() + tag = tag_string.strip().lower() category = None data = "" - tagtuple = tag.split(";", 2) - ntuple = len(tagtuple) + if 'delete' in kwargs: + tag = tag_string.lower().strip() + else: + nameparts = tag.split(";", 2) + ntuple = len(nameparts) + if ntuple == 2: + tag, category = nameparts + elif ntuple > 2: + tag, category, data = nameparts[:3] - if ntuple == 2: - tag, category = tagtuple - elif ntuple > 2: - tag, category, data = tagtuple - - tag_tuple = (tag, category, data) + tag_tuple = (tag.lower(), category.lower() if category else None, data) if tag: prot = _get_menu_prototype(caller) tags = prot.get('tags', []) - old_tag = kwargs.get("edit", None) + old_tag = _get_tup_by_tagname(caller, tag) - if not old_tag: + if 'delete' in kwargs: + + if old_tag: + tags.pop(tags.index(old_tag)) + text = "Removed tag '{}'".format(tag) + else: + text = "Found no tag to remove." + elif not old_tag: # a fresh, new tag tags.append(tag_tuple) + text = "Added Tag '{}'".format(tag) else: - # old tag exists; editing a tag means removing the old and replacing with new - try: - ind = [tup[0] for tup in tags].index(old_tag) - del tags[ind] - if tags: - tags.insert(ind, tag_tuple) - else: - tags = [tag_tuple] - except IndexError: - pass + # old tag exists; editing a tag means replacing old with new + ind = tags.index(old_tag) + tags[ind] = tag_tuple + text = "Edited Tag '{}'".format(tag) _set_prototype_value(caller, "tags", tags) - - text = kwargs.get('text') - if not text: - if 'edit' in kwargs: - text = "Edited " + _display_tag(tag_tuple) - else: - text = "Added " + _display_tag(tag_tuple) else: - text = "Tag must be given as 'tag[;category;data]." + text = "Tag must be given as 'tag[;category;data]'." - options = {"key": "_default", - "goto": lambda caller: None} - return text, options + return text -def _edit_tag(caller, old_tag, new_tag, **kwargs): - return _add_tag(caller, new_tag, edit=old_tag) +def _tag_select(caller, tagname): + tag_tup = _get_tup_by_tagname(caller, tagname) + if tag_tup: + return "node_examine_entity", \ + {"text": _display_tag(tag_tup), "back": "attrs"} + else: + caller.msg("Tag not found.") + return "node_attrs" -@list_node(_caller_tags) +def _tags_actions(caller, raw_inp, **kwargs): + """Parse actions for tags listing""" + choices = kwargs.get("available_choices", []) + tagname, action = _default_parse( + raw_inp, choices, ('examine', 'e'), ('remove', 'r', 'delete', 'd')) + + if tagname is None: + tagname = raw_inp.lower().strip() + + tag_tup = _get_tup_by_tagname(caller, tagname) + + if tag_tup: + if action == 'examine': + return "node_examine_entity", \ + {"text": _display_tag(tag_tup), 'back': 'tags'} + elif action == 'remove': + res = _add_tag(caller, tagname, delete=True) + caller.msg(res) + else: + res = _add_tag(caller, raw_inp) + caller.msg(res) + return "node_tags" + + +@list_node(_caller_tags, _tag_select) def node_tags(caller): text = """ |cTags|n are used to group objects so they can quickly be found later. Enter tags on one of @@ -1071,8 +1116,8 @@ def node_tags(caller): tagname;category tagname;category;data - {current} - """.format(current=_get_current_value(caller, 'tags')) + {actions} + """.format(actions=_format_list_actions("examine", "remove", prefix="Actions: ")) helptext = """ Tags are shared between all objects with that tag. So the 'data' field (which is not @@ -1082,10 +1127,12 @@ def node_tags(caller): All objects created with this prototype will automatically get assigned a tag named the same 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(protlib._PROTOTYPE_TAG_CATEGORY) + """.format(tag_category=protlib._PROTOTYPE_TAG_CATEGORY) text = (text, helptext) options = _wizard_options("tags", "attrs", "locks") + options.append({"key": "_default", + "goto": _tags_actions}) return text, options diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 4b16ad9ab2..299495628e 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -427,8 +427,6 @@ class TestMenuModule(EvenniaTest): with mock.patch("evennia.utils.utils.get_all_typeclasses", new=mock.MagicMock(return_value={"foo": None, "bar": None})): self.assertEqual(olc_menus._all_typeclasses(caller), ["bar", "foo"]) - self.assertTrue(olc_menus._typeclass_examine( - caller, "evennia.objects.objects.DefaultObject").startswith("Typeclass |y")) self.assertEqual(olc_menus._typeclass_select( caller, "evennia.objects.objects.DefaultObject"), "node_key") @@ -441,34 +439,35 @@ class TestMenuModule(EvenniaTest): # attr helpers self.assertEqual(olc_menus._caller_attrs(caller), []) - self.assertEqual(olc_menus._add_attr(caller, "test1=foo1"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._add_attr(caller, "test2;cat1=foo2"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._add_attr(caller, "test3;cat2;edit:false()=foo3"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._add_attr(caller, "test4;cat3;set:true();edit:false()=foo4"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._add_attr(caller, "test5;cat4;set:true();edit:false()=123"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._caller_attrs( - caller), + self.assertEqual(olc_menus._add_attr(caller, "test1=foo1"), Something) + self.assertEqual(olc_menus._add_attr(caller, "test2;cat1=foo2"), Something) + self.assertEqual(olc_menus._add_attr(caller, "test3;cat2;edit:false()=foo3"), Something) + self.assertEqual(olc_menus._add_attr(caller, "test4;cat3;set:true();edit:false()=foo4"), Something) + self.assertEqual(olc_menus._add_attr(caller, "test5;cat4;set:true();edit:false()=123"), Something) + self.assertEqual(olc_menus._get_menu_prototype(caller)['attrs'], [("test1", "foo1", None, ''), ("test2", "foo2", "cat1", ''), ("test3", "foo3", "cat2", "edit:false()"), ("test4", "foo4", "cat3", "set:true();edit:false()"), ("test5", '123', "cat4", "set:true();edit:false()")]) - self.assertEqual(olc_menus._edit_attr(caller, "test1", "1;cat5;edit:all()"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._examine_attr(caller, "test1"), Something) # tag helpers self.assertEqual(olc_menus._caller_tags(caller), []) - self.assertEqual(olc_menus._add_tag(caller, "foo1"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._add_tag(caller, "foo2;cat1"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._add_tag(caller, "foo3;cat2;dat1"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._caller_tags( - caller), + self.assertEqual(olc_menus._add_tag(caller, "foo1"), Something) + self.assertEqual(olc_menus._add_tag(caller, "foo2;cat1"), Something) + self.assertEqual(olc_menus._add_tag(caller, "foo3;cat2;dat1"), Something) + self.assertEqual(olc_menus._caller_tags(caller), ['foo1', 'foo2', 'foo3']) + self.assertEqual(olc_menus._get_menu_prototype(caller)['tags'], [('foo1', None, ""), ('foo2', 'cat1', ""), ('foo3', 'cat2', "dat1")]) - self.assertEqual(olc_menus._edit_tag(caller, "foo1", "bar1;cat1"), (Something, {"key": "_default", "goto": Something})) - self.assertEqual(olc_menus._display_tag(olc_menus._caller_tags(caller)[0]), Something) - self.assertEqual(olc_menus._caller_tags(caller)[0], ("bar1", "cat1", "")) + self.assertEqual(olc_menus._add_tag(caller, "foo1", delete=True), "Removed tag 'foo1'") + self.assertEqual(olc_menus._get_menu_prototype(caller)['tags'], + [('foo2', 'cat1', ""), + ('foo3', 'cat2', "dat1")]) + + self.assertEqual(olc_menus._display_tag(olc_menus._get_menu_prototype(caller)['tags'][0]), Something) + self.assertEqual(olc_menus._caller_tags(caller), ["foo2", "foo3"]) protlib.save_prototype(**self.test_prot) From 6e7986a915c1329ddb7b8c9a7cc5e66348a848bd Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 27 Jul 2018 18:46:30 +0200 Subject: [PATCH 089/103] Refactor locks and permissions in olc menu --- evennia/prototypes/menus.py | 185 +++++++++++++++++++++++++++++++----- evennia/prototypes/tests.py | 14 ++- 2 files changed, 173 insertions(+), 26 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index e5ff1179a2..138f97cf13 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -236,11 +236,13 @@ def _format_protfuncs(): def _format_lockfuncs(): out = [] sorted_funcs = [(key, func) for key, func in - sorted(get_all_lockfuncs(), key=lambda tup: tup[0])] + sorted(get_all_lockfuncs().items(), key=lambda tup: tup[0])] for lockfunc_name, lockfunc in sorted_funcs: + doc = (lockfunc.__doc__ or "").strip() out.append("- |c${name}|n - |W{docs}".format( name=lockfunc_name, - docs=utils.justify(lockfunc.__doc__.strip(), align='l', indent=10).strip())) + docs=utils.justify(doc, align='l', indent=10).strip())) + return "\n".join(out) def _format_list_actions(*args, **kwargs): @@ -769,7 +771,7 @@ def _aliases_actions(caller, raw_inp, **kwargs): else: # if not a valid remove, add as a new alias alias = raw_inp.lower().strip() - if alias not in aliases: + if alias and alias not in aliases: aliases.append(alias) _set_prototype_value(caller, "aliases", aliases) caller.msg("Added alias '{}'.".format(alias)) @@ -786,7 +788,7 @@ def node_aliases(caller): case sensitive. {actions} - """.format(_format_list_actions("remove", prefix="|w|W to add new alias. Other action: ")) + """.format(actions=_format_list_actions("remove", prefix="|w|W to add new alias. Other action: ")) helptext = """ Aliases are fixed alternative identifiers and are stored with the new object. @@ -1053,9 +1055,9 @@ def _add_tag(caller, tag_string, **kwargs): if old_tag: tags.pop(tags.index(old_tag)) - text = "Removed tag '{}'".format(tag) + text = "Removed Tag '{}'.".format(tag) else: - text = "Found no tag to remove." + text = "Found no Tag to remove." elif not old_tag: # a fresh, new tag tags.append(tag_tuple) @@ -1138,7 +1140,80 @@ def node_tags(caller): # locks node +def _caller_locks(caller): + locks = _get_menu_prototype(caller).get("locks", "") + return [lck for lck in locks.split(";") if lck] + +def _locks_display(caller, lock): + try: + locktype, lockdef = lock.split(":", 1) + except ValueError: + txt = "Malformed lock string - Missing ':'" + else: + txt = ("{lockstr}\n\n" + "|WLocktype: |w{locktype}|n\n" + "|WLock def: |w{lockdef}|n\n").format( + lockstr=lock, + locktype=locktype, + lockdef=lockdef) + return txt + + +def _lock_select(caller, lockstr): + return "node_examine_entity", {"text": _locks_display(caller, lockstr), "back": "locks"} + + +def _lock_add(caller, lock, **kwargs): + locks = _caller_locks(caller) + + try: + locktype, lockdef = lock.split(":", 1) + except ValueError: + return "Lockstring lacks ':'." + + locktype = locktype.strip().lower() + + if 'delete' in kwargs: + try: + ind = locks.index(lock) + locks.pop(ind) + _set_prototype_value(caller, "locks", ";".join(locks), parse=False) + ret = "Lock {} deleted.".format(lock) + except ValueError: + ret = "No lock found to delete." + return ret + try: + locktypes = [lck.split(":", 1)[0].strip().lower() for lck in locks] + ind = locktypes.index(locktype) + locks[ind] = lock + ret = "Lock with locktype '{}' updated.".format(locktype) + except ValueError: + locks.append(lock) + ret = "Added lock '{}'.".format(lock) + _set_prototype_value(caller, "locks", ";".join(locks)) + return ret + + +def _locks_actions(caller, raw_inp, **kwargs): + choices = kwargs.get("available_choices", []) + lock, action = _default_parse( + raw_inp, choices, ("examine", "e"), ("remove", "r", "delete", "d")) + + if lock: + if action == 'examine': + return "node_examine_entity", {"text": _locks_display(caller, lock), "back": "locks"} + elif action == 'remove': + ret = _lock_add(caller, lock, delete=True) + caller.msg(ret) + else: + ret = _lock_add(caller, raw_inp) + caller.msg(ret) + + return "node_locks" + + +@list_node(_caller_locks, _lock_select) def node_locks(caller): text = """ @@ -1148,22 +1223,21 @@ def node_locks(caller): locktype:[NOT] lockfunc(args) locktype: [NOT] lockfunc(args) [AND|OR|NOT] lockfunc(args) [AND|OR|NOT] ... - Separate multiple lockstrings by semicolons (;). - - {current} - """.format(current=_get_current_value(caller, 'locks')) + {action} + """.format(action=_format_list_actions("examine", "remove", prefix="Actions: ")) helptext = """ - Here is an example of a lock string constisting of two locks: + Here is an example of two lock strings: - edit:false();call:tag(Foo) OR perm(Builder) + edit:false() + call:tag(Foo) OR perm(Builder) Above locks limit two things, 'edit' and 'call'. Which lock types are actually checked depend on the typeclass of the object being spawned. Here 'edit' is never allowed by anyone while 'call' is allowed to all accessors with a |ctag|n 'Foo' OR which has the |cPermission|n 'Builder'. - |c$lockfuncs|n + |cAvailable lockfuncs:|n {lfuncs} """.format(lfuncs=_format_lockfuncs()) @@ -1172,24 +1246,87 @@ def node_locks(caller): options = _wizard_options("locks", "tags", "permissions") options.append({"key": "_default", - "goto": (_set_property, - dict(prop="locks", - processor=lambda s: s.strip(), - next_node="node_permissions"))}) + "goto": _locks_actions}) + return text, options # permissions node +def _caller_permissions(caller): + prototype = _get_menu_prototype(caller) + perms = prototype.get("permissions", []) + return perms + +def _display_perm(caller, permission): + hierarchy = settings.PERMISSION_HIERARCHY + perm_low = permission.lower() + if perm_low in [prm.lower() for prm in hierarchy]: + txt = "Permission (in hieararchy): {}".format( + ", ".join( + ["|w[{}]|n".format(prm) + if prm.lower() == perm_low else "|W{}|n".format(prm) + for prm in hierarchy])) + else: + txt = "Permission: '{}'".format(permission) + return txt + + +def _permission_select(caller, permission, **kwargs): + return "node_examine_entity", {"text": _display_perm(caller, permission), "back": "permissions"} + + +def _add_perm(caller, perm, **kwargs): + if perm: + perm_low = perm.lower() + perms = _caller_permissions(caller) + perms_low = [prm.lower() for prm in perms] + if 'delete' in kwargs: + try: + ind = perms_low.index(perm_low) + del perms[ind] + text = "Removed Permission '{}'.".format(perm) + except ValueError: + text = "Found no Permission to remove." + else: + if perm_low in perms_low: + text = "Permission already set." + else: + perms.append(perm) + _set_prototype_value(caller, "permissions", perms) + text = "Added Permission '{}'".format(perm) + return text + + +def _permissions_actions(caller, raw_inp, **kwargs): + """Parse actions for permission listing""" + choices = kwargs.get("available_choices", []) + perm, action = _default_parse( + raw_inp, choices, ('examine', 'e'), ('remove', 'r', 'delete', 'd')) + + if perm: + if action == 'examine': + return "node_examine_entity", \ + {"text": _display_perm(caller, perm), "back": "permissions"} + elif action == 'remove': + res = _add_perm(caller, perm, delete=True) + caller.msg(res) + else: + res = _add_perm(caller, raw_inp.strip()) + caller.msg(res) + return "node_permissions" + + +@list_node(_caller_permissions, _permission_select) def node_permissions(caller): text = """ |cPermissions|n are simple strings used to grant access to this object. A permission is used when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions. - {current} - """.format(current=_get_current_value(caller, "permissions")) + {actions} + """.format(actions=_format_list_actions("examine", "remove"), prefix="Actions: ") helptext = """ Any string can act as a permission as long as a lock is set to look for it. Depending on the @@ -1201,16 +1338,14 @@ def node_permissions(caller): For example, a |clock|n string like "edit:perm(Builder)" will grant access to accessors having the |cpermission|n "Builder" or higher. - """.format(settings.PERMISSION_HIERARCHY) + """.format(permissions=", ".join(settings.PERMISSION_HIERARCHY)) text = (text, helptext) - options = _wizard_options("permissions", "destination", "location") + options = _wizard_options("permissions", "locks", "location") options.append({"key": "_default", - "goto": (_set_property, - dict(prop="permissions", - processor=lambda s: [part.strip() for part in s.split(",")], - next_node="node_location"))}) + "goto": _permissions_actions}) + return text, options diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 299495628e..92b8e85a65 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -461,7 +461,7 @@ class TestMenuModule(EvenniaTest): [('foo1', None, ""), ('foo2', 'cat1', ""), ('foo3', 'cat2', "dat1")]) - self.assertEqual(olc_menus._add_tag(caller, "foo1", delete=True), "Removed tag 'foo1'") + self.assertEqual(olc_menus._add_tag(caller, "foo1", delete=True), "Removed Tag 'foo1'.") self.assertEqual(olc_menus._get_menu_prototype(caller)['tags'], [('foo2', 'cat1', ""), ('foo3', 'cat2', "dat1")]) @@ -471,6 +471,18 @@ class TestMenuModule(EvenniaTest): protlib.save_prototype(**self.test_prot) + # locks helpers + self.assertEqual(olc_menus._lock_add(caller, "foo:false()"), "Added lock 'foo:false()'.") + self.assertEqual(olc_menus._lock_add(caller, "foo2:false()"), "Added lock 'foo2:false()'.") + self.assertEqual(olc_menus._lock_add(caller, "foo2:true()"), "Lock with locktype 'foo2' updated.") + self.assertEqual(olc_menus._get_menu_prototype(caller)["locks"], "foo:false();foo2:true()") + + # perm helpers + self.assertEqual(olc_menus._add_perm(caller, "foo"), "Added Permission 'foo'") + self.assertEqual(olc_menus._add_perm(caller, "foo2"), "Added Permission 'foo2'") + self.assertEqual(olc_menus._get_menu_prototype(caller)["permissions"], ["foo", "foo2"]) + + # spawn helpers with mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])): From 23e2c0e34f03d3134794e0762c648ed100f08841 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Jul 2018 18:26:00 +0200 Subject: [PATCH 090/103] Add search-object functionality to olc menu --- evennia/prototypes/menus.py | 233 +++++++++++++++++++++++++------ evennia/prototypes/prototypes.py | 79 +++++++++-- evennia/prototypes/spawner.py | 13 +- evennia/utils/evmenu.py | 7 +- 4 files changed, 266 insertions(+), 66 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 138f97cf13..ab66363c71 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -7,7 +7,9 @@ OLC Prototype menu nodes import json import re from random import choice +from django.db.models import Q from django.conf import settings +from evennia.objects.models import ObjectDB from evennia.utils.evmenu import EvMenu, list_node from evennia.utils import evmore from evennia.utils.ansi import strip_ansi @@ -273,7 +275,11 @@ def _get_current_value(caller, keyname, formatter=str): flat_prot = _get_flat_menu_prototype(caller) if keyname in flat_prot: # value in flattened prot - return "Current {} (|binherited|n): {}".format(keyname, formatter(flat_prot[keyname])) + if keyname == 'prototype_key': + # we don't inherit prototype_keys + return "[No prototype_key set] (|rnot inherited|n)" + else: + return "Current {} (|binherited|n): {}".format(keyname, formatter(flat_prot[keyname])) return "[No {} set]".format(keyname) @@ -305,6 +311,180 @@ def _default_parse(raw_inp, choices, *args): # Menu nodes ------------------------------ +# helper nodes + +# validate prototype (available as option from all nodes) + +def node_validate_prototype(caller, raw_string, **kwargs): + """General node to view and validate a protototype""" + prototype = _get_flat_menu_prototype(caller, validate=False) + prev_node = kwargs.get("back", "index") + + _, text = _validate_prototype(prototype) + + helptext = """ + The validator checks if the prototype's various values are on the expected form. It also tests + any $protfuncs. + + """ + + text = (text, helptext) + + options = _wizard_options(None, prev_node, None) + options.append({"key": "_default", + "goto": "node_" + prev_node}) + + return text, options + + +def node_examine_entity(caller, raw_string, **kwargs): + """ + General node to view a text and then return to previous node. Kwargs should contain "text" for + the text to show and 'back" pointing to the node to return to. + """ + text = kwargs.get("text", "Nothing was found here.") + helptext = "Use |wback|n to return to the previous node." + prev_node = kwargs.get('back', 'index') + + text = (text, helptext) + + options = _wizard_options(None, prev_node, None) + options.append({"key": "_default", + "goto": "node_" + prev_node}) + + return text, options + + +def _search_object(caller): + "update search term based on query stored on menu; store match too" + try: + searchstring = caller.ndb._menutree.olc_search_object_term.strip() + caller.ndb._menutree.olc_search_object_matches = [] + except AttributeError: + return [] + + if not searchstring: + caller.msg("Must specify a search criterion.") + return [] + + is_dbref = utils.dbref(searchstring) + is_account = searchstring.startswith("*") + + if is_dbref or is_account: + + if is_dbref: + # a dbref search + results = caller.search(searchstring, global_search=True, quiet=True) + else: + # an account search + searchstring = searchstring.lstrip("*") + results = caller.search_account(searchstring, quiet=True) + else: + keyquery = Q(db_key__istartswith=searchstring) + aliasquery = Q(db_tags__db_key__istartswith=searchstring, + db_tags__db_tagtype__iexact="alias") + results = ObjectDB.objects.filter(keyquery | aliasquery).distinct() + + caller.msg("Searching for '{}' ...".format(searchstring)) + caller.ndb._menutree.olc_search_object_matches = results + return ["{}(#{})".format(obj.key, obj.id) for obj in results] + + +def _object_select(caller, obj_entry, **kwargs): + choices = kwargs['available_choices'] + num = choices.index(obj_entry) + matches = caller.ndb._menutree.olc_search_object_matches + obj = matches[num] + + if not obj.access(caller, 'examine'): + caller.msg("|rYou don't have 'examine' access on this object.|n") + del caller.ndb._menutree.olc_search_object_term + return "node_search_object" + + prot = spawner.prototype_from_object(obj) + txt = protlib.prototype_to_str(prot) + return "node_examine_entity", {"text": txt, "back": "search_object"} + + +def _object_actions(caller, raw_inp, **kwargs): + "All this does is to queue a search query" + choices = kwargs['available_choices'] + obj_entry, action = _default_parse( + raw_inp, choices, ("examine", "e"), ("create prototype from object", "create", "c")) + + if obj_entry: + + num = choices.index(obj_entry) + matches = caller.ndb._menutree.olc_search_object_matches + obj = matches[num] + prot = spawner.prototype_from_object(obj) + + if action == "examine": + + if not obj.access(caller, 'examine'): + caller.msg("\n|rYou don't have 'examine' access on this object.|n") + del caller.ndb._menutree.olc_search_object_term + return "node_search_object" + + txt = protlib.prototype_to_str(prot) + return "node_examine_entity", {"text": txt, "back": "search_object"} + else: + # load prototype + + if not obj.access(caller, 'control'): + caller.msg("|rYou don't have access to do this with this object.|n") + del caller.ndb._menutree.olc_search_object_term + return "node_search_object" + + _set_menu_prototype(caller, prot) + caller.msg("Created prototype from object.") + return "node_index" + else: + caller.ndb._menutree.olc_search_object_term = raw_inp + return "node_search_object", kwargs + + +@list_node(_search_object, _object_select) +def node_search_object(caller, raw_inp, **kwargs): + """ + Node for searching for an existing object. + """ + try: + matches = caller.ndb._menutree.olc_search_object_matches + except AttributeError: + matches = [] + nmatches = len(matches) + prev_node = kwargs.get("back", "index") + + if matches: + text = """ + Found {num} match{post}. + + {actions} + (|RWarning: creating a prototype will |roverwrite|r |Rthe current prototype!)|n""".format( + num=nmatches, post="es" if nmatches > 1 else "", + actions=_format_list_actions( + "examine", "create prototype from object", prefix="Actions: ")) + else: + text = "Enter search criterion." + + helptext = """ + You can search objects by specifying partial key, alias or its exact #dbref. Use *query to + search for an Account instead. + + Once having found any matches you can choose to examine it or use |ccreate prototype from + object|n. If doing the latter, a prototype will be calculated from the selected object and + loaded as the new 'current' prototype. This is useful for having a base to build from but be + careful you are not throwing away any existing, unsaved, prototype work! + """ + + text = (text, helptext) + + options = _wizard_options(None, prev_node, None) + options.append({"key": "_default", + "goto": (_object_actions, {"back": prev_node})}) + + return text, options # main index (start page) node @@ -382,49 +562,9 @@ def node_index(caller): {"key": ("|wSP|Wawn prototype", "spawn", "sp"), "goto": "node_prototype_spawn"}, {"key": ("|wLO|Wad prototype", "load", "lo"), - "goto": "node_prototype_load"})) - - return text, options - - -# validate prototype (available as option from all nodes) - -def node_validate_prototype(caller, raw_string, **kwargs): - """General node to view and validate a protototype""" - prototype = _get_menu_prototype(caller) - prev_node = kwargs.get("back", "index") - - _, text = _validate_prototype(prototype) - - helptext = """ - The validator checks if the prototype's various values are on the expected form. It also tests - any $protfuncs. - - """ - - text = (text, helptext) - - options = _wizard_options(None, prev_node, None) - options.append({"key": "_default", - "goto": "node_" + prev_node}) - - return text, options - - -def node_examine_entity(caller, raw_string, **kwargs): - """ - General node to view a text and then return to previous node. Kwargs should contain "text" for - the text to show and 'back" pointing to the node to return to. - """ - text = kwargs.get("text", "Nothing was found here.") - helptext = "Use |wback|n to return to the previous node." - prev_node = kwargs.get('back', 'index') - - text = (text, helptext) - - options = _wizard_options(None, prev_node, None) - options.append({"key": "_default", - "goto": "node_" + prev_node}) + "goto": "node_prototype_load"}, + {"key": ("|wSE|Warch objects|n", "search", "se"), + "goto": "node_search_object"})) return text, options @@ -811,7 +951,7 @@ def node_aliases(caller): def _caller_attrs(caller): prototype = _get_menu_prototype(caller) - attrs = ["{}={}".format(tup[0], utils.crop(utils.to_str(tup[1]), width=10)) + attrs = ["{}={}".format(tup[0], utils.crop(utils.to_str(tup[1], force_string=True), width=10)) for tup in prototype.get("attrs", [])] return attrs @@ -1837,7 +1977,7 @@ class OLCMenu(EvMenu): """ olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype", - "save prototype", "load prototype", "spawn prototype") + "save prototype", "load prototype", "spawn prototype", "search objects") olc_options = [] other_options = [] for key, desc in optionlist: @@ -1878,6 +2018,7 @@ def start_olc(caller, session=None, prototype=None): menudata = {"node_index": node_index, "node_validate_prototype": node_validate_prototype, "node_examine_entity": node_examine_entity, + "node_search_object": node_search_object, "node_prototype_key": node_prototype_key, "node_prototype_parent": node_prototype_parent, "node_typeclass": node_typeclass, diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 4c0a2d3186..8ce20d5311 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -150,7 +150,8 @@ def value_to_obj_or_any(value): stype = type(value) if is_iter(value): if stype == dict: - return {value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.items()} + return {value_to_obj_or_any(key): + value_to_obj_or_any(val) for key, val in value.items()} else: return stype([value_to_obj_or_any(val) for val in value]) obj = dbid_to_obj(value, ObjectDB) @@ -165,18 +166,70 @@ def prototype_to_str(prototype): prototype (dict): The prototype. """ - header = ( - "|cprototype key:|n {}, |ctags:|n {}, |clocks:|n {} \n" - "|cdesc:|n {} \n|cprototype:|n ".format( - prototype.get('prototype_key', None), - ", ".join(prototype.get('prototype_tags', ['None'])), - prototype.get('prototype_locks', None), - prototype.get('prototype_desc', None))) - proto = ("{{\n {} \n}}".format( - "\n ".join( - "{!r}: {!r},".format(key, value) for key, value in - sorted(prototype.items()) if key not in _PROTOTYPE_META_NAMES)).rstrip(",")) - return header + proto + header = """ +|cprototype-key:|n {prototype_key}, |c-tags:|n {prototype_tags}, |c-locks:|n {prototype_locks}|n +|c-desc|n: {prototype_desc} +|cprototype-parent:|n {prototype_parent} + \n""".format( + prototype_key=prototype.get('prototype_key', '|r[UNSET](required)|n'), + prototype_tags=prototype.get('prototype_tags', '|wNone|n'), + prototype_locks=prototype.get('prototype_locks', '|wNone|n'), + prototype_desc=prototype.get('prototype_desc', '|wNone|n'), + prototype_parent=prototype.get('prototype_parent', '|wNone|n')) + + key = prototype.get('key', '') + if key: + key = "|ckey:|n {key}".format(key=key) + aliases = prototype.get("aliases", '') + if aliases: + aliases = "|caliases:|n {aliases}".format( + aliases=", ".join(aliases)) + attrs = prototype.get("attrs", '') + if attrs: + out = [] + for (attrkey, value, category, locks) in attrs: + locks = ", ".join(lock for lock in locks if lock) + category = "|ccategory:|n {}".format(category) if category else '' + cat_locks = "" + if category or locks: + cat_locks = "(|ccategory:|n {category}, ".format( + category=category if category else "|wNone|n") + out.append( + "{attrkey} " + "{cat_locks}\n" + " |c=|n {value}".format( + attrkey=attrkey, + cat_locks=cat_locks, + locks=locks if locks else "|wNone|n", + value=value)) + attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out)) + tags = prototype.get('tags', '') + if tags: + out = [] + for (tagkey, category, data) in tags: + out.append("{tagkey} (category: {category}{dat})".format( + tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else "")) + tags = "|ctags:|n\n {tags}".format(tags="\n ".join(out)) + locks = prototype.get('locks', '') + if locks: + locks = "|clocks:|n\n {locks}".format(locks="\n ".join(locks.split(";"))) + permissions = prototype.get("permissions", '') + if permissions: + permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions)) + location = prototype.get("location", '') + if location: + location = "|clocation:|n {location}".format(location=location) + home = prototype.get("home", '') + if home: + home = "|chome:|n {home}".format(home=home) + destination = prototype.get("destination", '') + if destination: + destination = "|cdestination:|n {destination}".format(destination=destination) + + body = "\n".join(part for part in (key, aliases, attrs, tags, locks, permissions, + location, home, destination) if part) + + return header.lstrip() + body.strip() def check_permission(prototype_key, action, default=True): diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 3dd8e11d67..1bae219368 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -150,6 +150,8 @@ def _get_prototype(dic, prot, protparents): for infinite recursion here. """ + # we don't overload the prototype_key + prototype_key = prot.get('prototype_key', None) if "prototype_parent" in dic: # move backwards through the inheritance for prototype in make_iter(dic["prototype_parent"]): @@ -157,6 +159,7 @@ def _get_prototype(dic, prot, protparents): new_prot = _get_prototype(protparents.get(prototype.lower(), {}), prot, protparents) prot.update(new_prot) prot.update(dic) + prot['prototype_key'] = prototype_key prot.pop("prototype_parent", None) # we don't need this anymore return prot @@ -217,19 +220,19 @@ def prototype_from_object(obj): location = obj.db_location if location: - prot['location'] = location + prot['location'] = location.dbref home = obj.db_home if home: - prot['home'] = home + prot['home'] = home.dbref destination = obj.db_destination if destination: - prot['destination'] = destination + prot['destination'] = destination.dbref locks = obj.locks.all() if locks: - prot['locks'] = locks + prot['locks'] = ";".join(locks) perms = obj.permissions.get() if perms: - prot['permissions'] = perms + prot['permissions'] = make_iter(perms) aliases = obj.aliases.get() if aliases: prot['aliases'] = aliases diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index d21aec2c56..a2c7429d0d 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1055,7 +1055,10 @@ def list_node(option_generator, select=None, pagesize=10): else: if callable(select): try: - return select(caller, selection) + if bool(getargspec(select).keywords): + return select(caller, selection, available_choices=available_choices) + else: + return select(caller, selection) except Exception: logger.log_trace() elif select: @@ -1124,7 +1127,7 @@ def list_node(option_generator, select=None, pagesize=10): except Exception: logger.log_trace() else: - if isinstance(decorated_options, {}): + if isinstance(decorated_options, dict): decorated_options = [decorated_options] else: decorated_options = make_iter(decorated_options) From 0a73c731570151e1344844563610fcdd075327c5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 28 Jul 2018 19:58:20 +0200 Subject: [PATCH 091/103] Complete refactoring of main nodes. Remain spawn/load/save --- evennia/prototypes/menus.py | 216 +++++++++++++++++++++++++++++------- evennia/prototypes/tests.py | 4 + evennia/utils/evmenu.py | 11 +- 3 files changed, 191 insertions(+), 40 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index ab66363c71..9024223c31 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -174,7 +174,7 @@ def _set_property(caller, raw_string, **kwargs): return next_node -def _wizard_options(curr_node, prev_node, next_node, color="|W"): +def _wizard_options(curr_node, prev_node, next_node, color="|W", search=False): """Creates default navigation options available in the wizard.""" options = [] if prev_node: @@ -195,6 +195,9 @@ def _wizard_options(curr_node, prev_node, next_node, color="|W"): if curr_node: options.append({"key": ("|wV|Walidate prototype", "validate", "v"), "goto": ("node_validate_prototype", {"back": curr_node})}) + if search: + options.append({"key": ("|wSE|Warch objects", "search object", "search", "se"), + "goto": ("node_search_object", {"back": curr_node})}) return options @@ -1495,10 +1498,11 @@ def node_permissions(caller): def node_location(caller): text = """ - The |cLocation|n of this object in the world. If not given, the object will spawn - in the inventory of |c{caller}|n instead. + The |cLocation|n of this object in the world. If not given, the object will spawn in the + inventory of |c{caller}|n by default. {current} + """.format(caller=caller.key, current=_get_current_value(caller, "location")) helptext = """ @@ -1508,11 +1512,11 @@ def node_location(caller): |c$protfuncs|n {pfuncs} - """.format(pfuncs=_format_protfuncs) + """.format(pfuncs=_format_protfuncs()) text = (text, helptext) - options = _wizard_options("location", "permissions", "home") + options = _wizard_options("location", "permissions", "home", search=True) options.append({"key": "_default", "goto": (_set_property, dict(prop="location", @@ -1529,20 +1533,27 @@ def node_home(caller): text = """ The |cHome|n location of an object is often only used as a backup - this is where the object will be moved to if its location is deleted. The home location can also be used as an actual - home for characters to quickly move back to. If unset, the global home default will be used. + home for characters to quickly move back to. + + If unset, the global home default (|w{default}|n) will be used. {current} - """.format(current=_get_current_value(caller, "home")) + """.format(default=settings.DEFAULT_HOME, + current=_get_current_value(caller, "home")) helptext = """ - The location can be specified as as #dbref but can also be explicitly searched for using - $obj(name). + The home can be given as a #dbref but can also be specified using the protfunc + '$obj(name)'. Use |wSE|nearch to find objects in the database. - The home location is often not used except as a backup. It should never be unset. - """ + The home location is commonly not used except as a backup; using the global default is often + enough. + + |c$protfuncs|n + {pfuncs} + """.format(pfuncs=_format_protfuncs()) text = (text, helptext) - options = _wizard_options("home", "aliases", "destination") + options = _wizard_options("home", "location", "destination", search=True) options.append({"key": "_default", "goto": (_set_property, dict(prop="home", @@ -1557,20 +1568,23 @@ def node_home(caller): def node_destination(caller): text = """ - The object's |cDestination|n is usually only set for Exit-like objects and designates where + The object's |cDestination|n is generally only used by Exit-like objects to designate where the exit 'leads to'. It's usually unset for all other types of objects. {current} """.format(current=_get_current_value(caller, "destination")) helptext = """ - The destination can be given as a #dbref but can also be explicitly searched for using - $obj(name). - """ + The destination can be given as a #dbref but can also be specified using the protfunc + '$obj(name)'. Use |wSEearch to find objects in the database. + + |c$protfuncs|n + {pfuncs} + """.format(pfuncs=_format_protfuncs()) text = (text, helptext) - options = _wizard_options("destination", "home", "prototype_desc") + options = _wizard_options("destination", "home", "prototype_desc", search=True) options.append({"key": "_default", "goto": (_set_property, dict(prop="dest", @@ -1585,8 +1599,7 @@ def node_destination(caller): def node_prototype_desc(caller): text = """ - The |cPrototype-Description|n optionally briefly describes the prototype when it's viewed in - listings. + The |cPrototype-Description|n briefly describes the prototype when it's viewed in listings. {current} """.format(current=_get_current_value(caller, "prototype_desc")) @@ -1602,7 +1615,7 @@ def node_prototype_desc(caller): "goto": (_set_property, dict(prop='prototype_desc', processor=lambda s: s.strip(), - next_node="node_prototype_tags"))}) + next_node="node_prototype_desc"))}) return text, options @@ -1610,14 +1623,87 @@ def node_prototype_desc(caller): # prototype_tags node +def _caller_prototype_tags(caller): + prototype = _get_menu_prototype(caller) + tags = prototype.get("prototype_tags", []) + return tags + + +def _add_prototype_tag(caller, tag_string, **kwargs): + """ + Add prototype_tags to the system. We only support straight tags, no + categories (category is assigned automatically). + + Args: + caller (Object): Caller of menu. + tag_string (str): Input from user - only tagname + + Kwargs: + delete (str): If this is set, tag_string is considered + the name of the tag to delete. + + Returns: + result (str): Result string of action. + + """ + tag = tag_string.strip().lower() + + if tag: + prot = _get_menu_prototype(caller) + tags = prot.get('prototype_tags', []) + exists = tag in tags + + if 'delete' in kwargs: + if exists: + tags.pop(tags.index(tag)) + text = "Removed Prototype-Tag '{}'.".format(tag) + else: + text = "Found no Prototype-Tag to remove." + elif not exists: + # a fresh, new tag + tags.append(tag) + text = "Added Prototype-Tag '{}'.".format(tag) + else: + text = "Prototype-Tag already added." + + _set_prototype_value(caller, "prototype_tags", tags) + else: + text = "No Prototype-Tag specified." + + return text + + +def _prototype_tag_select(caller, tagname): + caller.msg("Prototype-Tag: {}".format(tagname)) + return "node_prototype_tags" + + +def _prototype_tags_actions(caller, raw_inp, **kwargs): + """Parse actions for tags listing""" + choices = kwargs.get("available_choices", []) + tagname, action = _default_parse( + raw_inp, choices, ('remove', 'r', 'delete', 'd')) + + if tagname: + if action == 'remove': + res = _add_prototype_tag(caller, tagname, delete=True) + caller.msg(res) + else: + res = _add_prototype_tag(caller, raw_inp.lower().strip()) + caller.msg(res) + return "node_prototype_tags" + + +@list_node(_caller_prototype_tags, _prototype_tag_select) def node_prototype_tags(caller): text = """ |cPrototype-Tags|n can be used to classify and find prototypes in listings Tag names are not - case-sensitive and can have not have a custom category. Separate multiple tags by commas. + case-sensitive and can have not have a custom category. - {current} - """.format(current=_get_current_value(caller, "prototype_tags")) + {actions} + """.format(actions=_format_list_actions( + "remove", prefix="|w|n|W to add Tag. Other Action:|n ")) helptext = """ Using prototype-tags is a good way to organize and group large numbers of prototypes by genre, type etc. Under the hood, prototypes' tags will all be stored with the category @@ -1628,17 +1714,73 @@ def node_prototype_tags(caller): options = _wizard_options("prototype_tags", "prototype_desc", "prototype_locks") options.append({"key": "_default", - "goto": (_set_property, - dict(prop="prototype_tags", - processor=lambda s: [ - str(part.strip().lower()) for part in s.split(",")], - next_node="node_prototype_locks"))}) + "goto": _prototype_tags_actions}) + return text, options # prototype_locks node +def _caller_prototype_locks(caller): + locks = _get_menu_prototype(caller).get("prototype_locks", "") + return [lck for lck in locks.split(";") if lck] + + +def _prototype_lock_select(caller, lockstr): + return "node_examine_entity", {"text": _locks_display(caller, lockstr), "back": "prototype_locks"} + + +def _prototype_lock_add(caller, lock, **kwargs): + locks = _caller_prototype_locks(caller) + + try: + locktype, lockdef = lock.split(":", 1) + except ValueError: + return "Lockstring lacks ':'." + + locktype = locktype.strip().lower() + + if 'delete' in kwargs: + try: + ind = locks.index(lock) + locks.pop(ind) + _set_prototype_value(caller, "prototype_locks", ";".join(locks), parse=False) + ret = "Prototype-lock {} deleted.".format(lock) + except ValueError: + ret = "No Prototype-lock found to delete." + return ret + try: + locktypes = [lck.split(":", 1)[0].strip().lower() for lck in locks] + ind = locktypes.index(locktype) + locks[ind] = lock + ret = "Prototype-lock with locktype '{}' updated.".format(locktype) + except ValueError: + locks.append(lock) + ret = "Added Prototype-lock '{}'.".format(lock) + _set_prototype_value(caller, "prototype_locks", ";".join(locks)) + return ret + + +def _prototype_locks_actions(caller, raw_inp, **kwargs): + choices = kwargs.get("available_choices", []) + lock, action = _default_parse( + raw_inp, choices, ("examine", "e"), ("remove", "r", "delete", "d")) + + if lock: + if action == 'examine': + return "node_examine_entity", {"text": _locks_display(caller, lock), "back": "locks"} + elif action == 'remove': + ret = _prototype_lock_add(caller, lock.strip(), delete=True) + caller.msg(ret) + else: + ret = _prototype_lock_add(caller, raw_inp.strip()) + caller.msg(ret) + + return "node_prototype_locks" + + +@list_node(_caller_prototype_locks, _prototype_lock_select) def node_prototype_locks(caller): text = """ @@ -1650,25 +1792,23 @@ def node_prototype_locks(caller): - 'edit': Who can edit the prototype. - 'spawn': Who can spawn new objects with this prototype. - If unsure, leave as default. + If unsure, keep the open defaults. - {current} - """.format(current=_get_current_value(caller, "prototype_locks")) + {actions} + """.format(actions=_format_list_actions('examine', "remove", prefix="Actions: ")) helptext = """ - Prototype locks can be used when there are different tiers of builders or for developers to - produce 'base prototypes' only meant for builders to inherit and expand on rather than - change. + Prototype locks can be used to vary access for different tiers of builders. It also allows + developers to produce 'base prototypes' only meant for builders to inherit and expand on + rather than tweak in-place. """ text = (text, helptext) options = _wizard_options("prototype_locks", "prototype_tags", "index") options.append({"key": "_default", - "goto": (_set_property, - dict(prop="prototype_locks", - processor=lambda s: s.strip().lower(), - next_node="node_index"))}) + "goto": _prototype_locks_actions}) + return text, options diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 92b8e85a65..9da6ef44bc 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -482,6 +482,10 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._add_perm(caller, "foo2"), "Added Permission 'foo2'") self.assertEqual(olc_menus._get_menu_prototype(caller)["permissions"], ["foo", "foo2"]) + # prototype_tags helpers + self.assertEqual(olc_menus._add_prototype_tag(caller, "foo"), "Added Tag 'foo'.") + self.assertEqual(olc_menus._add_prototype_tag(caller, "foo2"), "Added Tag 'foo2'.") + self.assertEqual(olc_menus._get_menu_prototype(caller)["prototype_tags"], ["foo", "foo2"]) # spawn helpers with mock.patch("evennia.prototypes.menus.protlib.search_prototype", diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index a2c7429d0d..078ddf89c6 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1117,11 +1117,18 @@ def list_node(option_generator, select=None, pagesize=10): # add data from the decorated node decorated_options = [] + supports_kwargs = bool(getargspec(func).keywords) try: - text, decorated_options = func(caller, raw_string) + if supports_kwargs: + text, decorated_options = func(caller, raw_string, **kwargs) + else: + text, decorated_options = func(caller, raw_string) except TypeError: try: - text, decorated_options = func(caller) + if supports_kwargs: + text, decorated_options = func(caller, **kwargs) + else: + text, decorated_options = func(caller) except Exception: raise except Exception: From 8b211ee249e0b767ddb6b68249a1dca58b47db43 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Jul 2018 10:51:52 +0200 Subject: [PATCH 092/103] Limit current view for certain fields in olc --- evennia/prototypes/menus.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 9024223c31..5c4bb200a2 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -269,11 +269,13 @@ def _format_list_actions(*args, **kwargs): return prefix + "|W,|n ".join(actions) -def _get_current_value(caller, keyname, formatter=str): +def _get_current_value(caller, keyname, formatter=str, only_inherit=False): "Return current value, marking if value comes from parent or set in this prototype" prot = _get_menu_prototype(caller) if keyname in prot: # value in current prot + if only_inherit: + return '' return "Current {}: {}".format(keyname, formatter(prot[keyname])) flat_prot = _get_flat_menu_prototype(caller) if keyname in flat_prot: @@ -282,7 +284,11 @@ def _get_current_value(caller, keyname, formatter=str): # we don't inherit prototype_keys return "[No prototype_key set] (|rnot inherited|n)" else: - return "Current {} (|binherited|n): {}".format(keyname, formatter(flat_prot[keyname])) + ret = "Current {} (|binherited|n): {}".format(keyname, formatter(flat_prot[keyname])) + if only_inherit: + return "{}\n\n".format(ret) + return ret + return "[No {} set]".format(keyname) @@ -930,7 +936,7 @@ def node_aliases(caller): |cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not case sensitive. - {actions} + {current}{actions} """.format(actions=_format_list_actions("remove", prefix="|w|W to add new alias. Other action: ")) helptext = """ From 5c84b1c4067190d2763d9d7ea8b84c0e136f7149 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Jul 2018 11:57:16 +0200 Subject: [PATCH 093/103] Refactor locale stepping in olc --- evennia/prototypes/menus.py | 40 +++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 5c4bb200a2..3a3a4c4ff3 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -131,7 +131,7 @@ def _set_property(caller, raw_string, **kwargs): """ prop = kwargs.get("prop", "prototype_key") processor = kwargs.get("processor", None) - next_node = kwargs.get("next_node", "node_index") + next_node = kwargs.get("next_node", None) if callable(processor): try: @@ -346,6 +346,8 @@ def node_validate_prototype(caller, raw_string, **kwargs): return text, options +# node examine_entity + def node_examine_entity(caller, raw_string, **kwargs): """ General node to view a text and then return to previous node. Kwargs should contain "text" for @@ -364,6 +366,8 @@ def node_examine_entity(caller, raw_string, **kwargs): return text, options +# node object_search + def _search_object(caller): "update search term based on query stored on menu; store match too" try: @@ -399,7 +403,7 @@ def _search_object(caller): return ["{}(#{})".format(obj.key, obj.id) for obj in results] -def _object_select(caller, obj_entry, **kwargs): +def _object_search_select(caller, obj_entry, **kwargs): choices = kwargs['available_choices'] num = choices.index(obj_entry) matches = caller.ndb._menutree.olc_search_object_matches @@ -415,12 +419,14 @@ def _object_select(caller, obj_entry, **kwargs): return "node_examine_entity", {"text": txt, "back": "search_object"} -def _object_actions(caller, raw_inp, **kwargs): +def _object_search_actions(caller, raw_inp, **kwargs): "All this does is to queue a search query" choices = kwargs['available_choices'] obj_entry, action = _default_parse( raw_inp, choices, ("examine", "e"), ("create prototype from object", "create", "c")) + raw_inp = raw_inp.strip() + if obj_entry: num = choices.index(obj_entry) @@ -448,12 +454,16 @@ def _object_actions(caller, raw_inp, **kwargs): _set_menu_prototype(caller, prot) caller.msg("Created prototype from object.") return "node_index" - else: + elif raw_inp: caller.ndb._menutree.olc_search_object_term = raw_inp return "node_search_object", kwargs + else: + # empty input - exit back to previous node + prev_node = "node_" + kwargs.get("back", "index") + return prev_node -@list_node(_search_object, _object_select) +@list_node(_search_object, _object_search_select) def node_search_object(caller, raw_inp, **kwargs): """ Node for searching for an existing object. @@ -491,7 +501,7 @@ def node_search_object(caller, raw_inp, **kwargs): options = _wizard_options(None, prev_node, None) options.append({"key": "_default", - "goto": (_object_actions, {"back": prev_node})}) + "goto": (_object_search_actions, {"back": prev_node})}) return text, options @@ -601,7 +611,7 @@ def _check_prototype_key(caller, key): caller.msg("Prototype already exists. Reloading.") return "node_index" - return _set_property(caller, key, prop='prototype_key', next_node="node_prototype_parent") + return _set_property(caller, key, prop='prototype_key') def node_prototype_key(caller): @@ -814,7 +824,7 @@ def _typeclass_actions(caller, raw_inp, **kwargs): def _typeclass_select(caller, typeclass): """Select typeclass from list and add it to prototype. Return next node to go to.""" - ret = _set_property(caller, typeclass, prop='typeclass', processor=str, next_node="node_key") + ret = _set_property(caller, typeclass, prop='typeclass', processor=str) caller.msg("Selected typeclass |c{}|n.".format(typeclass)) return ret @@ -874,8 +884,7 @@ def node_key(caller): options.append({"key": "_default", "goto": (_set_property, dict(prop="key", - processor=lambda s: s.strip(), - next_node="node_aliases"))}) + processor=lambda s: s.strip()))}) return text, options @@ -936,7 +945,7 @@ def node_aliases(caller): |cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not case sensitive. - {current}{actions} + {actions} """.format(actions=_format_list_actions("remove", prefix="|w|W to add new alias. Other action: ")) helptext = """ @@ -1526,8 +1535,7 @@ def node_location(caller): options.append({"key": "_default", "goto": (_set_property, dict(prop="location", - processor=lambda s: s.strip(), - next_node="node_home"))}) + processor=lambda s: s.strip()))}) return text, options @@ -1563,8 +1571,7 @@ def node_home(caller): options.append({"key": "_default", "goto": (_set_property, dict(prop="home", - processor=lambda s: s.strip(), - next_node="node_destination"))}) + processor=lambda s: s.strip()))}) return text, options @@ -1594,8 +1601,7 @@ def node_destination(caller): options.append({"key": "_default", "goto": (_set_property, dict(prop="dest", - processor=lambda s: s.strip(), - next_node="node_prototype_desc"))}) + processor=lambda s: s.strip()))}) return text, options From 39c6eaf8dec73bfda1893c91acf727169c1beb6c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Jul 2018 12:06:11 +0200 Subject: [PATCH 094/103] Fix destination setting in olc --- evennia/prototypes/menus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 3a3a4c4ff3..fb6543c70b 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -1600,7 +1600,7 @@ def node_destination(caller): options = _wizard_options("destination", "home", "prototype_desc", search=True) options.append({"key": "_default", "goto": (_set_property, - dict(prop="dest", + dict(prop="destination", processor=lambda s: s.strip()))}) return text, options From 7f8bd983f03638603905998b3a57c409acabfde8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Jul 2018 16:53:54 +0200 Subject: [PATCH 095/103] Refactor spawn, update remaining in olc --- evennia/prototypes/menus.py | 183 +++++++++++++++++++++++----------- evennia/prototypes/spawner.py | 12 ++- evennia/utils/evmenu.py | 5 +- 3 files changed, 135 insertions(+), 65 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index fb6543c70b..8574e944cf 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -1874,10 +1874,25 @@ def node_update_objects(caller, **kwargs): diff, obj_prototype = spawner.prototype_diff_from_object(prototype, obj) text = ["Suggested changes to {} objects. ".format(len(update_objects)), - "Showing random example obj to change: {name} (#{dbref}))\n".format(obj.key, obj.dbref)] - options = [] + "Showing random example obj to change: {name} ({dbref}))\n".format( + name=obj.key, dbref=obj.dbref)] + + helptext = """ + Be careful with this operation! The upgrade mechanism will try to automatically estimate + what changes need to be applied. But the estimate is |wonly based on the analysis of one + randomly selected object|n among all objects spawned by this prototype. If that object + happens to be unusual in some way the estimate will be off and may lead to unexpected + results for other objects. Always test your objects carefully after an upgrade and + consider being conservative (switch to KEEP) or even do the update manually if you are + unsure that the results will be acceptable. """ + + options = _wizard_options("update_objects", back_node[5:], None) io = 0 for (key, inst) in sorted(((key, val) for key, val in diff.items()), key=lambda tup: tup[0]): + + if key in protlib._PROTOTYPE_META_NAMES: + continue + line = "{iopt} |w{key}|n: {old}{sep}{new} {change}" old_val = utils.crop(str(obj_prototype[key]), width=20) @@ -1907,18 +1922,11 @@ def node_update_objects(caller, **kwargs): {"key": "|wb|rack ({})".format(back_node[5:], 'b'), "goto": back_node}]) - helptext = """ - Be careful with this operation! The upgrade mechanism will try to automatically estimate - what changes need to be applied. But the estimate is |wonly based on the analysis of one - randomly selected object|n among all objects spawned by this prototype. If that object - happens to be unusual in some way the estimate will be off and may lead to unexpected - results for other objects. Always test your objects carefully after an upgrade and - consider being conservative (switch to KEEP) or even do the update manually if you are - unsure that the results will be acceptable. """ + text = "\n".join(text) - text = (text, helptext) + text = (text, helptext) - return text, options + return text, options # prototype save node @@ -1928,7 +1936,8 @@ def node_prototype_save(caller, **kwargs): """Save prototype to disk """ # these are only set if we selected 'yes' to save on a previous pass prototype = kwargs.get("prototype", None) - accept_save = kwargs.get("accept_save", False) + # set to True/False if answered, None if first pass + accept_save = kwargs.get("accept_save", None) if accept_save and prototype: # we already validated and accepted the save, so this node acts as a goto callback and @@ -1939,22 +1948,38 @@ def node_prototype_save(caller, **kwargs): spawned_objects = protlib.search_objects_with_prototype(prototype_key) nspawned = spawned_objects.count() + text = ["|gPrototype saved.|n"] + if nspawned: - text = ("Do you want to update {} object(s) " - "already using this prototype?".format(nspawned)) + text.append("\nDo you want to update {} object(s) " + "already using this prototype?".format(nspawned)) options = ( {"key": ("|wY|Wes|n", "yes", "y"), + "desc": "Go to updating screen", "goto": ("node_update_objects", {"accept_update": True, "objects": spawned_objects, "prototype": prototype, "back_node": "node_prototype_save"})}, {"key": ("[|wN|Wo|n]", "n"), - "goto": "node_spawn"}, + "desc": "Return to index", + "goto": "node_index"}, {"key": "_default", - "goto": "node_spawn"}) + "goto": "node_index"}) else: - text = "|gPrototype saved.|n" + text.append("(press Return to continue)") options = {"key": "_default", - "goto": "node_spawn"} + "goto": "node_index"} + + text = "\n".join(text) + + helptext = """ + Updating objects means that the spawner will find all objects previously created by this + prototype. You will be presented with a list of the changes the system will try to apply to + each of these objects and you can choose to customize that change if needed. If you have + done a lot of manual changes to your objects after spawning, you might want to update those + objects manually instead. + """ + + text = (text, helptext) return text, options @@ -1967,27 +1992,19 @@ def node_prototype_save(caller, **kwargs): if error: # abort save text.append( - "Validation errors were found. They need to be corrected before this prototype " - "can be saved (or used to spawn).") - options = _wizard_options("prototype_save", "prototype_locks", "index") + "\n|yValidation errors were found. They need to be corrected before this prototype " + "can be saved (or used to spawn).|n") + options = _wizard_options("prototype_save", "index", None) return "\n".join(text), options prototype_key = prototype['prototype_key'] if protlib.search_prototype(prototype_key): - text.append("Do you want to save/overwrite the existing prototype '{name}'?".format( + text.append("\nDo you want to save/overwrite the existing prototype '{name}'?".format( name=prototype_key)) else: - text.append("Do you want to save the prototype as '{name}'?".format(prototype_key)) + text.append("\nDo you want to save the prototype as '{name}'?".format(name=prototype_key)) - options = ( - {"key": ("[|wY|Wes|n]", "yes", "y"), - "goto": ("node_prototype_save", - {"accept": True, "prototype": prototype})}, - {"key": ("|wN|Wo|n", "n"), - "goto": "node_spawn"}, - {"key": "_default", - "goto": ("node_prototype_save", - {"accept": True, "prototype": prototype})}) + text = "\n".join(text) helptext = """ Saving the prototype makes it available for use later. It can also be used to inherit from, @@ -1999,6 +2016,18 @@ def node_prototype_save(caller, **kwargs): text = (text, helptext) + options = ( + {"key": ("[|wY|Wes|n]", "yes", "y"), + "desc": "Save prototype", + "goto": ("node_prototype_save", + {"accept_save": True, "prototype": prototype})}, + {"key": ("|wN|Wo|n", "n"), + "desc": "Abort and return to Index", + "goto": "node_index"}, + {"key": "_default", + "goto": ("node_prototype_save", + {"accept_save": True, "prototype": prototype})}) + return text, options @@ -2015,26 +2044,42 @@ def _spawn(caller, **kwargs): obj = spawner.spawn(prototype) if obj: obj = obj[0] - caller.msg("|gNew instance|n {key} ({dbref}) |gspawned.|n".format( - key=obj.key, dbref=obj.dbref)) + text = "|gNew instance|n {key} ({dbref}) |gspawned.|n".format( + key=obj.key, dbref=obj.dbref) else: - caller.msg("|rError: Spawner did not return a new instance.|n") - return obj + text = "|rError: Spawner did not return a new instance.|n" + return "node_examine_entity", {"text": text, "back": "prototype_spawn"} def node_prototype_spawn(caller, **kwargs): """Submenu for spawning the prototype""" prototype = _get_menu_prototype(caller) - error, text = _validate_prototype(prototype) - text = [text] + already_validated = kwargs.get("already_validated", False) + + if already_validated: + error, text = None, [] + else: + error, text = _validate_prototype(prototype) + text = [text] if error: - text.append("|rPrototype validation failed. Correct the errors before spawning.|n") - options = _wizard_options("prototype_spawn", "prototype_locks", "index") + text.append("\n|rPrototype validation failed. Correct the errors before spawning.|n") + options = _wizard_options("prototype_spawn", "index", None) return "\n".join(text), options + text = "\n".join(text) + + helptext = """ + Spawning is the act of instantiating a prototype into an actual object. As a new object is + spawned, every $protfunc in the prototype is called anew. Since this is a common thing to + do, you may also temporarily change the |clocation|n of this prototype to bypass whatever + value is set in the prototype. + + """ + text = (text, helptext) + # show spawn submenu options options = [] prototype_key = prototype['prototype_key'] @@ -2064,18 +2109,10 @@ def node_prototype_spawn(caller, **kwargs): options.append( {"desc": "Update {num} existing objects with this prototype".format(num=nspawned), "goto": ("node_update_objects", - dict(prototype=prototype, opjects=spawned_objects, - back_node="node_prototype_spawn"))}) - options.extend(_wizard_options("prototype_spawn", "prototype_save", "index")) - - helptext = """ - Spawning is the act of instantiating a prototype into an actual object. As a new object is - spawned, every $protfunc in the prototype is called anew. Since this is a common thing to - do, you may also temporarily change the |clocation|n of this prototype to bypass whatever - value is set in the prototype. - - """ - text = (text, helptext) + {"objects": list(spawned_objects), + "prototype": prototype, + "back_node": "node_prototype_spawn"})}) + options.extend(_wizard_options("prototype_spawn", "index", None)) return text, options @@ -2088,30 +2125,56 @@ def _prototype_load_select(caller, prototype_key): if matches: prototype = matches[0] _set_menu_prototype(caller, prototype) - caller.msg("|gLoaded prototype '{}'.".format(prototype_key)) - return "node_index" + return "node_examine_entity", \ + {"text": "|gLoaded prototype {}.|n".format(prototype['prototype_key']), + "back": "index"} else: caller.msg("|rFailed to load prototype '{}'.".format(prototype_key)) return None +def _prototype_load_actions(caller, raw_inp, **kwargs): + """Parse the default Convert prototype to a string representation for closer inspection""" + choices = kwargs.get("available_choices", []) + prototype, action = _default_parse( + raw_inp, choices, ("examine", "e", "l")) + + if prototype: + # a selection of parent was made + prototype = protlib.search_prototype(key=prototype)[0] + + # which action to apply on the selection + if action == 'examine': + # examine the prototype + txt = protlib.prototype_to_str(prototype) + kwargs['text'] = txt + kwargs['back'] = 'prototype_load' + return "node_examine_entity", kwargs + + return 'node_prototype_load' + + @list_node(_all_prototype_parents, _prototype_load_select) def node_prototype_load(caller, **kwargs): """Load prototype""" text = """ Select a prototype to load. This will replace any prototype currently being edited! - """ + + {actions} + """.format(actions=_format_list_actions("examine")) + helptext = """ - Loading a prototype will load it and return you to the main index. It can be a good idea to - examine the prototype before loading it. + Loading a prototype will load it and return you to the main index. It can be a good idea + to examine the prototype before loading it. """ text = (text, helptext) - options = _wizard_options("prototype_load", "prototype_save", "index") + options = _wizard_options("prototype_load", "index", None) options.append({"key": "_default", - "goto": _prototype_parent_actions}) + "goto": _prototype_load_actions}) + return text, options diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 1bae219368..f250287c8f 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -553,7 +553,9 @@ def spawn(*prototypes, **kwargs): alias_string = init_spawn_value(val, make_iter) val = prot.pop("tags", []) - tags = init_spawn_value(val, make_iter) + tags = [] + for (tag, category, data) in tags: + tags.append((init_spawn_value(val, str), category, data)) prototype_key = prototype.get('prototype_key', None) if prototype_key: @@ -567,9 +569,11 @@ def spawn(*prototypes, **kwargs): nattributes = dict((key.split("_", 1)[1], init_spawn_value(val, value_to_obj)) for key, val in prot.items() if key.startswith("ndb_")) - # the rest are attributes - val = prot.pop("attrs", []) - attributes = init_spawn_value(val, list) + # the rest are attribute tuples (attrname, value, category, locks) + val = make_iter(prot.pop("attrs", [])) + attributes = [] + for (attrname, value, category, locks) in val: + attributes.append((attrname, init_spawn_value(val), category, locks)) simple_attributes = [] for key, value in ((key, value) for key, value in prot.items() diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 078ddf89c6..9941c81b11 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -938,7 +938,7 @@ class EvMenu(object): for key, desc in optionlist: if not (key or desc): continue - desc_string = ": %s" % (desc if desc else "") + desc_string = ": %s" % desc if desc else "" table_width_max = max(table_width_max, max(m_len(p) for p in key.split("\n")) + max(m_len(p) for p in desc_string.split("\n")) + colsep) @@ -1140,9 +1140,12 @@ def list_node(option_generator, select=None, pagesize=10): decorated_options = make_iter(decorated_options) extra_options = [] + if isinstance(decorated_options, dict): + decorated_options = [decorated_options] for eopt in decorated_options: cback = ("goto" in eopt and "goto") or ("exec" in eopt and "exec") or None if cback: + print("eopt, cback: {} {}".format(eopt, cback)) signature = eopt[cback] if callable(signature): # callable with no kwargs defined From 5cca160989bcc91ebd8861fec475b0ad32cdfab6 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 29 Jul 2018 20:58:56 +0200 Subject: [PATCH 096/103] Further cleanup and debugging of olc menu --- evennia/prototypes/menus.py | 99 ++++++++++++++++++++------------ evennia/prototypes/prototypes.py | 12 ++-- evennia/utils/evmenu.py | 1 - 3 files changed, 69 insertions(+), 43 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 8574e944cf..22a07903c3 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -518,14 +518,11 @@ def node_index(caller): can be hard-coded or scripted using |w$protfuncs|n - for example to randomize the value every time the prototype is used to spawn a new entity. - The prototype fields named 'prototype_*' are not used to create the entity itself but for - organizing the template when saving it for you (and maybe others) to use later. + The prototype fields whose names start with 'Prototype-' are not fields on the object itself + but are used in the template and when saving it for you (and maybe others) to use later. + Select prototype field to edit. If you are unsure, start from [|w1|n]. Enter [|wh|n]elp at + any menu node for more info. - Select prototype field to edit. If you are unsure, start from [|w1|n]. At any time you can - [|wV|n]alidate that the prototype works correctly and use it to [|wSP|n]awn a new entity. You - can also [|wSA|n]ve|n your work or [|wLO|n]oad an existing prototype to use as a base. Use - [|wL|n]ook to re-show a menu node. [|wQ|n]uit will always exit the menu and [|wH|n]elp will - show context-sensitive help. """ helptxt = """ |c- prototypes |n @@ -537,6 +534,13 @@ def node_index(caller): to spawn goblins with different names, looks, equipment and skill, each based on the same `Goblin` typeclass. + At any time you can [|wV|n]alidate that the prototype works correctly and use it to + [|wSP|n]awn a new entity. You can also [|wSA|n]ve|n your work, [|wLO|n]oad an existing + prototype to [|wSE|n]arch for existing objects to use as a base. Use [|wL|n]ook to re-show a + menu node. [|wQ|n]uit will always exit the menu and [|wH|n]elp will show context-sensitive + help. + + |c- $protfuncs |n Prototype-functions (protfuncs) allow for limited scripting within a prototype. These are @@ -553,11 +557,11 @@ def node_index(caller): {"desc": "|WPrototype-Key|n|n{}".format( _format_option_value("Key", "prototype_key" not in prototype, prototype, None)), "goto": "node_prototype_key"}) - for key in ('Prototype_parent', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', + for key in ('Prototype_Parent', 'Typeclass', 'Key', 'Aliases', 'Attrs', 'Tags', 'Locks', 'Permissions', 'Location', 'Home', 'Destination'): required = False cropper = None - if key in ("Prototype_parent", "Typeclass"): + if key in ("Prototype_Parent", "Typeclass"): required = ("prototype_parent" not in prototype) and ("typeclass" not in prototype) if key == 'Typeclass': cropper = _path_cropper @@ -1827,7 +1831,7 @@ def node_prototype_locks(caller): # update existing objects node -def _update_spawned(caller, **kwargs): +def _apply_diff(caller, **kwargs): """update existing objects""" prototype = kwargs['prototype'] objects = kwargs['objects'] @@ -1844,7 +1848,7 @@ def _keep_diff(caller, **kwargs): diff[key] = "KEEP" -def node_update_objects(caller, **kwargs): +def node_apply_diff(caller, **kwargs): """Offer options for updating objects""" def _keep_option(keyname, prototype, obj, obj_prototype, diff, objects, back_node): @@ -1886,8 +1890,9 @@ def node_update_objects(caller, **kwargs): consider being conservative (switch to KEEP) or even do the update manually if you are unsure that the results will be acceptable. """ - options = _wizard_options("update_objects", back_node[5:], None) - io = 0 + options = [] + + ichanges = 0 for (key, inst) in sorted(((key, val) for key, val in diff.items()), key=lambda tup: tup[0]): if key in protlib._PROTOTYPE_META_NAMES: @@ -1897,30 +1902,40 @@ def node_update_objects(caller, **kwargs): old_val = utils.crop(str(obj_prototype[key]), width=20) if inst == "KEEP": - text.append(line.format(iopt='', key=key, old=old_val, sep=" ", new='', change=inst)) + inst = "|b{}|n".format(inst) + text.append(line.format(iopt='', key=key, old=old_val, + sep=" ", new='', change=inst)) continue new_val = utils.crop(str(spawner.init_spawn_value(prototype[key])), width=20) - io += 1 + ichanges += 1 if inst in ("UPDATE", "REPLACE"): - text.append(line.format(iopt=io, key=key, old=old_val, + inst = "|y{}|n".format(inst) + text.append(line.format(iopt=ichanges, key=key, old=old_val, sep=" |y->|n ", new=new_val, change=inst)) options.append(_keep_option(key, prototype, obj, obj_prototype, diff, update_objects, back_node)) elif inst == "REMOVE": - text.append(line.format(iopt=io, key=key, old=old_val, + inst = "|r{}|n".format(inst) + text.append(line.format(iopt=ichanges, key=key, old=old_val, sep=" |r->|n ", new='', change=inst)) options.append(_keep_option(key, prototype, obj, obj_prototype, diff, update_objects, back_node)) options.extend( - [{"key": ("|wu|r update {} objects".format(len(update_objects)), "update", "u"), - "goto": (_update_spawned, {"prototype": prototype, "objects": update_objects, - "back_node": back_node, "diff": diff})}, - {"key": ("|wr|neset changes", "reset", "r"), - "goto": ("node_update_objects", {"prototype": prototype, "back_node": back_node, - "objects": update_objects})}, - {"key": "|wb|rack ({})".format(back_node[5:], 'b'), - "goto": back_node}]) + [{"key": ("|wu|Wupdate {} objects".format(len(update_objects)), "update", "u"), + "goto": (_apply_diff, {"prototye": prototype, "objects": update_objects, + "back_node": back_node, "diff": diff})}, + {"key": ("|wr|Wneset changes", "reset", "r"), + "goto": ("node_apply_diff", {"prototype": prototype, "back_node": back_node, + "objects": update_objects})}]) + + if ichanges < 1: + text = ["Analyzed a random sample object (out of {}) - " + "found no changes to apply.".format(len(update_objects))] + + options.extend(_wizard_options("update_objects", back_node[5:], None)) + options.append({"key": "_default", + "goto": back_node}) text = "\n".join(text) @@ -1956,7 +1971,7 @@ def node_prototype_save(caller, **kwargs): options = ( {"key": ("|wY|Wes|n", "yes", "y"), "desc": "Go to updating screen", - "goto": ("node_update_objects", + "goto": ("node_apply_diff", {"accept_update": True, "objects": spawned_objects, "prototype": prototype, "back_node": "node_prototype_save"})}, {"key": ("[|wN|Wo|n]", "n"), @@ -1995,6 +2010,8 @@ def node_prototype_save(caller, **kwargs): "\n|yValidation errors were found. They need to be corrected before this prototype " "can be saved (or used to spawn).|n") options = _wizard_options("prototype_save", "index", None) + options.append({"key": "_default", + "goto": "node_index"}) return "\n".join(text), options prototype_key = prototype['prototype_key'] @@ -2044,8 +2061,8 @@ def _spawn(caller, **kwargs): obj = spawner.spawn(prototype) if obj: obj = obj[0] - text = "|gNew instance|n {key} ({dbref}) |gspawned.|n".format( - key=obj.key, dbref=obj.dbref) + text = "|gNew instance|n {key} ({dbref}) |gspawned at location |n{loc}|n|g.|n".format( + key=obj.key, dbref=obj.dbref, loc=prototype['location']) else: text = "|rError: Spawner did not return a new instance.|n" return "node_examine_entity", {"text": text, "back": "prototype_spawn"} @@ -2108,11 +2125,13 @@ def node_prototype_spawn(caller, **kwargs): if spawned_objects: options.append( {"desc": "Update {num} existing objects with this prototype".format(num=nspawned), - "goto": ("node_update_objects", + "goto": ("node_apply_diff", {"objects": list(spawned_objects), "prototype": prototype, "back_node": "node_prototype_spawn"})}) options.extend(_wizard_options("prototype_spawn", "index", None)) + options.append({"key": "_default", + "goto": "node_index"}) return text, options @@ -2137,19 +2156,25 @@ def _prototype_load_actions(caller, raw_inp, **kwargs): """Parse the default Convert prototype to a string representation for closer inspection""" choices = kwargs.get("available_choices", []) prototype, action = _default_parse( - raw_inp, choices, ("examine", "e", "l")) + raw_inp, choices, ("examine", "e", "l"), ("delete", "del", "d")) if prototype: - # a selection of parent was made - prototype = protlib.search_prototype(key=prototype)[0] # which action to apply on the selection if action == 'examine': # examine the prototype + prototype = protlib.search_prototype(key=prototype)[0] txt = protlib.prototype_to_str(prototype) - kwargs['text'] = txt - kwargs['back'] = 'prototype_load' - return "node_examine_entity", kwargs + return "node_examine_entity", {"text": txt, "back": 'prototype_load'} + elif action == 'delete': + # delete prototype from disk + try: + protlib.delete_prototype(prototype, caller=caller) + except protlib.PermissionError as err: + txt = "|rDeletion error:|n {}".format(err) + else: + txt = "|gPrototype {} was deleted.|n".format(prototype) + return "node_examine_entity", {"text": txt, "back": "prototype_load"} return 'node_prototype_load' @@ -2162,7 +2187,7 @@ def node_prototype_load(caller, **kwargs): Select a prototype to load. This will replace any prototype currently being edited! {actions} - """.format(actions=_format_list_actions("examine")) + """.format(actions=_format_list_actions("examine", "delete")) helptext = """ Loading a prototype will load it and return you to the main index. It can be a good idea @@ -2246,7 +2271,7 @@ def start_olc(caller, session=None, prototype=None): "node_location": node_location, "node_home": node_home, "node_destination": node_destination, - "node_update_objects": node_update_objects, + "node_apply_diff": node_apply_diff, "node_prototype_desc": node_prototype_desc, "node_prototype_tags": node_prototype_tags, "node_prototype_locks": node_prototype_locks, diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 8ce20d5311..4c53ed7d1c 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -297,10 +297,10 @@ def init_spawn_value(value, validator=None): 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 = [(prototype_key, prot) for prototype_key, prot in all_from_module(mod).items() + prots = [(prototype_key.lower(), prot) for prototype_key, prot in all_from_module(mod).items() if prot and isinstance(prot, dict)] # assign module path to each prototype_key for easy reference - _MODULE_PROTOTYPE_MODULES.update({prototype_key: mod for prototype_key, _ in prots}) + _MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots}) # make sure the prototype contains all meta info for prototype_key, prot in prots: actual_prot_key = prot.get('prototype_key', prototype_key).lower() @@ -409,7 +409,7 @@ def save_prototype(**kwargs): create_prototype = save_prototype -def delete_prototype(key, caller=None): +def delete_prototype(prototype_key, caller=None): """ Delete a stored prototype @@ -424,14 +424,16 @@ def delete_prototype(key, caller=None): """ if prototype_key in _MODULE_PROTOTYPES: - mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A") + mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key.lower(), "N/A") raise PermissionError("{} is a read-only prototype " "(defined as code in {}).".format(prototype_key, mod)) - stored_prototype = DbPrototype.objects.filter(db_key=prototype_key) + stored_prototype = DbPrototype.objects.filter(db_key__iexact=prototype_key) if not stored_prototype: raise PermissionError("Prototype {} was not found.".format(prototype_key)) + + stored_prototype = stored_prototype[0] if caller: if not stored_prototype.access(caller, 'edit'): raise PermissionError("{} does not have permission to " diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 9941c81b11..638f4eef6e 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1145,7 +1145,6 @@ def list_node(option_generator, select=None, pagesize=10): for eopt in decorated_options: cback = ("goto" in eopt and "goto") or ("exec" in eopt and "exec") or None if cback: - print("eopt, cback: {} {}".format(eopt, cback)) signature = eopt[cback] if callable(signature): # callable with no kwargs defined From 44a2540341fbd83fe6ee2bece2e2a18529e8f7ab Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 30 Jul 2018 17:38:59 +0200 Subject: [PATCH 097/103] Fix attr assignmen issue in olc menu --- evennia/objects/objects.py | 1 + evennia/prototypes/menus.py | 5 ++++- evennia/prototypes/tests.py | 25 +++++++++++++------------ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 8ec1433dcd..d42f20c9ae 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1753,6 +1753,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): else: msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self msg_location = msg_location or '{object} says, "{speech}"' + msg_receivers = msg_receivers or message custom_mapping = kwargs.get('mapping', {}) receivers = make_iter(receivers) if receivers else None diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 22a07903c3..1141f27536 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -1023,6 +1023,7 @@ def _add_attr(caller, attr_string, **kwargs): result (str): Result string of action. """ attrname = '' + value = '' category = None locks = '' @@ -1097,7 +1098,7 @@ def _attrs_actions(caller, raw_inp, **kwargs): attrname = attrname.strip() attr_tup = _get_tup_by_attrname(caller, attrname) - if attr_tup: + if action and attr_tup: if action == 'examine': return "node_examine_entity", \ {"text": _display_attribute(attr_tup), "back": "attrs"} @@ -2057,6 +2058,8 @@ def _spawn(caller, **kwargs): new_location = kwargs.get('location', None) if new_location: prototype['location'] = new_location + if not prototype.get('location'): + prototype['location'] = caller obj = spawner.spawn(prototype) if obj: diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 9da6ef44bc..9fb47585c9 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -399,11 +399,9 @@ class TestMenuModule(EvenniaTest): with mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(return_value=[self.test_prot])): # prototype_key helpers - self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), - "node_prototype_parent") + self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), None) caller.ndb._menutree.olc_new = True - self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), - "node_index") + self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), "node_index") # prototype_parent helpers self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot']) @@ -429,7 +427,7 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._all_typeclasses(caller), ["bar", "foo"]) self.assertEqual(olc_menus._typeclass_select( - caller, "evennia.objects.objects.DefaultObject"), "node_key") + caller, "evennia.objects.objects.DefaultObject"), None) # prototype_parent should be popped off here self.assertEqual(olc_menus._get_menu_prototype(caller), {'prototype_key': 'test_prot', @@ -444,8 +442,9 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._add_attr(caller, "test3;cat2;edit:false()=foo3"), Something) self.assertEqual(olc_menus._add_attr(caller, "test4;cat3;set:true();edit:false()=foo4"), Something) self.assertEqual(olc_menus._add_attr(caller, "test5;cat4;set:true();edit:false()=123"), Something) + self.assertEqual(olc_menus._add_attr(caller, "test1=foo1_changed"), Something) self.assertEqual(olc_menus._get_menu_prototype(caller)['attrs'], - [("test1", "foo1", None, ''), + [("test1", "foo1_changed", None, ''), ("test2", "foo2", "cat1", ''), ("test3", "foo3", "cat2", "edit:false()"), ("test4", "foo4", "cat3", "set:true();edit:false()"), @@ -483,27 +482,29 @@ class TestMenuModule(EvenniaTest): self.assertEqual(olc_menus._get_menu_prototype(caller)["permissions"], ["foo", "foo2"]) # prototype_tags helpers - self.assertEqual(olc_menus._add_prototype_tag(caller, "foo"), "Added Tag 'foo'.") - self.assertEqual(olc_menus._add_prototype_tag(caller, "foo2"), "Added Tag 'foo2'.") + self.assertEqual(olc_menus._add_prototype_tag(caller, "foo"), "Added Prototype-Tag 'foo'.") + self.assertEqual(olc_menus._add_prototype_tag(caller, "foo2"), "Added Prototype-Tag 'foo2'.") self.assertEqual(olc_menus._get_menu_prototype(caller)["prototype_tags"], ["foo", "foo2"]) # spawn helpers with mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])): - obj = olc_menus._spawn(caller, prototype=self.test_prot) + self.assertEqual(olc_menus._spawn(caller, prototype=self.test_prot), Something) + obj = caller.contents[0] self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject") self.assertEqual(obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot['prototype_key']) # update helpers - self.assertEqual(olc_menus._update_spawned( + self.assertEqual(olc_menus._apply_diff( caller, prototype=self.test_prot, back_node="foo", objects=[obj]), 'foo') # no changes to apply self.test_prot['key'] = "updated key" # change prototype - self.assertEqual(olc_menus._update_spawned( + self.assertEqual(olc_menus._apply_diff( caller, prototype=self.test_prot, objects=[obj], back_node='foo'), 'foo') # apply change to the one obj # load helpers - self.assertEqual(olc_menus._prototype_load_select(caller, self.test_prot['prototype_key']), "node_index") + self.assertEqual(olc_menus._prototype_load_select(caller, self.test_prot['prototype_key']), + ('node_examine_entity', {'text': '|gLoaded prototype test_prot.|n', 'back': 'index'}) ) @mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock( From 994a5fd6184e448bafbb842068315649a092d555 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 30 Jul 2018 20:19:46 +0200 Subject: [PATCH 098/103] Correct unittests --- evennia/prototypes/tests.py | 62 +++++++++---------------------------- 1 file changed, 15 insertions(+), 47 deletions(-) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 9fb47585c9..71956efb91 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -74,7 +74,8 @@ class TestUtils(EvenniaTest): 'home': Something, 'key': 'Obj', 'location': Something, - 'locks': ['call:true()', + 'locks': ";".join([ + 'call:true()', 'control:perm(Developer)', 'delete:perm(Admin)', 'edit:perm(Admin)', @@ -82,7 +83,7 @@ class TestUtils(EvenniaTest): 'get:all()', 'puppet:pperm(Developer)', 'tell:perm(Admin)', - 'view:all()'], + 'view:all()']), 'prototype_desc': 'Built from Obj', 'prototype_key': Something, 'prototype_locks': 'spawn:all();edit:all()', @@ -132,15 +133,16 @@ class TestUtils(EvenniaTest): ('test', 'testval', None, [''])], 'prototype_locks': 'spawn:all();edit:all()', 'prototype_key': Something, - 'locks': ['call:true()', 'control:perm(Developer)', + 'locks': ";".join([ + 'call:true()', 'control:perm(Developer)', 'delete:perm(Admin)', 'edit:perm(Admin)', 'examine:perm(Builder)', 'get:all()', 'puppet:pperm(Developer)', 'tell:perm(Admin)', - 'view:all()'], + 'view:all()']), 'prototype_tags': [], - 'location': self.room1, + 'location': "#1", 'key': 'NewObj', - 'home': self.room1, + 'home': '#1', 'typeclass': 'evennia.objects.objects.DefaultObject', 'prototype_desc': 'Built from NewObj', 'aliases': 'foo'}) @@ -157,7 +159,8 @@ class TestUtils(EvenniaTest): 'home': Something, 'key': 'Obj', 'location': Something, - 'locks': ['call:true()', + 'locks': ";".join([ + 'call:true()', 'control:perm(Developer)', 'delete:perm(Admin)', 'edit:perm(Admin)', @@ -165,8 +168,8 @@ class TestUtils(EvenniaTest): 'get:all()', 'puppet:pperm(Developer)', 'tell:perm(Admin)', - 'view:all()'], - 'permissions': 'builder', + 'view:all()']), + 'permissions': ['builder'], 'prototype_desc': 'Built from Obj', 'prototype_key': Something, 'prototype_locks': 'spawn:all();edit:all()', @@ -184,7 +187,8 @@ class TestProtLib(EvenniaTest): def test_prototype_to_str(self): prstr = protlib.prototype_to_str(self.prot) - self.assertTrue(prstr.startswith("|cprototype key:|n")) + print("prst: {}".format(prstr)) + self.assertTrue(prstr.startswith("|cprototype-key:|n")) def test_check_permission(self): pass @@ -525,40 +529,4 @@ class TestOLCMenu(TestEvMenu): "node_index": "|c --- Prototype wizard --- |n" } - expected_tree = \ - ['node_index', - ['node_prototype_key', - ['node_index', - 'node_index', - 'node_validate_prototype', - ['node_index'], - 'node_index'], - 'node_typeclass', - ['node_key', - ['node_typeclass', - 'node_key', - 'node_index', - 'node_validate_prototype', - 'node_validate_prototype'], - 'node_index', - 'node_index', - 'node_index', - 'node_validate_prototype', - 'node_validate_prototype'], - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype', - 'node_validate_prototype']] + expected_tree = ['node_index', ['node_prototype_key', ['node_index', 'node_index', 'node_validate_prototype', ['node_index', 'node_index'], 'node_index'], 'node_prototype_parent', ['node_prototype_parent', 'node_prototype_key', 'node_prototype_parent', 'node_index', 'node_validate_prototype', 'node_index'], 'node_typeclass', ['node_typeclass', 'node_prototype_parent', 'node_typeclass', 'node_index', 'node_validate_prototype', 'node_index'], 'node_key', ['node_typeclass', 'node_key', 'node_index', 'node_validate_prototype', 'node_index'], 'node_aliases', ['node_key', 'node_aliases', 'node_index', 'node_validate_prototype', 'node_index'], 'node_attrs', ['node_aliases', 'node_attrs', 'node_index', 'node_validate_prototype', 'node_index'], 'node_tags', ['node_attrs', 'node_tags', 'node_index', 'node_validate_prototype', 'node_index'], 'node_locks', ['node_tags', 'node_locks', 'node_index', 'node_validate_prototype', 'node_index'], 'node_permissions', ['node_locks', 'node_permissions', 'node_index', 'node_validate_prototype', 'node_index'], 'node_location', ['node_permissions', 'node_location', 'node_index', 'node_validate_prototype', 'node_index', 'node_index'], 'node_home', ['node_location', 'node_home', 'node_index', 'node_validate_prototype', 'node_index', 'node_index'], 'node_destination', ['node_home', 'node_destination', 'node_index', 'node_validate_prototype', 'node_index', 'node_index'], 'node_prototype_desc', ['node_prototype_key', 'node_prototype_parent', 'node_index', 'node_validate_prototype', 'node_index'], 'node_prototype_tags', ['node_prototype_desc', 'node_prototype_tags', 'node_index', 'node_validate_prototype', 'node_index'], 'node_prototype_locks', ['node_examine_entity', ['node_prototype_locks', 'node_prototype_locks', 'node_prototype_locks'], 'node_examine_entity', 'node_prototype_locks', 'node_index', 'node_validate_prototype', 'node_index'], 'node_validate_prototype', 'node_index', 'node_prototype_spawn', ['node_index', 'node_validate_prototype'], 'node_index', 'node_search_object', ['node_index', 'node_index']]] From 5c88edcd71d73218afd9f55006a3e934ee66b8a2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 31 Jul 2018 11:48:18 +0200 Subject: [PATCH 099/103] Cleanup menu style --- evennia/prototypes/menus.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 1141f27536..37911d7010 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -845,7 +845,8 @@ def node_typeclass(caller): {actions} """.format(current=_get_current_value(caller, "typeclass"), - actions=_format_list_actions("examine", "remove")) + actions="|WSelect with |w|W. Other actions: " + "|we|Wxamine |w|W, |wr|Wemove selection") helptext = """ A |nTypeclass|n is specified by the actual python-path to the class definition in the From 6e887758509632873040e84215b5b5b685905b81 Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 10 Aug 2018 10:13:05 +0200 Subject: [PATCH 100/103] Create columnize (no ansi support at this point) --- evennia/accounts/manager.py | 1 - evennia/game_template/typeclasses/accounts.py | 1 - evennia/prototypes/menus.py | 29 ++++++-- evennia/utils/utils.py | 72 ++++++++++++++++++- 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/evennia/accounts/manager.py b/evennia/accounts/manager.py index c612cf930d..5d9bda2ab9 100644 --- a/evennia/accounts/manager.py +++ b/evennia/accounts/manager.py @@ -35,7 +35,6 @@ class AccountDBManager(TypedObjectManager, UserManager): get_account_from_uid get_account_from_name account_search (equivalent to evennia.search_account) - #swap_character """ diff --git a/evennia/game_template/typeclasses/accounts.py b/evennia/game_template/typeclasses/accounts.py index bbab3d4f22..99d861bf0b 100644 --- a/evennia/game_template/typeclasses/accounts.py +++ b/evennia/game_template/typeclasses/accounts.py @@ -65,7 +65,6 @@ class Account(DefaultAccount): * Helper methods msg(text=None, **kwargs) - swap_character(new_character, delete_old_character=False) execute_cmd(raw_string, session=None) search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False) is_typeclass(typeclass, exact=False) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 37911d7010..8e88cad13c 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -57,6 +57,18 @@ def _get_flat_menu_prototype(caller, refresh=False, validate=False): return flat_prototype +def _get_unchanged_inherited(caller, protname): + """Return prototype values inherited from parent(s), which are not replaced in child""" + protototype = _get_menu_prototype(caller) + if protname in prototype: + return protname[protname], False + else: + flattened = _get_flat_menu_prototype(caller) + if protname in flattened: + return protname[protname], True + return None, False + + def _set_menu_prototype(caller, prototype): """Set the prototype with existing one""" caller.ndb._menutree.olc_prototype = prototype @@ -515,11 +527,11 @@ def node_index(caller): |c --- Prototype wizard --- |n A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype - can be hard-coded or scripted using |w$protfuncs|n - for example to randomize the value - every time the prototype is used to spawn a new entity. + can either be hard-coded, left empty or scripted using |w$protfuncs|n - for example to + randomize the value every time a new entity is spawned. The fields whose names start with + 'Prototype-' are not fields on the object itself but are used for prototype-inheritance, or + when saving and loading. - The prototype fields whose names start with 'Prototype-' are not fields on the object itself - but are used in the template and when saving it for you (and maybe others) to use later. Select prototype field to edit. If you are unsure, start from [|w1|n]. Enter [|wh|n]elp at any menu node for more info. @@ -544,8 +556,8 @@ def node_index(caller): |c- $protfuncs |n Prototype-functions (protfuncs) allow for limited scripting within a prototype. These are - entered as a string $funcname(arg, arg, ...) and are evaluated |wat the time of spawning|n only. - They can also be nested for combined effects. + entered as a string $funcname(arg, arg, ...) and are evaluated |wat the time of spawning|n + only. They can also be nested for combined effects. {pfuncs} """.format(pfuncs=_format_protfuncs()) @@ -951,7 +963,10 @@ def node_aliases(caller): case sensitive. {actions} - """.format(actions=_format_list_actions("remove", prefix="|w|W to add new alias. Other action: ")) + {current} + """.format(actions=_format_list_actions("remove", + prefix="|w|W to add new alias. Other action: "), + current) helptext = """ Aliases are fixed alternative identifiers and are stored with the new object. diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 3d07a82e9a..60d5c160d6 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -7,6 +7,7 @@ be of use when designing your own game. """ from __future__ import division, print_function +import itertools from builtins import object, range from future.utils import viewkeys, raise_ @@ -33,6 +34,7 @@ _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE _EVENNIA_DIR = settings.EVENNIA_DIR _GAME_DIR = settings.GAME_DIR + try: import cPickle as pickle except ImportError: @@ -210,18 +212,27 @@ def justify(text, width=None, align="f", indent=0): gap = " " # minimum gap between words if line_rest > 0: if align == 'l': - line[-1] += " " * line_rest + if line[-1] == "\n\n": + line[-1] = " " * (line_rest-1) + "\n" + " " * width + "\n" + " " * width + else: + line[-1] += " " * line_rest elif align == 'r': line[0] = " " * line_rest + line[0] elif align == 'c': pad = " " * (line_rest // 2) line[0] = pad + line[0] - line[-1] = line[-1] + pad + " " * (line_rest % 2) + if line[-1] == "\n\n": + line[-1] = line[-1] + pad + " " * (line_rest % 2) + else: + line[-1] = pad + " " * (line_rest % 2 - 1) + \ + "\n" + " " * width + "\n" + " " * width else: # align 'f' gap += " " * (line_rest // max(1, ngaps)) rest_gap = line_rest % max(1, ngaps) for i in range(rest_gap): line[i] += " " + elif not any(line): + return [" " * width] return gap.join(line) # split into paragraphs and words @@ -262,6 +273,62 @@ def justify(text, width=None, align="f", indent=0): return "\n".join([indentstring + line for line in lines]) +def columnize(string, columns=2, spacing=4, align='l', width=None): + """ + Break a string into a number of columns, using as little + vertical space as possible. + + Args: + string (str): The string to columnize. + columns (int, optional): The number of columns to use. + spacing (int, optional): How much space to have between columns. + width (int, optional): The max width of the columns. + Defaults to client's default width. + + Returns: + columns (str): Text divided into columns. + + Raises: + RuntimeError: If given invalid values. + + """ + columns = max(1, columns) + spacing = max(1, spacing) + width = width if width else settings.CLIENT_DEFAULT_WIDTH + + w_spaces = (columns - 1) * spacing + w_txt = max(1, width - w_spaces) + + if w_spaces + columns > width: # require at least 1 char per column + raise RuntimeError("Width too small to fit columns") + + colwidth = int(w_txt / (1.0 * columns)) + + # first make a single column which we then split + onecol = justify(string, width=colwidth, align=align) + onecol = onecol.split("\n") + + nrows, dangling = divmod(len(onecol), columns) + nrows = [nrows + 1 if i < dangling else nrows for i in range(columns)] + + height = max(nrows) + cols = [] + istart = 0 + for irows in nrows: + cols.append(onecol[istart:istart+irows]) + istart = istart + irows + for col in cols: + if len(col) < height: + col.append(" " * colwidth) + + sep = " " * spacing + rows = [] + for irow in range(height): + rows.append(sep.join(col[irow] for col in cols)) + + return "\n".join(rows) + + def list_to_string(inlist, endsep="and", addquote=False): """ This pretty-formats a list as string output, adding an optional @@ -1548,6 +1615,7 @@ def format_table(table, extra_space=1): Examples: ```python + ftable = format_table([[...], [...], ...]) for ir, row in enumarate(ftable): if ir == 0: # make first row white From c48868be1e59935958dc9fe01dffc17a6f64bdd5 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 11 Aug 2018 11:49:10 +0200 Subject: [PATCH 101/103] Cleanup/refactoring of olc menus --- CHANGELOG.md | 15 ++ evennia/prototypes/menus.py | 243 +++++++++++++++++++++---------- evennia/prototypes/prototypes.py | 18 +-- evennia/utils/evmenu.py | 7 +- evennia/utils/evmore.py | 10 +- evennia/utils/utils.py | 15 +- 6 files changed, 216 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae990b85b..37ff5bfef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,23 @@ - A `goto` option callable returning None (rather than the name of the next node) will now rerun the current node instead of failing. - Better error handling of in-node syntax errors. +- Improve dedent of default text/helptext formatter. Right-strip whitespace. +### Utils + +- Added new `columnize` function for easily splitting text into multiple columns. At this point it + is not working too well with ansi-colored text however. +- Extend the `dedent` function with a new `baseline_index` kwarg. This allows to force all lines to + the indentation given by the given line regardless of if other lines were already a 0 indentation. + This removes a problem with the original `textwrap.dedent` which will only dedent to the least + indented part of a text. +- Added `exit_cmd` to EvMore pager, to allow for calling a command (e.g. 'look') when leaving the pager. + +### Genaral + +- Start structuring the `CHANGELOG` to list features in more detail. + # Overviews diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 8e88cad13c..c44cf0d5e2 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -214,6 +214,10 @@ def _wizard_options(curr_node, prev_node, next_node, color="|W", search=False): return options +def _set_actioninfo(caller, string): + caller.ndb._menutree.actioninfo = string + + def _path_cropper(pythonpath): "Crop path to only the last component" return pythonpath.split('.')[-1] @@ -278,30 +282,65 @@ def _format_list_actions(*args, **kwargs): prefix = kwargs.get('prefix', "|WSelect with |w|W. Other actions:|n ") for action in args: actions.append("|w{}|n|W{} |w|n".format(action[0], action[1:])) - return prefix + "|W,|n ".join(actions) + return prefix + " |W|||n ".join(actions) -def _get_current_value(caller, keyname, formatter=str, only_inherit=False): - "Return current value, marking if value comes from parent or set in this prototype" - prot = _get_menu_prototype(caller) - if keyname in prot: - # value in current prot +def _get_current_value(caller, keyname, comparer=None, formatter=str, only_inherit=False): + """ + Return current value, marking if value comes from parent or set in this prototype. + + Args: + keyname (str): Name of prototoype key to get current value of. + comparer (callable, optional): This will be called as comparer(prototype_value, + flattened_value) and is expected to return the value to show as the current + or inherited one. If not given, a straight comparison is used and what is returned + depends on the only_inherit setting. + formatter (callable, optional)): This will be called with the result of comparer. + only_inherit (bool, optional): If a current value should only be shown if all + the values are inherited from the prototype parent (otherwise, show an empty string). + Returns: + current (str): The current value. + + """ + def _default_comparer(protval, flatval): if only_inherit: - return '' - return "Current {}: {}".format(keyname, formatter(prot[keyname])) - flat_prot = _get_flat_menu_prototype(caller) - if keyname in flat_prot: - # value in flattened prot - if keyname == 'prototype_key': - # we don't inherit prototype_keys - return "[No prototype_key set] (|rnot inherited|n)" + return "" if protval else flatval else: - ret = "Current {} (|binherited|n): {}".format(keyname, formatter(flat_prot[keyname])) - if only_inherit: - return "{}\n\n".format(ret) - return ret + return protval if protval else flatval - return "[No {} set]".format(keyname) + if not callable(comparer): + comparer = _default_comparer + + prot = _get_menu_prototype(caller) + flat_prot = _get_flat_menu_prototype(caller) + + out = "" + if keyname in prot: + if keyname in flat_prot: + out = formatter(comparer(prot[keyname], flat_prot[keyname])) + if only_inherit: + if out: + return "|WCurrent|n {} |W(|binherited|W):|n {}".format(keyname, out) + return "" + else: + if out: + return "|WCurrent|n {}|W:|n {}".format(keyname, out) + return "|W[No {} set]|n".format(keyname) + elif only_inherit: + return "" + else: + out = formatter(prot[keyname]) + return "|WCurrent|n {}|W:|n {}".format(keyname, out) + elif keyname in flat_prot: + out = formatter(flat_prot[keyname]) + if out: + return "|WCurrent|n {} |W(|n|binherited|W):|n {}".format(keyname, out) + else: + return "" + elif only_inherit: + return "" + else: + return "|W[No {} set]|n".format(keyname) def _default_parse(raw_inp, choices, *args): @@ -491,10 +530,9 @@ def node_search_object(caller, raw_inp, **kwargs): text = """ Found {num} match{post}. - {actions} (|RWarning: creating a prototype will |roverwrite|r |Rthe current prototype!)|n""".format( - num=nmatches, post="es" if nmatches > 1 else "", - actions=_format_list_actions( + num=nmatches, post="es" if nmatches > 1 else "") + _set_actioninfo(caller, _format_list_actions( "examine", "create prototype from object", prefix="Actions: ")) else: text = "Enter search criterion." @@ -758,8 +796,6 @@ def node_prototype_parent(caller): parent is given, this prototype must define the typeclass (next menu node). {current} - - {actions} """ helptext = """ Prototypes can inherit from one another. Changes in the child replace any values set in a @@ -767,6 +803,8 @@ def node_prototype_parent(caller): prototype to be valid. """ + _set_actioninfo(caller, _format_list_actions("examine", "add", "remove")) + ptexts = [] if prot_parent_keys: for pkey in utils.make_iter(prot_parent_keys): @@ -782,8 +820,7 @@ def node_prototype_parent(caller): if not ptexts: ptexts.append("[No prototype_parent set]") - text = text.format(current="\n\n".join(ptexts), - actions=_format_list_actions("examine", "add", "remove")) + text = text.format(current="\n\n".join(ptexts)) text = (text, helptext) @@ -854,8 +891,6 @@ def node_typeclass(caller): one of the prototype's |cparents|n. {current} - - {actions} """.format(current=_get_current_value(caller, "typeclass"), actions="|WSelect with |w|W. Other actions: " "|we|Wxamine |w|W, |wr|Wemove selection") @@ -962,11 +997,15 @@ def node_aliases(caller): |cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not case sensitive. - {actions} {current} - """.format(actions=_format_list_actions("remove", - prefix="|w|W to add new alias. Other action: "), - current) + """.format(current=_get_current_value( + caller, 'aliases', + comparer=lambda propval, flatval: [al for al in flatval if al not in propval], + formatter=lambda lst: "\n" + ", ".join(lst), only_inherit=True)) + _set_actioninfo(caller, + _format_list_actions( + "remove", + prefix="|w|W to add new alias. Other action: ")) helptext = """ Aliases are fixed alternative identifiers and are stored with the new object. @@ -1009,14 +1048,13 @@ def _display_attribute(attr_tuple): attrkey, value, category, locks = attr_tuple value = protlib.protfunc_parser(value) typ = type(value) - out = ("|cAttribute key:|n '{attrkey}' " - "(|ccategory:|n {category}, " - "|clocks:|n {locks})\n" - "|cValue|n |W(parsed to {typ})|n:\n{value}").format( - attrkey=attrkey, - category=category if category else "|wNone|n", - locks=locks if locks else "|wNone|n", - typ=typ, value=value) + out = ("{attrkey} |c=|n {value} |W({typ}{category}{locks})|n".format( + attrkey=attrkey, + value=value, + typ=typ, + category=", category={}".format(category) if category else '', + locks=", locks={}".format(";".join(locks)) if any(locks) else '')) + return out @@ -1130,6 +1168,12 @@ def _attrs_actions(caller, raw_inp, **kwargs): @list_node(_caller_attrs, _attr_select) def node_attrs(caller): + def _currentcmp(propval, flatval): + "match by key + category" + cmp1 = [(tup[0].lower(), tup[2].lower() if tup[2] else None) for tup in propval] + return [tup for tup in flatval if (tup[0].lower(), tup[2].lower() + if tup[2] else None) not in cmp1] + text = """ |cAttributes|n are custom properties of the object. Enter attributes on one of these forms: @@ -1140,8 +1184,14 @@ def node_attrs(caller): To give an attribute without a category but with a lockstring, leave that spot empty (attrname;;lockstring=value). Attribute values can have embedded $protfuncs. - {actions} - """.format(actions=_format_list_actions("examine", "remove", prefix="Actions: ")) + {current} + """.format( + current=_get_current_value( + caller, "attrs", + comparer=_currentcmp, + formatter=lambda lst: "\n" + "\n".join(_display_attribute(tup) for tup in lst), + only_inherit=True)) + _set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: ")) helptext = """ Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types @@ -1290,6 +1340,13 @@ def _tags_actions(caller, raw_inp, **kwargs): @list_node(_caller_tags, _tag_select) def node_tags(caller): + + def _currentcmp(propval, flatval): + "match by key + category" + cmp1 = [(tup[0].lower(), tup[1].lower() if tup[2] else None) for tup in propval] + return [tup for tup in flatval if (tup[0].lower(), tup[1].lower() + if tup[1] else None) not in cmp1] + text = """ |cTags|n are used to group objects so they can quickly be found later. Enter tags on one of the following forms: @@ -1297,8 +1354,14 @@ def node_tags(caller): tagname;category tagname;category;data - {actions} - """.format(actions=_format_list_actions("examine", "remove", prefix="Actions: ")) + {current} + """.format( + current=_get_current_value( + caller, 'tags', + comparer=_currentcmp, + formatter=lambda lst: "\n" + "\n".join(_display_tag(tup) for tup in lst), + only_inherit=True)) + _set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: ")) helptext = """ Tags are shared between all objects with that tag. So the 'data' field (which is not @@ -1325,18 +1388,7 @@ def _caller_locks(caller): def _locks_display(caller, lock): - try: - locktype, lockdef = lock.split(":", 1) - except ValueError: - txt = "Malformed lock string - Missing ':'" - else: - txt = ("{lockstr}\n\n" - "|WLocktype: |w{locktype}|n\n" - "|WLock def: |w{lockdef}|n\n").format( - lockstr=lock, - locktype=locktype, - lockdef=lockdef) - return txt + return lock def _lock_select(caller, lockstr): @@ -1395,6 +1447,11 @@ def _locks_actions(caller, raw_inp, **kwargs): @list_node(_caller_locks, _lock_select) def node_locks(caller): + def _currentcmp(propval, flatval): + "match by locktype" + cmp1 = [lck.split(":", 1)[0] for lck in propval.split(';')] + return ";".join(lstr for lstr in flatval.split(';') if lstr.split(':', 1)[0] not in cmp1) + text = """ The |cLock string|n defines limitations for accessing various properties of the object once it's spawned. The string should be on one of the following forms: @@ -1402,8 +1459,15 @@ def node_locks(caller): locktype:[NOT] lockfunc(args) locktype: [NOT] lockfunc(args) [AND|OR|NOT] lockfunc(args) [AND|OR|NOT] ... - {action} - """.format(action=_format_list_actions("examine", "remove", prefix="Actions: ")) + {current}{action} + """.format( + current=_get_current_value( + caller, 'locks', + comparer=_currentcmp, + formatter=lambda lockstr: "\n".join(_locks_display(caller, lstr) + for lstr in lockstr.split(';')), + only_inherit=True), + action=_format_list_actions("examine", "remove", prefix="Actions: ")) helptext = """ Here is an example of two lock strings: @@ -1438,16 +1502,17 @@ def _caller_permissions(caller): return perms -def _display_perm(caller, permission): +def _display_perm(caller, permission, only_hierarchy=False): hierarchy = settings.PERMISSION_HIERARCHY perm_low = permission.lower() + txt = '' if perm_low in [prm.lower() for prm in hierarchy]: txt = "Permission (in hieararchy): {}".format( ", ".join( ["|w[{}]|n".format(prm) if prm.lower() == perm_low else "|W{}|n".format(prm) for prm in hierarchy])) - else: + elif not only_hierarchy: txt = "Permission: '{}'".format(permission) return txt @@ -1500,12 +1565,23 @@ def _permissions_actions(caller, raw_inp, **kwargs): @list_node(_caller_permissions, _permission_select) def node_permissions(caller): + def _currentcmp(pval, fval): + cmp1 = [perm.lower() for perm in pval] + return [perm for perm in fval if perm.lower() not in cmp1] + text = """ |cPermissions|n are simple strings used to grant access to this object. A permission is used - when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions. + when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions. Certain + permissions belong in the |cpermission hierarchy|n together with the |Wperm()|n lock + function. - {actions} - """.format(actions=_format_list_actions("examine", "remove"), prefix="Actions: ") + {current} + """.format( + current=_get_current_value( + caller, 'permissions', + comparer=_currentcmp, + formatter=lambda lst: "\n" + "\n".join(prm for prm in lst), only_inherit=True)) + _set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: ")) helptext = """ Any string can act as a permission as long as a lock is set to look for it. Depending on the @@ -1538,7 +1614,6 @@ def node_location(caller): inventory of |c{caller}|n by default. {current} - """.format(caller=caller.key, current=_get_current_value(caller, "location")) helptext = """ @@ -1734,9 +1809,13 @@ def node_prototype_tags(caller): |cPrototype-Tags|n can be used to classify and find prototypes in listings Tag names are not case-sensitive and can have not have a custom category. - {actions} - """.format(actions=_format_list_actions( - "remove", prefix="|w|n|W to add Tag. Other Action:|n ")) + {current} + """.format( + current=_get_current_value( + caller, 'prototype_tags', + formatter=lambda lst: ", ".join(tg for tg in lst), only_inherit=True)) + _set_actioninfo(caller, _format_list_actions( + "remove", prefix="|w|n|W to add Tag. Other Action:|n ")) helptext = """ Using prototype-tags is a good way to organize and group large numbers of prototypes by genre, type etc. Under the hood, prototypes' tags will all be stored with the category @@ -1827,8 +1906,14 @@ def node_prototype_locks(caller): If unsure, keep the open defaults. - {actions} - """.format(actions=_format_list_actions('examine', "remove", prefix="Actions: ")) + {current} + """.format( + current=_get_current_value( + caller, 'prototype_locks', + formatter=lambda lstring: "\n".join(_locks_display(caller, lstr) + for lstr in lstring.split(';')), + only_inherit=True)) + _set_actioninfo(caller, _format_list_actions('examine', "remove", prefix="Actions: ")) helptext = """ Prototype locks can be used to vary access for different tiers of builders. It also allows @@ -2204,9 +2289,8 @@ def node_prototype_load(caller, **kwargs): text = """ Select a prototype to load. This will replace any prototype currently being edited! - - {actions} - """.format(actions=_format_list_actions("examine", "delete")) + """ + _set_actioninfo(caller, _format_list_actions("examine", "delete")) helptext = """ Loading a prototype will load it and return you to the main index. It can be a good idea @@ -2230,6 +2314,13 @@ class OLCMenu(EvMenu): A custom EvMenu with a different formatting for the options. """ + def nodetext_formatter(self, nodetext): + """ + Format the node text itself. + + """ + return super(OLCMenu, self).nodetext_formatter(nodetext) + def options_formatter(self, optionlist): """ Split the options into two blocks - olc options and normal options @@ -2237,6 +2328,7 @@ class OLCMenu(EvMenu): """ olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype", "save prototype", "load prototype", "spawn prototype", "search objects") + actioninfo = self.actioninfo + "\n" if hasattr(self, 'actioninfo') else '' olc_options = [] other_options = [] for key, desc in optionlist: @@ -2247,7 +2339,8 @@ class OLCMenu(EvMenu): else: other_options.append((key, desc)) - olc_options = " | ".join(olc_options) + " | " + "|wQ|Wuit" if olc_options else "" + olc_options = actioninfo + \ + " |W|||n ".join(olc_options) + " |W|||n " + "|wQ|Wuit" if olc_options else "" other_options = super(OLCMenu, self).options_formatter(other_options) sep = "\n\n" if olc_options and other_options else "" @@ -2257,10 +2350,10 @@ class OLCMenu(EvMenu): """ Show help text """ - return "|c --- Help ---|n\n" + helptext + return "|c --- Help ---|n\n" + utils.dedent(helptext) def display_helptext(self): - evmore.msg(self.caller, self.helptext, session=self._session) + evmore.msg(self.caller, self.helptext, session=self._session, exit_cmd='look') def start_olc(caller, session=None, prototype=None): diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 4c53ed7d1c..0cc016300f 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -192,16 +192,14 @@ def prototype_to_str(prototype): category = "|ccategory:|n {}".format(category) if category else '' cat_locks = "" if category or locks: - cat_locks = "(|ccategory:|n {category}, ".format( + cat_locks = " (|ccategory:|n {category}, ".format( category=category if category else "|wNone|n") out.append( - "{attrkey} " - "{cat_locks}\n" - " |c=|n {value}".format( - attrkey=attrkey, - cat_locks=cat_locks, - locks=locks if locks else "|wNone|n", - value=value)) + "{attrkey}{cat_locks} |c=|n {value}".format( + attrkey=attrkey, + cat_locks=cat_locks, + locks=locks if locks else "|wNone|n", + value=value)) attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out)) tags = prototype.get('tags', '') if tags: @@ -209,10 +207,10 @@ def prototype_to_str(prototype): for (tagkey, category, data) in tags: out.append("{tagkey} (category: {category}{dat})".format( tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else "")) - tags = "|ctags:|n\n {tags}".format(tags="\n ".join(out)) + tags = "|ctags:|n\n {tags}".format(tags=", ".join(out)) locks = prototype.get('locks', '') if locks: - locks = "|clocks:|n\n {locks}".format(locks="\n ".join(locks.split(";"))) + locks = "|clocks:|n\n {locks}".format(locks=locks) permissions = prototype.get("permissions", '') if permissions: permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions)) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 638f4eef6e..0297170da2 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -167,14 +167,13 @@ from __future__ import print_function import random from builtins import object, range -from textwrap import dedent from inspect import isfunction, getargspec from django.conf import settings from evennia import Command, CmdSet from evennia.utils import logger from evennia.utils.evtable import EvTable from evennia.utils.ansi import strip_ansi -from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter +from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter, dedent from evennia.commands import cmdhandler # read from protocol NAWS later? @@ -896,7 +895,7 @@ class EvMenu(object): nodetext (str): The formatted node text. """ - return dedent(nodetext).strip() + return dedent(nodetext.strip('\n'), baseline_index=0).rstrip() def helptext_formatter(self, helptext): """ @@ -909,7 +908,7 @@ class EvMenu(object): helptext (str): The formatted help text. """ - return dedent(helptext).strip() + return dedent(helptext.strip('\n'), baseline_index=0).rstrip() def options_formatter(self, optionlist): """ diff --git a/evennia/utils/evmore.py b/evennia/utils/evmore.py index e0ec091005..94173b9eca 100644 --- a/evennia/utils/evmore.py +++ b/evennia/utils/evmore.py @@ -122,7 +122,8 @@ class EvMore(object): """ def __init__(self, caller, text, always_page=False, session=None, - justify_kwargs=None, exit_on_lastpage=False, **kwargs): + justify_kwargs=None, exit_on_lastpage=False, + exit_cmd=None, **kwargs): """ Initialization of the text handler. @@ -141,6 +142,10 @@ class EvMore(object): page being completely filled, exit pager immediately. If unset, another move forward is required to exit. If set, the pager exit message will not be shown. + exit_cmd (str, optional): If given, this command-string will be executed on + 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). kwargs (any, optional): These will be passed on to the `caller.msg` method. @@ -151,6 +156,7 @@ class EvMore(object): self._npages = [] self._npos = [] self.exit_on_lastpage = exit_on_lastpage + self.exit_cmd = exit_cmd self._exit_msg = "Exited |wmore|n pager." if not session: # if not supplied, use the first session to @@ -269,6 +275,8 @@ class EvMore(object): if not quiet: self._caller.msg(text=self._exit_msg, **self._kwargs) self._caller.cmdset.remove(CmdSetMore) + if self.exit_cmd: + self._caller.execute_cmd(self.exit_cmd, session=self._session) def msg(caller, text="", always_page=False, session=None, diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 60d5c160d6..abe7d3c1e3 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -160,12 +160,16 @@ def crop(text, width=None, suffix="[...]"): return to_str(utext) -def dedent(text): +def dedent(text, baseline_index=None): """ Safely clean all whitespace at the left of a paragraph. Args: text (str): The text to dedent. + baseline_index (int or None, optional): Which row to use as a 'base' + for the indentation. Lines will be dedented to this level but + no further. If None, indent so as to completely deindent the + least indented text. Returns: text (str): Dedented string. @@ -178,7 +182,14 @@ def dedent(text): """ if not text: return "" - return textwrap.dedent(text) + if baseline_index is None: + return textwrap.dedent(text) + else: + lines = text.split('\n') + baseline = lines[baseline_index] + spaceremove = len(baseline) - len(baseline.lstrip(' ')) + return "\n".join(line[min(spaceremove, len(line) - len(line.lstrip(' '))):] + for line in lines) def justify(text, width=None, align="f", indent=0): From 6cf6476417cef6a0434dba9ef595d84eba623f8b Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 12 Aug 2018 13:13:13 +0200 Subject: [PATCH 102/103] Fix further bugs in menu spawn --- evennia/prototypes/menus.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index c44cf0d5e2..0e4f59ffbc 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -1953,12 +1953,12 @@ def _keep_diff(caller, **kwargs): def node_apply_diff(caller, **kwargs): """Offer options for updating objects""" - def _keep_option(keyname, prototype, obj, obj_prototype, diff, objects, back_node): + def _keep_option(keyname, prototype, base_obj, obj_prototype, diff, objects, back_node): """helper returning an option dict""" options = {"desc": "Keep {} as-is".format(keyname), "goto": (_keep_diff, {"key": keyname, "prototype": prototype, - "obj": obj, "obj_prototype": obj_prototype, + "base_obj": base_obj, "obj_prototype": obj_prototype, "diff": diff, "objects": objects, "back_node": back_node})} return options @@ -1966,6 +1966,7 @@ def node_apply_diff(caller, **kwargs): update_objects = kwargs.get("objects", None) back_node = kwargs.get("back_node", "node_index") obj_prototype = kwargs.get("obj_prototype", None) + base_obj = kwargs.get("base_obj", None) diff = kwargs.get("diff", None) if not update_objects: @@ -1976,12 +1977,12 @@ def node_apply_diff(caller, **kwargs): if not diff: # use one random object as a reference to calculate a diff - obj = choice(update_objects) - diff, obj_prototype = spawner.prototype_diff_from_object(prototype, obj) + base_obj = choice(update_objects) + diff, obj_prototype = spawner.prototype_diff_from_object(prototype, base_obj) text = ["Suggested changes to {} objects. ".format(len(update_objects)), "Showing random example obj to change: {name} ({dbref}))\n".format( - name=obj.key, dbref=obj.dbref)] + name=base_obj.key, dbref=base_obj.dbref)] helptext = """ Be careful with this operation! The upgrade mechanism will try to automatically estimate @@ -2001,7 +2002,7 @@ def node_apply_diff(caller, **kwargs): continue line = "{iopt} |w{key}|n: {old}{sep}{new} {change}" - old_val = utils.crop(str(obj_prototype[key]), width=20) + old_val = str(obj_prototype.get(key, "")) if inst == "KEEP": inst = "|b{}|n".format(inst) @@ -2009,25 +2010,29 @@ def node_apply_diff(caller, **kwargs): sep=" ", new='', change=inst)) continue - new_val = utils.crop(str(spawner.init_spawn_value(prototype[key])), width=20) + if key in prototype: + new_val = str(spawner.init_spawn_value(prototype[key])) + else: + new_val = "" ichanges += 1 if inst in ("UPDATE", "REPLACE"): inst = "|y{}|n".format(inst) text.append(line.format(iopt=ichanges, key=key, old=old_val, sep=" |y->|n ", new=new_val, change=inst)) options.append(_keep_option(key, prototype, - obj, obj_prototype, diff, update_objects, back_node)) + base_obj, obj_prototype, diff, update_objects, back_node)) elif inst == "REMOVE": inst = "|r{}|n".format(inst) text.append(line.format(iopt=ichanges, key=key, old=old_val, sep=" |r->|n ", new='', change=inst)) options.append(_keep_option(key, prototype, - obj, obj_prototype, diff, update_objects, back_node)) + base_obj, obj_prototype, diff, update_objects, back_node)) options.extend( - [{"key": ("|wu|Wupdate {} objects".format(len(update_objects)), "update", "u"), - "goto": (_apply_diff, {"prototye": prototype, "objects": update_objects, - "back_node": back_node, "diff": diff})}, - {"key": ("|wr|Wneset changes", "reset", "r"), + [{"key": ("|wu|Wpdate {} objects".format(len(update_objects)), "update", "u"), + "desc": "Update {} objects".format(len(update_objects)), + "goto": (_apply_diff, {"prototype": prototype, "objects": update_objects, + "back_node": back_node, "diff": diff, "base_obj": base_obj})}, + {"key": ("|wr|Weset changes", "reset", "r"), "goto": ("node_apply_diff", {"prototype": prototype, "back_node": back_node, "objects": update_objects})}]) From 7dec566926efa6b85933b7aa8448bcbcf09666a8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 12 Aug 2018 13:37:19 +0200 Subject: [PATCH 103/103] Resolve unittests --- evennia/prototypes/tests.py | 1 - evennia/utils/utils.py | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index 71956efb91..1c77fd85c3 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -187,7 +187,6 @@ class TestProtLib(EvenniaTest): def test_prototype_to_str(self): prstr = protlib.prototype_to_str(self.prot) - print("prst: {}".format(prstr)) self.assertTrue(prstr.startswith("|cprototype-key:|n")) def test_check_permission(self): diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index abe7d3c1e3..cd6c57a21f 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -7,7 +7,6 @@ be of use when designing your own game. """ from __future__ import division, print_function -import itertools from builtins import object, range from future.utils import viewkeys, raise_ @@ -233,10 +232,10 @@ def justify(text, width=None, align="f", indent=0): pad = " " * (line_rest // 2) line[0] = pad + line[0] if line[-1] == "\n\n": - line[-1] = line[-1] + pad + " " * (line_rest % 2) - else: - line[-1] = pad + " " * (line_rest % 2 - 1) + \ + line[-1] += pad + " " * (line_rest % 2 - 1) + \ "\n" + " " * width + "\n" + " " * width + else: + line[-1] = line[-1] + pad + " " * (line_rest % 2) else: # align 'f' gap += " " * (line_rest // max(1, ngaps)) rest_gap = line_rest % max(1, ngaps)