tripleo-heat-templates/tools/yaml-diff.py

146 lines
4.5 KiB
Python
Executable File

#!/usr/bin/env python3
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import argparse
from collections import defaultdict
import difflib
import json
from pprint import pformat
import sys
import yaml
COMPARE_SECTIONS = ['parameters', 'conditions', 'resources', 'outputs']
def parse_args():
p = argparse.ArgumentParser()
p.add_argument('--details', '-d',
action='count',
default=0,
help='Show details, deeper comparison with -dd')
p.add_argument('--common', '-c',
action='store_true',
help='Show common items when comparing sections')
p.add_argument('--out', '-o',
action='store',
choices=['pformat', 'json'],
default='json',
help='Output format, either using pformat or json')
p.add_argument('--width', '-w',
action='store',
default=80,
type=int,
help='When using pformat, this is the max width')
p.add_argument('--section', '-s',
action='store',
nargs='*',
help="Sections to compare",
default=COMPARE_SECTIONS)
p.add_argument('path_args', action='store', nargs='*')
args = p.parse_args()
if len(args.path_args) != 2:
p.error("Need two files to compare")
return args
def diff_list(list_a, list_b):
"""Takes 2 lists and returns the differences between them"""
list_a = sorted(list_a)
list_b = sorted(list_b)
diffs = defaultdict(list)
sequence = difflib.SequenceMatcher(None, list_a, list_b).get_opcodes()
for tag, i, j, k, l in sequence:
if tag == 'equal' and show_common:
diffs['common'].extend(list_a[i:j])
if tag in ('delete', 'replace'):
diffs[FILE_A].extend(list_a[i:j])
if tag in ('insert', 'replace'):
diffs[FILE_B].extend(list_b[k:l])
return "\n".join([f"{fn} {dl}" for fn, dl in diffs.items()])
def diff_dict(dict_a, dict_b):
"""Compares two dicts
Converts 2 dicts to strings with pformat and returns
a unified diff formated string
"""
if output_format == "pformat":
str_a = pformat(dict_a, width=output_width)
str_b = pformat(dict_b, width=output_width)
else:
str_a = json.dumps(dict_a, indent=2)
str_b = json.dumps(dict_b, indent=2)
return "\n".join([d for d in difflib.unified_diff(
str_a.splitlines(),
str_b.splitlines(),
fromfile=FILE_A,
tofile=FILE_B)])
args = parse_args()
path_args = args.path_args
show_details = args.details
show_common = args.common
sections = args.section
output_format = args.out
output_width = args.width
FILE_A = path_args[0]
FILE_B = path_args[1]
with open(FILE_A, 'r') as file_a:
a = yaml.safe_load(file_a)
with open(FILE_B, 'r') as file_b:
b = yaml.safe_load(file_b)
exit = "Files are different" if a != b else 0
if not show_details:
sys.exit(exit)
# With -ddd, we print the full diff dict and exit
if show_details >= 3:
print(diff_dict(a, b))
sys.exit(exit)
section_diff = diff_list(list(a.keys()), list(a.keys()))
if section_diff:
print(f"Sections list\n{section_diff}")
for item in sections:
keys_a = list(a.get(item).keys())
keys_b = list(b.get(item).keys())
section_item_diff = diff_list(keys_a, keys_b)
if section_item_diff:
print(f"\n\nSection {item}\n{section_item_diff}")
if show_details > 1:
for key in list(set(keys_a + keys_b)):
key_a = a.get(item, {}).get(key, {})
key_b = b.get(item, {}).get(key, {})
diff = diff_dict(key_a, key_b)
# If a key is missing from either list, we don't want
# to see the full diff, it's already flagged when
# show_details == 1
# We just want to see if child dict is existent on both sides
# and different
if diff and key_a and key_b:
print(f"\n\n{item}/{key}\n{diff}")
sys.exit(exit)