mirror of
https://github.com/nlohmann/json.git
synced 2026-10-03 21:20:30 +00:00
Put multi-line docstring summaries on their own line, as a single sentence followed by a blank line (pydocstyle D205, D209, D213, D415). Annotate the subprocess import and calls with nosec: they only run fixed argument lists, never through a shell (Bandit B404, B603, B607). Do the same for the three broad except clauses in extract_api.py, which deliberately fall through to the next libclang candidate or skip an unresolvable alias (Bandit B110, B112). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
112 lines
4.5 KiB
Python
Executable File
112 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Capture immutable, per-release API surface records into tools/api_checker/history/.
|
|
|
|
These are the durable, committed counterpart to diff_api.py's live git-archive-and-extract path:
|
|
once a release is tagged, run this once to capture tools/api_checker/history/<tag>.json, commit
|
|
it, and future diffs against that tag hit the fast, no-libclang-needed stored-file path in
|
|
diff_api.py automatically. See tools/api_checker/README.md's "Workflow: Release Checklist" and
|
|
POLICY.md for the full policy (manual step, not CI-automated; files are immutable once committed
|
|
-- regenerate only via --force, and only as a deliberate, reviewed choice).
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import json
|
|
import os
|
|
# subprocess is only called with fixed argument lists, never through a shell.
|
|
import subprocess # nosec B404
|
|
import sys
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
HISTORY_DIR = os.path.join(SCRIPT_DIR, 'history')
|
|
|
|
sys.path.insert(0, SCRIPT_DIR)
|
|
from diff_api import extract_surface_for_ref # noqa: E402
|
|
|
|
|
|
def discover_v3_tags() -> list:
|
|
"""
|
|
List every v3.* git tag, sorted by dotted version.
|
|
|
|
Sorting is numeric, not lexicographic: v3.9.0 must sort before v3.10.0.
|
|
"""
|
|
result = subprocess.run(['git', 'tag', '--list', 'v3.*'], capture_output=True, text=True, check=True) # nosec B603 B607
|
|
tags = [t for t in result.stdout.splitlines() if t.strip()]
|
|
|
|
def version_key(tag):
|
|
parts = tag.lstrip('v').replace('-', '.').split('.')
|
|
return tuple(int(p) if p.isdigit() else p for p in parts)
|
|
|
|
return sorted(tags, key=version_key)
|
|
|
|
|
|
def snapshot_one(ref: str, output_dir: str, force: bool, header: str, include: str) -> tuple:
|
|
"""Capture one ref's surface. Returns (ref, success, message)."""
|
|
output_path = os.path.join(output_dir, f"{ref}.json")
|
|
if os.path.exists(output_path) and not force:
|
|
return ref, True, f"skipped (already exists at {output_path}; use --force to overwrite)"
|
|
|
|
try:
|
|
surface = extract_surface_for_ref(ref, header, include)
|
|
except SystemExit:
|
|
return ref, False, "extraction failed (see output above)"
|
|
except Exception as e: # noqa: BLE001 -- a batch backfill must not die on one bad tag
|
|
return ref, False, f"unexpected error: {e}"
|
|
|
|
surface['meta']['generated_at'] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
surface['meta']['generator'] = 'tools/api_checker/extract_api.py'
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
with open(output_path, 'w') as f:
|
|
json.dump(surface, f, indent=2, sort_keys=True)
|
|
f.write('\n')
|
|
|
|
return ref, True, f"{len(surface['public_api'])} entries -> {output_path}"
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Capture per-release API surface snapshots into tools/api_checker/history/')
|
|
parser.add_argument('--ref', action='append', default=[],
|
|
help='Git ref (tag) to snapshot; repeatable')
|
|
parser.add_argument('--all-tags', action='store_true',
|
|
help='Snapshot every v3.* tag (existing history files are skipped unless --force)')
|
|
parser.add_argument('--output-dir', default=HISTORY_DIR,
|
|
help='Directory to write history files into (default: tools/api_checker/history/)')
|
|
parser.add_argument('--force', action='store_true',
|
|
help='Overwrite an existing history file (history files are immutable by '
|
|
'convention -- only pass this for a deliberate, reviewed regeneration)')
|
|
parser.add_argument('--header', default='include/nlohmann/json.hpp')
|
|
parser.add_argument('--include', default='include')
|
|
|
|
args = parser.parse_args()
|
|
|
|
refs = list(args.ref)
|
|
if args.all_tags:
|
|
refs = discover_v3_tags()
|
|
|
|
if not refs:
|
|
parser.error('specify at least one --ref, or --all-tags')
|
|
|
|
print(f"Snapshotting {len(refs)} ref(s)...")
|
|
results = []
|
|
for ref in refs:
|
|
print(f"\n=== {ref} ===")
|
|
result = snapshot_one(ref, args.output_dir, args.force, args.header, args.include)
|
|
print(result[2])
|
|
results.append(result)
|
|
|
|
failures = [(ref, msg) for ref, ok, msg in results if not ok]
|
|
print("\n" + "=" * 70)
|
|
print(f"Done: {len(results) - len(failures)} succeeded, {len(failures)} failed")
|
|
if failures:
|
|
print("\nFailures (review and record accepted gaps in tools/api_checker/history/README.md):")
|
|
for ref, msg in failures:
|
|
print(f" {ref}: {msg}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|