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()