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