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 attrname, category = nameparts
elif nparts > 2: elif nparts > 2:
attrname, category, locks = nameparts attrname, category, locks = nameparts
attr_tuple = (attrname, value, category, locks) attr_tuple = (attrname, value, category, str(locks))
if attrname: if attrname:
prot = _get_menu_prototype(caller) 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 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" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
PROT_FUNCS = {} PROT_FUNCS = {}
_RE_DBREF = re.compile(r"(?<!\$obj\()(#[0-9]+)") _RE_DBREF = re.compile(r"(?<!\$obj\()(#[0-9]+)")
@ -46,248 +45,29 @@ class ValidationError(RuntimeError):
pass pass
# Protfunc parsing def homogenize_prototype(prototype, custom_keys=None):
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. Homogenize the more free-form prototype (where undefined keys are non-category attributes)
into the stricter form using `attrs` required by the system.
Available protfuncs are specified as callables in one of the modules of
`settings.PROTFUNC_MODULES`, or specified on the command line.
Args: Args:
value (any): The value to test for a parseable protfunc. Only strings will be parsed for prototype (dict): Prototype.
protfuncs, all other types are returned as-is. custom_keys (list, optional): Custom keys which should not be interpreted as attrs, beyond
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. the default reserved keys.
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: Returns:
testresult (tuple): If `testing` is set, returns a tuple (error, result) where error is homogenized (dict): Prototype where all non-identified keys grouped as attributes.
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): reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
try: attrs = list(prototype.get('attrs', [])) # break reference
value = value.dbref homogenized = {}
except AttributeError: for key, val in prototype.items():
pass if key in reserved:
value = to_str(value, force_string=True) homogenized[key] = val
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()}
else: else:
return stype([value_to_obj_or_any(val) for val in value]) attrs.append((key, val, None, ''))
return dbid_to_obj(value, ObjectDB) homogenized['attrs'] = attrs
return homogenized
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)
# module-based prototypes # module-based prototypes
@ -295,7 +75,8 @@ def init_spawn_value(value, validator=None):
for mod in settings.PROTOTYPE_MODULES: for mod in settings.PROTOTYPE_MODULES:
# to remove a default prototype, override it with an empty dict. # to remove a default prototype, override it with an empty dict.
# internally we store as (key, desc, locks, tags, prototype_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)] if prot and isinstance(prot, dict)]
# assign module path to each prototype_key for easy reference # assign module path to each prototype_key for easy reference
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots}) _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): def _to_batchtuple(inp, *args):
"build tuple suitable for batch-creation" "build tuple suitable for batch-creation"
if is_iter(inp): if is_iter(inp):
@ -403,8 +186,7 @@ def save_prototype(**kwargs):
attributes=[("prototype", prototype)]) attributes=[("prototype", prototype)])
return stored_prototype.db.prototype return stored_prototype.db.prototype
# alias create_prototype = save_prototype # alias
create_prototype = save_prototype
def delete_prototype(prototype_key, caller=None): 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 " _flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks "
"a typeclass or a prototype_parent.".format(protkey)) "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( _flags['errors'].append(
"Prototype {} is based on typeclass {}, which could not be imported!".format( "Prototype {} is based on typeclass {}, which could not be imported!".format(
protkey, typeclass)) protkey, typeclass))
@ -685,7 +468,8 @@ def validate_prototype(prototype, protkey=None, protparents=None,
# make sure prototype_locks are set to defaults # make sure prototype_locks are set to defaults
prototype_locks = [lstring.split(":", 1) 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] locktypes = [tup[0].strip() for tup in prototype_locks]
if "spawn" not in locktypes: if "spawn" not in locktypes:
prototype_locks.append(("spawn", "all()")) prototype_locks.append(("spawn", "all()"))
@ -693,3 +477,249 @@ def validate_prototype(prototype, protkey=None, protparents=None,
prototype_locks.append(("edit", "all()")) prototype_locks.append(("edit", "all()"))
prototype_locks = ";".join(":".join(tup) for tup in prototype_locks) prototype_locks = ";".join(":".join(tup) for tup in prototype_locks)
prototype['prototype_locks'] = 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 dictionary forms. These use a prototype architecture to define
unique objects without having to make a Typeclass for each. 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: is a dictionary like this:
```python ```python
GOBLIN = { from evennia.prototypes import prototypes
prot = {
"prototype_key": "goblin",
"typeclass": "types.objects.Monster", "typeclass": "types.objects.Monster",
"key": "goblin grunt", "key": "goblin grunt",
"health": lambda: randint(20,30), "health": lambda: randint(20,30),
@ -18,7 +21,10 @@ GOBLIN = {
"weaknesses": ["fire", "light"] "weaknesses": ["fire", "light"]
"tags": ["mob", "evil", ('greenskin','mob')] "tags": ["mob", "evil", ('greenskin','mob')]
"attrs": [("weapon", "sword")] "attrs": [("weapon", "sword")]
} }
prot = prototypes.create_prototype(**prot)
``` ```
Possible keywords are: Possible keywords are:
@ -57,7 +63,7 @@ Possible keywords are:
form allows more complex Attributes to be set. Tuples at least specify `(key, value)` 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 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`. 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 other (any): any other name is interpreted as the key of an Attribute with
its value. Such Attributes have no categories. 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 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. 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 By specifying the "prototype_parent" key, the prototype becomes a child of
that prototype, inheritng all prototype slots it does not explicitly the given prototype, inheritng all prototype slots it does not explicitly
define itself, while overloading those that it does specify. define itself, while overloading those that it does specify.
```python ```python
import random import random
GOBLIN_WIZARD = { {
"prototype_key": "goblin_wizard",
"prototype_parent": GOBLIN, "prototype_parent": GOBLIN,
"key": "goblin wizard", "key": "goblin wizard",
"spells": ["fire ball", "lighting bolt"] "spells": ["fire ball", "lighting bolt"]
@ -189,7 +196,9 @@ def flatten_prototype(prototype, validate=False):
flattened (dict): The final, flattened prototype. flattened (dict): The final, flattened prototype.
""" """
if prototype: if prototype:
prototype = protlib.homogenize_prototype(prototype)
protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()} protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()}
protlib.validate_prototype(prototype, None, protparents, protlib.validate_prototype(prototype, None, protparents,
is_prototype_base=validate, strict=validate) 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] for tag in obj.tags.get(return_tagobj=True, return_list=True) if tag]
if tags: if tags:
prot['tags'] = 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] for attr in obj.attributes.get(return_obj=True, return_list=True) if attr]
if attrs: if attrs:
prot['attrs'] = attrs prot['attrs'] = attrs
@ -261,7 +270,7 @@ def prototype_from_object(obj):
return prot return prot
def prototype_diff(prototype1, prototype2): def prototype_diff(prototype1, prototype2, maxdepth=2):
""" """
A 'detailed' diff specifies differences down to individual sub-sectiions A 'detailed' diff specifies differences down to individual sub-sectiions
of the prototype, like individual attributes, permissions etc. It is used of the prototype, like individual attributes, permissions etc. It is used
@ -270,6 +279,9 @@ def prototype_diff(prototype1, prototype2):
Args: Args:
prototype1 (dict): Original prototype. prototype1 (dict): Original prototype.
prototype2 (dict): Comparison 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: Returns:
diff (dict): A structure detailing how to convert prototype1 to prototype2. All 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". 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) old_type = type(old)
new_type = type(new) new_type = type(new)
@ -292,14 +304,14 @@ def prototype_diff(prototype1, prototype2):
return (old, new, "ADD") return (old, new, "ADD")
else: else:
return (old, new, "UPDATE") return (old, new, "UPDATE")
elif new_type == dict: elif depth < maxdepth and new_type == dict:
all_keys = set(old.keys() + new.keys()) all_keys = set(old.keys() + new.keys())
return {key: _recursive_diff(old.get(key), new.get(key)) for key in all_keys} return {key: _recursive_diff(old.get(key), new.get(key), depth=depth + 1) for key in all_keys}
elif is_iter(new): elif depth < maxdepth and is_iter(new):
old_map = {part[0] if is_iter(part) else part: part for part in old} 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} new_map = {part[0] if is_iter(part) else part: part for part in new}
all_keys = set(old_map.keys() + new_map.keys()) 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: elif old != new:
return (old, new, "UPDATE") return (old, new, "UPDATE")
else: else:
@ -346,13 +358,13 @@ def flatten_diff(diff):
typ = type(diffpart) typ = type(diffpart)
if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions: if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
out = [diffpart[2]] out = [diffpart[2]]
elif type == dict: elif typ == dict:
# all other are dicts # all other are dicts
for val in diffpart.values(): for val in diffpart.values():
out.extend(_get_all_nested_diff_instructions(val)) out.extend(_get_all_nested_diff_instructions(val))
else: else:
raise RuntimeError("Diff contains non-dicts that are not on the " 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 return out
flat_diff = {} flat_diff = {}
@ -402,7 +414,7 @@ def prototype_diff_from_object(prototype, obj):
""" """
obj_prototype = prototype_from_object(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 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. changed (int): The number of objects that had changes applied to them.
""" """
prototype = protlib.homogenize_prototype(prototype)
if isinstance(prototype, basestring): if isinstance(prototype, basestring):
new_prototype = protlib.search_prototype(prototype) new_prototype = protlib.search_prototype(prototype)
else: else:
@ -439,7 +453,6 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
# make sure the diff is flattened # make sure the diff is flattened
diff = flatten_diff(diff) diff = flatten_diff(diff)
changed = 0 changed = 0
for obj in objects: for obj in objects:
do_save = False do_save = False
@ -619,9 +632,9 @@ def spawn(*prototypes, **kwargs):
(no object creation) and return the create-kwargs. (no object creation) and return the create-kwargs.
Returns: 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 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 # get available protparents

View file

@ -70,7 +70,7 @@ class TestUtils(EvenniaTest):
self.obj1.tags.add('foo') self.obj1.tags.add('foo')
new_prot = spawner.prototype_from_object(self.obj1) new_prot = spawner.prototype_from_object(self.obj1)
self.assertEqual( self.assertEqual(
{'attrs': [('test', 'testval', None, [''])], {'attrs': [('test', 'testval', None, '')],
'home': Something, 'home': Something,
'key': 'Obj', 'key': 'Obj',
'location': Something, 'location': Something,
@ -94,14 +94,15 @@ class TestUtils(EvenniaTest):
def test_update_objects_from_prototypes(self): def test_update_objects_from_prototypes(self):
self.maxDiff = None 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) old_prot = spawner.prototype_from_object(self.obj1)
# modify object away from prototype # modify object away from prototype
self.obj1.attributes.add('test', 'testval') self.obj1.attributes.add('test', 'testval')
self.obj1.attributes.add('desc', 'changed desc')
self.obj1.aliases.add('foo') self.obj1.aliases.add('foo')
self.obj1.key = 'NewObj' self.obj1.tags.add('footag', 'foocategory')
# modify prototype # modify prototype
old_prot['new'] = 'new_val' old_prot['new'] = 'new_val'
@ -109,53 +110,111 @@ class TestUtils(EvenniaTest):
old_prot['permissions'] = 'Builder' old_prot['permissions'] = 'Builder'
# this will not update, since we don't update the prototype on-disk # this will not update, since we don't update the prototype on-disk
old_prot['prototype_desc'] = 'New version of prototype' old_prot['prototype_desc'] = 'New version of prototype'
old_prot['attrs'] += (("fooattr", "fooattrval", None, ''),)
# diff obj/prototype # 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( self.assertEqual(
pdiff, pdiff,
({'aliases': 'REMOVE', {'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', 'attrs': 'REPLACE',
'home': 'KEEP', 'home': 'KEEP',
'key': 'UPDATE', 'key': 'KEEP',
'location': 'KEEP', 'location': 'KEEP',
'locks': 'KEEP', 'locks': 'KEEP',
'new': 'UPDATE',
'permissions': 'UPDATE', 'permissions': 'UPDATE',
'prototype_desc': 'UPDATE', 'prototype_desc': 'UPDATE',
'prototype_key': 'UPDATE', 'prototype_key': 'UPDATE',
'prototype_locks': 'KEEP', 'prototype_locks': 'KEEP',
'prototype_tags': 'KEEP', 'prototype_tags': 'KEEP',
'test': 'UPDATE', 'typeclass': 'KEEP'}
'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'})
) )
# apply diff # apply diff
count = spawner.batch_update_objects_with_prototype( 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) self.assertEqual(count, 1)
new_prot = spawner.prototype_from_object(self.obj1) new_prot = spawner.prototype_from_object(self.obj1)
self.assertEqual({'attrs': [('test', 'testval_changed', None, ['']), self.assertEqual({'attrs': [('oldtest', 'to_keep', None, ''),
('new', 'new_val', None, [''])], ('fooattr', 'fooattrval', None, ''),
('new', 'new_val', None, ''),
('test', 'testval_changed', None, '')],
'home': Something, 'home': Something,
'key': 'Obj', 'key': 'Obj',
'location': Something, 'location': Something,

View file

@ -564,7 +564,7 @@ class AttributeHandler(object):
ntup = len(tup) ntup = len(tup)
keystr = str(tup[0]).strip().lower() keystr = str(tup[0]).strip().lower()
new_value = tup[1] 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 "" lockstring = tup[3] if ntup > 3 else ""
attr_objs = self._getcache(keystr, category) attr_objs = self._getcache(keystr, category)