From 5d41b671df93b0f55482d81a7dd89c351df1d8d3 Mon Sep 17 00:00:00 2001 From: John Mulligan Date: Tue, 14 Sep 2021 13:23:29 -0400 Subject: [PATCH] contrib: add apiage.py - a script to help track the "aging" of apis This tool can be run as part of our CI as a mechanism to check that (some) of the policy requirements are being met. It can also be run by humans as part of the process to update the tracked API information. Signed-off-by: John Mulligan --- contrib/apiage.py | 276 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100755 contrib/apiage.py diff --git a/contrib/apiage.py b/contrib/apiage.py new file mode 100755 index 0000000..f003798 --- /dev/null +++ b/contrib/apiage.py @@ -0,0 +1,276 @@ +#!/usr/bin/python3 +""" +apiage.py - a quick and dirty tool for tracking when apis become stable +and deprecated apis are to be removed. + +PDX-License-Identifier: MIT +""" + +import argparse +import copy +import json +import sys + + +def read_json(path): + try: + with open(path, "r") as fh: + data = json.load(fh) + except FileNotFoundError: + return {} + return data + + +def write_json(path, data): + if path is None: + raise ValueError("a valid path is required") + with open(path, "w") as fh: + json.dump(data, fh, indent=2) + + +def write_markdown(path, data): + if path is None: + return + with open(path, "w") as fh: + format_markdown(data, fh) + + +def copy_api(tracked, keys, src, defaults=None): + dst = tracked + for key in keys[:-1]: + dst = dst.setdefault(key, {}) + dst = dst.setdefault(keys[-1], []) + added = [] + for gfunc in src: + name = gfunc["name"] + if name in [d["name"] for d in dst]: + continue + gfunc.update(defaults or {}) + dst.append(gfunc) + added.append(gfunc) + return added + + +def compare_and_update(tracked, pkg, pkg_api, defaults=None): + if defaults is None: + defaults = {} + new_deprecated = new_preview = [] + if "deprecated_api" in pkg_api: + new_deprecated = copy_api( + tracked=tracked, + keys=[pkg, "deprecated_api"], + src=pkg_api["deprecated_api"], + defaults={ + "deprecated_in_version": defaults.get( + "deprecated_in_version", "" + ), + "expected_remove_version": defaults.get( + "expected_remove_version", "" + ), + }, + ) + if "preview_api" in pkg_api: + new_preview = copy_api( + tracked=tracked, + keys=[pkg, "preview_api"], + src=pkg_api["preview_api"], + defaults={ + "added_in_version": defaults.get("added_in_version", ""), + "expected_stable_version": defaults.get( + "expected_stable_version", "" + ), + }, + ) + if "stable_api" in pkg_api: + new_stable = copy_api( + tracked=tracked, + keys=[pkg, "stable_api"], + src=pkg_api["stable_api"], + ) + return new_deprecated, new_preview, new_stable + + +def api_update(tracked, src, copy_stable=False, defaults=None): + for pkg, pkg_api in src.items(): + _, _, new_stable = compare_and_update( + tracked, pkg, pkg_api, defaults=defaults + ) + if new_stable and not copy_stable: + return len(new_stable) + return 0 + + +def api_compare(tracked, src): + problems = 0 + tmp = copy.deepcopy(tracked) + for pkg, pkg_api in src.items(): + new_deprecated, new_preview, new_stable = compare_and_update( + tmp, pkg, pkg_api + ) + for dapi in new_deprecated: + print("not tracked (deprecated):", pkg, dapi["name"]) + problems += 1 + for papi in new_preview: + print("not tracked (preview):", pkg, papi["name"]) + problems += 1 + for sapi in new_stable: + print("not tracked (stable):", pkg, sapi["name"]) + problems += 1 + for pkg, pkg_api in tmp.items(): + for api in pkg_api.get("deprecated_api", []): + if not api.get("deprecated_in_version"): + print("no deprecated_in_version set:", pkg, api["name"]) + problems += 1 + for api in pkg_api.get("preview_api", []): + if not api.get("added_in_version"): + print("no added_in_version set:", pkg, api["name"]) + problems += 1 + if not api.get("expected_stable_version"): + print("no expected_stable_version set:", pkg, api["name"]) + problems += 1 + return problems + + +def format_markdown(tracked, outfh): + print("", file=outfh) + print("", file=outfh) + print("# go-ceph API Stability", file=outfh) + print("", file=outfh) + for pkg, pkg_api in tracked.items(): + print(f"## Package: {pkg}", file=outfh) + print("", file=outfh) + if "preview_api" in pkg_api: + print("### Preview APIs", file=outfh) + print("", file=outfh) + _table( + pkg_api["preview_api"], + columns=[ + ("Name", "name"), + ("Added in Version", "added_in_version"), + ("Expected Stable Version", "expected_stable_version"), + ], + outfh=outfh, + ) + print("", file=outfh) + if "deprecated_api" in pkg_api: + print("### Deprecated APIs", file=outfh) + print("", file=outfh) + _table( + pkg_api["deprecated_api"], + columns=[ + ("Name", "name"), + ("Deprecated in Version", "deprecated_in_version"), + ("Expected Removal Version", "expected_remove_version"), + ], + outfh=outfh, + ) + print("", file=outfh) + + +def _table(data, columns, outfh): + for key, _ in columns: + outfh.write(key) + outfh.write(" | ") + outfh.write("\n") + for key, _ in columns: + outfh.write("-" * len(key)) + outfh.write(" | ") + outfh.write("\n") + for entry in data: + for _, dname in columns: + outfh.write(entry[dname]) + outfh.write(" | ") + outfh.write("\n") + + +def _setif(dct, key, value): + if value: + dct[key] = value + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--source", + "-s", + default="./_results/implements.json", + help="json describing state of code", + ) + parser.add_argument( + "--current", + "-c", + default="./docs/api-status.json", + help="json tracking current apis", + ) + parser.add_argument( + "--document", + "-d", + default="./docs/api-status.md", + help="markdown file describing current apis", + ) + parser.add_argument( + "--mode", + choices=("compare", "update", "write-doc"), + default="compare", + help="either update current state or compare current state to source", + ) + parser.add_argument( + "--copy-stable-apis", + action="store_true", + help="allow copying of pre-existing stable APIs", + ) + parser.add_argument( + "--added-in-version", + "-A", + help="specify an added-in version for all new preview apis", + ) + parser.add_argument( + "--stable-in-version", + "-S", + help="specify a stable-in version for all new preview apis", + ) + parser.add_argument( + "--deprecated-in-version", + "-D", + help="specify a deprecated-in version for all newly deprecated apis", + ) + parser.add_argument( + "--remove-in-version", + "-R", + help="specify a version that this deprecated api is expected to be removed", + ) + cli = parser.parse_args() + + api_src = read_json(cli.source) if cli.source else {} + api_tracked = read_json(cli.current) if cli.current else {} + + if cli.mode == "compare": + # just compare the json files. useful for CI + pcount = api_compare(api_tracked, api_src) + if pcount: + print(f"error: {pcount} problems detected", file=sys.stderr) + sys.exit(1) + elif cli.mode == "update": + # update the current/tracked apis with those from the source + defaults = {} + _setif(defaults, "added_in_version", cli.added_in_version) + _setif(defaults, "expected_stable_version", cli.stable_in_version) + _setif(defaults, "deprecated_in_version", cli.deprecated_in_version) + _setif(defaults, "expected_remove_version", cli.remove_in_version) + pcount = api_update( + api_tracked, + api_src, + copy_stable=cli.copy_stable_apis, + defaults=defaults, + ) + if pcount: + print(f"error: {pcount} problems detected", file=sys.stderr) + sys.exit(1) + write_json(cli.current, api_tracked) + write_markdown(cli.document, api_tracked) + elif cli.mode == "write-doc": + write_markdown(cli.document, api_tracked) + + +if __name__ == "__main__": + main()