From e556e8316140535a64d6be4847012e2486706530 Mon Sep 17 00:00:00 2001 From: Denis Averin Date: Thu, 20 Aug 2026 15:27:34 +0700 Subject: [PATCH 1/5] Remove unused OpenAI code conversion script The script was a one-off helper for porting SDK examples between languages. It was never wired into any generate script or workflow and required an openai package that the repo never declared. --- scripts/open_ai_code_convertion.py | 100 ----------------------------- 1 file changed, 100 deletions(-) delete mode 100644 scripts/open_ai_code_convertion.py diff --git a/scripts/open_ai_code_convertion.py b/scripts/open_ai_code_convertion.py deleted file mode 100644 index 9cd0d14e..00000000 --- a/scripts/open_ai_code_convertion.py +++ /dev/null @@ -1,100 +0,0 @@ -import os -from openai import OpenAI -from pathlib import Path -import argparse -import re - -# Environment Variables for OpenAI API Authentication -client = OpenAI( - api_key=os.getenv("OPEN_AI_API_KEY"), - organization=os.getenv("OPEN_AI_ORG_ID"), - project=os.getenv("OPEN_AI_PROJECT_ID"), -) - - -# Helper function to convert file content -def convert_code(content, source_lang, target_lang, target_snippet): - """ - Convert code from one language to another using OpenAI API. - """ - prompt = f""" - Convert the following {source_lang} code to {target_lang}. - Please don't add any comments in code. Don't replace constant class properties with it string values. - Add use statements like in provided snippets in {source_lang}. - Use the provided {target_lang} code snippet as the basis for the conversion: - - {content} - - Here is a code snippet in {target_lang} to use as a reference: - - {target_snippet} - """ - try: - print("Start request to OpenAI") - response = client.chat.completions.create( - model="gpt-4o-mini", - messages=[ - {"role": "system", "content": "You are a helpful code conversion assistant."}, - {"role": "user", "content": prompt}, - ], - ) - print("Getted response") - return response.choices[0].message.content - except Exception as e: - print(f"Error during API call: {e}") - return None - - -# Main function to process files -def process_files(folder, source_ext, target_ext, source_lang, target_lang, target_snippet): - # Pattern to find code snippet in response message - pattern = f"```{target_lang}((.|\s)*?)```" - """ - Walk through the folder and convert files with the specified extension. - """ - for root, _, files in os.walk(folder): - for file in files: - # Process only files with the specified source extension - if file.endswith(source_ext): - file_path = Path(root) / file - print(f"Processing file: {file_path}") - - with open(file_path, "r") as f: - content = f.read() - - converted_content = convert_code(content, source_lang, target_lang, target_snippet) - if converted_content: - matches = re.findall(pattern, converted_content) - if matches: - converted_content = matches[0][0].strip() - target_file_path = file_path.with_suffix(target_ext) - with open(target_file_path, "w") as f: - f.write(converted_content) - print(f"Converted file saved as: {target_file_path}") - else: - print(f"Failed to convert file: {file_path}") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Convert source code files between programming languages.") - parser.add_argument("folder", type=str, help="Path to the folder containing source files.") - parser.add_argument("source_ext", type=str, help="Source file extension (e.g., .js).") - parser.add_argument("target_ext", type=str, help="Target file extension (e.g., .py).") - parser.add_argument("source_lang", type=str, help="Source language (e.g., JavaScript).") - parser.add_argument("target_lang", type=str, help="Target language (e.g., Python).") - parser.add_argument("target_snippet_file", type=str, help="File containing a code snippet in the target language.") - - args = parser.parse_args() - - # Read the target snippet from the provided file - try: - with open(args.target_snippet_file, "r") as snippet_file: - target_snippet = snippet_file.read() - except Exception as e: - print(f"Error reading target snippet file: {e}") - exit(1) - - print("Starting code conversion...") - process_files(args.folder, args.source_ext, args.target_ext, args.source_lang, args.target_lang, target_snippet) - - print("Conversion process completed.") From c3f1e08888ede7c0a3975d6d2b069e3b1688c199 Mon Sep 17 00:00:00 2001 From: Denis Averin Date: Thu, 20 Aug 2026 15:28:39 +0700 Subject: [PATCH 2/5] Add "make mypy" to type check own Python scripts Run mypy in strict mode over everything except submodules, which are generated SDKs checked in their own repos, and annotate the scripts that were still untyped so the target passes. Switch black to --extend-exclude, since --exclude replaces its default exclude list and would make it walk the new .mypy_cache directory. --- .gitignore | 1 + Makefile | 7 +++- codegen/Tools/split-cs-file.py | 11 +++--- codegen/Tools/split-java-file.py | 11 +++--- codegen/Tools/split-php-file.py | 11 +++--- mypy.ini | 5 +++ scripts/new-version.py | 57 ++++++++++++++++++++------------ 7 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 mypy.ini diff --git a/.gitignore b/.gitignore index 36cd4439..eef13e55 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ bin/ codegen/.generated/ codegen/debug*.json obj/ +.mypy_cache/ diff --git a/Makefile b/Makefile index 6f0d7bd2..9b266c96 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,12 @@ format: format-black .PHONY: format-black format-black: - python -m black --line-length=120 --exclude submodules -v . + python -m black --line-length=120 --extend-exclude submodules -v . + +# Type check our own scripts, SDKs in submodules have their own checks +.PHONY: mypy +mypy: + python -m mypy --config-file $(CURDIR)/mypy.ini . .PHONY: update update: diff --git a/codegen/Tools/split-cs-file.py b/codegen/Tools/split-cs-file.py index a8182932..169c2dc4 100644 --- a/codegen/Tools/split-cs-file.py +++ b/codegen/Tools/split-cs-file.py @@ -4,6 +4,7 @@ import errno import os import re +import typing SPLIT_RE = re.compile( r'//\s+[-]+\n//\s+', @@ -11,7 +12,7 @@ ) -def main(src_file, dst_dir): +def main(src_file: typing.IO[str], dst_dir: str) -> None: remaining = src_file.read() try: os.makedirs(dst_dir) @@ -34,14 +35,14 @@ def main(src_file, dst_dir): src_file.close() -def parse_args(): +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("src_file", type=argparse.FileType("rt+")) parser.add_argument("dst_dir", type=str) - args = parser.parse_args() - return vars(args) + return parser.parse_args() if __name__ == "__main__": - main(**parse_args()) + parsed_args = parse_args() + main(parsed_args.src_file, parsed_args.dst_dir) diff --git a/codegen/Tools/split-java-file.py b/codegen/Tools/split-java-file.py index 439cd5fc..7e96e831 100644 --- a/codegen/Tools/split-java-file.py +++ b/codegen/Tools/split-java-file.py @@ -4,6 +4,7 @@ import errno import os import re +import typing SPLIT_RE = re.compile( r'//\s+[-]+\n//\s+', @@ -11,7 +12,7 @@ ) -def main(src_file, dst_dir): +def main(src_file: typing.IO[str], dst_dir: str) -> None: remaining = src_file.read() try: os.makedirs(dst_dir) @@ -34,14 +35,14 @@ def main(src_file, dst_dir): src_file.close() -def parse_args(): +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("src_file", type=argparse.FileType("rt+")) parser.add_argument("dst_dir", type=str) - args = parser.parse_args() - return vars(args) + return parser.parse_args() if __name__ == "__main__": - main(**parse_args()) + parsed_args = parse_args() + main(parsed_args.src_file, parsed_args.dst_dir) diff --git a/codegen/Tools/split-php-file.py b/codegen/Tools/split-php-file.py index fc8aee22..d6765f25 100644 --- a/codegen/Tools/split-php-file.py +++ b/codegen/Tools/split-php-file.py @@ -4,6 +4,7 @@ import errno import os import re +import typing SPLIT_RE = re.compile( r'/[\*]+\s*\n\s\*\s+[-]+\n\s*\*\s+', @@ -11,7 +12,7 @@ ) -def main(src_file, dst_dir): +def main(src_file: typing.IO[str], dst_dir: str) -> None: remaining = src_file.read() try: os.makedirs(dst_dir) @@ -44,14 +45,14 @@ def main(src_file, dst_dir): src_file.close() -def parse_args(): +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("src_file", type=argparse.FileType("rt+")) parser.add_argument("dst_dir", type=str) - args = parser.parse_args() - return vars(args) + return parser.parse_args() if __name__ == "__main__": - main(**parse_args()) + parsed_args = parse_args() + main(parsed_args.src_file, parsed_args.dst_dir) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..cee75887 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +python_version = 3.12 +strict = True +# SDKs are generated code with their own type checks in their own repos +exclude = (^|/)submodules/ diff --git a/scripts/new-version.py b/scripts/new-version.py index 9ae8bdd2..f1a44372 100755 --- a/scripts/new-version.py +++ b/scripts/new-version.py @@ -15,34 +15,43 @@ GO_VERSION_FORMAT = "4.{0}{1:02d}.{2}" +# Normalized release version (year, month, patch), e.g. 26.7.0 +# The patch is optional on the command line and defaults to 0, see `doc/versioning.md` +Version = tuple[int, int, int] +Config = collections.OrderedDict[str, object] -def get_dart_pub_version(new_version): + +def get_dart_pub_version(new_version: Version) -> str: pub_version = str.join(".", map(str, (4,) + new_version[:2])) if new_version[2] > 0: pub_version += "+" + str(new_version[2]) return pub_version -def set_android_version(new_version, filename=os.path.join(BASE_CONFIG_DIR, "config-android.json")): +def set_android_version( + new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-android.json") +) -> None: config = read_config(filename) config["artifactVersion"] = str.join(".", map(str, new_version)) save_config(config, filename) -def set_go_version(new_version, filename=os.path.join(BASE_CONFIG_DIR, "config-go.json")): +def set_go_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-go.json")) -> None: go_version = GO_VERSION_FORMAT.format(*new_version) config = read_config(filename) config["packageVersion"] = go_version save_config(config, filename) -def set_dart_version(new_version, filename=os.path.join(BASE_CONFIG_DIR, "config-dart.json")): +def set_dart_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-dart.json")) -> None: config = read_config(filename) config["pubVersion"] = get_dart_pub_version(new_version) save_config(config, filename) -def update_dart_changelog(new_version, filename=os.path.join(SUBMODULES_DIR, "dart", "CHANGELOG.md")): +def update_dart_changelog( + new_version: Version, filename: str = os.path.join(SUBMODULES_DIR, "dart", "CHANGELOG.md") +) -> None: pub_version = get_dart_pub_version(new_version) entry_header = "## {}".format(pub_version) @@ -57,7 +66,7 @@ def update_dart_changelog(new_version, filename=os.path.join(SUBMODULES_DIR, "da title = "# CHANGELOG\n\n" if changelog.startswith(title): - changelog = title + entry + changelog[len(title):] + changelog = title + entry + changelog[len(title) :] else: changelog = entry + changelog @@ -65,51 +74,53 @@ def update_dart_changelog(new_version, filename=os.path.join(SUBMODULES_DIR, "da wf.write(changelog) -def set_java_version(new_version, filename=os.path.join(BASE_CONFIG_DIR, "config-java.json")): +def set_java_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-java.json")) -> None: config = read_config(filename) config["artifactVersion"] = str.join(".", map(str, new_version)) save_config(config, filename) -def set_net_version(new_version, filename=os.path.join(BASE_CONFIG_DIR, "config-dotnet.json")): +def set_net_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-dotnet.json")) -> None: config = read_config(filename) config["packageVersion"] = str.join(".", map(str, new_version)) save_config(config, filename) -def set_node_version(new_version, filename=os.path.join(BASE_CONFIG_DIR, "config-node.json")): +def set_node_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-node.json")) -> None: config = read_config(filename) config["npmVersion"] = str.join(".", map(str, new_version)) save_config(config, filename) -def set_php_version(new_version, filename=os.path.join(BASE_CONFIG_DIR, "config-php.json")): +def set_php_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-php.json")) -> None: config = read_config(filename) config["artifactVersion"] = str.join(".", map(str, new_version)) save_config(config, filename) -def set_python_version(new_version, filename=os.path.join(BASE_CONFIG_DIR, "config-python.json")): +def set_python_version( + new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-python.json") +) -> None: config = read_config(filename) config["packageVersion"] = str.join(".", map(str, new_version)) save_config(config, filename) -def set_swift_version(new_version, filename=os.path.join(BASE_CONFIG_DIR, "config-swift.json")): +def set_swift_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-swift.json")) -> None: config = read_config(filename) config["packageVersion"] = str.join(".", map(str, new_version)) save_config(config, filename) -def read_config(filename): +def read_config(filename: str) -> Config: # Preserve the on-disk key order so a version bump round-trips to a minimal, # stable diff instead of reshuffling hand-ordered keys. with open(filename, "rb") as rf: - config = json.load(rf, object_pairs_hook=collections.OrderedDict) + config: Config = json.load(rf, object_pairs_hook=collections.OrderedDict) return config -def save_config(config, filename): +def save_config(config: Config, filename: str) -> None: # Keep key order (no sort_keys) and always end with a single trailing newline # so releases produce stable, POSIX-friendly JSON. with open(filename, "wb") as wf: @@ -117,9 +128,13 @@ def save_config(config, filename): wf.write((string.replace("\r", "") + "\n").encode("utf-8")) -def main(new_versions): +def main(new_versions: list[int]) -> None: assert 2 <= len(new_versions) <= 3, "Version format should be: 23 7 or 23 7 1" - new_version = tuple(new_versions + [0] * (3 - len(new_versions))) + new_version: Version = ( + new_versions[0], + new_versions[1], + new_versions[2] if len(new_versions) > 2 else 0, + ) set_android_version(new_version) set_dart_version(new_version) @@ -133,12 +148,12 @@ def main(new_versions): set_swift_version(new_version) -def parse_args(): +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(usage="%s %s" % (sys.argv[0], datetime.today().strftime("%y %m"))) parser.add_argument("new_versions", type=int, nargs="+", help="Use separate int values like: 21 6 1") - args = parser.parse_args() - return vars(args) + + return parser.parse_args() if __name__ == "__main__": - main(**parse_args()) + main(parse_args().new_versions) From f1a1ed7a41d96040acabd80260f02c02f5c8280c Mon Sep 17 00:00:00 2001 From: Denis Averin Date: Thu, 20 Aug 2026 15:29:04 +0700 Subject: [PATCH 3/5] Fix version bump leaving the SDKs on mixed versions Match changelog headers on whole lines. "## 4.26.4" is a substring of both "## 4.26.4+1" and "## 4.26.41", so a patch release or a double-digit month silently suppressed the next entry, leaving a Dart package version with no matching CHANGELOG entry. Validate year, month and patch in argparse and check every file the release touches exists before the first write. "new-version.py 26 13" used to rewrite two configs and then die on ValueError, leaving the remaining seven SDKs on the old version. Update the Swift changelog alongside the Dart one. It had already fallen a release behind the Swift config. --- scripts/new-version.py | 141 +++++++++++++++++++++++++++++------------ 1 file changed, 99 insertions(+), 42 deletions(-) diff --git a/scripts/new-version.py b/scripts/new-version.py index f1a44372..30133483 100755 --- a/scripts/new-version.py +++ b/scripts/new-version.py @@ -7,6 +7,7 @@ import json import os import sys +import typing from datetime import datetime SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -15,12 +16,46 @@ GO_VERSION_FORMAT = "4.{0}{1:02d}.{2}" +ANDROID_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-android.json") +DART_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-dart.json") +DOTNET_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-dotnet.json") +GO_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-go.json") +JAVA_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-java.json") +NODE_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-node.json") +PHP_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-php.json") +PYTHON_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-python.json") +SWIFT_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-swift.json") + +DART_CHANGELOG = os.path.join(SUBMODULES_DIR, "dart", "CHANGELOG.md") +SWIFT_CHANGELOG = os.path.join(SUBMODULES_DIR, "swift", "CHANGELOG.md") + +# Every file a release rewrites, checked up front so a bad run cannot bump only some SDKs +RELEASE_FILES = ( + ANDROID_CONFIG, + DART_CONFIG, + DOTNET_CONFIG, + GO_CONFIG, + JAVA_CONFIG, + NODE_CONFIG, + PHP_CONFIG, + PYTHON_CONFIG, + SWIFT_CONFIG, + DART_CHANGELOG, + SWIFT_CHANGELOG, +) + +CHANGELOG_TITLE = "# CHANGELOG\n\n" + # Normalized release version (year, month, patch), e.g. 26.7.0 # The patch is optional on the command line and defaults to 0, see `doc/versioning.md` Version = tuple[int, int, int] Config = collections.OrderedDict[str, object] +def get_plain_version(new_version: Version) -> str: + return str.join(".", map(str, new_version)) + + def get_dart_pub_version(new_version: Version) -> str: pub_version = str.join(".", map(str, (4,) + new_version[:2])) if new_version[2] > 0: @@ -28,87 +63,99 @@ def get_dart_pub_version(new_version: Version) -> str: return pub_version -def set_android_version( - new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-android.json") -) -> None: +def get_release_month(new_version: Version) -> str: + return datetime(2000 + new_version[0], new_version[1], 1).strftime("%B %Y") + + +def check_release_files(filenames: typing.Iterable[str] = RELEASE_FILES) -> None: + # Fail before the first write, otherwise a missing file leaves the SDKs on mixed versions + missing = sorted(name for name in filenames if not os.path.isfile(name)) + if missing: + raise SystemExit("Cannot bump version, missing files:\n " + "\n ".join(missing)) + + +def set_android_version(new_version: Version, filename: str = ANDROID_CONFIG) -> None: config = read_config(filename) - config["artifactVersion"] = str.join(".", map(str, new_version)) + config["artifactVersion"] = get_plain_version(new_version) save_config(config, filename) -def set_go_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-go.json")) -> None: +def set_go_version(new_version: Version, filename: str = GO_CONFIG) -> None: go_version = GO_VERSION_FORMAT.format(*new_version) config = read_config(filename) config["packageVersion"] = go_version save_config(config, filename) -def set_dart_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-dart.json")) -> None: +def set_dart_version(new_version: Version, filename: str = DART_CONFIG) -> None: config = read_config(filename) config["pubVersion"] = get_dart_pub_version(new_version) save_config(config, filename) -def update_dart_changelog( - new_version: Version, filename: str = os.path.join(SUBMODULES_DIR, "dart", "CHANGELOG.md") -) -> None: - pub_version = get_dart_pub_version(new_version) - entry_header = "## {}".format(pub_version) +def update_changelog(entry_version: str, release_month: str, filename: str) -> None: + entry_header = "## {}".format(entry_version) with open(filename, "r", encoding="utf-8") as rf: changelog = rf.read() - if entry_header in changelog: + # Match whole lines only: "## 4.26.4" is a substring of both "## 4.26.4+1" and "## 4.26.41" + if any(line.rstrip() == entry_header for line in changelog.splitlines()): return - release_month = datetime(2000 + new_version[0], new_version[1], 1).strftime("%B %Y") entry = "{}\n\n* {} Release\n\n".format(entry_header, release_month) - title = "# CHANGELOG\n\n" - if changelog.startswith(title): - changelog = title + entry + changelog[len(title) :] + if changelog.startswith(CHANGELOG_TITLE): + changelog = CHANGELOG_TITLE + entry + changelog[len(CHANGELOG_TITLE) :] else: changelog = entry + changelog - with open(filename, "w", encoding="utf-8") as wf: + with open(filename, "w", encoding="utf-8", newline="\n") as wf: wf.write(changelog) -def set_java_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-java.json")) -> None: +def update_dart_changelog(new_version: Version, filename: str = DART_CHANGELOG) -> None: + update_changelog(get_dart_pub_version(new_version), get_release_month(new_version), filename) + + +def update_swift_changelog(new_version: Version, filename: str = SWIFT_CHANGELOG) -> None: + # Swift releases are tagged vYY.M.P, see `doc/versioning.md` + update_changelog("v" + get_plain_version(new_version), get_release_month(new_version), filename) + + +def set_java_version(new_version: Version, filename: str = JAVA_CONFIG) -> None: config = read_config(filename) - config["artifactVersion"] = str.join(".", map(str, new_version)) + config["artifactVersion"] = get_plain_version(new_version) save_config(config, filename) -def set_net_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-dotnet.json")) -> None: +def set_net_version(new_version: Version, filename: str = DOTNET_CONFIG) -> None: config = read_config(filename) - config["packageVersion"] = str.join(".", map(str, new_version)) + config["packageVersion"] = get_plain_version(new_version) save_config(config, filename) -def set_node_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-node.json")) -> None: +def set_node_version(new_version: Version, filename: str = NODE_CONFIG) -> None: config = read_config(filename) - config["npmVersion"] = str.join(".", map(str, new_version)) + config["npmVersion"] = get_plain_version(new_version) save_config(config, filename) -def set_php_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-php.json")) -> None: +def set_php_version(new_version: Version, filename: str = PHP_CONFIG) -> None: config = read_config(filename) - config["artifactVersion"] = str.join(".", map(str, new_version)) + config["artifactVersion"] = get_plain_version(new_version) save_config(config, filename) -def set_python_version( - new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-python.json") -) -> None: +def set_python_version(new_version: Version, filename: str = PYTHON_CONFIG) -> None: config = read_config(filename) - config["packageVersion"] = str.join(".", map(str, new_version)) + config["packageVersion"] = get_plain_version(new_version) save_config(config, filename) -def set_swift_version(new_version: Version, filename: str = os.path.join(BASE_CONFIG_DIR, "config-swift.json")) -> None: +def set_swift_version(new_version: Version, filename: str = SWIFT_CONFIG) -> None: config = read_config(filename) - config["packageVersion"] = str.join(".", map(str, new_version)) + config["packageVersion"] = get_plain_version(new_version) save_config(config, filename) @@ -116,7 +163,9 @@ def read_config(filename: str) -> Config: # Preserve the on-disk key order so a version bump round-trips to a minimal, # stable diff instead of reshuffling hand-ordered keys. with open(filename, "rb") as rf: - config: Config = json.load(rf, object_pairs_hook=collections.OrderedDict) + config = json.load(rf, object_pairs_hook=collections.OrderedDict) + if not isinstance(config, collections.OrderedDict): + raise SystemExit("Expected a JSON object in {}, got {}".format(filename, type(config).__name__)) return config @@ -128,13 +177,8 @@ def save_config(config: Config, filename: str) -> None: wf.write((string.replace("\r", "") + "\n").encode("utf-8")) -def main(new_versions: list[int]) -> None: - assert 2 <= len(new_versions) <= 3, "Version format should be: 23 7 or 23 7 1" - new_version: Version = ( - new_versions[0], - new_versions[1], - new_versions[2] if len(new_versions) > 2 else 0, - ) +def main(new_version: Version) -> None: + check_release_files() set_android_version(new_version) set_dart_version(new_version) @@ -146,14 +190,27 @@ def main(new_versions: list[int]) -> None: set_php_version(new_version) set_python_version(new_version) set_swift_version(new_version) + update_swift_changelog(new_version) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(usage="%s %s" % (sys.argv[0], datetime.today().strftime("%y %m"))) - parser.add_argument("new_versions", type=int, nargs="+", help="Use separate int values like: 21 6 1") + parser.add_argument("year", type=int, help="Two digit release year, like 26") + parser.add_argument("month", type=int, help="Release month, 1-12") + parser.add_argument("patch", type=int, nargs="?", default=0, help="Optional patch number, defaults to 0") + args = parser.parse_args() + + # Validate before anything is written, a bad month used to blow up mid-release + if not 0 <= args.year <= 99: + parser.error("year must be a two digit number in 0..99, got {}".format(args.year)) + if not 1 <= args.month <= 12: + parser.error("month must be in 1..12, got {}".format(args.month)) + if args.patch < 0: + parser.error("patch must not be negative, got {}".format(args.patch)) - return parser.parse_args() + return args if __name__ == "__main__": - main(parse_args().new_versions) + parsed_args = parse_args() + main((parsed_args.year, parsed_args.month, parsed_args.patch)) From 693be38eaec28d60c0855a391044673b155b289c Mon Sep 17 00:00:00 2001 From: Denis Averin Date: Thu, 20 Aug 2026 15:29:10 +0700 Subject: [PATCH 4/5] Fail file splitters when no parts match The C# and Java splitters exited 0 having split nothing when the copyright banner regex found no matches, so a changed banner would ship un-split API files and surface much later as a compile error. Raise instead, and replace the equivalent assert in the PHP splitter so python -O cannot strip it. --- codegen/Tools/split-cs-file.py | 5 ++++- codegen/Tools/split-java-file.py | 5 ++++- codegen/Tools/split-php-file.py | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/codegen/Tools/split-cs-file.py b/codegen/Tools/split-cs-file.py index 169c2dc4..683b6c03 100644 --- a/codegen/Tools/split-cs-file.py +++ b/codegen/Tools/split-cs-file.py @@ -21,7 +21,10 @@ def main(src_file: typing.IO[str], dst_dir: str) -> None: if e.errno != errno.EEXIST: raise - for match in reversed(list(SPLIT_RE.finditer(remaining))[1:]): + found = list(reversed(list(SPLIT_RE.finditer(remaining))[1:])) + if not found: + raise SystemExit("No parts matching regex '%s' found in %s" % (SPLIT_RE.pattern, src_file.name)) + for match in found: start_pos = match.span()[0] with open(os.path.join(dst_dir, match.groupdict()["file"]), "wt") as out_f: diff --git a/codegen/Tools/split-java-file.py b/codegen/Tools/split-java-file.py index 7e96e831..e1b53d9e 100644 --- a/codegen/Tools/split-java-file.py +++ b/codegen/Tools/split-java-file.py @@ -21,7 +21,10 @@ def main(src_file: typing.IO[str], dst_dir: str) -> None: if e.errno != errno.EEXIST: raise - for match in reversed(list(SPLIT_RE.finditer(remaining))[1:]): + found = list(reversed(list(SPLIT_RE.finditer(remaining))[1:])) + if not found: + raise SystemExit("No parts matching regex '%s' found in %s" % (SPLIT_RE.pattern, src_file.name)) + for match in found: start_pos = match.span()[0] with open(os.path.join(dst_dir, match.groupdict()["file"]), "wt") as out_f: diff --git a/codegen/Tools/split-php-file.py b/codegen/Tools/split-php-file.py index d6765f25..07db204b 100644 --- a/codegen/Tools/split-php-file.py +++ b/codegen/Tools/split-php-file.py @@ -22,7 +22,8 @@ def main(src_file: typing.IO[str], dst_dir: str) -> None: raise found = list(reversed(list(SPLIT_RE.finditer(remaining))[1:])) - assert found, "No parts matching regex '%s' found" % SPLIT_RE.pattern + if not found: + raise SystemExit("No parts matching regex '%s' found in %s" % (SPLIT_RE.pattern, src_file.name)) for match in found: filename = match.groupdict()["file"] classname = os.path.splitext(filename)[0] From 271dd7a30945d356ad184f981677aded677a2c9d Mon Sep 17 00:00:00 2001 From: Denis Averin Date: Thu, 20 Aug 2026 15:50:17 +0700 Subject: [PATCH 5/5] Drive the version bump from a single SDK table The config paths, the release file list and the nine set_*_version functions repeated the same nine SDKs four times over. Adding an SDK meant touching all four. Collapse them into one SDKS tuple. The config is always codegen/config-.json and the changelog, where there is one, is always submodules//CHANGELOG.md, so the name derives both, and the version format defaults to the plain YY.M.P that seven of the nine use. Adding an SDK is now one line. --- scripts/new-version.py | 193 ++++++++++++++++------------------------- 1 file changed, 74 insertions(+), 119 deletions(-) diff --git a/scripts/new-version.py b/scripts/new-version.py index 30133483..69ca4d1e 100755 --- a/scripts/new-version.py +++ b/scripts/new-version.py @@ -15,35 +15,6 @@ SUBMODULES_DIR = os.path.join(SCRIPT_DIR, "..", "submodules") GO_VERSION_FORMAT = "4.{0}{1:02d}.{2}" - -ANDROID_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-android.json") -DART_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-dart.json") -DOTNET_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-dotnet.json") -GO_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-go.json") -JAVA_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-java.json") -NODE_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-node.json") -PHP_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-php.json") -PYTHON_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-python.json") -SWIFT_CONFIG = os.path.join(BASE_CONFIG_DIR, "config-swift.json") - -DART_CHANGELOG = os.path.join(SUBMODULES_DIR, "dart", "CHANGELOG.md") -SWIFT_CHANGELOG = os.path.join(SUBMODULES_DIR, "swift", "CHANGELOG.md") - -# Every file a release rewrites, checked up front so a bad run cannot bump only some SDKs -RELEASE_FILES = ( - ANDROID_CONFIG, - DART_CONFIG, - DOTNET_CONFIG, - GO_CONFIG, - JAVA_CONFIG, - NODE_CONFIG, - PHP_CONFIG, - PYTHON_CONFIG, - SWIFT_CONFIG, - DART_CHANGELOG, - SWIFT_CHANGELOG, -) - CHANGELOG_TITLE = "# CHANGELOG\n\n" # Normalized release version (year, month, patch), e.g. 26.7.0 @@ -63,10 +34,58 @@ def get_dart_pub_version(new_version: Version) -> str: return pub_version +def get_go_version(new_version: Version) -> str: + return GO_VERSION_FORMAT.format(*new_version) + + def get_release_month(new_version: Version) -> str: return datetime(2000 + new_version[0], new_version[1], 1).strftime("%B %Y") +class Sdk(typing.NamedTuple): + """ + One SDK a release bumps. + + The config is always `codegen/config-.json` and the changelog, when there + is one, is always `submodules//CHANGELOG.md`, so the name drives both. + See `doc/versioning.md` for the per-language version formats. + """ + + name: str + version_field: str + get_version: typing.Callable[[Version], str] = get_plain_version + has_changelog: bool = False + # Swift tags its releases vYY.M.P, so its changelog headers carry a prefix + changelog_prefix: str = "" + + @property + def config_filename(self) -> str: + return os.path.join(BASE_CONFIG_DIR, "config-{}.json".format(self.name)) + + @property + def changelog_filename(self) -> str: + return os.path.join(SUBMODULES_DIR, self.name, "CHANGELOG.md") + + +# Adding an SDK is one line here +SDKS = ( + Sdk("android", "artifactVersion"), + Sdk("dart", "pubVersion", get_dart_pub_version, has_changelog=True), + Sdk("dotnet", "packageVersion"), + Sdk("go", "packageVersion", get_go_version), + Sdk("java", "artifactVersion"), + Sdk("node", "npmVersion"), + Sdk("php", "artifactVersion"), + Sdk("python", "packageVersion"), + Sdk("swift", "packageVersion", has_changelog=True, changelog_prefix="v"), +) + +# Every file a release rewrites, checked up front so a bad run cannot bump only some SDKs +RELEASE_FILES = tuple(sdk.config_filename for sdk in SDKS) + tuple( + sdk.changelog_filename for sdk in SDKS if sdk.has_changelog +) + + def check_release_files(filenames: typing.Iterable[str] = RELEASE_FILES) -> None: # Fail before the first write, otherwise a missing file leaves the SDKs on mixed versions missing = sorted(name for name in filenames if not os.path.isfile(name)) @@ -74,27 +93,33 @@ def check_release_files(filenames: typing.Iterable[str] = RELEASE_FILES) -> None raise SystemExit("Cannot bump version, missing files:\n " + "\n ".join(missing)) -def set_android_version(new_version: Version, filename: str = ANDROID_CONFIG) -> None: - config = read_config(filename) - config["artifactVersion"] = get_plain_version(new_version) - save_config(config, filename) +def read_config(filename: str) -> Config: + # Preserve the on-disk key order so a version bump round-trips to a minimal, + # stable diff instead of reshuffling hand-ordered keys. + with open(filename, "rb") as rf: + config = json.load(rf, object_pairs_hook=collections.OrderedDict) + if not isinstance(config, collections.OrderedDict): + raise SystemExit("Expected a JSON object in {}, got {}".format(filename, type(config).__name__)) + return config -def set_go_version(new_version: Version, filename: str = GO_CONFIG) -> None: - go_version = GO_VERSION_FORMAT.format(*new_version) - config = read_config(filename) - config["packageVersion"] = go_version - save_config(config, filename) +def save_config(config: Config, filename: str) -> None: + # Keep key order (no sort_keys) and always end with a single trailing newline + # so releases produce stable, POSIX-friendly JSON. + with open(filename, "wb") as wf: + string = json.dumps(config, indent=4, separators=(",", ": ")) + wf.write((string.replace("\r", "") + "\n").encode("utf-8")) -def set_dart_version(new_version: Version, filename: str = DART_CONFIG) -> None: - config = read_config(filename) - config["pubVersion"] = get_dart_pub_version(new_version) - save_config(config, filename) +def set_version(sdk: Sdk, new_version: Version) -> None: + config = read_config(sdk.config_filename) + config[sdk.version_field] = sdk.get_version(new_version) + save_config(config, sdk.config_filename) -def update_changelog(entry_version: str, release_month: str, filename: str) -> None: - entry_header = "## {}".format(entry_version) +def update_changelog(sdk: Sdk, new_version: Version) -> None: + entry_header = "## {}{}".format(sdk.changelog_prefix, sdk.get_version(new_version)) + filename = sdk.changelog_filename with open(filename, "r", encoding="utf-8") as rf: changelog = rf.read() @@ -103,7 +128,7 @@ def update_changelog(entry_version: str, release_month: str, filename: str) -> N if any(line.rstrip() == entry_header for line in changelog.splitlines()): return - entry = "{}\n\n* {} Release\n\n".format(entry_header, release_month) + entry = "{}\n\n* {} Release\n\n".format(entry_header, get_release_month(new_version)) if changelog.startswith(CHANGELOG_TITLE): changelog = CHANGELOG_TITLE + entry + changelog[len(CHANGELOG_TITLE) :] @@ -114,83 +139,13 @@ def update_changelog(entry_version: str, release_month: str, filename: str) -> N wf.write(changelog) -def update_dart_changelog(new_version: Version, filename: str = DART_CHANGELOG) -> None: - update_changelog(get_dart_pub_version(new_version), get_release_month(new_version), filename) - - -def update_swift_changelog(new_version: Version, filename: str = SWIFT_CHANGELOG) -> None: - # Swift releases are tagged vYY.M.P, see `doc/versioning.md` - update_changelog("v" + get_plain_version(new_version), get_release_month(new_version), filename) - - -def set_java_version(new_version: Version, filename: str = JAVA_CONFIG) -> None: - config = read_config(filename) - config["artifactVersion"] = get_plain_version(new_version) - save_config(config, filename) - - -def set_net_version(new_version: Version, filename: str = DOTNET_CONFIG) -> None: - config = read_config(filename) - config["packageVersion"] = get_plain_version(new_version) - save_config(config, filename) - - -def set_node_version(new_version: Version, filename: str = NODE_CONFIG) -> None: - config = read_config(filename) - config["npmVersion"] = get_plain_version(new_version) - save_config(config, filename) - - -def set_php_version(new_version: Version, filename: str = PHP_CONFIG) -> None: - config = read_config(filename) - config["artifactVersion"] = get_plain_version(new_version) - save_config(config, filename) - - -def set_python_version(new_version: Version, filename: str = PYTHON_CONFIG) -> None: - config = read_config(filename) - config["packageVersion"] = get_plain_version(new_version) - save_config(config, filename) - - -def set_swift_version(new_version: Version, filename: str = SWIFT_CONFIG) -> None: - config = read_config(filename) - config["packageVersion"] = get_plain_version(new_version) - save_config(config, filename) - - -def read_config(filename: str) -> Config: - # Preserve the on-disk key order so a version bump round-trips to a minimal, - # stable diff instead of reshuffling hand-ordered keys. - with open(filename, "rb") as rf: - config = json.load(rf, object_pairs_hook=collections.OrderedDict) - if not isinstance(config, collections.OrderedDict): - raise SystemExit("Expected a JSON object in {}, got {}".format(filename, type(config).__name__)) - return config - - -def save_config(config: Config, filename: str) -> None: - # Keep key order (no sort_keys) and always end with a single trailing newline - # so releases produce stable, POSIX-friendly JSON. - with open(filename, "wb") as wf: - string = json.dumps(config, indent=4, separators=(",", ": ")) - wf.write((string.replace("\r", "") + "\n").encode("utf-8")) - - def main(new_version: Version) -> None: check_release_files() - set_android_version(new_version) - set_dart_version(new_version) - update_dart_changelog(new_version) - set_go_version(new_version) - set_java_version(new_version) - set_net_version(new_version) - set_node_version(new_version) - set_php_version(new_version) - set_python_version(new_version) - set_swift_version(new_version) - update_swift_changelog(new_version) + for sdk in SDKS: + set_version(sdk, new_version) + if sdk.has_changelog: + update_changelog(sdk, new_version) def parse_args() -> argparse.Namespace: