#14723 Python: Pin grpcio-tools to keep protobuf 6 gencode loadable

grpcio-tools 1.82.0+ (including its 1.82.0 pre-releases) bundles a
protoc that emits protobuf 7.35.x gencode, which no protobuf 6 runtime
can load. Pin grpcio-tools below that (excluding pre-releases via the
'.dev0' trick, since a plain '<1.82' bound already let a 1.82.0rc
release slip through once) and add a CI check that verifies the
generated *_pb2.py files stay within the protobuf floor declared in
pyproject.toml.

Only the load-bearing bounds are kept: protobuf's '>=6.33.5' floor
(must match the emitted gencode) and grpcio-tools' '<1.82.0.dev0'
ceiling. Non-load-bearing bounds (protobuf's '<8', grpcio-tools'
'>=1.81') were dropped since they don't change dependency resolution
or protect against anything the above doesn't already cover.
This commit is contained in:
Magne Sjaastad committed 2026-09-11 05:46:28 -04:00
1 parent d92091cc91
commit d0d472aaa2
3 files changed
+102 -2

No files matched your search

@@ -324,6 +324,13 @@ jobs:
ls cmakebuild/${{ matrix.config.ri-unit-test-path }}
cmakebuild/${{ matrix.config.ri-unit-test-path }}
- name: (Python) Verify protobuf gencode compatibility
if: matrix.config.build-python-module
shell: bash
run: |
cd GrpcInterface/Python
${{ steps.python-path.outputs.PYTHON_EXECUTABLE }} check_gencode_compatibility.py
- name: (Python) Build Python module
if: matrix.config.build-python-module
shell: bash
@@ -0,0 +1,77 @@
"""Check that the generated protobuf code can be loaded by every supported runtime.
The protobuf runtime refuses to load gencode newer than itself, so the gencode
version emitted by grpcio-tools must not exceed the lower bound declared for
protobuf in pyproject.toml. Nothing ties those two together: a grpcio-tools
upgrade can move the gencode a full major version without any edit here, and
the resulting package still installs cleanly and only fails at import time.
"""
import re
import sys
import tomllib
from pathlib import Path
from packaging.requirements import Requirement
from packaging.version import Version
HERE = Path(__file__).parent
GENERATED = HERE / "rips" / "generated"
GENCODE_PATTERN = re.compile(r"^# Protobuf Python Version: (\S+)", re.MULTILINE)
def declared_protobuf_floor() -> Version:
"""Lowest protobuf runtime a user of this package can end up with."""
with (HERE / "pyproject.toml").open("rb") as f:
dependencies = tomllib.load(f)["project"]["dependencies"]
requirements = [Requirement(d) for d in dependencies]
protobuf = next((r for r in requirements if r.name == "protobuf"), None)
if protobuf is None:
sys.exit("protobuf is not declared in [project].dependencies")
floors = [
Version(s.version) for s in protobuf.specifier if s.operator in (">=", "==")
]
if not floors:
sys.exit(
f"protobuf dependency '{protobuf}' has no lower bound to check against"
)
return max(floors)
def gencode_versions() -> dict[str, Version]:
versions = {}
for path in sorted(GENERATED.glob("*_pb2.py")):
match = GENCODE_PATTERN.search(path.read_text(encoding="utf-8"))
if match is None:
sys.exit(f"{path.name} has no gencode version header")
versions[path.name] = Version(match.group(1))
return versions
def main() -> None:
floor = declared_protobuf_floor()
versions = gencode_versions()
if not versions:
sys.exit(f"no generated *_pb2.py files in {GENERATED}, nothing was verified")
too_new = {name: v for name, v in versions.items() if v > floor}
if too_new:
listing = "\n".join(f" {name}: gencode {v}" for name, v in too_new.items())
sys.exit(
f"Generated code is newer than the oldest supported protobuf runtime ({floor}).\n"
f"{listing}\n"
"Pin grpcio-tools to a release emitting older gencode, or raise the protobuf "
"lower bound in pyproject.toml. Note that raising it drops support for runtimes "
"below the new bound."
)
print(
f"{len(versions)} generated files, gencode {max(versions.values())}, runtime floor {floor}: OK"
)
if __name__ == "__main__":
main()
+18 -2
View File
@@ -13,13 +13,29 @@ requires-python = ">=3.11"
authors = [{name = "Ceetron Solutions", email = "info@ceetronsolutions.com"}]
dependencies = [
"grpcio",
"protobuf",
# Floor must match the gencode version emitted by the pinned grpcio-tools
# below: the protobuf runtime refuses to load gencode newer than itself.
# No upper bound: protobuf runtimes stay backward compatible with older
# gencode, so nothing here caps how new a runtime a user can install.
"protobuf>=6.33.5",
"typing_extensions",
]
[project.optional-dependencies]
dev = [
"grpcio-tools",
# 1.82.x bundles a protoc that emits protobuf 7.35.x gencode, which no
# protobuf 6 runtime can load - including its 1.82.0 pre-releases, which
# is why the bound goes below ".dev0" rather than just "<1.82": a plain
# "<1.82" bound already let a 1.82.0 release candidate slip through in
# CI once (pre-release versions sort below their final release).
# No lower bound: the resolver already prefers the newest version
# satisfying the upper bound, so ">=1.81" wasn't changing what gets
# installed, only guarding against a future resolver picking something
# much older if 1.81.x ever became unavailable.
# Generating 6.x gencode keeps rips importable on both 6.x and 7.x
# runtimes, so it stays installable next to packages that cap protobuf
# below 7 (ortools and friends).
"grpcio-tools<1.82.0.dev0",
"pytest",
"mypy",
"types-protobuf",