mirror of https://github.com/ceph/go-ceph
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 <jmulligan@redhat.com>
This commit is contained in:
parent
16ff05194c
commit
5d41b671df
|
@ -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("<!-- GENERATED FILE: DO NOT EDIT DIRECTLY -->", 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()
|
Loading…
Reference in New Issue