From f6ce479abe4e37f9d628b1ee33e32cf5f7d78075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Pr=C3=BCsse?= Date: Wed, 11 Dec 2024 09:51:20 -0300 Subject: [PATCH] Support for user plugins with table containing references --- CHANGELOG.rst | 1 + .../images/api/table_field_example_1.png | Bin 8859 -> 15376 bytes src/alfasim_sdk/__init__.py | 8 + .../_internal/alfacase/case_description.py | 33 ++ .../alfacase/case_description_attributes.py | 3 + .../alfacase/plugin_alfacase_to_case.py | 56 ++- src/alfasim_sdk/_internal/types.py | 79 +++-- .../alfacase/test_plugin_alfacase_to_case.py | 331 +++++++++++------- tests/plugins/test_types.py | 2 +- 9 files changed, 355 insertions(+), 158 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 46354684..596d6214 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ CHANGELOG * Changed the constructor of result metadata objects in ``alfasim_sdk.result_reader.aggregator`` from ``attr`` to ``dataclasses.dataclass`` to make then more easily integrated into the (de)serialization engine of popular packages such as ``pyserde`` and ``pydantic``. * Changed function ``read_global_sensitivity_coefficients`` and ``read_uncertainty_propagation_results`` to accept multiple keys so it can perform bulk reads without having to open the result file every time. * Added ``GlobalSensitivityAnalysisResults``, ``HistoryMatchingDeterministicResults``, ``HistoryMatchingProbabilisticResults`` and ``UncertaintyPropagationResults``, which are objects to read and interact with the Uncertainty Quantification analyses results in a more user-friendly way. +* Plugins can now define tables that contain references to other objects. 2024.2 (2024-09-10) =================== diff --git a/docs/source/_static/images/api/table_field_example_1.png b/docs/source/_static/images/api/table_field_example_1.png index d0dad677094fd863e58528e1daad24ca62278aa2..c95b761f2774aaa750ca84cfb3b2bfbe8a338ba0 100644 GIT binary patch literal 15376 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 AivR!s literal 8859 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 E0C< diff --git a/src/alfasim_sdk/__init__.py b/src/alfasim_sdk/__init__.py index 21a9815f..61a32d37 100644 --- a/src/alfasim_sdk/__init__.py +++ b/src/alfasim_sdk/__init__.py @@ -118,6 +118,12 @@ def get_alfasim_sdk_api_path(): from alfasim_sdk._internal.alfacase.case_description import ( PressureContainerDescription, ) +from alfasim_sdk._internal.alfacase.case_description import ( + InternalReferencePluginTableColumn, +) +from alfasim_sdk._internal.alfacase.case_description import ( + TracerReferencePluginTableColumn, +) from alfasim_sdk._internal.alfacase.case_description import ( PressureNodePropertiesDescription, ) @@ -431,6 +437,7 @@ def get_alfasim_sdk_api_path(): "InitialVelocitiesDescription", "InitialVolumeFractionsDescription", "InternalNodePropertiesDescription", + "InternalReferencePluginTableColumn", "InterpolationType", "LeakEquipmentDescription", "LeakLocation", @@ -516,6 +523,7 @@ def get_alfasim_sdk_api_path(): "TimeOptionsDescription", "TracerModelConstantCoefficientsDescription", "TracerModelType", + "TracerReferencePluginTableColumn", "TracerType", "TracersDescription", "TracersMassFractionsContainerDescription", diff --git a/src/alfasim_sdk/_internal/alfacase/case_description.py b/src/alfasim_sdk/_internal/alfacase/case_description.py index 804b6a66..6750b9e1 100644 --- a/src/alfasim_sdk/_internal/alfacase/case_description.py +++ b/src/alfasim_sdk/_internal/alfacase/case_description.py @@ -20,6 +20,7 @@ from barril.units import Array from barril.units import Scalar +from ..validators import non_empty_str from .case_description_attributes import attrib_array from .case_description_attributes import attrib_curve from .case_description_attributes import attrib_dict_of @@ -32,6 +33,7 @@ from .case_description_attributes import dict_of_array from .case_description_attributes import dict_with_scalar from .case_description_attributes import InvalidReferenceError +from .case_description_attributes import list_of_optional_integers from .case_description_attributes import list_of_strings from .case_description_attributes import Numpy1DArray from .case_description_attributes import numpy_array_validator @@ -86,6 +88,7 @@ class PluginTracerReference: @attr.s(frozen=True, slots=True) class PluginInternalReference: plugin_item_id = attr.ib(default=None) + container_key = attr.ib(default=None) @attr.s(frozen=True, slots=True) @@ -94,6 +97,36 @@ class PluginMultipleReference: item_id_list = attr.ib(default=attr.Factory(list)) +@attr.s(frozen=True, slots=True) +class TracerReferencePluginTableColumn: + """ + A column holding tracer references. + + :ivar tracer_ids: + A list of tracer indexes (by declaration order in the tracer container), entries with + the value `None` represent unset references. + """ + + tracer_ids = attr.ib(validator=list_of_optional_integers) + + +@attr.s(frozen=True, slots=True) +class InternalReferencePluginTableColumn: + """ + A column holding internal references. + + :ivar plugin_item_ids: + A list of item indexes (by declaration order in the relevant container), entries with + the value `None` represent unset references. + + :ivar container_key: + The name of a class defined in the plugin decorated with `container_model`. + """ + + plugin_item_ids = attr.ib(validator=list_of_optional_integers) + container_key = attr.ib(validator=non_empty_str) + + @attr.s(frozen=True, slots=True) class PluginTableContainer: columns = attr.ib(default=attr.Factory(dict)) diff --git a/src/alfasim_sdk/_internal/alfacase/case_description_attributes.py b/src/alfasim_sdk/_internal/alfacase/case_description_attributes.py index 7691cab7..0e4c29cf 100644 --- a/src/alfasim_sdk/_internal/alfacase/case_description_attributes.py +++ b/src/alfasim_sdk/_internal/alfacase/case_description_attributes.py @@ -30,6 +30,9 @@ list_of_strings = deep_iterable( member_validator=optional(instance_of(str)), iterable_validator=instance_of(list) ) +list_of_optional_integers = deep_iterable( + member_validator=optional(instance_of(int)), iterable_validator=instance_of(list) +) AttrNothingType = type(attr.NOTHING) ScalarLike = Union[Tuple[Number, str], Scalar] ArrayLike = Union[Tuple[Sequence[Number], str], Array] diff --git a/src/alfasim_sdk/_internal/alfacase/plugin_alfacase_to_case.py b/src/alfasim_sdk/_internal/alfacase/plugin_alfacase_to_case.py index 0bae6a0e..95059716 100644 --- a/src/alfasim_sdk/_internal/alfacase/plugin_alfacase_to_case.py +++ b/src/alfasim_sdk/_internal/alfacase/plugin_alfacase_to_case.py @@ -21,12 +21,18 @@ from alfasim_sdk._internal.alfacase.alfacase_to_case import load_value from alfasim_sdk._internal.alfacase.alfacase_to_case import to_case_values from alfasim_sdk._internal.alfacase.case_description import CaseDescription +from alfasim_sdk._internal.alfacase.case_description import ( + InternalReferencePluginTableColumn, +) from alfasim_sdk._internal.alfacase.case_description import PluginDescription from alfasim_sdk._internal.alfacase.case_description import PluginFileContent from alfasim_sdk._internal.alfacase.case_description import PluginInternalReference from alfasim_sdk._internal.alfacase.case_description import PluginMultipleReference from alfasim_sdk._internal.alfacase.case_description import PluginTableContainer from alfasim_sdk._internal.alfacase.case_description import PluginTracerReference +from alfasim_sdk._internal.alfacase.case_description import ( + TracerReferencePluginTableColumn, +) from alfasim_sdk._internal.alfacase.case_description_attributes import ( InvalidPluginDataError, ) @@ -311,8 +317,11 @@ def _convert_reference( if is_dict_with_keys(value, "tracer_id"): return PluginTracerReference(tracer_id=int(value["tracer_id"])) else: - if is_dict_with_keys(value, "plugin_item_id"): - return PluginInternalReference(plugin_item_id=int(value["plugin_item_id"])) + if is_dict_with_keys(value, "plugin_item_id", strict=False): + return PluginInternalReference( + container_key=value.get("container_key"), + plugin_item_id=int(value["plugin_item_id"]), + ) raise InvalidPluginDataError(f"Can not convert to a reference: {value!r}") @@ -341,6 +350,35 @@ def _convert_string( raise InvalidPluginDataError(f"Can not convert to a string: {value!r}") +def _convert_table_column_contents( + raw_col: object, +) -> Union[Array, InternalReferencePluginTableColumn, TracerReferencePluginTableColumn]: + """ + Try to convert an object into a valid table column content. + + This is a helper function to `_convert_table` and is not registered + in `_PLUGIN_FIELD_TO_CASEDESCRIPTION`. + """ + if is_dict_with_keys(raw_col, "values", "unit"): + # Quantity. + return Array([float(v) for v in raw_col["values"]], raw_col["unit"]) + elif is_dict_with_keys(raw_col, "tracer_ids"): + # Reference to tracer. + return TracerReferencePluginTableColumn( + [(int(v) if v != "None" else None) for v in raw_col["tracer_ids"]] + ) + elif is_dict_with_keys(raw_col, "plugin_item_ids", "container_key"): + # Reference to plugin model. + return InternalReferencePluginTableColumn( + container_key=raw_col["container_key"], + plugin_item_ids=[ + (int(v) if v != "None" else None) for v in raw_col["plugin_item_ids"] + ], + ) + else: + raise InvalidPluginDataError(f"Can not convert table column: {raw_col!r}") + + def _convert_table( value: object, type_from_plugin: BaseField, alfacase_path: Path ) -> PluginTableContainer: @@ -352,17 +390,9 @@ def _convert_table( raw_columns = value["columns"] col_ids = [col.id for col in type_from_plugin.rows] if is_dict_with_keys(raw_columns, *col_ids): - columns = {} - for col in col_ids: - raw_col = raw_columns[col] - if is_dict_with_keys(raw_col, "values", "unit"): - columns[col] = Array( - [float(v) for v in raw_col["values"]], raw_col["unit"] - ) - else: - raise InvalidPluginDataError( - f"Can not convert table column: {raw_col!r}" - ) + columns = { + col: _convert_table_column_contents(raw_columns[col]) for col in col_ids + } return PluginTableContainer(columns=columns) raise InvalidPluginDataError(f"Can not convert to a table: {value!r}") diff --git a/src/alfasim_sdk/_internal/types.py b/src/alfasim_sdk/_internal/types.py index 9e986fcb..b8701a8f 100644 --- a/src/alfasim_sdk/_internal/types.py +++ b/src/alfasim_sdk/_internal/types.py @@ -1,8 +1,8 @@ import numbers from typing import Callable -from typing import FrozenSet from typing import List from typing import Optional +from typing import Sequence from typing import Union import attr @@ -16,38 +16,38 @@ from alfasim_sdk._internal.validators import valid_unit -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class ALFAsimType: name: str = attrib(default="ALFAsim") -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class TracerType(ALFAsimType): _CONTAINER_TYPE = "TracerModelContainer" -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class Tab: """ Base class for tab attributes available at ALFAsim. """ -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class Tabs: """ Base class for tabs attributes available at ALFAsim. """ -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class Group: """ Base class for Group attribute available at ALFAsim. """ -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class BaseField: """ A base field for all types available at ALFAsim. @@ -205,7 +205,7 @@ def alfasim_get_data_model_type(): ) -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class String(BaseField): """ The String field represents an input that allows the user to enter and edit a single line of plain text. @@ -249,7 +249,7 @@ class MyModel: value: str = attrib(validator=non_empty_str) -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class Enum(BaseField): """ The Enum field provides list of options to the user, showing only the selected item but providing a way to display @@ -331,7 +331,7 @@ def check( # pylint: disable=arguments-differ ) -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class BaseReference(BaseField): ref_type: type = attrib() container_type: Optional[str] = attrib( @@ -340,7 +340,11 @@ class BaseReference(BaseField): def __attrs_post_init__(self): if issubclass(self.ref_type, ALFAsimType): - self.container_type = self.ref_type._CONTAINER_TYPE + if self.container_type is not None: + raise TypeError( + "When using a ALFAsimType the container_type field must be None" + ) + object.__setattr__(self, "container_type", self.ref_type._CONTAINER_TYPE) else: if self.container_type is None: raise TypeError( @@ -364,7 +368,7 @@ def check(self, attr: Attribute, value) -> None: ) -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class Reference(BaseReference): """ The Reference field provides a list of options to the user and displays the current item selected. @@ -464,7 +468,7 @@ class MyContainer: """ -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class MultipleReference(BaseReference): """ The MultipleReference field works similar to :class:`Reference`, providing a list of options @@ -638,13 +642,13 @@ class MyModel: class TableColumn(BaseField): """ The TableColumn component provides columns for a :class:`Table` field. - Currently only columns with a :class:`Quantity` fields are available. + Currently only columns with :class:`Quantity` and :class:`Reference` fields are available. Check out the documentation from :class:`Table` to see more details about the usage and how to retrieve values. """ id: str = attrib(validator=non_empty_str) - value: Quantity = attrib() + value: Union[Quantity, Reference] = attrib() caption: str = attrib(init=False, default="") def __attrs_post_init__(self) -> None: @@ -652,10 +656,19 @@ def __attrs_post_init__(self) -> None: @value.validator def check( # pylint: disable=arguments-differ - self, attr: Attribute, values: Quantity + self, attr: Attribute, values: Union[Quantity, Reference] ) -> None: - if not isinstance(values, Quantity): - raise TypeError(f"{attr.name} must be a Quantity, got a {type(values)}.") + if isinstance(values, Quantity): + return # Always OK. + elif isinstance(values, Reference): + if values.container_type is None: + raise ValueError( + "When placed on table columns a Reference requires a non None container_type." + ) + else: + raise TypeError( + f"{attr.name} must be a Quantity or a Reference, got a {type(values)}." + ) @attr.s(kw_only=True, frozen=True) @@ -667,6 +680,14 @@ class Table(BaseField): .. code-block:: python + @data_model(caption="Flow Type") + class FlowType: + name = String(value="Flow", caption="Name") + + @container_model(caption="Flow Types", model=FlowType, icon="") + class FlowTypeContainer: + pass + @data_model(icon="", caption="My Model") class MyModel: table_field=Table( @@ -687,6 +708,14 @@ class MyModel: caption="Pressure Column Caption", ), ), + TableColumn( + id="flow_type", + value=Reference( + ref_type=FlowType, + container_type="FlowTypeContainer", + caption="Flow Type Column Caption", + ), + ), ], caption="Table Field", ) @@ -707,7 +736,13 @@ class MyModel: .. rubric:: **Accessing Table Field from Plugin**: - In order to access this field from inside the plugin implementation, in C/C++, you need to use :cpp:func:`get_plugin_input_data_table_quantity` + To access this field from inside the plugin implementation, in C/C++, it is possible to access: + + * all the values from a :class:`Quantity` column using :cpp:func:`get_plugin_input_data_table_quantity`; + + * individual entries from a :class:`Quantity` column using :cpp:func:`get_plugin_input_data_quantity` and a `var_name` argument like `"MyModel.table_field[0,temperature]"` (the first item in the "temperature" column); + + * individual entries from a :class:`Reference` column with the ususal functions listed at :ref:`plugin_input_data` and `var_name` arguments like `"MyModel.table_field[1,flow_type]->name"` (the "name" of the second item in the "flow_type" column); .. rubric:: **Accessing Table Field from Context**: @@ -750,7 +785,7 @@ class MyModel: """ - rows: FrozenSet[TableColumn] = attrib(converter=tuple) + rows: Sequence[TableColumn] = attrib(converter=tuple) @rows.validator def check( # pylint: disable=arguments-differ @@ -763,7 +798,7 @@ def check( # pylint: disable=arguments-differ raise TypeError(f"{attr.name} must be a list of TableColumn.") -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class Boolean(BaseField): """ The Boolean field provides a checkbox to select/deselect a property. @@ -809,7 +844,7 @@ class MyModel: value: bool = attrib(validator=instance_of(bool)) -@attr.s(kw_only=True) +@attr.s(kw_only=True, frozen=True) class FileContent(BaseField): """ The FileContent component provides a platform-native file dialog to the user to be able to select a file. diff --git a/tests/alfacase/test_plugin_alfacase_to_case.py b/tests/alfacase/test_plugin_alfacase_to_case.py index 486cd89c..c83ab60d 100644 --- a/tests/alfacase/test_plugin_alfacase_to_case.py +++ b/tests/alfacase/test_plugin_alfacase_to_case.py @@ -16,11 +16,17 @@ from alfasim_sdk import convert_alfacase_to_description from alfasim_sdk import PluginDescription from alfasim_sdk._internal.alfacase.case_description import CaseDescription +from alfasim_sdk._internal.alfacase.case_description import ( + InternalReferencePluginTableColumn, +) from alfasim_sdk._internal.alfacase.case_description import PluginFileContent from alfasim_sdk._internal.alfacase.case_description import PluginInternalReference from alfasim_sdk._internal.alfacase.case_description import PluginMultipleReference from alfasim_sdk._internal.alfacase.case_description import PluginTableContainer from alfasim_sdk._internal.alfacase.case_description import PluginTracerReference +from alfasim_sdk._internal.alfacase.case_description import ( + TracerReferencePluginTableColumn, +) from alfasim_sdk._internal.alfacase.case_description_attributes import ( InvalidPluginDataError, ) @@ -279,6 +285,10 @@ def prepare_foobar_plugin_impl(data_model_fields: List[str]) -> None: class Bar: pass + @alfasim_sdk.container_model(model=Bar, icon="", caption="Bar Container") + class BarContainer: + pass + {data_model_source} @alfasim_sdk.container_model(model=Foo, icon="", caption="Foo Container") @@ -287,7 +297,7 @@ class FooContainer: @alfasim_sdk.hookimpl def alfasim_get_data_model_type(): - return [FooContainer] + return [FooContainer, BarContainer] """ ) plugin_file_source = plugin_file_source.format( @@ -703,138 +713,215 @@ def test_load_string(datadir, prepare_foobar_plugin): convert_alfacase_to_description(alfacase) -def test_load_table(datadir, prepare_foobar_plugin): - prepare_foobar_plugin( - [ - """x = alfasim_sdk.Table( - rows=[ - alfasim_sdk.TableColumn(id='aaa', value=alfasim_sdk.Quantity(value=1, unit='m', caption='a')), - alfasim_sdk.TableColumn(id='sss', value=alfasim_sdk.Quantity(value=1, unit='m', caption='s')), - ], - caption='x', - )""" - ] - ) - alfacase = datadir / "test.alfacase" - - alfacase.write_text( - textwrap.dedent( - """\ - plugins: - - name: foobar - is_enabled: True - gui_models: - FooContainer: - _children_list: - - x: - columns: - aaa: - values: [1.2, 2.3, 3.4] - unit: s - sss: - values: [9.8, 8.7, 7.6] - unit: m - """ - ) - ) - case = convert_alfacase_to_description(alfacase) - assert case.plugins == [ - PluginDescription( - name="foobar", - is_enabled=True, - gui_models={ - "FooContainer": { - "_children_list": [ - { - "x": PluginTableContainer( - columns={ - "aaa": Array([1.2, 2.3, 3.4], "s"), - "sss": Array([9.8, 8.7, 7.6], "m"), - } - ) - }, +class TestLoadTable: + def test_quantity( + self, datadir: Path, prepare_foobar_plugin: Callable[[List[str]], None] + ) -> None: + prepare_foobar_plugin( + [ + """x = alfasim_sdk.Table( + rows=[ + alfasim_sdk.TableColumn(id='aaa', value=alfasim_sdk.Quantity(value=1, unit='m', caption='a')), + alfasim_sdk.TableColumn(id='sss', value=alfasim_sdk.Quantity(value=1, unit='m', caption='s')), ], + caption='x', + )""" + ] + ) + alfacase = datadir / "test.alfacase" + + alfacase.write_text( + textwrap.dedent( + """\ + plugins: + - name: foobar + is_enabled: True + gui_models: + FooContainer: + _children_list: + - x: + columns: + aaa: + values: [1.2, 2.3, 3.4] + unit: s + sss: + values: [9.8, 8.7, 7.6] + unit: m + """ + ) + ) + case = convert_alfacase_to_description(alfacase) + assert case.plugins == [ + PluginDescription( + name="foobar", + is_enabled=True, + gui_models={ + "FooContainer": { + "_children_list": [ + { + "x": PluginTableContainer( + columns={ + "aaa": Array([1.2, 2.3, 3.4], "s"), + "sss": Array([9.8, 8.7, 7.6], "m"), + } + ) + }, + ], + }, }, - }, - ), - ] + ), + ] - alfacase.write_text( - textwrap.dedent( - """\ - plugins: - - name: foobar - is_enabled: True - gui_models: - FooContainer: - _children_list: - - x: - columns: - aaa: - values: [1.2, 2.3, 3.4] - unit: s - """ + alfacase.write_text( + textwrap.dedent( + """\ + plugins: + - name: foobar + is_enabled: True + gui_models: + FooContainer: + _children_list: + - x: + columns: + aaa: + values: [1.2, 2.3, 3.4] + unit: s + """ + ) ) - ) - with pytest.raises(InvalidPluginDataError, match="Can not convert"): - convert_alfacase_to_description(alfacase) - - alfacase.write_text( - textwrap.dedent( - """\ - plugins: - - name: foobar - is_enabled: True - gui_models: - FooContainer: - _children_list: - - x: - columns: + with pytest.raises(InvalidPluginDataError, match="Can not convert"): + convert_alfacase_to_description(alfacase) + + alfacase.write_text( + textwrap.dedent( + """\ + plugins: + - name: foobar + is_enabled: True + gui_models: + FooContainer: + _children_list: + - x: + columns: + aaa: + values: [1.2, 2.3, 3.4] + unit: s + sss: + values: [9.8, 8.7, 7.6] + """ + ) + ) + with pytest.raises(InvalidPluginDataError, match="Can not convert"): + convert_alfacase_to_description(alfacase) + + alfacase.write_text( + textwrap.dedent( + """\ + plugins: + - name: foobar + is_enabled: True + gui_models: + FooContainer: + _children_list: + - x: aaa: values: [1.2, 2.3, 3.4] unit: s - sss: - values: [9.8, 8.7, 7.6] - """ + """ + ) ) - ) - with pytest.raises(InvalidPluginDataError, match="Can not convert"): - convert_alfacase_to_description(alfacase) - - alfacase.write_text( - textwrap.dedent( - """\ - plugins: - - name: foobar - is_enabled: True - gui_models: - FooContainer: - _children_list: - - x: - aaa: - values: [1.2, 2.3, 3.4] - unit: s - """ + with pytest.raises(InvalidPluginDataError, match="Can not convert"): + convert_alfacase_to_description(alfacase) + + alfacase.write_text( + textwrap.dedent( + """\ + plugins: + - name: foobar + is_enabled: True + gui_models: + FooContainer: + _children_list: + - x: A + """ + ) ) - ) - with pytest.raises(InvalidPluginDataError, match="Can not convert"): - convert_alfacase_to_description(alfacase) + with pytest.raises(InvalidPluginDataError, match="Can not convert"): + convert_alfacase_to_description(alfacase) - alfacase.write_text( - textwrap.dedent( - """\ - plugins: - - name: foobar - is_enabled: True - gui_models: - FooContainer: - _children_list: - - x: A - """ + def test_references( + self, datadir: Path, prepare_foobar_plugin: Callable[[List[str]], None] + ) -> None: + # Setup. + prepare_foobar_plugin( + [ + """x = alfasim_sdk.Table( + rows=[ + alfasim_sdk.TableColumn( + id='aaa', + value=alfasim_sdk.Reference( + container_type='BarContainer', ref_type=Bar, caption="A Bar" + ), + ), + alfasim_sdk.TableColumn( + id='sss', + value=alfasim_sdk.Reference( + ref_type=alfasim_sdk.TracerType, caption="A Tracer" + ), + ), + ], + caption='x', + )""" + ] ) - ) - with pytest.raises(InvalidPluginDataError, match="Can not convert"): - convert_alfacase_to_description(alfacase) + alfacase = datadir / "test.alfacase" + + alfacase.write_text( + textwrap.dedent( + """\ + plugins: + - name: foobar + is_enabled: True + gui_models: + FooContainer: + _children_list: + - x: + columns: + aaa: + container_key: BarContainer + plugin_item_ids: [0, 0, 2, 2] + sss: + tracer_ids: [1, 2, 3, 3] + """ + ) + ) + # Test. + case = convert_alfacase_to_description(alfacase) + assert case.plugins == [ + PluginDescription( + name="foobar", + is_enabled=True, + gui_models={ + "FooContainer": { + "_children_list": [ + { + "x": PluginTableContainer( + columns={ + "aaa": InternalReferencePluginTableColumn( + container_key="BarContainer", + plugin_item_ids=[0, 0, 2, 2], + ), + "sss": TracerReferencePluginTableColumn( + tracer_ids=[1, 2, 3, 3] + ), + } + ) + }, + ], + }, + }, + ), + ] def test_dump_file_contents_and_update_plugins(datadir, monkeypatch): diff --git a/tests/plugins/test_types.py b/tests/plugins/test_types.py index 2573e5bf..447138b2 100644 --- a/tests/plugins/test_types.py +++ b/tests/plugins/test_types.py @@ -163,7 +163,7 @@ def test_table_column(): from alfasim_sdk._internal.types import TableColumn, Quantity with pytest.raises( - TypeError, match="value must be a Quantity, got a ." + TypeError, match="value must be a Quantity or a Reference, got a ." ): TableColumn(id="id", value="")