|
1 | | -"""Functions to convert between different data formats.""" |
| 1 | +"""Classes to convert between builtin Python scalars and containers.""" |
2 | 2 |
|
3 | | -from __future__ import annotations |
4 | | - |
5 | | -import logging |
6 | | -from abc import ABC, abstractmethod |
7 | 3 | from collections.abc import Collection |
8 | | -from typing import Any, Generic, Type, TypeVar |
| 4 | +from typing import Type |
9 | 5 |
|
10 | | -from google.protobuf import any_pb2, wrappers_pb2 |
11 | | -from google.protobuf.message import Message |
| 6 | +from google.protobuf import wrappers_pb2 |
12 | 7 | from ni.pythonpanel.v1 import python_panel_types_pb2 |
13 | 8 |
|
14 | | -_TPythonType = TypeVar("_TPythonType") |
15 | | -_TProtobufType = TypeVar("_TProtobufType", bound=Message) |
16 | | - |
17 | | -_logger = logging.getLogger(__name__) |
18 | | - |
19 | | - |
20 | | -class Converter(Generic[_TPythonType, _TProtobufType], ABC): |
21 | | - """A class that defines how to convert between Python objects and protobuf Any messages.""" |
22 | | - |
23 | | - @property |
24 | | - @abstractmethod |
25 | | - def python_typename(self) -> str: |
26 | | - """The Python type that this converter handles.""" |
27 | | - |
28 | | - @property |
29 | | - @abstractmethod |
30 | | - def protobuf_message(self) -> Type[_TProtobufType]: |
31 | | - """The type-specific protobuf message for the Python type.""" |
32 | | - |
33 | | - @property |
34 | | - def protobuf_typename(self) -> str: |
35 | | - """The protobuf name for the type.""" |
36 | | - return self.protobuf_message.DESCRIPTOR.full_name # type: ignore[no-any-return] |
37 | | - |
38 | | - def to_protobuf_any(self, python_value: _TPythonType) -> any_pb2.Any: |
39 | | - """Convert the Python object to its type-specific message and pack it as any_pb2.Any.""" |
40 | | - message = self.to_protobuf_message(python_value) |
41 | | - as_any = any_pb2.Any() |
42 | | - as_any.Pack(message) |
43 | | - return as_any |
44 | | - |
45 | | - @abstractmethod |
46 | | - def to_protobuf_message(self, python_value: _TPythonType) -> _TProtobufType: |
47 | | - """Convert the Python object to its type-specific message.""" |
48 | | - |
49 | | - def to_python(self, protobuf_value: any_pb2.Any) -> _TPythonType: |
50 | | - """Convert the protobuf Any message to its matching Python type.""" |
51 | | - protobuf_message = self.protobuf_message() |
52 | | - did_unpack = protobuf_value.Unpack(protobuf_message) |
53 | | - if not did_unpack: |
54 | | - raise ValueError(f"Failed to unpack Any with type '{protobuf_value.TypeName()}'") |
55 | | - return self.to_python_value(protobuf_message) |
56 | | - |
57 | | - @abstractmethod |
58 | | - def to_python_value(self, protobuf_message: _TProtobufType) -> _TPythonType: |
59 | | - """Convert the protobuf wrapper message to its matching Python type.""" |
| 9 | +from nipanel.converters import Converter |
60 | 10 |
|
61 | 11 |
|
62 | 12 | class BoolConverter(Converter[bool, wrappers_pb2.BoolValue]): |
@@ -301,82 +251,3 @@ def to_python_value( |
301 | 251 | ) -> Collection[str]: |
302 | 252 | """Convert the protobuf message to a Python collection of strings.""" |
303 | 253 | return list(protobuf_value.values) |
304 | | - |
305 | | - |
306 | | -# FFV -- consider adding a RegisterConverter mechanism |
307 | | -_CONVERTIBLE_TYPES: list[Converter[Any, Any]] = [ |
308 | | - # Scalars first |
309 | | - BoolConverter(), |
310 | | - BytesConverter(), |
311 | | - FloatConverter(), |
312 | | - IntConverter(), |
313 | | - StrConverter(), |
314 | | - # Containers next |
315 | | - BoolCollectionConverter(), |
316 | | - BytesCollectionConverter(), |
317 | | - FloatCollectionConverter(), |
318 | | - IntCollectionConverter(), |
319 | | - StrCollectionConverter(), |
320 | | -] |
321 | | - |
322 | | -_CONVERTIBLE_COLLECTION_TYPES = { |
323 | | - frozenset, |
324 | | - list, |
325 | | - set, |
326 | | - tuple, |
327 | | -} |
328 | | - |
329 | | -_CONVERTER_FOR_PYTHON_TYPE = {entry.python_typename: entry for entry in _CONVERTIBLE_TYPES} |
330 | | -_CONVERTER_FOR_GRPC_TYPE = {entry.protobuf_typename: entry for entry in _CONVERTIBLE_TYPES} |
331 | | -_SUPPORTED_PYTHON_TYPES = _CONVERTER_FOR_PYTHON_TYPE.keys() |
332 | | - |
333 | | - |
334 | | -def to_any(python_value: object) -> any_pb2.Any: |
335 | | - """Convert a Python object to a protobuf Any.""" |
336 | | - underlying_parents = type(python_value).mro() # This covers enum.IntEnum and similar |
337 | | - |
338 | | - container_type = None |
339 | | - value_is_collection = _CONVERTIBLE_COLLECTION_TYPES.intersection(underlying_parents) |
340 | | - if value_is_collection: |
341 | | - # Assume Sized -- Generators not supported, callers must use list(), set(), ... as desired |
342 | | - if not isinstance(python_value, Collection): |
343 | | - raise TypeError() |
344 | | - if len(python_value) == 0: |
345 | | - underlying_parents = type(None).mro() |
346 | | - else: |
347 | | - # Assume homogenous -- collections of mixed-types not supported |
348 | | - visitor = iter(python_value) |
349 | | - first_value = next(visitor) |
350 | | - underlying_parents = type(first_value).mro() |
351 | | - container_type = Collection |
352 | | - |
353 | | - best_matching_type = None |
354 | | - candidates = [parent.__name__ for parent in underlying_parents] |
355 | | - for candidate in candidates: |
356 | | - python_typename = f"{container_type.__name__}.{candidate}" if container_type else candidate |
357 | | - if python_typename not in _SUPPORTED_PYTHON_TYPES: |
358 | | - continue |
359 | | - best_matching_type = python_typename |
360 | | - break |
361 | | - |
362 | | - if not best_matching_type: |
363 | | - payload_type = underlying_parents[0] |
364 | | - raise TypeError( |
365 | | - f"Unsupported type: ({container_type}, {payload_type}) with parents {underlying_parents}. Supported types are: {_SUPPORTED_PYTHON_TYPES}" |
366 | | - ) |
367 | | - _logger.debug(f"Best matching type for '{repr(python_value)}' resolved to {best_matching_type}") |
368 | | - |
369 | | - converter = _CONVERTER_FOR_PYTHON_TYPE[best_matching_type] |
370 | | - return converter.to_protobuf_any(python_value) |
371 | | - |
372 | | - |
373 | | -def from_any(protobuf_any: any_pb2.Any) -> object: |
374 | | - """Convert a protobuf Any to a Python object.""" |
375 | | - if not isinstance(protobuf_any, any_pb2.Any): |
376 | | - raise ValueError(f"Unexpected type: {type(protobuf_any)}") |
377 | | - |
378 | | - underlying_typename = protobuf_any.TypeName() |
379 | | - _logger.debug(f"Unpacking type '{underlying_typename}'") |
380 | | - |
381 | | - converter = _CONVERTER_FOR_GRPC_TYPE[underlying_typename] |
382 | | - return converter.to_python(protobuf_any) |
0 commit comments