Skip to content

Commit fb78c09

Browse files
jakobcornellcopybara-github
authored andcommitted
Add constructing unpack routine to Python Protobuf Any API.
The main benefit here is enabling unpacking in an expression context whereas previously multiple statements were required. The return/raise API is also more idiomatic to Python than the current one using an out parameter and status return. PiperOrigin-RevId: 731911306
1 parent d6341a5 commit fb78c09

File tree

2 files changed

+38
-9
lines changed

2 files changed

+38
-9
lines changed

python/google/protobuf/any.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77

88
"""Contains the Any helper APIs."""
99

10-
from typing import Optional
10+
from typing import Optional, TypeVar
1111

1212
from google.protobuf import descriptor
1313
from google.protobuf.message import Message
1414

1515
from google.protobuf.any_pb2 import Any
1616

1717

18+
_MessageT = TypeVar('_MessageT', bound=Message)
19+
20+
1821
def pack(
1922
msg: Message,
2023
type_url_prefix: Optional[str] = 'type.googleapis.com/',
@@ -31,6 +34,17 @@ def unpack(any_msg: Any, msg: Message) -> bool:
3134
return any_msg.Unpack(msg=msg)
3235

3336

37+
def unpack_as(any_msg: Any, message_type: type[_MessageT]) -> _MessageT:
38+
unpacked = message_type()
39+
if unpack(any_msg, unpacked):
40+
return unpacked
41+
else:
42+
raise TypeError(
43+
f'Attempted to unpack {type_name(any_msg)} to'
44+
f' {message_type.__qualname__}'
45+
)
46+
47+
3448
def type_name(any_msg: Any) -> str:
3549
return any_msg.TypeName()
3650

python/google/protobuf/internal/any_test.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import unittest
1212

13-
from google.protobuf import any
13+
from google.protobuf import any as proto_any
1414

1515
from google.protobuf import any_pb2
1616
from google.protobuf import unittest_pb2
@@ -20,27 +20,42 @@ class AnyTest(unittest.TestCase):
2020

2121
def test_pack_unpack(self):
2222
all_types = unittest_pb2.TestAllTypes()
23-
any_msg = any.pack(all_types)
23+
any_msg = proto_any.pack(all_types)
2424
all_descriptor = all_types.DESCRIPTOR
2525
self.assertEqual(
2626
any_msg.type_url, 'type.googleapis.com/%s' % all_descriptor.full_name
2727
)
28+
29+
# Any can be successfully unpacked to the correct message type.
2830
unpacked_message = unittest_pb2.TestAllTypes()
29-
self.assertTrue(any.unpack(any_msg, unpacked_message))
31+
self.assertTrue(proto_any.unpack(any_msg, unpacked_message))
32+
33+
proto_any.unpack_as(any_msg, unittest_pb2.TestAllTypes)
34+
35+
# Any can't be unpacked to an incorrect message type.
36+
self.assertFalse(
37+
proto_any.unpack(any_msg, unittest_pb2.TestAllTypes.NestedMessage())
38+
)
39+
40+
with self.assertRaises(TypeError) as catcher:
41+
proto_any.unpack_as(any_msg, unittest_pb2.TestAllTypes.NestedMessage)
42+
self.assertIn('Attempted to unpack', catcher.exception.args[0])
3043

3144
def test_type_name(self):
3245
all_types = unittest_pb2.TestAllTypes()
33-
any_msg = any.pack(all_types)
34-
self.assertEqual(any.type_name(any_msg), 'proto2_unittest.TestAllTypes')
46+
any_msg = proto_any.pack(all_types)
47+
self.assertEqual(
48+
proto_any.type_name(any_msg), 'proto2_unittest.TestAllTypes'
49+
)
3550

3651
def test_is_type(self):
3752
all_types = unittest_pb2.TestAllTypes()
38-
any_msg = any.pack(all_types)
53+
any_msg = proto_any.pack(all_types)
3954
all_descriptor = all_types.DESCRIPTOR
40-
self.assertTrue(any.is_type(any_msg, all_descriptor))
55+
self.assertTrue(proto_any.is_type(any_msg, all_descriptor))
4156

4257
empty_any = any_pb2.Any()
43-
self.assertFalse(any.is_type(empty_any, all_descriptor))
58+
self.assertFalse(proto_any.is_type(empty_any, all_descriptor))
4459

4560

4661
if __name__ == '__main__':

0 commit comments

Comments
 (0)