Skip to content

Commit ca34529

Browse files
committed
Refactor converters as a submodule
Signed-off-by: Joe Friedrichsen <[email protected]>
1 parent 620a3cd commit ca34529

File tree

4 files changed

+162
-134
lines changed

4 files changed

+162
-134
lines changed

src/nipanel/_convert.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Functions to convert between different data formats."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from collections.abc import Collection
7+
from typing import Any
8+
9+
from google.protobuf import any_pb2
10+
11+
from nipanel.converters import Converter
12+
from nipanel.converters.builtin import (
13+
BoolConverter,
14+
BytesConverter,
15+
FloatConverter,
16+
IntConverter,
17+
StrConverter,
18+
BoolCollectionConverter,
19+
BytesCollectionConverter,
20+
FloatCollectionConverter,
21+
IntCollectionConverter,
22+
StrCollectionConverter,
23+
)
24+
25+
_logger = logging.getLogger(__name__)
26+
27+
# FFV -- consider adding a RegisterConverter mechanism
28+
_CONVERTIBLE_TYPES: list[Converter[Any, Any]] = [
29+
# Scalars first
30+
BoolConverter(),
31+
BytesConverter(),
32+
FloatConverter(),
33+
IntConverter(),
34+
StrConverter(),
35+
# Containers next
36+
BoolCollectionConverter(),
37+
BytesCollectionConverter(),
38+
FloatCollectionConverter(),
39+
IntCollectionConverter(),
40+
StrCollectionConverter(),
41+
]
42+
43+
_CONVERTIBLE_COLLECTION_TYPES = {
44+
frozenset,
45+
list,
46+
set,
47+
tuple,
48+
}
49+
50+
_CONVERTER_FOR_PYTHON_TYPE = {entry.python_typename: entry for entry in _CONVERTIBLE_TYPES}
51+
_CONVERTER_FOR_GRPC_TYPE = {entry.protobuf_typename: entry for entry in _CONVERTIBLE_TYPES}
52+
_SUPPORTED_PYTHON_TYPES = _CONVERTER_FOR_PYTHON_TYPE.keys()
53+
54+
55+
def to_any(python_value: object) -> any_pb2.Any:
56+
"""Convert a Python object to a protobuf Any."""
57+
underlying_parents = type(python_value).mro() # This covers enum.IntEnum and similar
58+
59+
container_type = None
60+
value_is_collection = _CONVERTIBLE_COLLECTION_TYPES.intersection(underlying_parents)
61+
if value_is_collection:
62+
# Assume Sized -- Generators not supported, callers must use list(), set(), ... as desired
63+
if not isinstance(python_value, Collection):
64+
raise TypeError()
65+
if len(python_value) == 0:
66+
underlying_parents = type(None).mro()
67+
else:
68+
# Assume homogenous -- collections of mixed-types not supported
69+
visitor = iter(python_value)
70+
first_value = next(visitor)
71+
underlying_parents = type(first_value).mro()
72+
container_type = Collection
73+
74+
best_matching_type = None
75+
candidates = [parent.__name__ for parent in underlying_parents]
76+
for candidate in candidates:
77+
python_typename = f"{container_type.__name__}.{candidate}" if container_type else candidate
78+
if python_typename not in _SUPPORTED_PYTHON_TYPES:
79+
continue
80+
best_matching_type = python_typename
81+
break
82+
83+
if not best_matching_type:
84+
payload_type = underlying_parents[0]
85+
raise TypeError(
86+
f"Unsupported type: ({container_type}, {payload_type}) with parents {underlying_parents}. Supported types are: {_SUPPORTED_PYTHON_TYPES}"
87+
)
88+
_logger.debug(f"Best matching type for '{repr(python_value)}' resolved to {best_matching_type}")
89+
90+
converter = _CONVERTER_FOR_PYTHON_TYPE[best_matching_type]
91+
return converter.to_protobuf_any(python_value)
92+
93+
94+
def from_any(protobuf_any: any_pb2.Any) -> object:
95+
"""Convert a protobuf Any to a Python object."""
96+
if not isinstance(protobuf_any, any_pb2.Any):
97+
raise ValueError(f"Unexpected type: {type(protobuf_any)}")
98+
99+
underlying_typename = protobuf_any.TypeName()
100+
_logger.debug(f"Unpacking type '{underlying_typename}'")
101+
102+
converter = _CONVERTER_FOR_GRPC_TYPE[underlying_typename]
103+
return converter.to_python(protobuf_any)

src/nipanel/_panel_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ni_measurement_plugin_sdk_service.grpc.channelpool import GrpcChannelPool
2020
from typing_extensions import ParamSpec
2121

22-
from nipanel._converters import (
22+
from nipanel._convert import (
2323
from_any,
2424
to_any,
2525
)

src/nipanel/converters/__init__.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Functions and classes to convert types between Python and protobuf."""
2+
3+
from __future__ import annotations
4+
5+
from abc import ABC, abstractmethod
6+
from typing import Generic, Type, TypeVar
7+
8+
from google.protobuf import any_pb2
9+
from google.protobuf.message import Message
10+
11+
_TPythonType = TypeVar("_TPythonType")
12+
_TProtobufType = TypeVar("_TProtobufType", bound=Message)
13+
14+
15+
class Converter(Generic[_TPythonType, _TProtobufType], ABC):
16+
"""A class that defines how to convert between Python objects and protobuf Any messages."""
17+
18+
@property
19+
@abstractmethod
20+
def python_typename(self) -> str:
21+
"""The Python type that this converter handles."""
22+
23+
@property
24+
@abstractmethod
25+
def protobuf_message(self) -> Type[_TProtobufType]:
26+
"""The type-specific protobuf message for the Python type."""
27+
28+
@property
29+
def protobuf_typename(self) -> str:
30+
"""The protobuf name for the type."""
31+
return self.protobuf_message.DESCRIPTOR.full_name # type: ignore[no-any-return]
32+
33+
def to_protobuf_any(self, python_value: _TPythonType) -> any_pb2.Any:
34+
"""Convert the Python object to its type-specific message and pack it as any_pb2.Any."""
35+
message = self.to_protobuf_message(python_value)
36+
as_any = any_pb2.Any()
37+
as_any.Pack(message)
38+
return as_any
39+
40+
@abstractmethod
41+
def to_protobuf_message(self, python_value: _TPythonType) -> _TProtobufType:
42+
"""Convert the Python object to its type-specific message."""
43+
44+
def to_python(self, protobuf_value: any_pb2.Any) -> _TPythonType:
45+
"""Convert the protobuf Any message to its matching Python type."""
46+
protobuf_message = self.protobuf_message()
47+
did_unpack = protobuf_value.Unpack(protobuf_message)
48+
if not did_unpack:
49+
raise ValueError(f"Failed to unpack Any with type '{protobuf_value.TypeName()}'")
50+
return self.to_python_value(protobuf_message)
51+
52+
@abstractmethod
53+
def to_python_value(self, protobuf_message: _TProtobufType) -> _TPythonType:
54+
"""Convert the protobuf wrapper message to its matching Python type."""

src/nipanel/_converters.py renamed to src/nipanel/converters/builtin.py

Lines changed: 4 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,12 @@
1-
"""Functions to convert between different data formats."""
1+
"""Classes to convert between builtin Python scalars and containers."""
22

3-
from __future__ import annotations
4-
5-
import logging
6-
from abc import ABC, abstractmethod
73
from collections.abc import Collection
8-
from typing import Any, Generic, Type, TypeVar
4+
from typing import Type
95

10-
from google.protobuf import any_pb2, wrappers_pb2
11-
from google.protobuf.message import Message
6+
from google.protobuf import wrappers_pb2
127
from ni.pythonpanel.v1 import python_panel_types_pb2
138

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
6010

6111

6212
class BoolConverter(Converter[bool, wrappers_pb2.BoolValue]):
@@ -301,82 +251,3 @@ def to_python_value(
301251
) -> Collection[str]:
302252
"""Convert the protobuf message to a Python collection of strings."""
303253
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

Comments
 (0)