Skip to content

Commit d3bb400

Browse files
committed
Add support for converting collections of builtin types
Signed-off-by: Joe Friedrichsen <[email protected]>
1 parent 7370b6a commit d3bb400

File tree

7 files changed

+386
-4
lines changed

7 files changed

+386
-4
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
syntax = "proto3";
2+
3+
package ni.pythonpanel.v1;
4+
5+
option cc_enable_arenas = true;
6+
option csharp_namespace = "NationalInstruments.PythonPanel.V1";
7+
option go_package = "pythonpanelv1";
8+
option java_multiple_files = true;
9+
option java_outer_classname = "PythonPanelServiceProto";
10+
option java_package = "com.ni.pythonpanel.v1";
11+
option objc_class_prefix = "NIPP";
12+
option php_namespace = "NI\\PythonPanel\\V1";
13+
option ruby_package = "NI::PythonPanel::V1";
14+
15+
message BoolCollection {
16+
repeated bool values = 1;
17+
}
18+
19+
message ByteStringCollection {
20+
repeated bytes values = 1;
21+
}
22+
23+
message FloatCollection {
24+
repeated double values = 1;
25+
}
26+
27+
message IntCollection {
28+
repeated sint64 values = 1;
29+
}
30+
31+
message StringCollection {
32+
repeated string values = 1;
33+
}

src/ni/pythonpanel/v1/python_panel_types_pb2.py

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
@generated by mypy-protobuf. Do not edit manually!
3+
isort:skip_file
4+
"""
5+
6+
import builtins
7+
import collections.abc
8+
import google.protobuf.descriptor
9+
import google.protobuf.internal.containers
10+
import google.protobuf.message
11+
import typing
12+
13+
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
14+
15+
@typing.final
16+
class BoolCollection(google.protobuf.message.Message):
17+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
18+
19+
VALUES_FIELD_NUMBER: builtins.int
20+
@property
21+
def values(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bool]: ...
22+
def __init__(
23+
self,
24+
*,
25+
values: collections.abc.Iterable[builtins.bool] | None = ...,
26+
) -> None: ...
27+
def ClearField(self, field_name: typing.Literal["values", b"values"]) -> None: ...
28+
29+
global___BoolCollection = BoolCollection
30+
31+
@typing.final
32+
class ByteStringCollection(google.protobuf.message.Message):
33+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
34+
35+
VALUES_FIELD_NUMBER: builtins.int
36+
@property
37+
def values(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bytes]: ...
38+
def __init__(
39+
self,
40+
*,
41+
values: collections.abc.Iterable[builtins.bytes] | None = ...,
42+
) -> None: ...
43+
def ClearField(self, field_name: typing.Literal["values", b"values"]) -> None: ...
44+
45+
global___ByteStringCollection = ByteStringCollection
46+
47+
@typing.final
48+
class FloatCollection(google.protobuf.message.Message):
49+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
50+
51+
VALUES_FIELD_NUMBER: builtins.int
52+
@property
53+
def values(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]: ...
54+
def __init__(
55+
self,
56+
*,
57+
values: collections.abc.Iterable[builtins.float] | None = ...,
58+
) -> None: ...
59+
def ClearField(self, field_name: typing.Literal["values", b"values"]) -> None: ...
60+
61+
global___FloatCollection = FloatCollection
62+
63+
@typing.final
64+
class IntCollection(google.protobuf.message.Message):
65+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
66+
67+
VALUES_FIELD_NUMBER: builtins.int
68+
@property
69+
def values(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
70+
def __init__(
71+
self,
72+
*,
73+
values: collections.abc.Iterable[builtins.int] | None = ...,
74+
) -> None: ...
75+
def ClearField(self, field_name: typing.Literal["values", b"values"]) -> None: ...
76+
77+
global___IntCollection = IntCollection
78+
79+
@typing.final
80+
class StringCollection(google.protobuf.message.Message):
81+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
82+
83+
VALUES_FIELD_NUMBER: builtins.int
84+
@property
85+
def values(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ...
86+
def __init__(
87+
self,
88+
*,
89+
values: collections.abc.Iterable[builtins.str] | None = ...,
90+
) -> None: ...
91+
def ClearField(self, field_name: typing.Literal["values", b"values"]) -> None: ...
92+
93+
global___StringCollection = StringCollection
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2+
"""Client and server classes corresponding to protobuf-defined services."""
3+
import grpc
4+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
@generated by mypy-protobuf. Do not edit manually!
3+
isort:skip_file
4+
"""
5+
6+
import abc
7+
import collections.abc
8+
import grpc
9+
import grpc.aio
10+
import typing
11+
12+
_T = typing.TypeVar("_T")
13+
14+
class _MaybeAsyncIterator(collections.abc.AsyncIterator[_T], collections.abc.Iterator[_T], metaclass=abc.ABCMeta): ...
15+
16+
class _ServicerContext(grpc.ServicerContext, grpc.aio.ServicerContext): # type: ignore[misc, type-arg]
17+
...

src/nipanel/_converters.py

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
import logging
66
from abc import ABC, abstractmethod
7+
from collections.abc import Collection
78
from typing import Any, Generic, Type, TypeVar
89

910
from google.protobuf import any_pb2, wrappers_pb2
1011
from google.protobuf.message import Message
12+
from ni.pythonpanel.v1 import python_panel_types_pb2
1113

1214
_TPythonType = TypeVar("_TPythonType")
1315
_TProtobufType = TypeVar("_TProtobufType", bound=Message)
@@ -167,15 +169,139 @@ def to_python_value(self, protobuf_value: wrappers_pb2.StringValue) -> str:
167169
return protobuf_value.value
168170

169171

172+
class BoolCollectionConverter(Converter[Collection[bool], python_panel_types_pb2.BoolCollection]):
173+
"""A converter for a Collection of bools."""
174+
175+
@property
176+
def python_typename(self) -> str:
177+
"""The Python type that this converter handles."""
178+
return f"{Collection.__name__}.{bool.__name__}"
179+
180+
@property
181+
def protobuf_message(self) -> Type[python_panel_types_pb2.BoolCollection]:
182+
"""The type-specific protobuf message for the Python type."""
183+
return python_panel_types_pb2.BoolCollection
184+
185+
def to_protobuf_message(self, python_value: Collection[bool]) -> python_panel_types_pb2.BoolCollection:
186+
"""Convert the Python collection of bools to a protobuf python_panel_types_pb2.BoolCollection."""
187+
return self.protobuf_message(values=python_value)
188+
189+
def to_python_value(self, protobuf_value: python_panel_types_pb2.BoolCollection) -> Collection[bool]:
190+
"""Convert the protobuf message to a Python collection of bools."""
191+
return list(protobuf_value.values)
192+
193+
194+
class BytesCollectionConverter(Converter[Collection[bytes], python_panel_types_pb2.ByteStringCollection]):
195+
"""A converter for a Collection of byte strings."""
196+
197+
@property
198+
def python_typename(self) -> str:
199+
"""The Python type that this converter handles."""
200+
return f"{Collection.__name__}.{bytes.__name__}"
201+
202+
@property
203+
def protobuf_message(self) -> Type[python_panel_types_pb2.ByteStringCollection]:
204+
"""The type-specific protobuf message for the Python type."""
205+
return python_panel_types_pb2.ByteStringCollection
206+
207+
def to_protobuf_message(self, python_value: Collection[bytes]) -> python_panel_types_pb2.ByteStringCollection:
208+
"""Convert the Python collection of byte strings to a protobuf python_panel_types_pb2.ByteStringCollection."""
209+
return self.protobuf_message(values=python_value)
210+
211+
def to_python_value(self, protobuf_value: python_panel_types_pb2.ByteStringCollection) -> Collection[bytes]:
212+
"""Convert the protobuf message to a Python collection of byte strings."""
213+
return list(protobuf_value.values)
214+
215+
216+
class FloatCollectionConverter(Converter[Collection[float], python_panel_types_pb2.FloatCollection]):
217+
"""A converter for a Collection of floats."""
218+
219+
@property
220+
def python_typename(self) -> str:
221+
"""The Python type that this converter handles."""
222+
return f"{Collection.__name__}.{float.__name__}"
223+
224+
@property
225+
def protobuf_message(self) -> Type[python_panel_types_pb2.FloatCollection]:
226+
"""The type-specific protobuf message for the Python type."""
227+
return python_panel_types_pb2.FloatCollection
228+
229+
def to_protobuf_message(self, python_value: Collection[float]) -> python_panel_types_pb2.FloatCollection:
230+
"""Convert the Python collection of floats to a protobuf python_panel_types_pb2.FloatCollection."""
231+
return self.protobuf_message(values=python_value)
232+
233+
def to_python_value(self, protobuf_value: python_panel_types_pb2.FloatCollection) -> Collection[float]:
234+
"""Convert the protobuf message to a Python collection of floats."""
235+
return list(protobuf_value.values)
236+
237+
238+
class IntCollectionConverter(Converter[Collection[int], python_panel_types_pb2.IntCollection]):
239+
"""A converter for a Collection of integers."""
240+
241+
@property
242+
def python_typename(self) -> str:
243+
"""The Python type that this converter handles."""
244+
return f"{Collection.__name__}.{int.__name__}"
245+
246+
@property
247+
def protobuf_message(self) -> Type[python_panel_types_pb2.IntCollection]:
248+
"""The type-specific protobuf message for the Python type."""
249+
return python_panel_types_pb2.IntCollection
250+
251+
def to_protobuf_message(self, python_value: Collection[int]) -> python_panel_types_pb2.IntCollection:
252+
"""Convert the Python collection of integers to a protobuf python_panel_types_pb2.IntCollection."""
253+
return self.protobuf_message(values=python_value)
254+
255+
def to_python_value(self, protobuf_value: python_panel_types_pb2.IntCollection) -> Collection[int]:
256+
"""Convert the protobuf message to a Python collection of integers."""
257+
return list(protobuf_value.values)
258+
259+
260+
class StrCollectionConverter(Converter[Collection[str], python_panel_types_pb2.StringCollection]):
261+
"""A converter for a Collection of strings."""
262+
263+
@property
264+
def python_typename(self) -> str:
265+
"""The Python type that this converter handles."""
266+
return f"{Collection.__name__}.{str.__name__}"
267+
268+
@property
269+
def protobuf_message(self) -> Type[python_panel_types_pb2.StringCollection]:
270+
"""The type-specific protobuf message for the Python type."""
271+
return python_panel_types_pb2.StringCollection
272+
273+
def to_protobuf_message(self, python_value: Collection[str]) -> python_panel_types_pb2.StringCollection:
274+
"""Convert the Python collection of strings to a protobuf python_panel_types_pb2.StringCollection."""
275+
return self.protobuf_message(values=python_value)
276+
277+
def to_python_value(self, protobuf_value: python_panel_types_pb2.StringCollection) -> Collection[str]:
278+
"""Convert the protobuf message to a Python collection of strings."""
279+
return list(protobuf_value.values)
280+
281+
170282
# FFV -- consider adding a RegisterConverter mechanism
171283
_CONVERTIBLE_TYPES: list[Converter[Any, Any]] = [
284+
# Scalars first
172285
BoolConverter(),
173286
BytesConverter(),
174287
FloatConverter(),
175288
IntConverter(),
176289
StrConverter(),
290+
# Containers next
291+
BoolCollectionConverter(),
292+
BytesCollectionConverter(),
293+
FloatCollectionConverter(),
294+
IntCollectionConverter(),
295+
StrCollectionConverter(),
177296
]
178297

298+
_CONVERTIBLE_COLLECTION_TYPES = {
299+
frozenset,
300+
list,
301+
set,
302+
tuple,
303+
}
304+
179305
_CONVERTER_FOR_PYTHON_TYPE = {entry.python_typename: entry for entry in _CONVERTIBLE_TYPES}
180306
_CONVERTER_FOR_GRPC_TYPE = {entry.protobuf_typename: entry for entry in _CONVERTIBLE_TYPES}
181307
_SUPPORTED_PYTHON_TYPES = _CONVERTER_FOR_PYTHON_TYPE.keys()
@@ -185,12 +311,34 @@ def to_any(python_value: object) -> any_pb2.Any:
185311
"""Convert a Python object to a protobuf Any."""
186312
underlying_parents = type(python_value).mro() # This covers enum.IntEnum and similar
187313

188-
best_matching_type = next(
189-
(parent.__name__ for parent in underlying_parents if parent.__name__ in _SUPPORTED_PYTHON_TYPES), None
190-
)
314+
container_type = None
315+
value_is_collection = _CONVERTIBLE_COLLECTION_TYPES.intersection(underlying_parents)
316+
if value_is_collection:
317+
# Assume Sized -- Generators not supported, callers must use list(), set(), ... as desired
318+
if not isinstance(python_value, Collection):
319+
raise TypeError()
320+
if len(python_value) == 0:
321+
underlying_parents = type(None).mro()
322+
else:
323+
# Assume homogenous -- collections of mixed-types not supported
324+
visitor = iter(python_value)
325+
first_value = next(visitor)
326+
underlying_parents = type(first_value).mro()
327+
container_type = Collection
328+
329+
best_matching_type = None
330+
candidates = [parent.__name__ for parent in underlying_parents]
331+
for candidate in candidates:
332+
python_typename = f"{container_type.__name__}.{candidate}" if container_type else candidate
333+
if python_typename not in _SUPPORTED_PYTHON_TYPES:
334+
continue
335+
best_matching_type = python_typename
336+
break
337+
191338
if not best_matching_type:
339+
payload_type = underlying_parents[0]
192340
raise TypeError(
193-
f"Unsupported type: {type(python_value)} with parents {underlying_parents}. Supported types are: {_SUPPORTED_PYTHON_TYPES}"
341+
f"Unsupported type: ({container_type}, {payload_type}) with parents {underlying_parents}. Supported types are: {_SUPPORTED_PYTHON_TYPES}"
194342
)
195343
_logger.debug(f"Best matching type for '{repr(python_value)}' resolved to {best_matching_type}")
196344

0 commit comments

Comments
 (0)