Finish refactor prototypes/spawner/menus

This commit is contained in:
Griatch 2018-06-07 22:40:03 +02:00
parent 1a8651f18b
commit 55f8e58c43
4 changed files with 399 additions and 389 deletions

View file

@ -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.evmenu import EvMenu, list_node
from evennia.utils.ansi import strip_ansi 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 # Helper functions
@ -48,11 +60,11 @@ def _format_property(prop, required=False, prototype=None, cropper=None):
out = "<{}>".format(prop.__name__) out = "<{}>".format(prop.__name__)
else: else:
out = repr(prop) out = repr(prop)
if is_iter(prop): if utils.is_iter(prop):
out = ", ".join(str(pr) for pr in prop) out = ", ".join(str(pr) for pr in prop)
if not out and required: if not out and required:
out = "|rrequired" 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): def _set_property(caller, raw_string, **kwargs):
@ -166,7 +178,8 @@ def node_index(caller):
required = False required = False
for key in ('Desc', 'Tags', 'Locks'): for key in ('Desc', 'Tags', 'Locks'):
options.append( 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())}) "goto": "node_prototype_{}".format(key.lower())})
return text, options return text, options
@ -175,11 +188,11 @@ def node_index(caller):
def node_validate_prototype(caller, raw_string, **kwargs): def node_validate_prototype(caller, raw_string, **kwargs):
prototype = _get_menu_prototype(caller) 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)" errors = "\n\n|g No validation errors found.|n (but errors could still happen at spawn-time)"
try: try:
# validate, don't spawn # validate, don't spawn
spawn(prototype, return_prototypes=True) spawner.spawn(prototype, return_prototypes=True)
except RuntimeError as err: except RuntimeError as err:
errors = "\n\n|rError: {}|n".format(err) errors = "\n\n|rError: {}|n".format(err)
text = (txt + errors) text = (txt + errors)
@ -190,7 +203,7 @@ def node_validate_prototype(caller, raw_string, **kwargs):
def _check_prototype_key(caller, key): def _check_prototype_key(caller, key):
old_prototype = search_prototype(key) old_prototype = protlib.search_prototype(key)
olc_new = _is_new_prototype(caller) olc_new = _is_new_prototype(caller)
key = key.strip().lower() key = key.strip().lower()
if old_prototype: if old_prototype:
@ -231,13 +244,13 @@ def node_prototype_key(caller):
def _all_prototypes(caller): def _all_prototypes(caller):
return [prototype["prototype_key"] 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): def _prototype_examine(caller, prototype_name):
prototypes = search_prototype(key=prototype_name) prototypes = protlib.search_prototype(key=prototype_name)
if prototypes: if prototypes:
caller.msg(prototype_to_str(prototypes[0])) caller.msg(protlib.prototype_to_str(prototypes[0]))
caller.msg("Prototype not registered.") caller.msg("Prototype not registered.")
return None 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."] text = ["Set the prototype's |yParent Prototype|n. If this is unset, Typeclass will be used."]
if prot_parent_key: if prot_parent_key:
prot_parent = search_prototype(prot_parent_key) prot_parent = protlib.search_prototype(prot_parent_key)
if prot_parent: 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: else:
text.append("Current parent prototype |r{prototype}|n " text.append("Current parent prototype |r{prototype}|n "
"does not appear to exist.".format(prot_parent_key)) "does not appear to exist.".format(prot_parent_key))
@ -273,7 +287,7 @@ def node_prototype(caller):
def _all_typeclasses(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): def _typeclass_examine(caller, typeclass_path):
@ -281,7 +295,7 @@ def _typeclass_examine(caller, typeclass_path):
# this means we are exiting the listing # this means we are exiting the listing
return "node_key" return "node_key"
typeclass = get_all_typeclasses().get(typeclass_path) typeclass = utils.get_all_typeclasses().get(typeclass_path)
if typeclass: if typeclass:
docstr = [] docstr = []
for line in typeclass.__doc__.split("\n"): for line in typeclass.__doc__.split("\n"):
@ -453,8 +467,8 @@ def _add_tag(caller, tag, **kwargs):
tags.append(tag) tags.append(tag)
else: else:
tags = [tag] tags = [tag]
prot['tags'] = tags prototype['tags'] = tags
_set_menu_prototype(caller, "prototype", prot) _set_menu_prototype(caller, "prototype", prototype)
text = kwargs.get("text") text = kwargs.get("text")
if not text: if not text:
text = "Added tag {}. (return to continue)".format(tag) 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, "node_prototype_locks": node_prototype_locks,
} }
OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=prototype) OLCMenu(caller, menudata, startnode='node_index', session=session, olc_prototype=prototype)

View file

@ -6,16 +6,26 @@ Handling storage of prototypes, both database-based ones (DBPrototypes) and thos
""" """
from django.conf import settings from django.conf import settings
from evennia.scripts.scripts import DefaultScript from evennia.scripts.scripts import DefaultScript
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.utils.create import create_script 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.locks.lockhandler import validate_lockstring, check_lockstring
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.evtable import EvTable
from evennia.utils.prototypes.protfuncs import protfunc_parser
_MODULE_PROTOTYPE_MODULES = {} _MODULE_PROTOTYPE_MODULES = {}
_MODULE_PROTOTYPES = {} _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): class ValidationError(RuntimeError):
@ -25,6 +35,99 @@ class ValidationError(RuntimeError):
pass 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 # module-based prototypes
for mod in settings.PROTOTYPE_MODULES: for mod in settings.PROTOTYPE_MODULES:
@ -59,39 +162,7 @@ class DbPrototype(DefaultScript):
self.db.prototype = {} # actual prototype self.db.prototype = {} # actual prototype
# General prototype functions # Prototype manager 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): 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) 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): 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. 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(2, width=11, align='c')
table.reformat_column(3, width=16) table.reformat_column(3, width=16)
return table 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

View file

@ -126,70 +126,25 @@ from __future__ import print_function
import copy import copy
import hashlib import hashlib
import time import time
from ast import literal_eval
from django.conf import settings from django.conf import settings
from random import randint
import evennia import evennia
from random import randint
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.utils.utils import ( from evennia.utils.utils import (
make_iter, dbid_to_obj, make_iter, dbid_to_obj,
is_iter, crop, get_all_typeclasses) is_iter, get_all_typeclasses)
from evennia.prototypes import prototypes as protlib
from evennia.utils.evtable import EvTable from evennia.prototypes.prototypes import value_to_obj, value_to_obj_or_any, init_spawn_value
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks") _PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks")
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES _NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES
_MENU_CROP_WIDTH = 15
_PROTOTYPE_TAG_CATEGORY = "spawned_by_prototype" _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): def _get_prototype(dic, prot, protparents):
""" """
@ -209,6 +164,246 @@ def _get_prototype(dic, prot, protparents):
return prot 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): def spawn(*prototypes, **kwargs):
""" """
@ -234,12 +429,12 @@ def spawn(*prototypes, **kwargs):
""" """
# get available protparents # 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 # overload module's protparents with specifically given protparents
protparents.update(kwargs.get("prototype_parents", {})) protparents.update(kwargs.get("prototype_parents", {}))
for key, prototype in protparents.items(): for key, prototype in protparents.items():
validate_prototype(prototype, key.lower(), protparents) protlib.validate_prototype(prototype, key.lower(), protparents)
if "return_prototypes" in kwargs: if "return_prototypes" in kwargs:
# only return the parents # only return the parents
@ -248,7 +443,7 @@ def spawn(*prototypes, **kwargs):
objsparams = [] objsparams = []
for prototype in prototypes: for prototype in prototypes:
validate_prototype(prototype, None, protparents) protlib.validate_prototype(prototype, None, protparents)
prot = _get_prototype(prototype, {}, protparents) prot = _get_prototype(prototype, {}, protparents)
if not prot: if not prot:
continue continue
@ -260,30 +455,30 @@ def spawn(*prototypes, **kwargs):
# chance this is not unique but it should usually not be a problem. # chance this is not unique but it should usually not be a problem.
val = prot.pop("key", "Spawned-{}".format( val = prot.pop("key", "Spawned-{}".format(
hashlib.md5(str(time.time())).hexdigest()[:6])) 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) 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) 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) 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) 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 # extract calls to handlers
val = prot.pop("permissions", []) val = prot.pop("permissions", [])
permission_string = validate_spawn_value(val, make_iter) permission_string = init_spawn_value(val, make_iter)
val = prot.pop("locks", "") val = prot.pop("locks", "")
lock_string = validate_spawn_value(val, str) lock_string = init_spawn_value(val, str)
val = prot.pop("aliases", []) val = prot.pop("aliases", [])
alias_string = validate_spawn_value(val, make_iter) alias_string = init_spawn_value(val, make_iter)
val = prot.pop("tags", []) 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) prototype_key = prototype.get('prototype_key', None)
if prototype_key: if prototype_key:
@ -291,15 +486,15 @@ def spawn(*prototypes, **kwargs):
tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY)) tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY))
val = prot.pop("exec", "") val = prot.pop("exec", "")
execs = validate_spawn_value(val, make_iter) execs = init_spawn_value(val, make_iter)
# extract ndb assignments # 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_")) for key, val in prot.items() if key.startswith("ndb_"))
# the rest are attributes # the rest are attributes
val = prot.pop("attrs", []) val = prot.pop("attrs", [])
attributes = validate_spawn_value(val, list) attributes = init_spawn_value(val, list)
simple_attributes = [] simple_attributes = []
for key, value in ((key, value) for key, value in prot.items() 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: if is_iter(value) and len(value) > 1:
# (value, category) # (value, category)
simple_attributes.append((key, simple_attributes.append((key,
validate_spawn_value(value[0], _to_obj_or_any), init_spawn_value(value[0], value_to_obj_or_any),
validate_spawn_value(value[1], str))) init_spawn_value(value[1], str)))
else: else:
simple_attributes.append((key, 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 = attributes + simple_attributes
attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS] 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, objsparams.append((create_kwargs, permission_string, lock_string,
alias_string, nattributes, attributes, tags, execs)) alias_string, nattributes, attributes, tags, execs))
return _batch_create_object(*objsparams) return batch_create_object(*objsparams)
# Testing # Testing

View file

@ -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