1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
|
import json
import logging
from json_delta import diff
_LOGGER = logging.getLogger(__name__)
class ChangeLog:
def __init__(self, old_report, new_report):
self.features = []
self.breaking_changes = []
self._old_report = old_report
self._new_report = new_report
def build_md(self):
buffer = []
if self.features:
buffer.append("**Features**")
buffer.append("")
for feature in self.features:
buffer.append("- "+feature)
buffer.append("")
if self.breaking_changes:
buffer.append("**Breaking changes**")
buffer.append("")
for breaking_change in self.breaking_changes:
buffer.append("- "+breaking_change)
return "\n".join(buffer).strip()
@staticmethod
def _unpack_diff_entry(diff_entry):
return diff_entry[0], len(diff_entry) == 1
def operation(self, diff_entry):
path, is_deletion = self._unpack_diff_entry(diff_entry)
# Is this a new operation group?
_, operation_name, *remaining_path = path
if not remaining_path:
if is_deletion:
self.breaking_changes.append(_REMOVE_OPERATION_GROUP.format(operation_name))
else:
self.features.append(_ADD_OPERATION_GROUP.format(operation_name))
return
_, *remaining_path = remaining_path
if not remaining_path:
# Not common, but this means this has changed a lot. Compute the list manually
old_ops_name = list(self._old_report["operations"][operation_name]["functions"])
new_ops_name = list(self._new_report["operations"][operation_name]["functions"])
for removed_function in set(old_ops_name) - set(new_ops_name):
self.breaking_changes.append(_REMOVE_OPERATION.format(operation_name, removed_function))
for added_function in set(new_ops_name) - set(old_ops_name):
self.features.append(_ADD_OPERATION.format(operation_name, added_function))
return
# Is this a new operation, inside a known operation group?
function_name, *remaining_path = remaining_path
if not remaining_path:
if is_deletion:
self.breaking_changes.append(_REMOVE_OPERATION.format(operation_name, function_name))
else:
self.features.append(_ADD_OPERATION.format(operation_name, function_name))
return
if remaining_path[0] == "metadata":
# Ignore change in metadata for now, they have no impact
return
# So method signaure changed. Be vague for now
self.breaking_changes.append(_SIGNATURE_CHANGE.format(operation_name, function_name))
def models(self, diff_entry):
path, is_deletion = self._unpack_diff_entry(diff_entry)
# Is this a new model?
_, mtype, *remaining_path = path
if not remaining_path:
# Seen once in Network, because exceptions were added. Bypass
return
model_name, *remaining_path = remaining_path
if not remaining_path:
# A new model or a model deletion is not very interesting by itself
# since it usually means that there is a new operation
#
# We might miss some discrimanator new sub-classes however
return
# That's a model signature change
if mtype in ["enums", "exceptions"]:
# Don't change log anything for Enums for now
return
_, *remaining_path = remaining_path
if not remaining_path: # This means massive signature changes, that we don't even try to list them
self.breaking_changes.append(_MODEL_SIGNATURE_CHANGE.format(model_name))
return
# This is a real model
parameter_name, *remaining_path = remaining_path
is_required = lambda report, model_name, param_name: report["models"]["models"][model_name]["parameters"][param_name]["properties"]["required"]
if not remaining_path:
if is_deletion:
self.breaking_changes.append(_MODEL_PARAM_DELETE.format(model_name, parameter_name))
else:
# This one is tough, if the new parameter is "required",
# then it's breaking. If not, it's a feature
if is_required(self._new_report, model_name, parameter_name):
self.breaking_changes.append(_MODEL_PARAM_ADD_REQUIRED.format(model_name, parameter_name))
else:
self.features.append(_MODEL_PARAM_ADD.format(model_name, parameter_name))
return
# The parameter already exists
new_is_required = is_required(self._new_report, model_name, parameter_name)
old_is_required = is_required(self._old_report, model_name, parameter_name)
if new_is_required and not old_is_required:
# This shift from optional to required
self.breaking_changes.append(_MODEL_PARAM_CHANGE_REQUIRED.format(parameter_name, model_name))
return
## Features
_ADD_OPERATION_GROUP = "Added operation group {}"
_ADD_OPERATION = "Added operation {}.{}"
_MODEL_PARAM_ADD = "Model {} has a new parameter {}"
## Breaking Changes
_REMOVE_OPERATION_GROUP = "Removed operation group {}"
_REMOVE_OPERATION = "Removed operation {}.{}"
_SIGNATURE_CHANGE = "Operation {}.{} has a new signature"
_MODEL_SIGNATURE_CHANGE = "Model {} has a new signature"
_MODEL_PARAM_DELETE = "Model {} no longer has parameter {}"
_MODEL_PARAM_ADD_REQUIRED = "Model {} has a new required parameter {}"
_MODEL_PARAM_CHANGE_REQUIRED = "Parameter {} of model {} is now required"
def build_change_log(old_report, new_report):
change_log = ChangeLog(old_report, new_report)
result = diff(old_report, new_report)
for diff_line in result:
# Operations
if diff_line[0][0] == "operations":
change_log.operation(diff_line)
else:
change_log.models(diff_line)
return change_log
def get_report_from_parameter(input_parameter):
if ":" in input_parameter:
package_name, version = input_parameter.split(":")
from .code_report import main
result = main(
package_name,
version=version if version not in ["pypi", "latest"] else None,
last_pypi=version == "pypi"
)
if not result:
raise ValueError("Was not able to build a report")
if len(result) == 1:
with open(result[0], "r") as fd:
return json.load(fd)
raise NotImplementedError("Multi-api changelog not yet implemented")
with open(input_parameter, "r") as fd:
return json.load(fd)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description='ChangeLog computation',
)
parser.add_argument('base',
help='Base. Could be a file path, or <package_name>:<version>. Version can be pypi, latest or a real version')
parser.add_argument('latest',
help='Latest. Could be a file path, or <package_name>:<version>. Version can be pypi, latest or a real version')
parser.add_argument("--debug",
dest="debug", action="store_true",
help="Verbosity in DEBUG mode")
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
old_report = get_report_from_parameter(args.base)
new_report = get_report_from_parameter(args.latest)
# result = diff(old_report, new_report)
# with open("result.json", "w") as fd:
# json.dump(result, fd)
change_log = build_change_log(old_report, new_report)
print(change_log.build_md())
|