|
| 1 | +#!/usr/bin/python3 |
| 2 | +""" |
| 3 | +The following script takes care of adding new/removing versions or |
| 4 | +replacing a version in the compatibility-test-matrices JSON files. |
| 5 | +
|
| 6 | +To use this script, you'll need to have Python 3.9+ installed. |
| 7 | +
|
| 8 | +Invocation: |
| 9 | +
|
| 10 | +By default, the script assumes that adding a new version is the desired operation. |
| 11 | +Furthermore, it assumes that the compatibility-test-matrices directory is located |
| 12 | +in the .github directory and the script is invoked from the root of the repository. |
| 13 | +
|
| 14 | +If any of the above is not true, you can use the '--type' and '--directory' flags |
| 15 | +to specify the operation and the directory respectively. |
| 16 | +
|
| 17 | +Typically, an invocation would look like: |
| 18 | +
|
| 19 | + scripts/update_compatibility_tests.py --recent_version v4.3.0 --new_version v4.4.0 |
| 20 | +
|
| 21 | +The three operations currently added are: |
| 22 | +
|
| 23 | + - ADD: Add a new version to the JSON files. Requires both '--recent_version' and |
| 24 | + '--new_version' options to be set. |
| 25 | + - REPLACE: Replace an existing version with a new one. Requires both '--recent_version' |
| 26 | + and '--new_version' options to be set. |
| 27 | + - REMOVE: Remove an existing version from the JSON files. Requires only the |
| 28 | + '--recent_version' options to be set. |
| 29 | +
|
| 30 | +For more information, use the '--help' flag to see the available options. |
| 31 | +""" |
| 32 | +import argparse |
| 33 | +import os |
| 34 | +import json |
| 35 | +import enum |
| 36 | +from collections import defaultdict |
| 37 | +from typing import Tuple, Generator, Optional, Dict, List, Any |
| 38 | + |
| 39 | +# Directory to operate in |
| 40 | +DIRECTORY: str = ".github/compatibility-test-matrices" |
| 41 | +# JSON keys to search in. |
| 42 | +KEYS: Tuple[str, str] = ("chain-a", "chain-b") |
| 43 | +# Toggle if required. Indent = 2 matches our current formatting. |
| 44 | +DUMP_ARGS: Dict[Any, Any] = { |
| 45 | + "indent": 2, |
| 46 | + "sort_keys": False, |
| 47 | + "ensure_ascii": False, |
| 48 | +} |
| 49 | +# Suggestions for recent and new versions. |
| 50 | +SUGGESTION: str = "for example, v4.3.0 or 4.3.0" |
| 51 | +# Supported Operations. |
| 52 | +Operation = enum.Enum("Operation", ["ADD", "REMOVE", "REPLACE"]) |
| 53 | + |
| 54 | + |
| 55 | +def find_json_files( |
| 56 | + directory: str, ignores: Tuple[str] = (".",) |
| 57 | +) -> Generator[str, None, None]: |
| 58 | + """Find JSON files in a directory. By default, ignore hidden directories.""" |
| 59 | + for root, dirs, files in os.walk(directory): |
| 60 | + dirs[:] = (d for d in dirs if not d.startswith(ignores)) |
| 61 | + for file_ in files: |
| 62 | + if file_.endswith(".json"): |
| 63 | + yield os.path.join(root, file_) |
| 64 | + |
| 65 | + |
| 66 | +def has_release_version(json_file: Any, keys: Tuple[str, str], version: str) -> bool: |
| 67 | + """Check if the json file has the version in question.""" |
| 68 | + rows = (json_file[key] for key in keys) |
| 69 | + return any(version in row for row in rows) |
| 70 | + |
| 71 | + |
| 72 | +def sorter(key: str) -> str: |
| 73 | + """Since 'main' < 'vX.X.X' and we want to have 'main' as the first entry |
| 74 | + in the list, we return a version that is considerably large. If ibc-go |
| 75 | + reaches this version I'll wear my dunce hat and go sit in the corner. |
| 76 | + """ |
| 77 | + return "v99999.9.9" if key == "main" else key |
| 78 | + |
| 79 | + |
| 80 | +def update_version(json_file: Any, keys: Tuple[str, str], args: argparse.Namespace): |
| 81 | + """Update the versions as required in the json file.""" |
| 82 | + recent, new, op = args.recent, args.new, args.type |
| 83 | + for row in (json_file[key] for key in keys): |
| 84 | + if recent not in row: |
| 85 | + continue |
| 86 | + if op == Operation.ADD: |
| 87 | + row.append(new) |
| 88 | + row.sort(key=sorter, reverse=True) |
| 89 | + else: |
| 90 | + index = row.index(recent) |
| 91 | + if op == Operation.REPLACE: |
| 92 | + row[index] = new |
| 93 | + elif op == Operation.REMOVE: |
| 94 | + del row[index] |
| 95 | + |
| 96 | + |
| 97 | +def version_input(prompt: str, version: Optional[str]) -> str: |
| 98 | + """Input version if not supplied, make it start with a 'v' if it doesn't.""" |
| 99 | + if version is None: |
| 100 | + version = input(prompt) |
| 101 | + return version if version.startswith(("v", "V")) else f"v{version}" |
| 102 | + |
| 103 | + |
| 104 | +def require_version(args: argparse.Namespace): |
| 105 | + """Allow non-required version in argparse but request it if not provided.""" |
| 106 | + args.recent = version_input(f"Recent version ({SUGGESTION}): ", args.recent) |
| 107 | + if args.type == Operation.REMOVE: |
| 108 | + return |
| 109 | + args.new = version_input(f"New version ({SUGGESTION}): ", args.new) |
| 110 | + |
| 111 | + |
| 112 | +def parse_args() -> argparse.Namespace: |
| 113 | + """Parse command line arguments.""" |
| 114 | + parser = argparse.ArgumentParser(description="Update JSON files.") |
| 115 | + parser.add_argument( |
| 116 | + "--type", |
| 117 | + choices=[Operation.ADD.name, Operation.REPLACE.name, Operation.REMOVE.name], |
| 118 | + default=Operation.ADD, |
| 119 | + help="Type of version update: add a version, replace one or remove one.", |
| 120 | + ) |
| 121 | + parser.add_argument( |
| 122 | + "--directory", |
| 123 | + default=DIRECTORY, |
| 124 | + help="Directory path where JSON files are located", |
| 125 | + ) |
| 126 | + parser.add_argument( |
| 127 | + "--recent_version", |
| 128 | + dest="recent", |
| 129 | + help=f"Recent version to search in JSON files ({SUGGESTION})", |
| 130 | + ) |
| 131 | + parser.add_argument( |
| 132 | + "--new_version", |
| 133 | + dest="new", |
| 134 | + help=f"New version to add in JSON files ({SUGGESTION})", |
| 135 | + ) |
| 136 | + parser.add_argument( |
| 137 | + "--verbose", |
| 138 | + "-v", |
| 139 | + action="store_true", |
| 140 | + help="Allow for verbose output", |
| 141 | + default=False, |
| 142 | + ) |
| 143 | + |
| 144 | + args = parser.parse_args() |
| 145 | + require_version(args) |
| 146 | + return args |
| 147 | + |
| 148 | + |
| 149 | +def print_logs(logs: Dict[str, List[str]], verbose: bool): |
| 150 | + """Print the logs. Verbosity controls if each individual |
| 151 | + file is printed or not. |
| 152 | + """ |
| 153 | + updated, skipped = logs["updated"], logs["skipped"] |
| 154 | + if updated: |
| 155 | + if verbose: |
| 156 | + print("Updated files:", *updated, sep="\n - ") |
| 157 | + else: |
| 158 | + print("No files were updated.") |
| 159 | + if skipped: |
| 160 | + if verbose: |
| 161 | + print("The following files were skipped:", *skipped, sep="\n - ") |
| 162 | + else: |
| 163 | + print("No files skipped.") |
| 164 | + |
| 165 | + |
| 166 | +def main(args: argparse.Namespace): |
| 167 | + """ Main driver function.""" |
| 168 | + # Hold logs for 'updated' and 'skipped' files. |
| 169 | + logs = defaultdict(list) |
| 170 | + |
| 171 | + # Go through each file and operate on it, if applicable. |
| 172 | + for file_ in find_json_files(args.directory): |
| 173 | + with open(file_, "r+") as fp: |
| 174 | + json_file = json.load(fp) |
| 175 | + if not has_release_version(json_file, KEYS, args.recent): |
| 176 | + logs["skipped"].append( |
| 177 | + f"Version '{args.recent}' not found in '{file_}'" |
| 178 | + ) |
| 179 | + continue |
| 180 | + update_version(json_file, KEYS, args) |
| 181 | + fp.seek(0) |
| 182 | + json.dump(json_file, fp, **DUMP_ARGS) |
| 183 | + logs["updated"].append(f"Updated '{file_}'") |
| 184 | + |
| 185 | + # Print logs collected. |
| 186 | + print_logs(logs, args.verbose) |
| 187 | + |
| 188 | + |
| 189 | +if __name__ == "__main__": |
| 190 | + args = parse_args() |
| 191 | + main(args) |
0 commit comments