diff --git a/google/api_core/protobuf_helpers.py b/google/api_core/protobuf_helpers.py index 365ef25c..8aff79aa 100644 --- a/google/api_core/protobuf_helpers.py +++ b/google/api_core/protobuf_helpers.py @@ -357,6 +357,13 @@ def _field_mask_helper(original, modified, current=""): def _get_path(current, name): + # gapic-generator-python appends underscores to field names + # that collide with python keywords. + # `_` is stripped away as it is not possible to + # natively define a field with a trailing underscore in protobuf. + # APIs will reject field masks if fields have trailing underscores. + # See https://github.com/googleapis/python-api-core/issues/227 + name = name.rstrip("_") if not current: return name return "%s.%s" % (current, name) diff --git a/noxfile.py b/noxfile.py index 25609920..2f11137d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -98,9 +98,10 @@ def default(session): ] pytest_args.extend(session.posargs) - # Inject AsyncIO content, if version >= 3.6. + # Inject AsyncIO content and proto-plus, if version >= 3.6. + # proto-plus is needed for a field mask test in test_protobuf_helpers.py if _greater_or_equal_than_36(session.python): - session.install("asyncmock", "pytest-asyncio") + session.install("asyncmock", "pytest-asyncio", "proto-plus") pytest_args.append("--cov=tests.asyncio") pytest_args.append(os.path.join("tests", "asyncio")) diff --git a/tests/unit/test_protobuf_helpers.py b/tests/unit/test_protobuf_helpers.py index db972383..3df45df5 100644 --- a/tests/unit/test_protobuf_helpers.py +++ b/tests/unit/test_protobuf_helpers.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import sys + import pytest from google.api import http_pb2 @@ -472,3 +474,45 @@ def test_field_mask_different_level_diffs(): "alpha", "red", ] + + +@pytest.mark.skipif( + sys.version_info.major == 2, + reason="Field names with trailing underscores can only be created" + "through proto-plus, which is Python 3 only.", +) +def test_field_mask_ignore_trailing_underscore(): + import proto + + class Foo(proto.Message): + type_ = proto.Field(proto.STRING, number=1) + input_config = proto.Field(proto.STRING, number=2) + + modified = Foo(type_="bar", input_config="baz") + + assert sorted(protobuf_helpers.field_mask(None, Foo.pb(modified)).paths) == [ + "input_config", + "type", + ] + + +@pytest.mark.skipif( + sys.version_info.major == 2, + reason="Field names with trailing underscores can only be created" + "through proto-plus, which is Python 3 only.", +) +def test_field_mask_ignore_trailing_underscore_with_nesting(): + import proto + + class Bar(proto.Message): + class Baz(proto.Message): + input_config = proto.Field(proto.STRING, number=1) + + type_ = proto.Field(Baz, number=1) + + modified = Bar() + modified.type_.input_config = "foo" + + assert sorted(protobuf_helpers.field_mask(None, Bar.pb(modified)).paths) == [ + "type.input_config", + ]