From 297d0e91a4a579c6279c2b339154516d1b04ce8d Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 25 Jul 2024 13:57:19 -0400 Subject: [PATCH] Restrict enumerated design space data value types. The backend has always restricted them to strings, but the SDK never enforced it. And since the backend used a lax serde library, it would automatically coerce ints and floats to strings. To preserve backwards compatability while moving in the right direction, `data` is restricted to int, float, and str, and when assigning a data list, if any int or float values trigger a deprecation warning. --- src/citrine/__version__.py | 2 +- .../design_spaces/enumerated_design_space.py | 29 +++++++++++++++---- tests/conftest.py | 4 +-- tests/informatics/test_design_spaces.py | 4 +-- tests/resources/test_design_space.py | 4 +-- tests/serialization/test_design_spaces.py | 18 ++++++++++-- 6 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index 3e7e964d9..e3a316a7c 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "3.4.6" +__version__ = "3.4.7" diff --git a/src/citrine/informatics/design_spaces/enumerated_design_space.py b/src/citrine/informatics/design_spaces/enumerated_design_space.py index 8d1043216..5b5115cfa 100644 --- a/src/citrine/informatics/design_spaces/enumerated_design_space.py +++ b/src/citrine/informatics/design_spaces/enumerated_design_space.py @@ -1,4 +1,5 @@ -from typing import List, Mapping, Any +from typing import List, Mapping, Union +from warnings import warn from citrine._rest.engine_resource import EngineResource from citrine._serialization import properties @@ -29,8 +30,11 @@ class EnumeratedDesignSpace(EngineResource['EnumeratedDesignSpace'], DesignSpace """ descriptors = properties.List(properties.Object(Descriptor), 'data.instance.descriptors') - data = properties.List(properties.Mapping(properties.String, properties.Raw), - 'data.instance.data') + _data = properties.List(properties.Mapping(properties.String, + properties.Union([properties.String(), + properties.Integer(), + properties.Float()])), + 'data.instance.data') typ = properties.String('data.instance.type', default='EnumeratedDesignSpace', deserializable=False) @@ -40,11 +44,26 @@ def __init__(self, *, description: str, descriptors: List[Descriptor], - data: List[Mapping[str, Any]]): + data: List[Mapping[str, Union[int, float, str]]]): self.name: str = name self.description: str = description self.descriptors: List[Descriptor] = descriptors - self.data: List[Mapping[str, Any]] = data + self.data: List[Mapping[str, Union[int, float, str]]] = data def __str__(self): return ''.format(self.name) + + @property + def data(self) -> List[Mapping[str, Union[int, float, str]]]: + """List of dicts corresponding to candidates in the design space.""" + return self._data + + @data.setter + def data(self, value: List[Mapping[str, Union[int, float, str]]]): + for item in value: + for el in item.values(): + if isinstance(el, (int, float)): + warn("Providing numeric data values is deprecated as of 3.4.7, and will be " + "dropped in 4.0.0. Please use strings instead.", + DeprecationWarning) + self._data = value diff --git a/tests/conftest.py b/tests/conftest.py index 687f8af1e..74112dfc5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -149,8 +149,8 @@ def valid_enumerated_design_space_data(): ) ], data=[ - dict(x=1, color='red', formula='C44H54Si2'), - dict(x=2.0, color='green', formula='V2O3') + dict(x='1', color='red', formula='C44H54Si2'), + dict(x='2.0', color='green', formula='V2O3') ] ) ), diff --git a/tests/informatics/test_design_spaces.py b/tests/informatics/test_design_spaces.py index d74c3ca78..9f92188f7 100644 --- a/tests/informatics/test_design_spaces.py +++ b/tests/informatics/test_design_spaces.py @@ -31,7 +31,7 @@ def enumerated_design_space() -> EnumeratedDesignSpace: """Build an EnumeratedDesignSpace for testing.""" x = RealDescriptor('x', lower_bound=0.0, upper_bound=1.0, units='') color = CategoricalDescriptor('color', categories=['r', 'g', 'b']) - data = [dict(x=0, color='r'), dict(x=1.0, color='b')] + data = [dict(x='0', color='r'), dict(x='1.0', color='b')] return EnumeratedDesignSpace('enumerated', description='desc', descriptors=[x, color], data=data) @@ -105,7 +105,7 @@ def test_enumerated_initialization(enumerated_design_space): assert len(enumerated_design_space.descriptors) == 2 assert enumerated_design_space.descriptors[0].key == 'x' assert enumerated_design_space.descriptors[1].key == 'color' - assert enumerated_design_space.data == [{'x': 0.0, 'color': 'r'}, {'x': 1.0, 'color': 'b'}] + assert enumerated_design_space.data == [{'x': '0', 'color': 'r'}, {'x': '1.0', 'color': 'b'}] def test_hierarchical_initialization(hierarchical_design_space): diff --git a/tests/resources/test_design_space.py b/tests/resources/test_design_space.py index 0554e5c81..42f0c1093 100644 --- a/tests/resources/test_design_space.py +++ b/tests/resources/test_design_space.py @@ -138,14 +138,14 @@ def test_design_space_limits(): "foo", description="bar", descriptors=[RealDescriptor("R-{}".format(i), lower_bound=0, upper_bound=1, units="") for i in range(128)], - data=[{"R-{}".format(i): random.random() for i in range(128)} for _ in range(2001)] + data=[{f"R-{i}": str(random.random()) for i in range(128)} for _ in range(2001)] ) just_right = EnumeratedDesignSpace( "foo", description="bar", descriptors=[RealDescriptor("R-{}".format(i), lower_bound=0, upper_bound=1, units="") for i in range(128)], - data=[{"R-{}".format(i): random.random() for i in range(128)} for _ in range(2000)] + data=[{f"R-{i}": str(random.random()) for i in range(128)} for _ in range(2000)] ) # create mock post response by setting the status diff --git a/tests/serialization/test_design_spaces.py b/tests/serialization/test_design_spaces.py index e63bb7580..515a61c94 100644 --- a/tests/serialization/test_design_spaces.py +++ b/tests/serialization/test_design_spaces.py @@ -2,6 +2,8 @@ from copy import copy, deepcopy from uuid import UUID +import pytest + from . import design_space_serialization_check, valid_serialization_output from citrine.informatics.constraints import IngredientCountConstraint from citrine.informatics.descriptors import CategoricalDescriptor, RealDescriptor, ChemicalFormulaDescriptor,\ @@ -67,8 +69,20 @@ def test_enumerated_deserialization(valid_enumerated_design_space_data): assert formula.key == 'formula' assert len(design_space.data) == 2 - assert design_space.data[0] == {'x': 1.0, 'color': 'red', 'formula': 'C44H54Si2'} - assert design_space.data[1] == {'x': 2.0, 'color': 'green', 'formula': 'V2O3'} + assert design_space.data[0] == {'x': '1', 'color': 'red', 'formula': 'C44H54Si2'} + assert design_space.data[1] == {'x': '2.0', 'color': 'green', 'formula': 'V2O3'} + + +def test_enumerated_serialization_data_int_deprecated(valid_enumerated_design_space_data): + design_space = EnumeratedDesignSpace.build(valid_enumerated_design_space_data) + with pytest.deprecated_call(): + design_space.data = [dict(x=1, color='red', formula='C44H54Si2')] + + +def test_enumerated_serialization_data_float_deprecated(valid_enumerated_design_space_data): + design_space = EnumeratedDesignSpace.build(valid_enumerated_design_space_data) + with pytest.deprecated_call(): + design_space.data = [dict(x=1.0, color='red', formula='C44H54Si2')] def test_enumerated_serialization(valid_enumerated_design_space_data):