Cleanup, bug fixes, refactoring

This commit is contained in:
Griatch 2018-09-19 22:51:27 +02:00
parent 174113b9ab
commit a29b46d091
5 changed files with 404 additions and 302 deletions

View file

@ -1092,7 +1092,7 @@ def _add_attr(caller, attr_string, **kwargs):
attrname, category = nameparts
elif nparts > 2:
attrname, category, locks = nameparts
attr_tuple = (attrname, value, category, locks)
attr_tuple = (attrname, value, category, str(locks))
if attrname:
prot = _get_menu_prototype(caller)

View file

@ -1,7 +1,7 @@
"""
Handling storage of prototypes, both database-based ones (DBPrototypes) and those defined in modules
(Read-only prototypes).
(Read-only prototypes). Also contains utility functions, formatters and manager functions.
"""
@ -31,7 +31,6 @@ _PROTOTYPE_TAG_CATEGORY = "from_prototype"
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
PROT_FUNCS = {}
_RE_DBREF = re.compile(r"(?<!\$obj\()(#[0-9]+)")
@ -46,248 +45,29 @@ 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, stacktrace=False, **kwargs):
def homogenize_prototype(prototype, custom_keys=None):
"""
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.
Homogenize the more free-form prototype (where undefined keys are non-category attributes)
into the stricter form using `attrs` required by the system.
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.
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.
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.
prototype (dict): Prototype.
custom_keys (list, optional): Custom keys which should not be interpreted as attrs, beyond
the default reserved keys.
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.
homogenized (dict): Prototype where all non-identified keys grouped as attributes.
"""
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
# insert $obj(#dbref) for #dbref
value = _RE_DBREF.sub("$obj(\\1)", value)
result = inlinefuncs.parse_inlinefunc(
value, available_funcs=available_functions,
stacktrace=stacktrace, testing=testing, **kwargs)
err = None
try:
result = literal_eval(result)
except ValueError:
pass
except Exception as err:
err = str(err)
if testing:
return err, result
return result
def format_available_protfuncs():
"""
Get all protfuncs in a pretty-formatted form.
Args:
clr (str, optional): What coloration tag to use.
"""
out = []
for protfunc_name, protfunc in PROT_FUNCS.items():
out.append("- |c${name}|n - |W{docs}".format(
name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "")))
return justify("\n".join(out), indent=8)
# 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()}
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
attrs = list(prototype.get('attrs', [])) # break reference
homogenized = {}
for key, val in prototype.items():
if key in reserved:
homogenized[key] = val
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
def prototype_to_str(prototype):
"""
Format a prototype to a nice string representation.
Args:
prototype (dict): The prototype.
"""
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} |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=", ".join(out))
locks = prototype.get('locks', '')
if locks:
locks = "|clocks:|n\n {locks}".format(locks=locks)
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):
"""
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)
attrs.append((key, val, None, ''))
homogenized['attrs'] = attrs
return homogenized
# module-based prototypes
@ -295,7 +75,8 @@ 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.lower(), prot) for prototype_key, prot in all_from_module(mod).items()
prots = [(prototype_key.lower(), homogenize_prototype(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.lower(): mod for prototype_key, _ in prots})
@ -347,6 +128,8 @@ def save_prototype(**kwargs):
"""
kwargs = homogenize_prototype(kwargs)
def _to_batchtuple(inp, *args):
"build tuple suitable for batch-creation"
if is_iter(inp):
@ -403,8 +186,7 @@ def save_prototype(**kwargs):
attributes=[("prototype", prototype)])
return stored_prototype.db.prototype
# alias
create_prototype = save_prototype
create_prototype = save_prototype # alias
def delete_prototype(prototype_key, caller=None):
@ -640,7 +422,8 @@ 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 strict and 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))
@ -685,7 +468,8 @@ 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(';') if ":" in lstring]
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()"))
@ -693,3 +477,249 @@ def validate_prototype(prototype, protkey=None, protparents=None,
prototype_locks.append(("edit", "all()"))
prototype_locks = ";".join(":".join(tup) for tup in prototype_locks)
prototype['prototype_locks'] = prototype_locks
# Protfunc parsing (in-prototype functions)
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, stacktrace=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.
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.
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):
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
# insert $obj(#dbref) for #dbref
value = _RE_DBREF.sub("$obj(\\1)", value)
result = inlinefuncs.parse_inlinefunc(
value, available_funcs=available_functions,
stacktrace=stacktrace, testing=testing, **kwargs)
err = None
try:
result = literal_eval(result)
except ValueError:
pass
except Exception as err:
err = str(err)
if testing:
return err, result
return result
# Various prototype utilities
def format_available_protfuncs():
"""
Get all protfuncs in a pretty-formatted form.
Args:
clr (str, optional): What coloration tag to use.
"""
out = []
for protfunc_name, protfunc in PROT_FUNCS.items():
out.append("- |c${name}|n - |W{docs}".format(
name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "")))
return justify("\n".join(out), indent=8)
def prototype_to_str(prototype):
"""
Format a prototype to a nice string representation.
Args:
prototype (dict): The prototype.
"""
prototype = homogenize_prototype(prototype)
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} |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=", ".join(out))
locks = prototype.get('locks', '')
if locks:
locks = "|clocks:|n\n {locks}".format(locks=locks)
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):
"""
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)
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
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)

View file

@ -5,11 +5,14 @@ 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`
There main function is `spawn(*prototype)`, where the `prototype`
is a dictionary like this:
```python
GOBLIN = {
from evennia.prototypes import prototypes
prot = {
"prototype_key": "goblin",
"typeclass": "types.objects.Monster",
"key": "goblin grunt",
"health": lambda: randint(20,30),
@ -18,7 +21,10 @@ GOBLIN = {
"weaknesses": ["fire", "light"]
"tags": ["mob", "evil", ('greenskin','mob')]
"attrs": [("weapon", "sword")]
}
}
prot = prototypes.create_prototype(**prot)
```
Possible keywords are:
@ -57,7 +63,7 @@ Possible keywords are:
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_<name> (any): value of a nattribute (ndb_ is stripped)
ndb_<name> (any): value of a nattribute (ndb_ is stripped) - this is of limited use.
other (any): any other name is interpreted as the key of an Attribute with
its value. Such Attributes have no categories.
@ -66,15 +72,16 @@ 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
By specifying the "prototype_parent" key, the prototype becomes a child of
the given prototype, inheritng all prototype slots it does not explicitly
define itself, while overloading those that it does specify.
```python
import random
GOBLIN_WIZARD = {
{
"prototype_key": "goblin_wizard",
"prototype_parent": GOBLIN,
"key": "goblin wizard",
"spells": ["fire ball", "lighting bolt"]
@ -189,7 +196,9 @@ def flatten_prototype(prototype, validate=False):
flattened (dict): The final, flattened prototype.
"""
if prototype:
prototype = protlib.homogenize_prototype(prototype)
protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()}
protlib.validate_prototype(prototype, None, protparents,
is_prototype_base=validate, strict=validate)
@ -253,7 +262,7 @@ def prototype_from_object(obj):
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())
attrs = [(attr.key, attr.value, attr.category, ';'.join(attr.locks.all()))
for attr in obj.attributes.get(return_obj=True, return_list=True) if attr]
if attrs:
prot['attrs'] = attrs
@ -261,7 +270,7 @@ def prototype_from_object(obj):
return prot
def prototype_diff(prototype1, prototype2):
def prototype_diff(prototype1, prototype2, maxdepth=2):
"""
A 'detailed' diff specifies differences down to individual sub-sectiions
of the prototype, like individual attributes, permissions etc. It is used
@ -270,6 +279,9 @@ def prototype_diff(prototype1, prototype2):
Args:
prototype1 (dict): Original prototype.
prototype2 (dict): Comparison prototype.
maxdepth (int, optional): The maximum depth into the diff we go before treating the elements
of iterables as individual entities to compare. This is important since a single
attr/tag (for example) are represented by a tuple.
Returns:
diff (dict): A structure detailing how to convert prototype1 to prototype2. All
@ -280,7 +292,7 @@ def prototype_diff(prototype1, prototype2):
instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP".
"""
def _recursive_diff(old, new):
def _recursive_diff(old, new, depth=0):
old_type = type(old)
new_type = type(new)
@ -292,14 +304,14 @@ def prototype_diff(prototype1, prototype2):
return (old, new, "ADD")
else:
return (old, new, "UPDATE")
elif new_type == dict:
elif depth < maxdepth and new_type == dict:
all_keys = set(old.keys() + new.keys())
return {key: _recursive_diff(old.get(key), new.get(key)) for key in all_keys}
elif is_iter(new):
return {key: _recursive_diff(old.get(key), new.get(key), depth=depth + 1) for key in all_keys}
elif depth < maxdepth and is_iter(new):
old_map = {part[0] if is_iter(part) else part: part for part in old}
new_map = {part[0] if is_iter(part) else part: part for part in new}
all_keys = set(old_map.keys() + new_map.keys())
return {key: _recursive_diff(old_map.get(key), new_map.get(key)) for key in all_keys}
return {key: _recursive_diff(old_map.get(key), new_map.get(key), depth=depth + 1) for key in all_keys}
elif old != new:
return (old, new, "UPDATE")
else:
@ -346,13 +358,13 @@ def flatten_diff(diff):
typ = type(diffpart)
if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
out = [diffpart[2]]
elif type == dict:
elif typ == dict:
# all other are dicts
for val in diffpart.values():
out.extend(_get_all_nested_diff_instructions(val))
else:
raise RuntimeError("Diff contains non-dicts that are not on the "
"form (old, new, inst): {}".format(diff))
"form (old, new, inst): {}".format(diffpart))
return out
flat_diff = {}
@ -402,7 +414,7 @@ def prototype_diff_from_object(prototype, obj):
"""
obj_prototype = prototype_from_object(obj)
diff = prototype_diff(obj_prototype, prototype)
diff = prototype_diff(obj_prototype, protlib.homogenize_prototype(prototype))
return diff, obj_prototype
@ -421,6 +433,8 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
changed (int): The number of objects that had changes applied to them.
"""
prototype = protlib.homogenize_prototype(prototype)
if isinstance(prototype, basestring):
new_prototype = protlib.search_prototype(prototype)
else:
@ -439,7 +453,6 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
# make sure the diff is flattened
diff = flatten_diff(diff)
changed = 0
for obj in objects:
do_save = False
@ -619,9 +632,9 @@ def spawn(*prototypes, **kwargs):
(no object creation) and return the create-kwargs.
Returns:
object (Object, dict or list): Spawned object. If `only_validate` is given, return
object (Object, dict or list): Spawned object(s). 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.
`return_parents` is set, instead return dict of prototype parents.
"""
# get available protparents

View file

@ -70,7 +70,7 @@ class TestUtils(EvenniaTest):
self.obj1.tags.add('foo')
new_prot = spawner.prototype_from_object(self.obj1)
self.assertEqual(
{'attrs': [('test', 'testval', None, [''])],
{'attrs': [('test', 'testval', None, '')],
'home': Something,
'key': 'Obj',
'location': Something,
@ -94,14 +94,15 @@ class TestUtils(EvenniaTest):
def test_update_objects_from_prototypes(self):
self.maxDiff = None
self.obj1.attributes.add('oldtest', 'to_remove')
self.obj1.attributes.add('oldtest', 'to_keep')
old_prot = spawner.prototype_from_object(self.obj1)
# modify object away from prototype
self.obj1.attributes.add('test', 'testval')
self.obj1.attributes.add('desc', 'changed desc')
self.obj1.aliases.add('foo')
self.obj1.key = 'NewObj'
self.obj1.tags.add('footag', 'foocategory')
# modify prototype
old_prot['new'] = 'new_val'
@ -109,53 +110,111 @@ class TestUtils(EvenniaTest):
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'
old_prot['attrs'] += (("fooattr", "fooattrval", None, ''),)
# diff obj/prototype
pdiff = spawner.prototype_diff_from_object(old_prot, self.obj1)
old_prot_copy = old_prot.copy()
pdiff, obj_prototype = spawner.prototype_diff_from_object(old_prot, self.obj1)
self.assertEqual(old_prot_copy, old_prot)
self.assertEqual(obj_prototype,
{'aliases': ['foo'],
'attrs': [('oldtest', 'to_keep', None, ''),
('test', 'testval', None, ''),
('desc', 'changed desc', None, '')],
'key': 'Obj',
'home': '#1',
'location': '#1',
'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()',
'prototype_tags': [],
'typeclass': 'evennia.objects.objects.DefaultObject'})
self.assertEqual(old_prot,
{'attrs': [('oldtest', 'to_keep', None, ''),
('fooattr', 'fooattrval', None, '')],
'home': '#1',
'key': 'Obj',
'location': '#1',
'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()',
'new': 'new_val',
'permissions': 'Builder',
'prototype_desc': 'New version of prototype',
'prototype_key': Something,
'prototype_locks': 'spawn:all();edit:all()',
'prototype_tags': [],
'test': 'testval_changed',
'typeclass': 'evennia.objects.objects.DefaultObject'})
# from evennia import set_trace; set_trace(term_size=(182, 50))
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'},
{'attrs': [('oldtest', 'to_remove', None, ['']),
('test', 'testval', None, [''])],
'prototype_locks': 'spawn:all();edit:all()',
'prototype_key': Something,
'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()']),
'prototype_tags': [],
'location': "#1",
'key': 'NewObj',
'home': '#1',
'typeclass': 'evennia.objects.objects.DefaultObject',
'prototype_desc': 'Built from NewObj',
'aliases': 'foo'})
{'home': ('#1', '#1', 'KEEP'),
'prototype_locks': ('spawn:all();edit:all()',
'spawn:all();edit:all()', 'KEEP'),
'prototype_key': (Something, Something, 'UPDATE'),
'location': ('#1', '#1', 'KEEP'),
'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()',
'call:true();control:perm(Developer);delete:perm(Admin);'
'edit:perm(Admin);examine:perm(Builder);get:all();'
'puppet:pperm(Developer);tell:perm(Admin);view:all()', 'KEEP'),
'prototype_tags': {},
'attrs': {'oldtest': (('oldtest', 'to_keep', None, ''),
('oldtest', 'to_keep', None, ''), 'KEEP'),
'test': (('test', 'testval', None, ''),
None, 'REMOVE'),
'desc': (('desc', 'changed desc', None, ''),
None, 'REMOVE'),
'fooattr': (None, ('fooattr', 'fooattrval', None, ''), 'ADD'),
'test': (('test', 'testval', None, ''),
('test', 'testval_changed', None, ''), 'UPDATE'),
'new': (None, ('new', 'new_val', None, ''), 'ADD')},
'key': ('Obj', 'Obj', 'KEEP'),
'typeclass': ('evennia.objects.objects.DefaultObject',
'evennia.objects.objects.DefaultObject', 'KEEP'),
'aliases': (['foo'], None, 'REMOVE'),
'prototype_desc': ('Built from Obj',
'New version of prototype', 'UPDATE'),
'permissions': (None, 'Builder', 'ADD')}
)
# from evennia import set_trace;set_trace()
self.assertEqual(
spawner.flatten_diff(pdiff),
{'aliases': 'REMOVE',
'attrs': 'REPLACE',
'home': 'KEEP',
'key': 'KEEP',
'location': 'KEEP',
'locks': 'KEEP',
'permissions': 'UPDATE',
'prototype_desc': 'UPDATE',
'prototype_key': 'UPDATE',
'prototype_locks': 'KEEP',
'prototype_tags': 'KEEP',
'typeclass': 'KEEP'}
)
# apply diff
count = spawner.batch_update_objects_with_prototype(
old_prot, diff=pdiff[0], objects=[self.obj1])
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, [''])],
self.assertEqual({'attrs': [('oldtest', 'to_keep', None, ''),
('fooattr', 'fooattrval', None, ''),
('new', 'new_val', None, ''),
('test', 'testval_changed', None, '')],
'home': Something,
'key': 'Obj',
'location': Something,

View file

@ -564,7 +564,7 @@ class AttributeHandler(object):
ntup = len(tup)
keystr = str(tup[0]).strip().lower()
new_value = tup[1]
category = str(tup[2]).strip().lower() if ntup > 2 else None
category = str(tup[2]).strip().lower() if ntup > 2 and tup[2] is not None else None
lockstring = tup[3] if ntup > 3 else ""
attr_objs = self._getcache(keystr, category)