Skip to content

Commit 91778be

Browse files
[Backport][v1.80.x][Python] New _create method for aio.Metadata (#41888)
Backport of #41678 to v1.80.x. --- This PR adds new create method for `aio.Metadata`, as described in the comment here - #40226 (comment) Reason being `from_tuple` as the name says should actually create only from `tuple` whereas the method actually takes other arguments as well. Hence we introduce the new `_create` method, which is internal as of now. Co-authored-by: Ashesh Vidyut <asheshvidyut@google.com>
1 parent f10b9f2 commit 91778be

5 files changed

Lines changed: 92 additions & 17 deletions

File tree

src/python/grpcio/grpc/aio/_call.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def _create_rpc_error(
182182
) -> AioRpcError:
183183
return AioRpcError(
184184
_common.CYGRPC_STATUS_CODE_TO_STATUS_CODE[status.code()],
185-
Metadata.from_tuple(initial_metadata),
185+
Metadata._create(initial_metadata),
186186
Metadata.from_tuple(status.trailing_metadata()),
187187
details=status.details(),
188188
debug_error_string=status.debug_error_string(),

src/python/grpcio/grpc/aio/_metadata.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,27 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
"""Implementation of the metadata abstraction for gRPC Asyncio Python."""
15+
from __future__ import annotations
16+
1517
from collections import OrderedDict
16-
from collections import abc
17-
from typing import Any, Iterator, List, Optional, Tuple, Union
18+
from collections.abc import Collection
19+
from collections.abc import ItemsView
20+
from collections.abc import Iterable
21+
from collections.abc import Iterator
22+
from collections.abc import KeysView
23+
from collections.abc import Sequence
24+
from collections.abc import ValuesView
25+
from typing import Any, List, Optional, Tuple, Union
1826

1927
from typing_extensions import Self
2028

2129
MetadataKey = str
2230
MetadataValue = Union[str, bytes]
31+
MetadatumType = Tuple[MetadataKey, MetadataValue]
32+
MetadataType = Union["Metadata", Sequence[MetadatumType]]
2333

2434

25-
class Metadata(abc.Collection): # noqa: PLW1641
35+
class Metadata(Collection): # noqa: PLW1641
2636
"""Metadata abstraction for the asynchronous calls and interceptors.
2737
2838
The metadata is a mapping from str -> List[str]
@@ -35,13 +45,28 @@ class Metadata(abc.Collection): # noqa: PLW1641
3545
* Allows partial mutation on the data without recreating the new object from scratch.
3646
"""
3747

38-
def __init__(self, *args: Tuple[MetadataKey, MetadataValue]) -> None:
48+
def __init__(self, *args: MetadatumType) -> None:
3949
self._metadata = OrderedDict()
4050
for md_key, md_value in args:
4151
self.add(md_key, md_value)
4252

4353
@classmethod
44-
def from_tuple(cls, raw_metadata: Union[tuple, Self]):
54+
def from_tuple(cls, raw_metadata: tuple):
55+
# Note: We unintentionally support non-tuple arguments here. We plan
56+
# to emit a DeprecationWarning when a non-tuple type is used.
57+
if raw_metadata:
58+
return cls(*raw_metadata)
59+
return cls()
60+
61+
@classmethod
62+
def _create(
63+
cls,
64+
raw_metadata: Union[None, Self, Iterable[MetadatumType]],
65+
) -> Self:
66+
# TODO(asheshvidyut): Make this method public and encourage people to use it instead
67+
# of `from_tuple` to create metadata from non-tuple types.
68+
if raw_metadata is None:
69+
return Metadata()
4570
if isinstance(raw_metadata, cls):
4671
return raw_metadata
4772
if raw_metadata:
@@ -94,14 +119,14 @@ def __iter__(self) -> Iterator[Tuple[MetadataKey, MetadataValue]]:
94119
for value in values:
95120
yield (key, value)
96121

97-
def keys(self) -> abc.KeysView:
98-
return abc.KeysView(self._metadata)
122+
def keys(self) -> KeysView:
123+
return KeysView(self._metadata)
99124

100-
def values(self) -> abc.ValuesView:
101-
return abc.ValuesView(self._metadata)
125+
def values(self) -> ValuesView:
126+
return ValuesView(self._metadata)
102127

103-
def items(self) -> abc.ItemsView:
104-
return abc.ItemsView(self._metadata)
128+
def items(self) -> ItemsView:
129+
return ItemsView(self._metadata)
105130

106131
def get(
107132
self, key: MetadataKey, default: Optional[MetadataValue] = None

src/python/grpcio/grpc/aio/_typing.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@
2626

2727
from grpc._cython.cygrpc import EOF
2828

29+
# pylint: disable=unused-import
2930
from ._metadata import Metadata
3031
from ._metadata import MetadataKey
32+
from ._metadata import MetadataType
3133
from ._metadata import MetadataValue
34+
from ._metadata import MetadatumType
35+
36+
# pylint: enable=unused-import
3237

3338
RequestType = TypeVar("RequestType")
3439
ResponseType = TypeVar("ResponseType")
3540
SerializingFunction = Callable[[Any], bytes]
3641
DeserializingFunction = Callable[[bytes], Any]
37-
MetadatumType = Tuple[MetadataKey, MetadataValue]
38-
MetadataType = Union[Metadata, Sequence[MetadatumType]]
3942
ChannelArgumentType = Sequence[Tuple[str, Any]]
4043
EOFType = type(EOF)
4144
DoneCallbackType = Callable[[Any], None]

src/python/grpcio_tests/tests_aio/unit/_common.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from grpc.aio._metadata import Metadata
2121
from grpc.aio._typing import MetadataKey
2222
from grpc.aio._typing import MetadataValue
23-
from grpc.aio._typing import MetadatumType
2423
from grpc.experimental import aio
2524

2625
from tests.unit.framework.common import test_constants

src/python/grpcio_tests/tests_aio/unit/_metadata_test.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import grpc
1919
from grpc.experimental import aio
2020
from grpc.experimental.aio import Metadata
21+
import typeguard
2122

2223
from tests_aio.unit import _common
2324
from tests_aio.unit._test_base import AioTestBase
@@ -196,15 +197,62 @@ def test_delete_values(self):
196197

197198
def test_metadata_from_tuple(self):
198199
scenarios = (
199-
(Metadata(), Metadata()),
200200
(self._DEFAULT_DATA, Metadata(*self._DEFAULT_DATA)),
201201
(self._MULTI_ENTRY_DATA, Metadata(*self._MULTI_ENTRY_DATA)),
202-
(Metadata(*self._DEFAULT_DATA), Metadata(*self._DEFAULT_DATA)),
203202
)
204203
for source, expected in scenarios:
205204
with self.subTest(raw_metadata=source, expected=expected):
206205
self.assertEqual(expected, Metadata.from_tuple(source))
207206

207+
@typeguard.suppress_type_checks
208+
def test_metadata_from_tuple_non_tuple(self):
209+
scenarios = (
210+
(None, Metadata()),
211+
(Metadata(), Metadata()),
212+
(Metadata(*self._DEFAULT_DATA), Metadata(*self._DEFAULT_DATA)),
213+
)
214+
for source, expected in scenarios:
215+
with self.subTest(raw_metadata=source, expected=expected):
216+
self.assertEqual(expected, Metadata.from_tuple(source)) # type: ignore
217+
218+
@typeguard.suppress_type_checks
219+
def test_create_invalid_type(self):
220+
221+
# 2. raw_metadata is string
222+
l = "key, value"
223+
with self.assertRaises(ValueError) as container:
224+
Metadata._create(l) # type: ignore
225+
self.assertEqual(
226+
str(container.exception),
227+
"not enough values to unpack (expected 2, got 1)",
228+
)
229+
230+
def test_create(self):
231+
# 1. raw_metadata is None
232+
self.assertEqual(Metadata._create(None), Metadata())
233+
234+
# 2. raw_metadata is Metadata
235+
m = Metadata(("key", "value"))
236+
self.assertIs(Metadata._create(m), m)
237+
238+
# 3. raw_metadata is tuple
239+
t = (("key", "value"),)
240+
self.assertEqual(Metadata._create(t), Metadata(("key", "value")))
241+
242+
# 4. raw_metadata is list
243+
l = [("key", "value")]
244+
self.assertEqual(Metadata._create(l), Metadata(("key", "value")))
245+
246+
# 5. raw_metadata is set
247+
s = {("key", "value")}
248+
self.assertEqual(Metadata._create(s), Metadata(("key", "value")))
249+
250+
# 5. raw_metadata is empty list
251+
self.assertEqual(Metadata._create([]), Metadata())
252+
253+
# 6. raw_metadata is empty tuple
254+
self.assertEqual(Metadata._create(()), Metadata())
255+
208256
def test_keys_values_items(self):
209257
metadata = Metadata(*self._MULTI_ENTRY_DATA)
210258
self.assertEqual(list(metadata.keys()), ["key1", "key2"])

0 commit comments

Comments
 (0)