diff --git a/src/nitypes/scalar.py b/src/nitypes/scalar.py index f208410c..82aa2763 100644 --- a/src/nitypes/scalar.py +++ b/src/nitypes/scalar.py @@ -9,12 +9,14 @@ from __future__ import annotations +from collections.abc import Mapping from typing import TYPE_CHECKING, Any, Generic, Union -from typing_extensions import TypeVar, final +from typing_extensions import Self, TypeVar, final from nitypes._exceptions import invalid_arg_type from nitypes.waveform._extended_properties import UNIT_DESCRIPTION +from nitypes.waveform.typing import ExtendedPropertyValue if TYPE_CHECKING: # Import from the public package so the docs don't reference private submodules. @@ -36,9 +38,9 @@ class Scalar(Generic[TScalar_co]): To construct a scalar data object, use the :class:`Scalar` class: >>> Scalar(False) - nitypes.scalar.Scalar(value=False, units='') + nitypes.scalar.Scalar(value=False) >>> Scalar(0) - nitypes.scalar.Scalar(value=0, units='') + nitypes.scalar.Scalar(value=0) >>> Scalar(5.0, 'volts') nitypes.scalar.Scalar(value=5.0, units='volts') >>> Scalar("value", "volts") @@ -60,12 +62,18 @@ def __init__( self, value: TScalar_co, units: str = "", + *, + extended_properties: Mapping[str, ExtendedPropertyValue] | None = None, + copy_extended_properties: bool = True, ) -> None: """Initialize a new scalar. Args: value: The scalar data to store in this object. units: The units string associated with this data. + extended_properties: The extended properties of the Scalar. + copy_extended_properties: Specifies whether to copy the extended properties or take + ownership. Returns: A scalar data object. @@ -77,8 +85,20 @@ def __init__( raise invalid_arg_type("units", "str", units) self._value = value - self._extended_properties = ExtendedPropertyDictionary() - self._extended_properties[UNIT_DESCRIPTION] = units + if copy_extended_properties or not isinstance( + extended_properties, ExtendedPropertyDictionary + ): + extended_properties = ExtendedPropertyDictionary(extended_properties) + self._extended_properties = extended_properties + + # If units are not already in extended properties, set them. + if UNIT_DESCRIPTION not in self._extended_properties: + self._extended_properties[UNIT_DESCRIPTION] = units + elif units and units != self._extended_properties.get(UNIT_DESCRIPTION): + raise ValueError( + "The specified units input does not match the units specified in " + "extended_properties." + ) @property def value(self) -> TScalar_co: @@ -164,11 +184,28 @@ def __le__(self, value: Scalar[TScalar_co]) -> bool: def __reduce__(self) -> tuple[Any, ...]: """Return object state for pickling.""" - return (self.__class__, (self.value, self.units)) + ctor_args = (self.value,) + ctor_kwargs: dict[str, Any] = { + "extended_properties": self._extended_properties, + "copy_extended_properties": False, + } + return (self.__class__._unpickle, (ctor_args, ctor_kwargs)) + + @classmethod + def _unpickle(cls, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Self: + return cls(*args, **kwargs) def __repr__(self) -> str: """Return repr(self).""" - args = [f"value={self.value!r}", f"units={self.units!r}"] + args = [f"value={self.value!r}"] + + if self.units: + args.append(f"units={self.units!r}") + + # Only display the extended properties if non-units entries are specified. + if any(key for key in self.extended_properties.keys() if key != UNIT_DESCRIPTION): + args.append(f"extended_properties={self.extended_properties!r}") + return f"{self.__class__.__module__}.{self.__class__.__name__}({', '.join(args)})" def __str__(self) -> str: diff --git a/src/nitypes/vector.py b/src/nitypes/vector.py index 5ff2d3d5..ff719c4d 100644 --- a/src/nitypes/vector.py +++ b/src/nitypes/vector.py @@ -8,13 +8,14 @@ from __future__ import annotations -from collections.abc import Iterable, MutableSequence -from typing import TYPE_CHECKING, overload, Any, Union +from collections.abc import Iterable, Mapping, MutableSequence +from typing import TYPE_CHECKING, Any, Union, overload -from typing_extensions import TypeVar, final, override +from typing_extensions import Self, TypeVar, final, override from nitypes._exceptions import invalid_arg_type from nitypes.waveform._extended_properties import UNIT_DESCRIPTION +from nitypes.waveform.typing import ExtendedPropertyValue if TYPE_CHECKING: # Import from the public package so the docs don't reference private submodules. @@ -35,9 +36,9 @@ class Vector(MutableSequence[TScalar]): To construct a vector data object, use the :class:`Vector` class: >>> Vector([False, True]) - nitypes.vector.Vector(values=[False, True], units='') + nitypes.vector.Vector(values=[False, True]) >>> Vector([0, 1, 2]) - nitypes.vector.Vector(values=[0, 1, 2], units='') + nitypes.vector.Vector(values=[0, 1, 2]) >>> Vector([5.0, 6.0], 'volts') nitypes.vector.Vector(values=[5.0, 6.0], units='volts') >>> Vector(["one", "two"], "volts") @@ -60,6 +61,8 @@ def __init__( units: str = "", *, value_type: type[TScalar] | None = None, + extended_properties: Mapping[str, ExtendedPropertyValue] | None = None, + copy_extended_properties: bool = True, ) -> None: """Initialize a new vector. @@ -69,6 +72,9 @@ def __init__( value_type: The type of values that will be added to this Vector. This parameter should only be used when creating a Vector with an empty Iterable. + extended_properties: The extended properties of the Vector. + copy_extended_properties: Specifies whether to copy the extended properties or take + ownership. Returns: A vector data object. @@ -94,8 +100,20 @@ def __init__( raise invalid_arg_type("units", "str", units) self._values = list(values) - self._extended_properties = ExtendedPropertyDictionary() - self._extended_properties[UNIT_DESCRIPTION] = units + if copy_extended_properties or not isinstance( + extended_properties, ExtendedPropertyDictionary + ): + extended_properties = ExtendedPropertyDictionary(extended_properties) + self._extended_properties = extended_properties + + # If units are not already in extended properties, set them. + if UNIT_DESCRIPTION not in self._extended_properties: + self._extended_properties[UNIT_DESCRIPTION] = units + elif units and units != self._extended_properties.get(UNIT_DESCRIPTION): + raise ValueError( + "The specified units input does not match the units specified in " + "extended_properties." + ) @property def units(self) -> str: @@ -191,11 +209,29 @@ def __eq__(self, value: object, /) -> bool: def __reduce__(self) -> tuple[Any, ...]: """Return object state for pickling.""" - return (self.__class__, (self._values, self.units)) + ctor_args = (self._values,) + ctor_kwargs: dict[str, Any] = { + "value_type": self._value_type, + "extended_properties": self._extended_properties, + "copy_extended_properties": False, + } + return (self.__class__._unpickle, (ctor_args, ctor_kwargs)) + + @classmethod + def _unpickle(cls, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Self: + return cls(*args, **kwargs) def __repr__(self) -> str: """Return repr(self).""" - args = [f"values={self._values!r}", f"units={self.units!r}"] + args = [f"values={self._values!r}"] + + if self.units: + args.append(f"units={self.units!r}") + + # Only display the extended properties if non-units entries are specified. + if any(key for key in self.extended_properties.keys() if key != UNIT_DESCRIPTION): + args.append(f"extended_properties={self.extended_properties!r}") + return f"{self.__class__.__module__}.{self.__class__.__name__}({', '.join(args)})" def __str__(self) -> str: diff --git a/src/nitypes/xy_data.py b/src/nitypes/xy_data.py index e5b05e47..375b4882 100644 --- a/src/nitypes/xy_data.py +++ b/src/nitypes/xy_data.py @@ -71,20 +71,16 @@ class XYData(Generic[TData]): To construct an XYData object, use the :class:`XYData` class: >>> XYData(np.array([1.1], np.float64), np.array([4.1], np.float64)) - nitypes.xy_data.XYData(x_data=array([1.1]), y_data=array([4.1]), - extended_properties={'NI_UnitDescription_X': '', 'NI_UnitDescription_Y': ''}) + nitypes.xy_data.XYData(x_data=array([1.1]), y_data=array([4.1])) >>> XYData(np.array([1, 2]), np.array([4, 5]), x_units="A", y_units="V") - nitypes.xy_data.XYData(x_data=array([1, 2]), y_data=array([4, 5]), - extended_properties={'NI_UnitDescription_X': 'A', 'NI_UnitDescription_Y': 'V'}) + nitypes.xy_data.XYData(x_data=array([1, 2]), y_data=array([4, 5]), x_units='A', y_units='V') To construct an XYData object using built-in lists, use from_arrays_1d(): >>> XYData.from_arrays_1d([1, 2], [5, 6], np.int32) - nitypes.xy_data.XYData(x_data=array([1, 2], dtype=int32), y_data=array([5, 6], dtype=int32), - extended_properties={'NI_UnitDescription_X': '', 'NI_UnitDescription_Y': ''}) + nitypes.xy_data.XYData(x_data=array([1, 2], dtype=int32), y_data=array([5, 6], dtype=int32)) >>> XYData.from_arrays_1d([1.0, 1.1], [1.2, 1.3], np.float64) - nitypes.xy_data.XYData(x_data=array([1. , 1.1]), y_data=array([1.2, 1.3]), - extended_properties={'NI_UnitDescription_X': '', 'NI_UnitDescription_Y': ''}) + nitypes.xy_data.XYData(x_data=array([1. , 1.1]), y_data=array([1.2, 1.3])) """ __slots__ = [ @@ -246,12 +242,23 @@ def __init__( extended_properties = ExtendedPropertyDictionary(extended_properties) self._extended_properties = extended_properties - # If x and y units are not already in extended properties, set them. - # If the caller specifies a non-blank x or y units, overwrite the existing entry. - if _UNIT_DESCRIPTION_X not in self._extended_properties or x_units: + # If x_units are not already in extended properties, set them. + if _UNIT_DESCRIPTION_X not in self._extended_properties: self._extended_properties[_UNIT_DESCRIPTION_X] = x_units - if _UNIT_DESCRIPTION_Y not in self._extended_properties or y_units: + elif x_units and x_units != self._extended_properties.get(_UNIT_DESCRIPTION_X): + raise ValueError( + "The specified x_units input does not match the units specified in " + "extended_properties." + ) + + # If y_units are not already in extended properties, set them. + if _UNIT_DESCRIPTION_Y not in self._extended_properties: self._extended_properties[_UNIT_DESCRIPTION_Y] = y_units + elif y_units and y_units != self._extended_properties.get(_UNIT_DESCRIPTION_Y): + raise ValueError( + "The specified y_units input does not match the units specified in " + "extended_properties." + ) def _init_with_provided_arrays( self, @@ -365,11 +372,22 @@ def _unpickle(cls, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Self: def __repr__(self) -> str: """Return repr(self).""" - args = [ - f"x_data={self.x_data!r}", - f"y_data={self.y_data!r}", - f"extended_properties={self._extended_properties._properties!r}", - ] + args = [f"x_data={self.x_data!r}", f"y_data={self.y_data!r}"] + + if self.x_units: + args.append(f"x_units={self.x_units!r}") + + if self.y_units: + args.append(f"y_units={self.y_units!r}") + + # Only display the extended properties if non-units entries are specified. + if any( + key + for key in self.extended_properties.keys() + if key not in [_UNIT_DESCRIPTION_X, _UNIT_DESCRIPTION_Y] + ): + args.append(f"extended_properties={self.extended_properties!r}") + return f"{self.__class__.__module__}.{self.__class__.__name__}({', '.join(args)})" def __str__(self) -> str: diff --git a/tests/unit/scalar/test_scalar.py b/tests/unit/scalar/test_scalar.py index b83756dc..6c5d73ed 100644 --- a/tests/unit/scalar/test_scalar.py +++ b/tests/unit/scalar/test_scalar.py @@ -68,6 +68,20 @@ def test___invalid_data_value___create___raises_type_error(data_value: Any) -> N assert exc.value.args[0].startswith("The scalar input data must be a bool, int, float, or str.") +def test___both_units_specified_unequal__create___raises_value_error() -> None: + with pytest.raises(ValueError) as exc: + _ = Scalar(10, "Volts", extended_properties={UNIT_DESCRIPTION: "Amps"}) + + assert exc.value.args[0].startswith( + "The specified units input does not match the units specified in extended_properties." + ) + + +def test___units_only_specified_in_extended_properties__create___creates_with_units() -> None: + data = Scalar(10, extended_properties={UNIT_DESCRIPTION: "Volts"}) + assert data.units == "Volts" + + ############################################################################### # compare ############################################################################### @@ -182,14 +196,20 @@ def test___different_units___comparison___throws_exception() -> None: @pytest.mark.parametrize( "value, expected_repr", [ - (Scalar(False), "nitypes.scalar.Scalar(value=False, units='')"), - (Scalar(10), "nitypes.scalar.Scalar(value=10, units='')"), - (Scalar(20.0), "nitypes.scalar.Scalar(value=20.0, units='')"), - (Scalar("value"), "nitypes.scalar.Scalar(value='value', units='')"), + (Scalar(False), "nitypes.scalar.Scalar(value=False)"), + (Scalar(10), "nitypes.scalar.Scalar(value=10)"), + (Scalar(20.0), "nitypes.scalar.Scalar(value=20.0)"), + (Scalar("value"), "nitypes.scalar.Scalar(value='value')"), (Scalar(False, "amps"), "nitypes.scalar.Scalar(value=False, units='amps')"), (Scalar(10, "volts"), "nitypes.scalar.Scalar(value=10, units='volts')"), (Scalar(20.0, "watts"), "nitypes.scalar.Scalar(value=20.0, units='watts')"), - (Scalar("value", ""), "nitypes.scalar.Scalar(value='value', units='')"), + (Scalar("value", ""), "nitypes.scalar.Scalar(value='value')"), + ( + Scalar(10, units="volts", extended_properties={"Prop1": "Value1"}), + "nitypes.scalar.Scalar(value=10, units='volts', " + "extended_properties=nitypes.waveform.ExtendedPropertyDictionary(" + "{'Prop1': 'Value1', 'NI_UnitDescription': 'volts'}))", + ), ], ) def test___various_values___repr___looks_ok(value: Scalar[Any], expected_repr: str) -> None: @@ -244,12 +264,14 @@ def test___scalar_with_units___set_units___units_updated_correctly() -> None: Scalar(10, "volts"), Scalar(20.0, "watts"), Scalar("value", ""), + Scalar(10, "Volts", extended_properties={"one": 1}), ], ) def test___various_values___copy___makes_copy(value: Scalar[TScalar_co]) -> None: new_value = copy.copy(value) assert new_value is not value assert new_value == value + assert new_value.extended_properties == value.extended_properties @pytest.mark.parametrize( @@ -263,12 +285,15 @@ def test___various_values___copy___makes_copy(value: Scalar[TScalar_co]) -> None Scalar(10, "volts"), Scalar(20.0, "watts"), Scalar("value", ""), + Scalar(10, "Volts", extended_properties={"one": 1}), ], ) def test___various_values___pickle_unpickle___makes_copy(value: Scalar[TScalar_co]) -> None: new_value = pickle.loads(pickle.dumps(value)) + assert isinstance(new_value, Scalar) assert new_value is not value assert new_value == value + assert new_value.extended_properties == value.extended_properties def test___scalar___pickle___references_public_modules() -> None: diff --git a/tests/unit/vector/test_vector.py b/tests/unit/vector/test_vector.py index 85019112..5d706619 100644 --- a/tests/unit/vector/test_vector.py +++ b/tests/unit/vector/test_vector.py @@ -79,6 +79,20 @@ def test___data_value_and_units___create___creates_scalar_data_with_data_and_uni assert data.units == units +def test___both_units_specified_unequal__create___raises_value_error() -> None: + with pytest.raises(ValueError) as exc: + _ = Vector([10], "Volts", extended_properties={UNIT_DESCRIPTION: "Amps"}) + + assert exc.value.args[0].startswith( + "The specified units input does not match the units specified in extended_properties." + ) + + +def test___units_only_specified_in_extended_properties__create___creates_with_units() -> None: + data = Vector([10], extended_properties={UNIT_DESCRIPTION: "Volts"}) + assert data.units == "Volts" + + @pytest.mark.parametrize("data_value", [[[1.0, 2.0]], [{"key", "value"}]]) def test___invalid_data_value___create___raises_type_error(data_value: Any) -> None: with pytest.raises(TypeError) as exc: @@ -293,14 +307,20 @@ def test___different_units___comparison___not_equal() -> None: @pytest.mark.parametrize( "value, expected_repr", [ - (Vector([False, True]), "nitypes.vector.Vector(values=[False, True], units='')"), - (Vector([10, 20]), "nitypes.vector.Vector(values=[10, 20], units='')"), - (Vector([20.0, 20.1]), "nitypes.vector.Vector(values=[20.0, 20.1], units='')"), - (Vector(["a", "b"]), "nitypes.vector.Vector(values=['a', 'b'], units='')"), + (Vector([False, True]), "nitypes.vector.Vector(values=[False, True])"), + (Vector([10, 20]), "nitypes.vector.Vector(values=[10, 20])"), + (Vector([20.0, 20.1]), "nitypes.vector.Vector(values=[20.0, 20.1])"), + (Vector(["a", "b"]), "nitypes.vector.Vector(values=['a', 'b'])"), (Vector([False, True], "f"), "nitypes.vector.Vector(values=[False, True], units='f')"), (Vector([10, 20], "volts"), "nitypes.vector.Vector(values=[10, 20], units='volts')"), (Vector([20.0, 20.1], "w"), "nitypes.vector.Vector(values=[20.0, 20.1], units='w')"), - (Vector(["a", "b"], ""), "nitypes.vector.Vector(values=['a', 'b'], units='')"), + (Vector(["a", "b"], ""), "nitypes.vector.Vector(values=['a', 'b'])"), + ( + Vector([10, 20], "volts", extended_properties={"Prop1": "Value1"}), + "nitypes.vector.Vector(values=[10, 20], units='volts', " + "extended_properties=nitypes.waveform.ExtendedPropertyDictionary(" + "{'Prop1': 'Value1', 'NI_UnitDescription': 'volts'}))", + ), ], ) def test___various_values___repr___looks_ok(value: Vector[Any], expected_repr: str) -> None: @@ -359,12 +379,14 @@ def test___vector_with_units___set_units___units_updated_correctly() -> None: Vector([10, 20], "volts"), Vector([20.0, 20.1], "watts"), Vector(["a", "b"], ""), + Vector([10, 20], "volts", extended_properties={"one": 1}), ], ) def test___various_values___copy___makes_copy(value: Vector[TScalar]) -> None: new_value = copy.copy(value) assert new_value is not value assert new_value == value + assert new_value.extended_properties == value.extended_properties @pytest.mark.parametrize( @@ -378,12 +400,15 @@ def test___various_values___copy___makes_copy(value: Vector[TScalar]) -> None: Vector([10, 20], "volts"), Vector([20.0, 20.1], "watts"), Vector(["a", "b"], ""), + Vector([10, 20], "volts", extended_properties={"one": 1}), ], ) def test___various_values___pickle_unpickle___makes_copy(value: Vector[TScalar]) -> None: new_value = pickle.loads(pickle.dumps(value)) + assert isinstance(new_value, Vector) assert new_value is not value assert new_value == value + assert new_value.extended_properties == value.extended_properties def test___vector___pickle___references_public_modules() -> None: diff --git a/tests/unit/xy_data/test_xy_data.py b/tests/unit/xy_data/test_xy_data.py index 8b92fca1..246a7da1 100644 --- a/tests/unit/xy_data/test_xy_data.py +++ b/tests/unit/xy_data/test_xy_data.py @@ -13,7 +13,7 @@ from typing_extensions import assert_type from nitypes.waveform._extended_properties import ExtendedPropertyDictionary -from nitypes.xy_data import _UNIT_DESCRIPTION_X, _UNIT_DESCRIPTION_Y, XYData, TData +from nitypes.xy_data import _UNIT_DESCRIPTION_X, _UNIT_DESCRIPTION_Y, TData, XYData ############################################################################### @@ -39,6 +39,38 @@ def test___data_dtype_and_units___create___creates_xydata_with_data_dtype_and_un assert xydata.y_units == "seconds" +def test___both_x_units_specified_unequal__create___raises_value_error() -> None: + data = np.array([1, 2, 3, 4, 5], np.int32) + with pytest.raises(ValueError) as exc: + _ = XYData(data, data, x_units="Volts", extended_properties={_UNIT_DESCRIPTION_X: "Amps"}) + + assert exc.value.args[0].startswith( + "The specified x_units input does not match the units specified in extended_properties." + ) + + +def test___both_y_units_specified_unequal__create___raises_value_error() -> None: + data = np.array([1, 2, 3, 4, 5], np.int32) + with pytest.raises(ValueError) as exc: + _ = XYData(data, data, y_units="Volts", extended_properties={_UNIT_DESCRIPTION_Y: "Amps"}) + + assert exc.value.args[0].startswith( + "The specified y_units input does not match the units specified in extended_properties." + ) + + +def test___x_units_only_specified_in_extended_properties__create___creates_with_units() -> None: + data = np.array([1, 2, 3, 4, 5], np.int32) + xydata = XYData(data, data, extended_properties={_UNIT_DESCRIPTION_X: "Volts"}) + assert xydata.x_units == "Volts" + + +def test___y_units_only_specified_in_extended_properties__create___creates_with_units() -> None: + data = np.array([1, 2, 3, 4, 5], np.int32) + xydata = XYData(data, data, extended_properties={_UNIT_DESCRIPTION_Y: "Volts"}) + assert xydata.y_units == "Volts" + + def test___mismatched_dtypes___create___raises_type_error() -> None: data = np.array([1, 2, 3, 4, 5], np.int32) data2 = np.array([1, 2, 3, 4, 5], np.float64) @@ -313,19 +345,16 @@ def test___different_units___comparison___not_equal() -> None: ( XYData.from_arrays_1d([10], [20], np.int32), f"nitypes.xy_data.XYData(x_data=array([10]{_NDARRAY_DTYPE_INT32}), " - f"y_data=array([20]{_NDARRAY_DTYPE_INT32}), " - "extended_properties={'NI_UnitDescription_X': '', 'NI_UnitDescription_Y': ''})", + f"y_data=array([20]{_NDARRAY_DTYPE_INT32}))", ), ( XYData.from_arrays_1d([1.0, 1.1], [1.2, 1.3], np.float64), - "nitypes.xy_data.XYData(x_data=array([1. , 1.1]), y_data=array([1.2, 1.3]), " - "extended_properties={'NI_UnitDescription_X': '', 'NI_UnitDescription_Y': ''})", + "nitypes.xy_data.XYData(x_data=array([1. , 1.1]), y_data=array([1.2, 1.3]))", ), ( XYData.from_arrays_1d([10], [20], np.int32, x_units="volts", y_units="s"), f"nitypes.xy_data.XYData(x_data=array([10]{_NDARRAY_DTYPE_INT32}), " - f"y_data=array([20]{_NDARRAY_DTYPE_INT32}), " - "extended_properties={'NI_UnitDescription_X': 'volts', 'NI_UnitDescription_Y': 's'})", + f"y_data=array([20]{_NDARRAY_DTYPE_INT32}), x_units='volts', y_units='s')", ), ( XYData.from_arrays_1d( @@ -335,8 +364,8 @@ def test___different_units___comparison___not_equal() -> None: extended_properties={"NI_ChannelName": "Dev1/ai0"}, ), "nitypes.xy_data.XYData(x_data=array([1. , 1.1]), y_data=array([1.2, 1.3]), " - "extended_properties={'NI_ChannelName': 'Dev1/ai0', 'NI_UnitDescription_X': '', " - "'NI_UnitDescription_Y': ''})", + "extended_properties=nitypes.waveform.ExtendedPropertyDictionary({'NI_ChannelName': " + "'Dev1/ai0', 'NI_UnitDescription_X': '', 'NI_UnitDescription_Y': ''}))", ), ], ) @@ -407,12 +436,21 @@ def test___xy_data_with_units___set_units___units_updated_correctly() -> None: XYData.from_arrays_1d([20.0, 20.1], [20.3, 20.4], np.float64), XYData.from_arrays_1d([10, 20], [30, 40], np.int32, x_units="A", y_units="B"), XYData.from_arrays_1d([20.0, 20.1], [20.3, 20.4], np.float64, x_units="C", y_units="D"), + XYData.from_arrays_1d( + [20.0, 20.1], + [20.3, 20.4], + np.float64, + x_units="C", + y_units="D", + extended_properties={"one": 1}, + ), ], ) def test___various_values___copy___makes_copy(value: XYData[TData]) -> None: new_value = copy.copy(value) assert new_value is not value assert new_value == value + assert new_value.extended_properties == value.extended_properties @pytest.mark.parametrize(