diff --git a/Lib/__init__.py b/Lib/__init__.py index db020b8a..bb44efbc 100644 --- a/Lib/__init__.py +++ b/Lib/__init__.py @@ -4,6 +4,8 @@ # Errors from .error import CDMSError # noqa +from .error import CdunifError # noqa +from .error import ReadOnlyKeyError # noqa from lazy_object_proxy import Proxy from . import dataset from . import selectors @@ -137,5 +139,3 @@ from . import dask_protocol # noqa except BaseException: pass - -from .Cdunif import CdunifError diff --git a/Lib/dataset.py b/Lib/dataset.py index a2553952..3381eabf 100644 --- a/Lib/dataset.py +++ b/Lib/dataset.py @@ -2090,7 +2090,13 @@ def createVariableCopy(self, var, id=None, attributes=None, axes=None, extbounds if attname not in ["id", "datatype", "parent"]: if isinstance(attval, string_types): attval = str(attval) - setattr(newvar, str(attname), attval) + try: + setattr(newvar, str(attname), attval) + except Cdunif.ReadOnlyKeyError as e: + # Suppress the exception context + err = "Cannot write read-only attribute '{!s}', must be " \ + "removed from '{!s}' variable before writing to file".format(e, var.id) + raise CDMSError(err) from None if (attname == "_FillValue") or (attname == "missing_value"): setattr(newvar, "_FillValue", attval) setattr(newvar, "missing_value", attval) diff --git a/Lib/error.py b/Lib/error.py index ba970ad0..983ef39e 100644 --- a/Lib/error.py +++ b/Lib/error.py @@ -1,3 +1,5 @@ "Error object for cdms module" from .Cdunif import CDMSError # noqa: F401 +from .Cdunif import CdunifError # noqa: F401 +from .Cdunif import ReadOnlyKeyError # noqa: F401 diff --git a/Lib/fvariable.py b/Lib/fvariable.py index a3625c2d..ddf5c70b 100644 --- a/Lib/fvariable.py +++ b/Lib/fvariable.py @@ -157,12 +157,7 @@ def __setattr__(self, name, value): if hasattr(self, "parent") and self.parent is None: raise CDMSError(FileClosedWrite + self.id) if (name not in self.__cdms_internals__) and (value is not None): - try: - setattr(self._obj_, str(name), value) - except Exception: - raise CDMSError( - "Setting %s.%s=%s" % - (self.id, name, repr(value))) + setattr(self._obj_, str(name), value) self.attributes[name] = value self.__dict__[name] = value diff --git a/Makefile b/Makefile index 4211143a..53346dbf 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,14 @@ build: install-conda prep-feedstock create-conda-env EXTRA_CB_OPTIONS='--croot $(LOCAL_CHANNEL_DIR)' \ $(SCRIPTS_DIR)/build_steps.sh | tee $(WORK_DIR)/`cat $(PWD)/.variant` +.PHONY: build-env +build-env: + $(CONDA_ENV); \ + $(CONDA_ACTIVATE) base; \ + $(CONDA_RC); \ + conda activate build; \ + bash + .PHONY: build-docs build-docs: ENV := docs build-docs: CHANNELS := -c file://$(LOCAL_CHANNEL_DIR) -c conda-forge @@ -126,7 +134,7 @@ test: create-conda-env $(CONDA_RC); \ conda config --set channel_priority strict; \ conda activate $(ENV); \ - conda install --yes $(CHANNELS) cdms2 testsrunner cdat_info pytest pip \ + conda install --yes $(CHANNELS) cdms2 testsrunner cdat_info pytest pip nco \ $(CONDA_TEST_PACKAGES); \ conda info; \ conda list --explicit > $(TEST_OUTPUT_DIR)/environment.txt; \ @@ -134,6 +142,18 @@ test: create-conda-env cp -rf $(PWD)/tests_html $(TEST_OUTPUT_DIR)/ +.PHONY: test-env +test-env: + $(CONDA_ENV); \ + $(CONDA_ACTIVATE) base; \ + $(CONDA_RC); \ + conda activate test; \ + bash + +.PHONY: test-clean +test-clean: + rm -rf $(ENVS_DIR)/test + .PHONY: upload upload: ENV := upload upload: PACKAGES := 'python=3.8' anaconda-client diff --git a/Src/Cdunifmodule.c b/Src/Cdunifmodule.c index 62e0b985..be9f981d 100644 --- a/Src/Cdunifmodule.c +++ b/Src/Cdunifmodule.c @@ -71,6 +71,7 @@ PyThread_type_lock Cdunif_lock; #endif static PyObject *CdunifError; +static PyObject *ReadOnlyKeyError; static PyObject *CDMSError; /* Set error string */ @@ -2098,10 +2099,10 @@ static int PyCdunifFile_SetAttribute(PyCdunifFileObject *self, PyObject *nameobj char *name = PyStr_AsString(nameobj); if (check_if_open(self, 1)) { if (strcmp(name, "dimensions") == 0 - || strcmp(name, "variables") == 0 + || strcmp(name, "variables") == 0 || strcmp(name, "dimensioninfo") == 0 || strcmp(name, "__dict__") == 0) { - PyErr_SetString(PyExc_TypeError, "object has read-only attributes"); + PyErr_Format(ReadOnlyKeyError, "%s", name); return -1; } define_mode(self, 1); @@ -2454,9 +2455,10 @@ static int PyCdunifVariable_SetAttribute(PyCdunifVariableObject *self, PyObject *nameobj, PyObject *value) { char *name = PyStr_AsString(nameobj); if (check_if_open(self->file, 1)) { - if (strcmp(name, "shape") == 0 || strcmp(name, "dimensions") == 0 + if (strcmp(name, "shape") == 0 + || strcmp(name, "dimensions") == 0 || strcmp(name, "__dict__") == 0) { - PyErr_SetString(PyExc_TypeError, "object has read-only attributes"); + PyErr_Format(ReadOnlyKeyError, "%s", name); return -1; } define_mode(self->file, 1); @@ -3489,9 +3491,11 @@ MODULE_INIT_FUNC (Cdunif) { CDMSError = PyErr_NewException("cdms2.CDMSError", NULL, NULL); CdunifError = PyErr_NewException("Cdunif.CdunifError", CDMSError, NULL); + ReadOnlyKeyError = PyErr_NewException("Cdunif.ReadOnlyKeyError", CDMSError, NULL); PyDict_SetItemString(d, "CDMSError", CDMSError); PyDict_SetItemString(d, "CdunifError", CdunifError); + PyDict_SetItemString(d, "ReadOnlyKeyError", ReadOnlyKeyError); /* Check for errors */ if (PyErr_Occurred()) diff --git a/tests/test_dataset_io.py b/tests/test_dataset_io.py index d3ff7ffd..e23bbecf 100644 --- a/tests/test_dataset_io.py +++ b/tests/test_dataset_io.py @@ -3,6 +3,9 @@ import string import os import sys +import cdat_info +import subprocess +import shutil cdms2.setNetcdfUseParallelFlag(0) @@ -21,6 +24,34 @@ def setUp(self): def testFileAttributes(self): self.assertEqual(self.file.id, "test") + def testWriteReadOnlyAttribute(self): + out = self.getTempFile("read_only.nc", "w") + out.write(self.u) + out.close() + + temp = self.getTempFile("read_only.nc", "a") + + with self.assertRaises(cdms2.ReadOnlyKeyError): + setattr(temp, "dimensions", "test") + + with self.assertRaises(cdms2.ReadOnlyKeyError): + setattr(temp["u"], "dimensions", "test") + + def testWriteThroughReadOnlyAtttribute(self): + iname = os.path.join(cdat_info.get_sampledata_path(), "clt.nc") + oname = os.path.join(os.getcwd(), "clt-modded.nc") + + shutil.copyfile(iname, oname) + + subprocess.run(["ncatted", "-a", "dimensions,clt,c,c,'lat lon'", oname]) + + ifile = cdms2.open(oname) + data = ifile("clt") + ifile.close() + + with cdms2.open(oname, "w") as ofile, self.assertRaises(cdms2.CDMSError): + ofile.write(data) + def testScalarSlice(self): u = self.u scalar = u[0,0,0]