Skip to content

Commit 1af7fd4

Browse files
mkruskal-googlecopybara-github
authored andcommitted
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
1 parent 4ba9733 commit 1af7fd4

File tree

6 files changed

+171
-81
lines changed

6 files changed

+171
-81
lines changed

.github/workflows/test_upb.yml

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

ci/python_compatibility.sh

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

831831
def _AddStaticMethods(cls):
832+
833+
def RegisterExtension(_):
834+
"""no-op to keep generated code <=4.23 working with new runtimes."""
835+
# This was originally removed in 5.26 (cl/595989309).
836+
pass
837+
838+
cls.RegisterExtension = staticmethod(RegisterExtension)
832839
def FromString(s):
833840
message = cls()
834841
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)