diff --git a/src/nipanel/_convert.py b/src/nipanel/_convert.py index 103741a..3a1ef18 100644 --- a/src/nipanel/_convert.py +++ b/src/nipanel/_convert.py @@ -4,7 +4,7 @@ import logging from collections.abc import Collection -from typing import Any +from typing import Any, Iterable from google.protobuf import any_pb2 @@ -101,13 +101,12 @@ def _get_best_matching_type(python_value: object) -> str: value_is_collection = any( _CONVERTIBLE_COLLECTION_TYPES.intersection(underlying_parents) ) - container_types.append(Collection.__name__) + container_types.append(Collection) best_matching_type = None - candidates = [parent.__name__ for parent in underlying_parents] + candidates = _get_candidate_strings(underlying_parents) for candidate in candidates: - containers_str = ".".join(container_types) - python_typename = f"{containers_str}.{candidate}" if containers_str else candidate + python_typename = _create_python_typename(candidate, container_types) if python_typename not in _SUPPORTED_PYTHON_TYPES: continue best_matching_type = python_typename @@ -142,3 +141,18 @@ def is_supported_type(value: object) -> bool: return True except TypeError: return False + + +def _get_candidate_strings(candidates: Iterable[type]) -> list[str]: + candidate_names = [] + for candidate in candidates: + candidate_names.append(f"{candidate.__module__}.{candidate.__name__}") + + return candidate_names + + +def _create_python_typename(candidate_name: str, container_types: Iterable[type]) -> str: + name = candidate_name + for container_type in container_types: + name = f"{container_type.__module__}.{container_type.__name__}[{name}]" + return name diff --git a/src/nipanel/converters/__init__.py b/src/nipanel/converters/__init__.py index 4c613cf..92b189d 100644 --- a/src/nipanel/converters/__init__.py +++ b/src/nipanel/converters/__init__.py @@ -3,11 +3,13 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections.abc import Collection from typing import Generic, Type, TypeVar from google.protobuf import any_pb2 from google.protobuf.message import Message +_TItemType = TypeVar("_TItemType") _TPythonType = TypeVar("_TPythonType") _TProtobufType = TypeVar("_TProtobufType", bound=Message) @@ -17,9 +19,14 @@ class Converter(Generic[_TPythonType, _TProtobufType], ABC): @property @abstractmethod - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" + @property + def python_typename(self) -> str: + """The Python type name that this converter handles.""" + return f"{self.python_type.__module__}.{self.python_type.__name__}" + @property @abstractmethod def protobuf_message(self) -> Type[_TProtobufType]: @@ -52,3 +59,56 @@ def to_python(self, protobuf_value: any_pb2.Any) -> _TPythonType: @abstractmethod def to_python_value(self, protobuf_message: _TProtobufType) -> _TPythonType: """Convert the protobuf wrapper message to its matching Python type.""" + + +class CollectionConverter( + Generic[_TItemType, _TProtobufType], + Converter[Collection[_TItemType], _TProtobufType], + ABC, +): + """A converter between a collection of Python objects and protobuf Any messages.""" + + @property + @abstractmethod + def item_type(self) -> type: + """The Python item type that this converter handles.""" + + @property + def python_type(self) -> type: + """The Python type that this converter handles.""" + return Collection + + @property + def python_typename(self) -> str: + """The Python type name that this converter handles.""" + return "{}[{}]".format( + f"{Collection.__module__}.{Collection.__name__}", + f"{self.item_type.__module__}.{self.item_type.__name__}", + ) + + +class CollectionConverter2D( + Generic[_TItemType, _TProtobufType], + Converter[Collection[Collection[_TItemType]], _TProtobufType], + ABC, +): + """A converter between a 2D collection of Python objects and protobuf Any messages.""" + + @property + @abstractmethod + def item_type(self) -> type: + """The Python item type that this converter handles.""" + + @property + def python_type(self) -> type: + """The Python type that this converter handles.""" + return Collection + + @property + def python_typename(self) -> str: + """The Python type name that this converter handles.""" + return "{}[{}[{}]]".format( + f"{Collection.__module__}.{Collection.__name__}", + f"{Collection.__module__}.{Collection.__name__}", + f"{self.item_type.__module__}.{self.item_type.__name__}", + ) diff --git a/src/nipanel/converters/builtin.py b/src/nipanel/converters/builtin.py index 2f8cd86..72d0077 100644 --- a/src/nipanel/converters/builtin.py +++ b/src/nipanel/converters/builtin.py @@ -7,16 +7,16 @@ from google.protobuf import duration_pb2, timestamp_pb2, wrappers_pb2 from ni.panels.v1 import panel_types_pb2 -from nipanel.converters import Converter +from nipanel.converters import Converter, CollectionConverter class BoolConverter(Converter[bool, wrappers_pb2.BoolValue]): """A converter for boolean types.""" @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return bool.__name__ + return bool @property def protobuf_message(self) -> Type[wrappers_pb2.BoolValue]: @@ -36,9 +36,9 @@ class BytesConverter(Converter[bytes, wrappers_pb2.BytesValue]): """A converter for byte string types.""" @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return bytes.__name__ + return bytes @property def protobuf_message(self) -> Type[wrappers_pb2.BytesValue]: @@ -58,9 +58,9 @@ class FloatConverter(Converter[float, wrappers_pb2.DoubleValue]): """A converter for floating point types.""" @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return float.__name__ + return float @property def protobuf_message(self) -> Type[wrappers_pb2.DoubleValue]: @@ -80,9 +80,9 @@ class IntConverter(Converter[int, wrappers_pb2.Int64Value]): """A converter for integer types.""" @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return int.__name__ + return int @property def protobuf_message(self) -> Type[wrappers_pb2.Int64Value]: @@ -102,9 +102,9 @@ class StrConverter(Converter[str, wrappers_pb2.StringValue]): """A converter for text string types.""" @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return str.__name__ + return str @property def protobuf_message(self) -> Type[wrappers_pb2.StringValue]: @@ -124,9 +124,9 @@ class DTDateTimeConverter(Converter[dt.datetime, timestamp_pb2.Timestamp]): """A converter for datetime.datetime types.""" @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return dt.datetime.__name__ + return dt.datetime @property def protobuf_message(self) -> Type[timestamp_pb2.Timestamp]: @@ -148,9 +148,9 @@ class DTTimeDeltaConverter(Converter[dt.timedelta, duration_pb2.Duration]): """A converter for datetime.timedelta types.""" @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return dt.timedelta.__name__ + return dt.timedelta @property def protobuf_message(self) -> Type[duration_pb2.Duration]: @@ -168,13 +168,13 @@ def to_python_value(self, protobuf_message: duration_pb2.Duration) -> dt.timedel return protobuf_message.ToTimedelta() -class BoolCollectionConverter(Converter[Collection[bool], panel_types_pb2.BoolCollection]): +class BoolCollectionConverter(CollectionConverter[bool, panel_types_pb2.BoolCollection]): """A converter for a Collection of bools.""" @property - def python_typename(self) -> str: + def item_type(self) -> type: """The Python type that this converter handles.""" - return f"{Collection.__name__}.{bool.__name__}" + return bool @property def protobuf_message(self) -> Type[panel_types_pb2.BoolCollection]: @@ -190,13 +190,13 @@ def to_python_value(self, protobuf_message: panel_types_pb2.BoolCollection) -> C return list(protobuf_message.values) -class BytesCollectionConverter(Converter[Collection[bytes], panel_types_pb2.ByteStringCollection]): +class BytesCollectionConverter(CollectionConverter[bytes, panel_types_pb2.ByteStringCollection]): """A converter for a Collection of byte strings.""" @property - def python_typename(self) -> str: + def item_type(self) -> type: """The Python type that this converter handles.""" - return f"{Collection.__name__}.{bytes.__name__}" + return bytes @property def protobuf_message(self) -> Type[panel_types_pb2.ByteStringCollection]: @@ -216,13 +216,13 @@ def to_python_value( return list(protobuf_message.values) -class FloatCollectionConverter(Converter[Collection[float], panel_types_pb2.FloatCollection]): +class FloatCollectionConverter(CollectionConverter[float, panel_types_pb2.FloatCollection]): """A converter for a Collection of floats.""" @property - def python_typename(self) -> str: + def item_type(self) -> type: """The Python type that this converter handles.""" - return f"{Collection.__name__}.{float.__name__}" + return float @property def protobuf_message(self) -> Type[panel_types_pb2.FloatCollection]: @@ -242,13 +242,13 @@ def to_python_value( return list(protobuf_message.values) -class IntCollectionConverter(Converter[Collection[int], panel_types_pb2.IntCollection]): +class IntCollectionConverter(CollectionConverter[int, panel_types_pb2.IntCollection]): """A converter for a Collection of integers.""" @property - def python_typename(self) -> str: + def item_type(self) -> type: """The Python type that this converter handles.""" - return f"{Collection.__name__}.{int.__name__}" + return int @property def protobuf_message(self) -> Type[panel_types_pb2.IntCollection]: @@ -264,13 +264,13 @@ def to_python_value(self, protobuf_message: panel_types_pb2.IntCollection) -> Co return list(protobuf_message.values) -class StrCollectionConverter(Converter[Collection[str], panel_types_pb2.StringCollection]): +class StrCollectionConverter(CollectionConverter[str, panel_types_pb2.StringCollection]): """A converter for a Collection of strings.""" @property - def python_typename(self) -> str: + def item_type(self) -> type: """The Python type that this converter handles.""" - return f"{Collection.__name__}.{str.__name__}" + return str @property def protobuf_message(self) -> Type[panel_types_pb2.StringCollection]: diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index 2cef62f..d02e1f6 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -31,7 +31,7 @@ from nitypes.waveform.typing import ExtendedPropertyValue from typing_extensions import TypeAlias -from nipanel.converters import Converter +from nipanel.converters import Converter, CollectionConverter2D _AnyScalarType: TypeAlias = Union[bool, int, float, str] _SCALAR_TYPE_TO_PB_ATTR_MAP = { @@ -42,13 +42,13 @@ } -class Double2DArrayConverter(Converter[Collection[Collection[float]], Double2DArray]): +class Double2DArrayConverter(CollectionConverter2D[float, Double2DArray]): """A converter between Collection[Collection[float]] and Double2DArray.""" @property - def python_typename(self) -> str: - """The Python type that this converter handles.""" - return f"{Collection.__name__}.{Collection.__name__}.{float.__name__}" + def item_type(self) -> type: + """The Python item type that this converter handles.""" + return float @property def protobuf_message(self) -> Type[Double2DArray]: @@ -95,9 +95,9 @@ def __init__(self) -> None: self._pt_converter = PrecisionTimestampConverter() @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return AnalogWaveform.__name__ + return AnalogWaveform @property def protobuf_message(self) -> Type[DoubleAnalogWaveform]: @@ -192,9 +192,9 @@ class PrecisionTimestampConverter(Converter[bt.DateTime, PrecisionTimestamp]): """A converter for bintime.DateTime types.""" @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return bt.DateTime.__name__ + return bt.DateTime @property def protobuf_message(self) -> Type[PrecisionTimestamp]: @@ -218,9 +218,9 @@ class ScalarConverter(Converter[Scalar[_AnyScalarType], scalar_pb2.ScalarData]): """A converter for Scalar objects.""" @property - def python_typename(self) -> str: + def python_type(self) -> type: """The Python type that this converter handles.""" - return Scalar.__name__ + return Scalar @property def protobuf_message(self) -> Type[scalar_pb2.ScalarData]: diff --git a/tests/unit/test_convert.py b/tests/unit/test_convert.py index 115d33d..46c5325 100644 --- a/tests/unit/test_convert.py +++ b/tests/unit/test_convert.py @@ -44,55 +44,91 @@ @pytest.mark.parametrize( "python_object, expected_type_string", [ - (False, "bool"), - (b"mystr", "bytes"), - (456.2, "float"), - (123, "int"), - ("mystr", "str"), - (tests.types.MyIntFlags.VALUE1, "int"), - (tests.types.MyIntEnum.VALUE10, "int"), - (tests.types.MixinIntEnum.VALUE11, "int"), - (tests.types.MyStrEnum.VALUE1, "str"), - (tests.types.MixinStrEnum.VALUE11, "str"), - (dt.datetime.now(), "datetime"), - (dt.timedelta(days=1), "timedelta"), - ([False, False], "Collection.bool"), - ([b"mystr", b"mystr"], "Collection.bytes"), - ([456.2, 1.0], "Collection.float"), - ([123, 456], "Collection.int"), - (["mystr", "mystr"], "Collection.str"), - ((False, False), "Collection.bool"), - ((b"mystr", b"mystr"), "Collection.bytes"), - ((456.2, 1.0), "Collection.float"), - ((123, 456), "Collection.int"), - (("mystr", "mystr"), "Collection.str"), - ((False, False), "Collection.bool"), - ((b"mystr", b"mystr"), "Collection.bytes"), - ((456.2, 1.0), "Collection.float"), - ((123, 456), "Collection.int"), - (("mystr", "mystr"), "Collection.str"), - (set([False, True]), "Collection.bool"), - (set([b"mystr", b"mystr2"]), "Collection.bytes"), - (set([456.2, 1.0]), "Collection.float"), - (set([123, 456]), "Collection.int"), - (set(["mystr", "mystr2"]), "Collection.str"), - (frozenset([False, True]), "Collection.bool"), - (frozenset([b"mystr", b"mystr2"]), "Collection.bytes"), - (frozenset([456.2, 1.0]), "Collection.float"), - (frozenset([123, 456]), "Collection.int"), - (frozenset(["mystr", "mystr2"]), "Collection.str"), - ([[1.0, 2.0], [1.0, 2.0]], "Collection.Collection.float"), - ([(1.0, 2.0), (3.0, 4.0)], "Collection.Collection.float"), - ([set([1.0, 2.0]), set([3.0, 4.0])], "Collection.Collection.float"), - ([frozenset([1.0, 2.0]), frozenset([3.0, 4.0])], "Collection.Collection.float"), - (([1.0, 2.0], [3.0, 4.0]), "Collection.Collection.float"), - (((1.0, 2.0), (3.0, 4.0)), "Collection.Collection.float"), - ((set([1.0, 2.0]), set([3.0, 4.0])), "Collection.Collection.float"), - ((frozenset([1.0, 2.0]), frozenset([3.0, 4.0])), "Collection.Collection.float"), - (set([(1.0, 2.0), (3.0, 4.0)]), "Collection.Collection.float"), - (set([frozenset([1.0, 2.0]), frozenset([3.0, 4.0])]), "Collection.Collection.float"), - (frozenset([(1.0, 2.0), (3.0, 4.0)]), "Collection.Collection.float"), - (frozenset([frozenset([1.0, 2.0]), frozenset([3.0, 4.0])]), "Collection.Collection.float"), + (False, "builtins.bool"), + (b"mystr", "builtins.bytes"), + (456.2, "builtins.float"), + (123, "builtins.int"), + ("mystr", "builtins.str"), + (tests.types.MyIntFlags.VALUE1, "builtins.int"), + (tests.types.MyIntEnum.VALUE10, "builtins.int"), + (tests.types.MixinIntEnum.VALUE11, "builtins.int"), + (tests.types.MyStrEnum.VALUE1, "builtins.str"), + (tests.types.MixinStrEnum.VALUE11, "builtins.str"), + (dt.datetime.now(), "datetime.datetime"), + (dt.timedelta(days=1), "datetime.timedelta"), + ([False, False], "collections.abc.Collection[builtins.bool]"), + ([b"mystr", b"mystr"], "collections.abc.Collection[builtins.bytes]"), + ([456.2, 1.0], "collections.abc.Collection[builtins.float]"), + ([123, 456], "collections.abc.Collection[builtins.int]"), + (["mystr", "mystr"], "collections.abc.Collection[builtins.str]"), + ((False, False), "collections.abc.Collection[builtins.bool]"), + ((b"mystr", b"mystr"), "collections.abc.Collection[builtins.bytes]"), + ((456.2, 1.0), "collections.abc.Collection[builtins.float]"), + ((123, 456), "collections.abc.Collection[builtins.int]"), + (("mystr", "mystr"), "collections.abc.Collection[builtins.str]"), + ((False, False), "collections.abc.Collection[builtins.bool]"), + ((b"mystr", b"mystr"), "collections.abc.Collection[builtins.bytes]"), + ((456.2, 1.0), "collections.abc.Collection[builtins.float]"), + ((123, 456), "collections.abc.Collection[builtins.int]"), + (("mystr", "mystr"), "collections.abc.Collection[builtins.str]"), + (set([False, True]), "collections.abc.Collection[builtins.bool]"), + (set([b"mystr", b"mystr2"]), "collections.abc.Collection[builtins.bytes]"), + (set([456.2, 1.0]), "collections.abc.Collection[builtins.float]"), + (set([123, 456]), "collections.abc.Collection[builtins.int]"), + (set(["mystr", "mystr2"]), "collections.abc.Collection[builtins.str]"), + (frozenset([False, True]), "collections.abc.Collection[builtins.bool]"), + (frozenset([b"mystr", b"mystr2"]), "collections.abc.Collection[builtins.bytes]"), + (frozenset([456.2, 1.0]), "collections.abc.Collection[builtins.float]"), + (frozenset([123, 456]), "collections.abc.Collection[builtins.int]"), + (frozenset(["mystr", "mystr2"]), "collections.abc.Collection[builtins.str]"), + ( + [[1.0, 2.0], [1.0, 2.0]], + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + [(1.0, 2.0), (3.0, 4.0)], + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + [set([1.0, 2.0]), set([3.0, 4.0])], + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + [frozenset([1.0, 2.0]), frozenset([3.0, 4.0])], + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + ([1.0, 2.0], [3.0, 4.0]), + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + ((1.0, 2.0), (3.0, 4.0)), + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + (set([1.0, 2.0]), set([3.0, 4.0])), + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + (frozenset([1.0, 2.0]), frozenset([3.0, 4.0])), + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + set([(1.0, 2.0), (3.0, 4.0)]), + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + set([frozenset([1.0, 2.0]), frozenset([3.0, 4.0])]), + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + frozenset([(1.0, 2.0), (3.0, 4.0)]), + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), + ( + frozenset([frozenset([1.0, 2.0]), frozenset([3.0, 4.0])]), + "collections.abc.Collection[collections.abc.Collection[builtins.float]]", + ), ], ) def test___various_python_objects___get_best_matching_type___returns_correct_type_string(