New version of prototype diff management

This commit is contained in:
Griatch 2018-09-15 17:10:50 +02:00
parent c4f501123a
commit d55634d542
2 changed files with 149 additions and 151 deletions

View file

@ -1945,9 +1945,79 @@ def _apply_diff(caller, **kwargs):
def _keep_diff(caller, **kwargs):
key = kwargs['key']
path = kwargs['path']
diff = kwargs['diff']
diff[key] = "KEEP"
tmp = diff
for key in path[:-1]:
tmp = diff[key]
tmp[path[-1]] = "KEEP"
def _format_diff_text_and_options(diff, exclude=None):
"""
Reformat the diff in a way suitable for the olc menu.
Args:
diff (dict): A diff as produced by `prototype_diff`.
exclude (list, optional): List of root keys to skip, regardless
of diff instruction.
Returns:
options (list): List of options dict.
"""
valid_instructions = ('KEEP', 'REMOVE', 'ADD', 'UPDATE')
def _visualize(obj, rootname, get_name=False):
if utils.is_iter(obj):
if get_name:
return obj[0]
if rootname == "attrs":
return "{} |W=|n {} |W(category:|n {}|W, locks:|n {}|W)|n".format(*obj)
elif rootname == "tags":
return "{} |W(category:|n {}|W)|n".format(obj[0], obj[1])
return obj
def _parse_diffpart(diffpart, optnum, indent, *args):
typ = type(diffpart)
texts = []
options = []
if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
old, new, instruction = diffpart
if instruction == 'KEEP':
texts.append("{old} |gKEEP|n".format(old=old))
else:
texts.append("{indent}|c({num}) {inst}|W:|n {old} |W->|n {new}".format(
indent=" " * indent,
inst="|rREMOVE|n" if instruction == 'REMOVE' else "|y{}|n".format(instruction),
num=optnum,
old=_visualize(old, args[-1]),
new=_visualize(new, args[-1])))
options.append({"key": str(optnum),
"desc": "|gKEEP|n {}".format(
_visualize(old, args[-1], get_name=True)),
"goto": (_keep_diff, {"path": args, "diff": diff})})
optnum += 1
else:
for key, subdiffpart in diffpart.items():
text, option, optnum = _parse_diffpart(
subdiffpart, optnum, indent + 1, *(args + (key, )))
texts.extend(text)
options.extend(option)
return text, options, optnum
texts = []
options = []
# we use this to allow for skipping full KEEP instructions
flattened_diff = spawner.flatten_diff(diff)
optnum = 1
for root_key, diffpart in flattened_diff.items():
text, option, optnum = _parse_diffpart(diffpart, optnum, 1, root_key)
texts.extend(text)
options.extend(option)
return texts, options
def node_apply_diff(caller, **kwargs):
@ -1984,10 +2054,6 @@ def node_apply_diff(caller, **kwargs):
diff, obj_prototype = spawner.prototype_diff_from_object(
prototype, base_obj, exceptions={"location": "KEEP"})
text = ["Suggested changes to {} objects. ".format(len(update_objects)),
"Showing random example obj to change: {name} ({dbref}))\n".format(
name=base_obj.key, dbref=base_obj.dbref)]
helptext = """
This will go through all existing objects and apply the changes you accept.
@ -2003,53 +2069,21 @@ def node_apply_diff(caller, **kwargs):
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 = []
txt, options = _format_diff_text_and_options(diff, exclude=['location'] if custom_location else None)
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]):
if key in protlib._PROTOTYPE_META_NAMES:
continue
line = "{iopt} |w{key}|n: {old}{sep}{new} {change}"
old_val = str(obj_prototype.get(key, "<unset>"))
if inst == "KEEP":
inst = "|b{}|n".format(inst)
text.append(line.format(iopt='', key=key, old=old_val,
sep=" ", new='', change=inst))
continue
if key in prototype:
new_val = str(spawner.init_spawn_value(prototype[key]))
else:
new_val = "<unset>"
ichanges += 1
if inst in ("UPDATE", "REPLACE"):
inst = "|y{}|n".format(inst)
text.append(line.format(iopt=ichanges, key=key, old=old_val,
sep=" |y->|n ", new=new_val, change=inst))
options.append(_keep_option(key, prototype,
base_obj, obj_prototype, diff, update_objects, back_node))
elif inst == "REMOVE":
inst = "|r{}|n".format(inst)
text.append(line.format(iopt=ichanges, key=key, old=old_val,
sep=" |r->|n ", new='', change=inst))
options.append(_keep_option(key, prototype,
base_obj, obj_prototype, diff, update_objects, back_node))
options.extend(
[{"key": ("|wu|Wpdate {} objects".format(len(update_objects)), "update", "u"),
"desc": "Update {} objects".format(len(update_objects)),
"goto": (_apply_diff, {"prototype": prototype, "objects": update_objects,
"back_node": back_node, "diff": diff, "base_obj": base_obj})},
{"key": ("|wr|Weset changes", "reset", "r"),
"goto": ("node_apply_diff", {"prototype": prototype, "back_node": back_node,
"objects": update_objects})}])
if ichanges < 1:
if options:
text = ["Suggested changes to {} objects. ".format(len(update_objects)),
"Showing random example obj to change: {name} ({dbref}))\n".format(
name=base_obj.key, dbref=base_obj.dbref)] + txt
options.extend(
[{"key": ("|wu|Wpdate {} objects".format(len(update_objects)), "update", "u"),
"desc": "Update {} objects".format(len(update_objects)),
"goto": (_apply_diff, {"prototype": prototype, "objects": update_objects,
"back_node": back_node, "diff": diff, "base_obj": base_obj})},
{"key": ("|wr|Weset changes", "reset", "r"),
"goto": ("node_apply_diff", {"prototype": prototype, "back_node": back_node,
"objects": update_objects})}])
else:
text = ["Analyzed a random sample object (out of {}) - "
"found no changes to apply.".format(len(update_objects))]
@ -2058,7 +2092,6 @@ def node_apply_diff(caller, **kwargs):
"goto": back_node})
text = "\n".join(text)
text = (text, helptext)
return text, options

View file

@ -261,7 +261,7 @@ def prototype_from_object(obj):
return prot
def get_detailed_prototype_diff(prototype1, prototype2):
def 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
@ -272,10 +272,12 @@ def get_detailed_prototype_diff(prototype1, prototype2):
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.
diff (dict): A structure detailing how to convert prototype1 to prototype2. All
nested structures are dicts with keys matching either the prototype's matching
key or the first element in the tuple describing the prototype value (so for
a tag tuple `(tagname, category)` the second-level key in the diff would be tagname).
The the bottom level of the diff consist of tuples `(old, new, instruction)`, where
instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP".
"""
def _recursive_diff(old, new):
@ -297,8 +299,7 @@ def get_detailed_prototype_diff(prototype1, prototype2):
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)
return {key: _recursive_diff(old_map.get(key), new_map.get(key)) for key in all_keys}
elif old != new:
return (old, new, "UPDATE")
else:
@ -309,14 +310,15 @@ def get_detailed_prototype_diff(prototype1, prototype2):
return diff
def flatten_diff(detailed_diff):
def flatten_diff(diff):
"""
For spawning, a 'detailed' diff is not necessary, rather we just
want instructions on how to handle each root key.
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.
diff (dict): Diff produced by `prototype_diff` and
possibly modified by the user. Note that also a pre-flattened diff will come out
unchanged by this function.
Returns:
flattened_diff (dict): A flat structure detailing how to operate on each
@ -331,117 +333,77 @@ def flatten_diff(detailed_diff):
Here's how they are translated:
- All REMOVE -> REMOVE
- All ADD|UPDATE -> UPDATE
- All KEEP -> (remove from flattened diff)
- All KEEP -> KEEP
- Mix KEEP, UPDATE, ADD -> UPDATE
- Mix REMOVE, KEEP, UPDATE, ADD -> REPLACE
"""
valid_instructions = ('KEEP', 'REMOVE', 'ADD', 'UPDATE')
def _get_all_nested_diff_instructions(diffpart):
"Started for each root key, returns all instructions nested under it"
out = []
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, )))
if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
out = [diffpart[2]]
elif type == dict:
# all other are dicts
for val in diffpart.values():
out.extend(_get_all_nested_diff_instructions(val))
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], )))
raise RuntimeError("Diff contains non-dicts that are not on the "
"form (old, new, inst): {}".format(diff))
return out
flat_diff = {}
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, )))
# flatten diff based on rules
for rootkey, diffpart in diff.items():
insts = _get_all_nested_diff_instructions(diffpart)
if all(inst == "KEEP" for inst in insts):
rootinst = "KEEP"
elif all(inst in ("ADD", "UPDATE") for inst in insts):
rootinst = "UPDATE"
elif all(inst == "REMOVE" for inst in insts):
rootinst = "REMOVE"
elif "REMOVE" in insts:
rootinst = "REPLACE"
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], )))
rootinst = "UPDATE"
flat_diff[rootkey] = rootinst
return flat_diff
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):
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.
prototype (dict): New prototype.
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:
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
this prototype into the new prototype.
obj_prototype (dict): The prototype calculated for the given object. The diff is how to
convert 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...", ...},
... }
Notes:
The `diff` is on the following form:
{"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...", ...},
... }
"""
prot2 = prototype_from_object(obj)
diff = prototype_diff(prototype, prot2)
return diff, prot2
obj_prototype = prototype_from_object(obj)
diff = prototype_diff(obj_prototype, prototype)
return diff, obj_prototype
def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
@ -475,6 +437,9 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
if not diff:
diff, _ = prototype_diff_from_object(new_prototype, objects[0])
# make sure the diff is flattened
diff = flatten_diff(diff)
changed = 0
for obj in objects:
do_save = False