Starting to fix prototype diffs which don't work right
This commit is contained in:
parent
ff5ffa8d0e
commit
c4f501123a
3 changed files with 202 additions and 35 deletions
|
|
@ -59,7 +59,7 @@ def _get_flat_menu_prototype(caller, refresh=False, validate=False):
|
||||||
|
|
||||||
def _get_unchanged_inherited(caller, protname):
|
def _get_unchanged_inherited(caller, protname):
|
||||||
"""Return prototype values inherited from parent(s), which are not replaced in child"""
|
"""Return prototype values inherited from parent(s), which are not replaced in child"""
|
||||||
protototype = _get_menu_prototype(caller)
|
prototype = _get_menu_prototype(caller)
|
||||||
if protname in prototype:
|
if protname in prototype:
|
||||||
return protname[protname], False
|
return protname[protname], False
|
||||||
else:
|
else:
|
||||||
|
|
@ -1968,6 +1968,7 @@ def node_apply_diff(caller, **kwargs):
|
||||||
obj_prototype = kwargs.get("obj_prototype", None)
|
obj_prototype = kwargs.get("obj_prototype", None)
|
||||||
base_obj = kwargs.get("base_obj", None)
|
base_obj = kwargs.get("base_obj", None)
|
||||||
diff = kwargs.get("diff", None)
|
diff = kwargs.get("diff", None)
|
||||||
|
custom_location = kwargs.get("custom_location", None)
|
||||||
|
|
||||||
if not update_objects:
|
if not update_objects:
|
||||||
text = "There are no existing objects to update."
|
text = "There are no existing objects to update."
|
||||||
|
|
@ -1978,24 +1979,36 @@ def node_apply_diff(caller, **kwargs):
|
||||||
if not diff:
|
if not diff:
|
||||||
# use one random object as a reference to calculate a diff
|
# use one random object as a reference to calculate a diff
|
||||||
base_obj = choice(update_objects)
|
base_obj = choice(update_objects)
|
||||||
diff, obj_prototype = spawner.prototype_diff_from_object(prototype, base_obj)
|
|
||||||
|
# from evennia import set_trace
|
||||||
|
diff, obj_prototype = spawner.prototype_diff_from_object(
|
||||||
|
prototype, base_obj, exceptions={"location": "KEEP"})
|
||||||
|
|
||||||
text = ["Suggested changes to {} objects. ".format(len(update_objects)),
|
text = ["Suggested changes to {} objects. ".format(len(update_objects)),
|
||||||
"Showing random example obj to change: {name} ({dbref}))\n".format(
|
"Showing random example obj to change: {name} ({dbref}))\n".format(
|
||||||
name=base_obj.key, dbref=base_obj.dbref)]
|
name=base_obj.key, dbref=base_obj.dbref)]
|
||||||
|
|
||||||
helptext = """
|
helptext = """
|
||||||
|
This will go through all existing objects and apply the changes you accept.
|
||||||
|
|
||||||
Be careful with this operation! The upgrade mechanism will try to automatically estimate
|
Be careful with this operation! The upgrade mechanism will try to automatically estimate
|
||||||
what changes need to be applied. But the estimate is |wonly based on the analysis of one
|
what changes need to be applied. But the estimate is |wonly based on the analysis of one
|
||||||
randomly selected object|n among all objects spawned by this prototype. If that object
|
randomly selected object|n among all objects spawned by this prototype. If that object
|
||||||
happens to be unusual in some way the estimate will be off and may lead to unexpected
|
happens to be unusual in some way the estimate will be off and may lead to unexpected
|
||||||
results for other objects. Always test your objects carefully after an upgrade and
|
results for other objects. Always test your objects carefully after an upgrade and consider
|
||||||
consider being conservative (switch to KEEP) or even do the update manually if you are
|
being conservative (switch to KEEP) for things you are unsure of. For complex upgrades it
|
||||||
unsure that the results will be acceptable. """
|
may be better to get help from an administrator with access to the `@py` command for doing
|
||||||
|
this manually.
|
||||||
|
|
||||||
|
Note that the `location` will never be auto-adjusted because it's so rare to want to
|
||||||
|
homogenize the location of all object instances."""
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
|
|
||||||
ichanges = 0
|
ichanges = 0
|
||||||
|
|
||||||
|
# convert diff to a menu text + options to edit
|
||||||
|
|
||||||
for (key, inst) in sorted(((key, val) for key, val in diff.items()), key=lambda tup: tup[0]):
|
for (key, inst) in sorted(((key, val) for key, val in diff.items()), key=lambda tup: tup[0]):
|
||||||
|
|
||||||
if key in protlib._PROTOTYPE_META_NAMES:
|
if key in protlib._PROTOTYPE_META_NAMES:
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ prototype key (this value must be possible to serialize in an Attribute).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from random import randint as base_randint, random as base_random
|
from random import randint as base_randint, random as base_random, choice as base_choice
|
||||||
|
|
||||||
from evennia.utils import search
|
from evennia.utils import search
|
||||||
from evennia.utils.utils import justify as base_justify, is_iter, to_str
|
from evennia.utils.utils import justify as base_justify, is_iter, to_str
|
||||||
|
|
@ -101,6 +101,16 @@ def center_justify(*args, **kwargs):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def choice(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $choice(val, val, val, ...)
|
||||||
|
Returns one of the values randomly
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
return base_choice(args)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def full_justify(*args, **kwargs):
|
def full_justify(*args, **kwargs):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ import hashlib
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
import evennia
|
import evennia
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.utils.utils import make_iter, is_iter
|
from evennia.utils.utils import make_iter, is_iter
|
||||||
|
|
@ -138,6 +139,8 @@ from evennia.prototypes.prototypes import (
|
||||||
|
|
||||||
_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")
|
||||||
|
_PROTOTYPE_ROOT_NAMES = ('typeclass', 'key', 'aliases', 'attrs', 'tags', 'locks', 'permissions',
|
||||||
|
'location', 'home', 'destination')
|
||||||
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES
|
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -240,10 +243,10 @@ def prototype_from_object(obj):
|
||||||
locks = obj.locks.all()
|
locks = obj.locks.all()
|
||||||
if locks:
|
if locks:
|
||||||
prot['locks'] = ";".join(locks)
|
prot['locks'] = ";".join(locks)
|
||||||
perms = obj.permissions.get()
|
perms = obj.permissions.get(return_list=True)
|
||||||
if perms:
|
if perms:
|
||||||
prot['permissions'] = make_iter(perms)
|
prot['permissions'] = make_iter(perms)
|
||||||
aliases = obj.aliases.get()
|
aliases = obj.aliases.get(return_list=True)
|
||||||
if aliases:
|
if aliases:
|
||||||
prot['aliases'] = aliases
|
prot['aliases'] = aliases
|
||||||
tags = [(tag.db_key, tag.db_category, tag.db_data)
|
tags = [(tag.db_key, tag.db_category, tag.db_data)
|
||||||
|
|
@ -258,7 +261,160 @@ def prototype_from_object(obj):
|
||||||
return prot
|
return prot
|
||||||
|
|
||||||
|
|
||||||
def prototype_diff_from_object(prototype, obj):
|
def get_detailed_prototype_diff(prototype1, prototype2):
|
||||||
|
"""
|
||||||
|
A 'detailed' diff specifies differences down to individual sub-sectiions
|
||||||
|
of the prototype, like individual attributes, permissions etc. It is used
|
||||||
|
by the menu to allow a user to customize what should be kept.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prototype1 (dict): Original prototype.
|
||||||
|
prototype2 (dict): Comparison prototype.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
diff (dict): A structure detailing how to convert prototype1 to prototype2.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
A detailed diff has instructions REMOVE, ADD, UPDATE and KEEP.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def _recursive_diff(old, new):
|
||||||
|
|
||||||
|
old_type = type(old)
|
||||||
|
new_type = type(new)
|
||||||
|
|
||||||
|
if old_type != new_type:
|
||||||
|
if old and not new:
|
||||||
|
return (old, new, "REMOVE")
|
||||||
|
elif not old and new:
|
||||||
|
return (old, new, "ADD")
|
||||||
|
else:
|
||||||
|
return (old, new, "UPDATE")
|
||||||
|
elif 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):
|
||||||
|
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 new_type(_recursive_diff(old_map.get(key), new_map.get(key))
|
||||||
|
for key in all_keys)
|
||||||
|
elif old != new:
|
||||||
|
return (old, new, "UPDATE")
|
||||||
|
else:
|
||||||
|
return (old, new, "KEEP")
|
||||||
|
|
||||||
|
diff = _recursive_diff(prototype1, prototype2)
|
||||||
|
|
||||||
|
return diff
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_diff(detailed_diff):
|
||||||
|
"""
|
||||||
|
For spawning, a 'detailed' diff is not necessary, rather we just
|
||||||
|
want instructions on how to handle each root key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detailed_diff (dict): Diff produced by `get_detailed_prototype_diff` and
|
||||||
|
possibly modified by the user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
flattened_diff (dict): A flat structure detailing how to operate on each
|
||||||
|
root component of the prototype.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The flattened diff has the following possible instructions:
|
||||||
|
UPDATE, REPLACE, REMOVE
|
||||||
|
Many of the detailed diff's values can hold nested structures with their own
|
||||||
|
individual instructions. A detailed diff can have the following instructions:
|
||||||
|
REMOVE, ADD, UPDATE, KEEP
|
||||||
|
Here's how they are translated:
|
||||||
|
- All REMOVE -> REMOVE
|
||||||
|
- All ADD|UPDATE -> UPDATE
|
||||||
|
- All KEEP -> (remove from flattened diff)
|
||||||
|
- Mix KEEP, UPDATE, ADD -> UPDATE
|
||||||
|
- Mix REMOVE, KEEP, UPDATE, ADD -> REPLACE
|
||||||
|
"""
|
||||||
|
|
||||||
|
typ = type(diffpart)
|
||||||
|
if typ == tuple and _is_diff_instruction(diffpart):
|
||||||
|
key = args[0]
|
||||||
|
_, val, inst = diffpart
|
||||||
|
elif typ == dict:
|
||||||
|
for key, subdiffpart in diffpart:
|
||||||
|
_apply_diff(subdiffpart, obj, *(args + (key, )))
|
||||||
|
else:
|
||||||
|
# all other types in the diff are iterables (tups or lists) and
|
||||||
|
# are identified by their first element.
|
||||||
|
for tup in diffpart:
|
||||||
|
_apply_diff(tup, obj, *(args + (tup[0], )))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _is_diff_instruction(obj):
|
||||||
|
return (isinstance(obj, tuple) and
|
||||||
|
len(obj) == 3 and
|
||||||
|
obj[2] in ('KEEP', 'REMOVE', 'ADD', 'UPDATE'))
|
||||||
|
|
||||||
|
|
||||||
|
def apply_diff_to_prototype(prototype, diff):
|
||||||
|
"""
|
||||||
|
When spawning we don't need the full details of the diff; we have (in the menu) had our
|
||||||
|
chance to customize we just want to know if the
|
||||||
|
current root key should be
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def menu_format_diff(diff):
|
||||||
|
"""
|
||||||
|
Reformat the diff in a way suitable for the olc menu.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diff (dict): A diff as produced by `prototype_diff`. The root level of this diff
|
||||||
|
(which is always a dict) is used to group sub-changes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _apply_diff(diffpart, obj, *args):
|
||||||
|
"""
|
||||||
|
Recursively apply the diff for a given rootname.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diffpart (tuple or dict): Part of diff to apply.
|
||||||
|
obj (Object): Object to apply diff to.
|
||||||
|
args (str): Listing of identifiers for the part to apply,
|
||||||
|
starting from the root.
|
||||||
|
|
||||||
|
"""
|
||||||
|
typ = type(diffpart)
|
||||||
|
if typ == tuple and _is_diff_instruction(diffpart):
|
||||||
|
key = args[0]
|
||||||
|
_, val, inst = diffpart
|
||||||
|
elif typ == dict:
|
||||||
|
for key, subdiffpart in diffpart:
|
||||||
|
_apply_diff(subdiffpart, obj, *(args + (key, )))
|
||||||
|
else:
|
||||||
|
# all other types in the diff are iterables (tups or lists) and
|
||||||
|
# are identified by their first element.
|
||||||
|
for tup in diffpart:
|
||||||
|
_apply_diff(tup, obj, *(args + (tup[0], )))
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_diff(obj):
|
||||||
|
if _is_diff_instruction(obj):
|
||||||
|
old, new, inst = obj
|
||||||
|
|
||||||
|
out_dict = {}
|
||||||
|
for root_key, root_val in diff.items():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def prototype_diff_from_object(prototype, obj, exceptions=None):
|
||||||
"""
|
"""
|
||||||
Get a simple diff for a prototype compared to an object which may or may not already have a
|
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
|
prototype (or has one but changed locally). For more complex migratations a manual diff may be
|
||||||
|
|
@ -266,32 +422,25 @@ def prototype_diff_from_object(prototype, obj):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prototype (dict): Prototype.
|
prototype (dict): Prototype.
|
||||||
obj (Object): Object to
|
obj (Object): Object to compare prototype against.
|
||||||
|
exceptions (dict, optional): A mapping {"key": "KEEP|REPLACE|UPDATE|REMOVE" for
|
||||||
|
enforcing a specific outcome for that key regardless of the diff.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
|
diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
|
||||||
other_prototype (dict): The prototype for the given object. The diff is a how to convert
|
other_prototype (dict): The prototype for the given object. The diff is a how to convert
|
||||||
this prototype into the new prototype.
|
this prototype into the new prototype.
|
||||||
|
|
||||||
|
diff = {"key": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"),
|
||||||
|
"attrs": {"attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"),
|
||||||
|
"attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), ...},
|
||||||
|
"aliases": {"aliasname": (old, new, "KEEP...", ...},
|
||||||
|
... }
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
prot1 = prototype
|
|
||||||
prot2 = prototype_from_object(obj)
|
prot2 = prototype_from_object(obj)
|
||||||
|
diff = prototype_diff(prototype, prot2)
|
||||||
diff = {}
|
|
||||||
for key, value in prot1.items():
|
|
||||||
diff[key] = "KEEP"
|
|
||||||
if key in prot2:
|
|
||||||
if callable(prot2[key]) or value != prot2[key]:
|
|
||||||
if key in ('attrs', 'tags', 'permissions', 'locks', 'aliases'):
|
|
||||||
diff[key] = 'REPLACE'
|
|
||||||
else:
|
|
||||||
diff[key] = "UPDATE"
|
|
||||||
elif key not in prot2:
|
|
||||||
diff[key] = "UPDATE"
|
|
||||||
for key in prot2:
|
|
||||||
if key not in diff and key not in prot1:
|
|
||||||
diff[key] = "REMOVE"
|
|
||||||
|
|
||||||
return diff, prot2
|
return diff, prot2
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -589,17 +738,12 @@ def spawn(*prototypes, **kwargs):
|
||||||
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()
|
||||||
if not (key.startswith("ndb_"))):
|
if not (key.startswith("ndb_"))):
|
||||||
|
# we don't support categories, nor locks for simple attributes
|
||||||
if key in _PROTOTYPE_META_NAMES:
|
if key in _PROTOTYPE_META_NAMES:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if is_iter(value) and len(value) > 1:
|
|
||||||
# (value, category)
|
|
||||||
simple_attributes.append((key,
|
|
||||||
init_spawn_value(value[0], value_to_obj_or_any),
|
|
||||||
init_spawn_value(value[1], str)))
|
|
||||||
else:
|
else:
|
||||||
simple_attributes.append((key,
|
simple_attributes.append(
|
||||||
init_spawn_value(value, value_to_obj_or_any)))
|
(key, init_spawn_value(value, value_to_obj_or_any), None, None))
|
||||||
|
|
||||||
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]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue