Skip to content

Commit a532f52

Browse files
Extend gencode compatibility support back to 3.20.0 (#22423)
* Extend gencode compatibility support back to 3.20.0 #test-continuous This adds 2 shims for some previous breakages, and compatibility tests to avoid further accidents. This will be backported to 25.x and 29.x. PiperOrigin-RevId: 776414924 * Remove 30.x parameterization * Fix requirements install * Desynchronize upb/pure tests * Update comment
1 parent 09082d4 commit a532f52

File tree

6 files changed

+166
-81
lines changed

6 files changed

+166
-81
lines changed

.github/workflows/test_upb.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,63 @@ jobs:
306306
for test in $TESTS; do
307307
python -m unittest -v $test
308308
done
309+
310+
test_python_compatibility:
311+
name: ${{ inputs.continuous-prefix }} Test Python Compatibility (${{ matrix.implementation }} - ${{ matrix.version }})
312+
needs: build_wheels
313+
strategy:
314+
fail-fast: false # Don't cancel all jobs if one fails.
315+
matrix:
316+
# Test the first release of each python major version, plus some extra coverage for 3.x.y
317+
# since it predates our breaking change policies.
318+
# Major versions: 4.21.0, 5.26.0
319+
version: ["3.18.0", "3.20.0", "21.0", "26.0"]
320+
implementation: ["Pure", "upb"]
321+
include:
322+
# We don't support 3.18.0 anymore, so our infra should show a failure.
323+
- version: "3.18.0"
324+
fail: true
325+
runs-on: ubuntu-latest
326+
if: ${{ github.event_name != 'pull_request_target' }}
327+
steps:
328+
- name: Checkout pending changes
329+
if: ${{ inputs.continuous-run }}
330+
uses: protocolbuffers/protobuf-ci/checkout@v4
331+
with:
332+
ref: ${{ inputs.safe-checkout }}
333+
- name: Download Wheels
334+
if: ${{ inputs.continuous-run }}
335+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 #4.1.8
336+
with:
337+
name: python-wheels
338+
path: wheels
339+
- name: Delete Binary Wheels (pure python only)
340+
if: ${{ inputs.continuous-run && matrix.implementation == 'Pure' }}
341+
run: find wheels -type f | grep -v none-any | xargs rm
342+
- name: Setup Python
343+
if: ${{ inputs.continuous-run }}
344+
uses: actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1 # v4.7.0
345+
with:
346+
python-version: 3.13
347+
- name: Setup Python venv
348+
if: ${{ inputs.continuous-run }}
349+
run: |
350+
python -m pip install --upgrade pip
351+
python -m venv env
352+
source env/bin/activate
353+
echo "VIRTUAL ENV:" $VIRTUAL_ENV
354+
- name: Install numpy
355+
if: ${{ inputs.continuous-run }}
356+
run: pip install numpy
357+
- name: Install Protobuf Wheels
358+
if: ${{ inputs.continuous-run }}
359+
run: pip install -vvv --no-index --find-links wheels protobuf protobuftests
360+
- name: Make the test script executable
361+
if: ${{ inputs.continuous-run }}
362+
run: chmod +x ci/python_compatibility.sh
363+
- name: Run compatibility tests
364+
if: ${{ inputs.continuous-run && !matrix.fail }}
365+
run: ci/python_compatibility.sh ${{ matrix.version }} ${{ matrix.implementation == 'upb' && 'pb_unit_tests.*py$' || '_test.py' }}
366+
- name: Run failing compatibility tests
367+
if: ${{ inputs.continuous-run && matrix.fail }}
368+
run: (! ci/python_compatibility.sh ${{ matrix.version }} ${{ matrix.implementation == 'upb' && 'pb_unit_tests.*py$' || '_test.py' }})

ci/python_compatibility.sh

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/bin/bash
2+
3+
set -ex
4+
5+
PROTOC_VERSION=$1
6+
TEST_FILTER=$2
7+
PROTOC_DOWNLOAD=https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOC_VERSION/protoc-$PROTOC_VERSION-linux-x86_64.zip
8+
PY_SITE_PACKAGES=$(python -c 'import site; print(site.getsitepackages()[0])')
9+
10+
rm -rf protoc-old
11+
mkdir protoc-old
12+
pushd protoc-old
13+
wget $PROTOC_DOWNLOAD
14+
unzip *.zip
15+
PROTOC=$(pwd)/bin/protoc
16+
popd
17+
18+
# protoc prior to 28.0 doesn't support inf/nan option values.
19+
sed -i 's/\(inf\|nan\)/0/g' src/google/protobuf/unittest_custom_options.proto
20+
21+
bazel build //python:copied_test_proto_files //python:copied_wkt_proto_files
22+
23+
COMPAT_COPIED_PROTOS=(
24+
# Well-known types give good build coverage
25+
any
26+
api
27+
duration
28+
empty
29+
field_mask
30+
source_context
31+
struct
32+
timestamp
33+
type
34+
wrappers
35+
# These protos are used in tests of custom options handling.
36+
unittest_custom_options
37+
unittest_import
38+
)
39+
40+
for proto in ${COMPAT_COPIED_PROTOS[@]}; do
41+
$PROTOC --python_out=$PY_SITE_PACKAGES \
42+
bazel-bin/python/google/protobuf/$proto.proto \
43+
-Ibazel-bin/python
44+
done
45+
46+
COMPAT_PROTOS=(
47+
# All protos without transitive dependencies on edition 2023+.
48+
descriptor_pool_test1
49+
descriptor_pool_test2
50+
factory_test1
51+
factory_test2
52+
file_options_test
53+
import_test_package/import_public
54+
import_test_package/import_public_nested
55+
import_test_package/inner
56+
import_test_package/outer
57+
message_set_extensions
58+
missing_enum_values
59+
more_extensions
60+
more_extensions_dynamic
61+
more_messages
62+
no_package
63+
packed_field_test
64+
test_bad_identifiers
65+
test_proto2
66+
test_proto3_optional
67+
)
68+
69+
for proto in ${COMPAT_PROTOS[@]}; do
70+
$PROTOC --python_out=$PY_SITE_PACKAGES \
71+
python/google/protobuf/internal/$proto.proto \
72+
-Ipython
73+
done
74+
75+
# Exclude pybind11 tests because they require C++ code that doesn't ship with
76+
# our test wheels.
77+
TEST_EXCLUSIONS="_pybind11_test.py"
78+
TESTS=$(pip show -f protobuftests | grep -i $TEST_FILTER | grep --invert-match $TEST_EXCLUSIONS | sed 's,/,.,g' | sed -E 's,.py$,,g')
79+
python -m unittest -v ${TESTS[@]}

python/google/protobuf/descriptor.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,16 @@ class of the options message. The name of the class is required in case
128128
"""
129129
self._features = None
130130
self.file = file
131-
self._options = options
132-
self._loaded_options = None
131+
self._original_options = options
132+
# These two fields are duplicated as a compatibility shim for old gencode
133+
# that resets them. In 26.x (cl/580304039) we renamed _options to,
134+
# _loaded_options breaking backwards compatibility.
135+
self._options = self._loaded_options = None
133136
self._options_class_name = options_class_name
134137
self._serialized_options = serialized_options
135138

136139
# Does this descriptor have non-default options?
137-
self.has_options = (self._options is not None) or (
140+
self.has_options = (self._original_options is not None) or (
138141
self._serialized_options is not None
139142
)
140143

@@ -186,7 +189,8 @@ def _ResolveFeatures(self, edition, raw_options):
186189

187190
def _LazyLoadOptions(self):
188191
"""Lazily initializes descriptor options towards the end of the build."""
189-
if self._loaded_options:
192+
if self._options and self._loaded_options == self._options:
193+
# If neither has been reset by gencode, use the cache.
190194
return
191195

192196
# pylint: disable=g-import-not-at-top
@@ -206,12 +210,12 @@ def _LazyLoadOptions(self):
206210
descriptor_pb2.Edition.Value(edition), options_class()
207211
)
208212
with _lock:
209-
self._loaded_options = options_class()
213+
self._options = self._loaded_options = options_class()
210214
if not self._features:
211215
self._features = features
212216
else:
213217
if not self._serialized_options:
214-
options = self._options
218+
options = self._original_options
215219
else:
216220
options = _ParseOptions(options_class(), self._serialized_options)
217221

@@ -220,13 +224,13 @@ def _LazyLoadOptions(self):
220224
descriptor_pb2.Edition.Value(edition), options
221225
)
222226
with _lock:
223-
self._loaded_options = options
227+
self._options = self._loaded_options = options
224228
if not self._features:
225229
self._features = features
226230
if options.HasField('features'):
227231
options.ClearField('features')
228232
if not options.SerializeToString():
229-
self._loaded_options = options_class()
233+
self._options = self._loaded_options = options_class()
230234
self.has_options = False
231235

232236
def GetOptions(self):
@@ -235,9 +239,10 @@ def GetOptions(self):
235239
Returns:
236240
The options set on this descriptor.
237241
"""
238-
if not self._loaded_options:
242+
# If either has been reset by gencode, reload options.
243+
if not self._options or not self._loaded_options:
239244
self._LazyLoadOptions()
240-
return self._loaded_options
245+
return self._options
241246

242247

243248
class _NestedDescriptorBase(DescriptorBase):

python/google/protobuf/internal/python_message.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,13 @@ def _AddPropertiesForExtensions(descriptor, cls):
822822
pool = descriptor.file.pool
823823

824824
def _AddStaticMethods(cls):
825+
826+
def RegisterExtension(_):
827+
"""no-op to keep generated code <=4.23 working with new runtimes."""
828+
# This was originally removed in 5.26 (cl/595989309).
829+
pass
830+
831+
cls.RegisterExtension = staticmethod(RegisterExtension)
825832
def FromString(s):
826833
message = cls()
827834
message.MergeFromString(s)

python/google/protobuf/internal/runtime_version_test.py

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,6 @@ def test_cross_domain_disallowed(self):
3636
gencode_domain, 1, 2, 3, '', 'foo.proto'
3737
)
3838

39-
def test_mismatched_major_disallowed(self):
40-
gencode_version_string = f'{1}.{runtime_version.MINOR}.{runtime_version.PATCH}{runtime_version.SUFFIX}'
41-
runtime_version_string = f'{runtime_version.MAJOR}.{runtime_version.MINOR}.{runtime_version.PATCH}{runtime_version.SUFFIX}'
42-
with self.assertRaisesRegex(
43-
runtime_version.VersionError,
44-
'Detected mismatched Protobuf Gencode/Runtime major versions when'
45-
f' loading foo.proto: gencode {gencode_version_string} runtime'
46-
f' {runtime_version_string}',
47-
):
48-
runtime_version.ValidateProtobufRuntimeVersion(
49-
runtime_version.DOMAIN,
50-
1,
51-
runtime_version.MINOR,
52-
runtime_version.PATCH,
53-
runtime_version.SUFFIX,
54-
'foo.proto',
55-
)
56-
5739
def test_same_version_allowed(self):
5840
runtime_version.ValidateProtobufRuntimeVersion(
5941
runtime_version.DOMAIN,
@@ -103,34 +85,6 @@ def test_older_runtime_version_disallowed(self):
10385
'foo.proto',
10486
)
10587

106-
def test_different_suffix_disallowed(self):
107-
with self.assertRaisesRegex(
108-
runtime_version.VersionError,
109-
'Detected mismatched Protobuf Gencode/Runtime version suffixes when'
110-
' loading foo.proto',
111-
):
112-
runtime_version.ValidateProtobufRuntimeVersion(
113-
runtime_version.DOMAIN,
114-
runtime_version.MAJOR,
115-
runtime_version.MINOR,
116-
runtime_version.PATCH,
117-
'-noflag',
118-
'foo.proto',
119-
)
120-
121-
def test_gencode_one_major_version_older_warning(self):
122-
with self.assertWarnsRegex(
123-
Warning, expected_regex='is exactly one major version older'
124-
):
125-
runtime_version.ValidateProtobufRuntimeVersion(
126-
runtime_version.DOMAIN,
127-
runtime_version.MAJOR - 1,
128-
runtime_version.MINOR,
129-
runtime_version.PATCH,
130-
runtime_version.SUFFIX,
131-
'foo.proto',
132-
)
133-
13488

13589
if __name__ == '__main__':
13690
unittest.main()

python/google/protobuf/runtime_version.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -92,33 +92,13 @@ def ValidateProtobufRuntimeVersion(
9292
' Cross-domain usage of Protobuf is not supported.'
9393
)
9494

95-
if gen_major != MAJOR:
96-
if gen_major == MAJOR - 1:
97-
if _warning_count < _MAX_WARNING_COUNT:
98-
warnings.warn(
99-
'Protobuf gencode version %s is exactly one major version older'
100-
' than the runtime version %s at %s. Please update the gencode to'
101-
' avoid compatibility violations in the next runtime release.'
102-
% (gen_version, version, location)
103-
)
104-
_warning_count += 1
105-
else:
106-
_ReportVersionError(
107-
'Detected mismatched Protobuf Gencode/Runtime major versions when'
108-
f' loading {location}: gencode {gen_version} runtime {version}.'
109-
f' Same major version is required. {error_prompt}'
110-
)
111-
112-
if MINOR < gen_minor or (MINOR == gen_minor and PATCH < gen_patch):
95+
if (
96+
MAJOR < gen_major
97+
or (MAJOR == gen_major and MINOR < gen_minor)
98+
or (MAJOR == gen_major and MINOR == gen_minor and PATCH < gen_patch)
99+
):
113100
_ReportVersionError(
114101
'Detected incompatible Protobuf Gencode/Runtime versions when loading'
115102
f' {location}: gencode {gen_version} runtime {version}. Runtime version'
116103
f' cannot be older than the linked gencode version. {error_prompt}'
117104
)
118-
119-
if gen_suffix != SUFFIX:
120-
_ReportVersionError(
121-
'Detected mismatched Protobuf Gencode/Runtime version suffixes when'
122-
f' loading {location}: gencode {gen_version} runtime {version}.'
123-
f' Version suffixes must be the same. {error_prompt}'
124-
)

0 commit comments

Comments
 (0)