44
55import  logging 
66from  abc  import  ABC , abstractmethod 
7+ from  collections .abc  import  Collection 
78from  typing  import  Any , Generic , Type , TypeVar 
89
910from  google .protobuf  import  any_pb2 , wrappers_pb2 
1011from  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