diff --git a/news/setter-property.rst b/news/setter-property.rst new file mode 100644 index 00000000..8b2ddc97 --- /dev/null +++ b/news/setter-property.rst @@ -0,0 +1,23 @@ +**Added:** + +* prevent direct modification of `all_arrays` using `@property` + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/requirements/test.txt b/requirements/test.txt index 9966e09d..a392bd23 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,3 +6,4 @@ pytest-env pytest-mock pytest-cov freezegun +DeepDiff diff --git a/src/diffpy/utils/diffraction_objects.py b/src/diffpy/utils/diffraction_objects.py index 54374fa7..82ad9cf4 100644 --- a/src/diffpy/utils/diffraction_objects.py +++ b/src/diffpy/utils/diffraction_objects.py @@ -181,6 +181,17 @@ def __rtruediv__(self, other): divided.on_q[1] = other.on_q[1] / self.on_q[1] return divided + @property + def all_arrays(self): + return self._all_arrays + + @all_arrays.setter + def all_arrays(self, value): + raise AttributeError( + "Direct modification of attribute 'all_arrays' is not allowed." + "Please use 'insert_scattering_quantity' to modify `all_arrays`." + ) + def set_angles_from_list(self, angles_list): self.angles = angles_list self.n_steps = len(angles_list) - 1.0 @@ -259,25 +270,25 @@ def get_angle_index(self, angle): raise IndexError(f"WARNING: no angle {angle} found in angles list") def _set_xarrays(self, xarray, xtype): - self.all_arrays = np.empty(shape=(len(xarray), 4)) + self._all_arrays = np.empty(shape=(len(xarray), 4)) if xtype.lower() in QQUANTITIES: - self.all_arrays[:, 1] = xarray - self.all_arrays[:, 2] = q_to_tth(xarray, self.wavelength) - self.all_arrays[:, 3] = q_to_d(xarray) + self._all_arrays[:, 1] = xarray + self._all_arrays[:, 2] = q_to_tth(xarray, self.wavelength) + self._all_arrays[:, 3] = q_to_d(xarray) elif xtype.lower() in ANGLEQUANTITIES: - self.all_arrays[:, 2] = xarray - self.all_arrays[:, 1] = tth_to_q(xarray, self.wavelength) - self.all_arrays[:, 3] = tth_to_d(xarray, self.wavelength) + self._all_arrays[:, 2] = xarray + self._all_arrays[:, 1] = tth_to_q(xarray, self.wavelength) + self._all_arrays[:, 3] = tth_to_d(xarray, self.wavelength) elif xtype.lower() in DQUANTITIES: - self.all_arrays[:, 3] = xarray - self.all_arrays[:, 1] = d_to_q(xarray) - self.all_arrays[:, 2] = d_to_tth(xarray, self.wavelength) - self.qmin = np.nanmin(self.all_arrays[:, 1], initial=np.inf) - self.qmax = np.nanmax(self.all_arrays[:, 1], initial=0.0) - self.tthmin = np.nanmin(self.all_arrays[:, 2], initial=np.inf) - self.tthmax = np.nanmax(self.all_arrays[:, 2], initial=0.0) - self.dmin = np.nanmin(self.all_arrays[:, 3], initial=np.inf) - self.dmax = np.nanmax(self.all_arrays[:, 3], initial=0.0) + self._all_arrays[:, 3] = xarray + self._all_arrays[:, 1] = d_to_q(xarray) + self._all_arrays[:, 2] = d_to_tth(xarray, self.wavelength) + self.qmin = np.nanmin(self._all_arrays[:, 1], initial=np.inf) + self.qmax = np.nanmax(self._all_arrays[:, 1], initial=0.0) + self.tthmin = np.nanmin(self._all_arrays[:, 2], initial=np.inf) + self.tthmax = np.nanmax(self._all_arrays[:, 2], initial=0.0) + self.dmin = np.nanmin(self._all_arrays[:, 3], initial=np.inf) + self.dmax = np.nanmax(self._all_arrays[:, 3], initial=0.0) def insert_scattering_quantity( self, @@ -309,7 +320,7 @@ def insert_scattering_quantity( """ self._set_xarrays(xarray, xtype) - self.all_arrays[:, 0] = yarray + self._all_arrays[:, 0] = yarray self.input_xtype = xtype # only update these optional values if non-empty quantities are passed to avoid overwriting # valid data inadvertently diff --git a/tests/test_diffraction_objects.py b/tests/test_diffraction_objects.py index 9db6932e..b7d8eae3 100644 --- a/tests/test_diffraction_objects.py +++ b/tests/test_diffraction_objects.py @@ -2,6 +2,7 @@ import numpy as np import pytest +from deepdiff import DeepDiff from freezegun import freeze_time from diffpy.utils.diffraction_objects import DiffractionObject @@ -248,7 +249,7 @@ def test_dump(tmp_path, mocker): ( {}, { - "all_arrays": np.empty(shape=(0, 4)), # instantiate empty + "_all_arrays": np.empty(shape=(0, 4)), # instantiate empty "metadata": {}, "input_xtype": "", "name": "", @@ -265,7 +266,7 @@ def test_dump(tmp_path, mocker): ( # instantiate just non-array attributes {"name": "test", "scat_quantity": "x-ray", "metadata": {"thing": "1", "another": "2"}}, { - "all_arrays": np.empty(shape=(0, 4)), + "_all_arrays": np.empty(shape=(0, 4)), "metadata": {"thing": "1", "another": "2"}, "input_xtype": "", "name": "test", @@ -287,7 +288,7 @@ def test_dump(tmp_path, mocker): "wavelength": 4.0 * np.pi, }, { - "all_arrays": np.array( + "_all_arrays": np.array( [ [1.0, 0.0, 0.0, np.float64(np.inf)], [2.0, 1.0 / np.sqrt(2), 90.0, np.sqrt(2) * 2 * np.pi], @@ -316,7 +317,7 @@ def test_dump(tmp_path, mocker): "scat_quantity": "x-ray", }, { - "all_arrays": np.array( + "_all_arrays": np.array( [ [1.0, 0.0, 0.0, np.float64(np.inf)], [2.0, 1.0 / np.sqrt(2), 90.0, np.sqrt(2) * 2 * np.pi], @@ -341,5 +342,35 @@ def test_dump(tmp_path, mocker): @pytest.mark.parametrize("inputs, expected", tc_params) def test_constructor(inputs, expected): - actualdo = DiffractionObject(**inputs) - compare_dicts(actualdo.__dict__, expected) + actual_do = DiffractionObject(**inputs) + diff = DeepDiff(actual_do.__dict__, expected, ignore_order=True, significant_digits=13) + assert diff == {} + + +def test_all_array_getter(): + actual_do = DiffractionObject( + xarray=np.array([0.0, 90.0, 180.0]), + yarray=np.array([1.0, 2.0, 3.0]), + xtype="tth", + wavelength=4.0 * np.pi, + ) + expected_all_arrays = np.array( + [ + [1.0, 0.0, 0.0, np.float64(np.inf)], + [2.0, 1.0 / np.sqrt(2), 90.0, np.sqrt(2) * 2 * np.pi], + [3.0, 1.0, 180.0, 1.0 * 2 * np.pi], + ] + ) + assert np.allclose(actual_do.all_arrays, expected_all_arrays) + + +def test_all_array_setter(): + actual_do = DiffractionObject() + + # Attempt to directly modify the property + with pytest.raises( + AttributeError, + match="Direct modification of attribute 'all_arrays' is not allowed." + "Please use 'insert_scattering_quantity' to modify `all_arrays`.", + ): + actual_do.all_arrays = np.empty((4, 4))