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): def _keep_diff(caller, **kwargs):
key = kwargs['key'] path = kwargs['path']
diff = kwargs['diff'] 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): def node_apply_diff(caller, **kwargs):
@ -1984,10 +2054,6 @@ def node_apply_diff(caller, **kwargs):
diff, obj_prototype = spawner.prototype_diff_from_object( diff, obj_prototype = spawner.prototype_diff_from_object(
prototype, base_obj, exceptions={"location": "KEEP"}) 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 = """ helptext = """
This will go through all existing objects and apply the changes you accept. This will go through all existing objects and apply the changes you accept.
@ -2003,43 +2069,12 @@ def node_apply_diff(caller, **kwargs):
Note that the `location` will never be auto-adjusted because it's so rare to want to Note that the `location` will never be auto-adjusted because it's so rare to want to
homogenize the location of all object instances.""" 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 if options:
text = ["Suggested changes to {} objects. ".format(len(update_objects)),
# convert diff to a menu text + options to edit "Showing random example obj to change: {name} ({dbref}))\n".format(
name=base_obj.key, dbref=base_obj.dbref)] + txt
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( options.extend(
[{"key": ("|wu|Wpdate {} objects".format(len(update_objects)), "update", "u"), [{"key": ("|wu|Wpdate {} objects".format(len(update_objects)), "update", "u"),
"desc": "Update {} objects".format(len(update_objects)), "desc": "Update {} objects".format(len(update_objects)),
@ -2048,8 +2083,7 @@ def node_apply_diff(caller, **kwargs):
{"key": ("|wr|Weset changes", "reset", "r"), {"key": ("|wr|Weset changes", "reset", "r"),
"goto": ("node_apply_diff", {"prototype": prototype, "back_node": back_node, "goto": ("node_apply_diff", {"prototype": prototype, "back_node": back_node,
"objects": update_objects})}]) "objects": update_objects})}])
else:
if ichanges < 1:
text = ["Analyzed a random sample object (out of {}) - " text = ["Analyzed a random sample object (out of {}) - "
"found no changes to apply.".format(len(update_objects))] "found no changes to apply.".format(len(update_objects))]
@ -2058,7 +2092,6 @@ def node_apply_diff(caller, **kwargs):
"goto": back_node}) "goto": back_node})
text = "\n".join(text) text = "\n".join(text)
text = (text, helptext) text = (text, helptext)
return text, options return text, options

View file

@ -261,7 +261,7 @@ def prototype_from_object(obj):
return prot return prot
def get_detailed_prototype_diff(prototype1, prototype2): def prototype_diff(prototype1, prototype2):
""" """
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
@ -272,10 +272,12 @@ def get_detailed_prototype_diff(prototype1, prototype2):
prototype2 (dict): Comparison prototype. prototype2 (dict): Comparison prototype.
Returns: Returns:
diff (dict): A structure detailing how to convert prototype1 to prototype2. diff (dict): A structure detailing how to convert prototype1 to prototype2. All
nested structures are dicts with keys matching either the prototype's matching
Notes: key or the first element in the tuple describing the prototype value (so for
A detailed diff has instructions REMOVE, ADD, UPDATE and KEEP. 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): 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} 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 new_type(_recursive_diff(old_map.get(key), new_map.get(key)) return {key: _recursive_diff(old_map.get(key), new_map.get(key)) for key in all_keys}
for key in all_keys)
elif old != new: elif old != new:
return (old, new, "UPDATE") return (old, new, "UPDATE")
else: else:
@ -309,14 +310,15 @@ def get_detailed_prototype_diff(prototype1, prototype2):
return diff return diff
def flatten_diff(detailed_diff): def flatten_diff(diff):
""" """
For spawning, a 'detailed' diff is not necessary, rather we just For spawning, a 'detailed' diff is not necessary, rather we just want instructions on how to
want instructions on how to handle each root key. handle each root key.
Args: Args:
detailed_diff (dict): Diff produced by `get_detailed_prototype_diff` and diff (dict): Diff produced by `prototype_diff` and
possibly modified by the user. possibly modified by the user. Note that also a pre-flattened diff will come out
unchanged by this function.
Returns: Returns:
flattened_diff (dict): A flat structure detailing how to operate on each 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: Here's how they are translated:
- All REMOVE -> REMOVE - All REMOVE -> REMOVE
- All ADD|UPDATE -> UPDATE - All ADD|UPDATE -> UPDATE
- All KEEP -> (remove from flattened diff) - All KEEP -> KEEP
- Mix KEEP, UPDATE, ADD -> UPDATE - Mix KEEP, UPDATE, ADD -> UPDATE
- Mix REMOVE, KEEP, UPDATE, ADD -> REPLACE - 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) typ = type(diffpart)
if typ == tuple and _is_diff_instruction(diffpart): if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
key = args[0] out = [diffpart[2]]
_, val, inst = diffpart elif type == dict:
elif typ == dict: # all other are dicts
for key, subdiffpart in diffpart: for val in diffpart.values():
_apply_diff(subdiffpart, obj, *(args + (key, ))) out.extend(_get_all_nested_diff_instructions(val))
else: else:
# all other types in the diff are iterables (tups or lists) and raise RuntimeError("Diff contains non-dicts that are not on the "
# are identified by their first element. "form (old, new, inst): {}".format(diff))
for tup in diffpart: return out
_apply_diff(tup, obj, *(args + (tup[0], )))
flat_diff = {}
# flatten diff based on rules
for rootkey, diffpart in diff.items():
def _is_diff_instruction(obj): insts = _get_all_nested_diff_instructions(diffpart)
return (isinstance(obj, tuple) and if all(inst == "KEEP" for inst in insts):
len(obj) == 3 and rootinst = "KEEP"
obj[2] in ('KEEP', 'REMOVE', 'ADD', 'UPDATE')) elif all(inst in ("ADD", "UPDATE") for inst in insts):
rootinst = "UPDATE"
elif all(inst == "REMOVE" for inst in insts):
def apply_diff_to_prototype(prototype, diff): rootinst = "REMOVE"
""" elif "REMOVE" in insts:
When spawning we don't need the full details of the diff; we have (in the menu) had our rootinst = "REPLACE"
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: else:
# all other types in the diff are iterables (tups or lists) and rootinst = "UPDATE"
# are identified by their first element.
for tup in diffpart: flat_diff[rootkey] = rootinst
_apply_diff(tup, obj, *(args + (tup[0], )))
return flat_diff
def _iter_diff(obj): def prototype_diff_from_object(prototype, 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
needed. needed.
Args: Args:
prototype (dict): Prototype. prototype (dict): New prototype.
obj (Object): Object to compare prototype against. 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 obj_prototype (dict): The prototype calculated for the given object. The diff is how to
this prototype into the new prototype. convert this prototype into the new prototype.
diff = {"key": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), Notes:
The `diff` is on the following form:
{"key": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"),
"attrs": {"attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), "attrs": {"attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"),
"attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), ...}, "attrkey": (old, new, "KEEP|REPLACE|UPDATE|REMOVE"), ...},
"aliases": {"aliasname": (old, new, "KEEP...", ...}, "aliases": {"aliasname": (old, new, "KEEP...", ...},
... } ... }
""" """
prot2 = prototype_from_object(obj) obj_prototype = prototype_from_object(obj)
diff = prototype_diff(prototype, prot2) diff = prototype_diff(obj_prototype, prototype)
return diff, prot2 return diff, obj_prototype
def batch_update_objects_with_prototype(prototype, diff=None, objects=None): 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: if not diff:
diff, _ = prototype_diff_from_object(new_prototype, objects[0]) diff, _ = prototype_diff_from_object(new_prototype, objects[0])
# make sure the diff is flattened
diff = flatten_diff(diff)
changed = 0 changed = 0
for obj in objects: for obj in objects:
do_save = False do_save = False