@@ -109,7 +109,10 @@ class SamplefileDetails:
109109def make_testfile (
110110 testfile_path : Path ,
111111 encoding_str : str ,
112- coords_on_separate_dim : bool ,
112+ coords_on_separate_dim : bool = False ,
113+ # If set, determines the "_Encoding" attrs content, including None --> no attr.
114+ # Otherwise, they follow 'encoding_str', including NO_ENCODING_STR --> no attr.
115+ encoding_attr : str | None = "<as_encoding_str>" ,
113116) -> SamplefileDetails :
114117 """Create a test netcdf file.
115118
@@ -120,6 +123,9 @@ def make_testfile(
120123 else :
121124 encoding = encoding_str
122125
126+ if encoding_attr == "<as_encoding_str>" :
127+ encoding_attr = encoding
128+
123129 data_is_ascii = encoding in (None , "ascii" )
124130
125131 numeric_values = np .arange (3.0 )
@@ -156,8 +162,8 @@ def make_testfile(
156162 )
157163 v_co [:] = coordvar_bytearray
158164
159- if encoding is not None :
160- v_co ._Encoding = encoding
165+ if encoding_attr is not None :
166+ v_co ._Encoding = encoding_attr
161167
162168 v_numeric = ds .createVariable (
163169 "v_numeric" ,
@@ -176,8 +182,8 @@ def make_testfile(
176182 )
177183 v_datavar [:] = datavar_bytearray
178184
179- if encoding is not None :
180- v_datavar ._Encoding = encoding
185+ if encoding_attr is not None :
186+ v_datavar ._Encoding = encoding_attr
181187
182188 v_datavar .coordinates = "v_co v_numeric"
183189 finally :
@@ -600,3 +606,82 @@ def test_save_single_unicode__okay(self, tmp_path):
600606 result = iris .load_cube (filepath )
601607 result .attributes .pop ("Conventions" , None )
602608 assert result == scalar_char_cube
609+
610+
611+ class TestReadSpecialCases :
612+ @pytest .mark .parametrize ("data_encoding" , ["utf8" , "utf16" , "utf32" ])
613+ def test_read_no_encoding (self , tmp_path , data_encoding ):
614+ # Check that we can read UTF-8 encoded data, even with no _Encoding attribute.
615+ # This is a common case in the wild, and now accepted by CF as a default.
616+ filepath = tmp_path / "utf8_no_encoding.nc"
617+ testdata = make_testfile (
618+ testfile_path = filepath ,
619+ encoding_str = data_encoding ,
620+ encoding_attr = None ,
621+ )
622+ cube = iris .load_cube (filepath )
623+ assert "_Encoding" not in cube .attributes
624+
625+ if data_encoding == "utf8" :
626+ assert np .all (cube .data == testdata .datavar_data )
627+ else :
628+ msg = "Character data .* could not be decoded with the 'utf-8' encoding"
629+ with pytest .raises (ValueError , match = msg ):
630+ cube .data
631+
632+ def test_read_wrong_encoding__fail (self , tmp_path ):
633+ filepath = tmp_path / "missing_encoding.nc"
634+ testdata = make_testfile (
635+ testfile_path = filepath ,
636+ encoding_str = "utf-16" ,
637+ encoding_attr = "utf-8" ,
638+ )
639+
640+ from pp_utils import ncdump
641+
642+ ncdump (filepath , opts = "-s" )
643+
644+ cube = iris .load_cube (filepath )
645+ # NOTE: error only occurs when you attempt to fetch + translate the content.
646+ msg = "Character data .* could not be decoded with the 'utf-8' encoding."
647+ with pytest .raises (ValueError , match = msg ):
648+ data = cube .data
649+
650+
651+ class TestWriteSpecialCases :
652+ def test_write_unicode_no_encoding__fail (self , tmp_path ):
653+ cube = Cube (np .array ("éclair" ))
654+ filepath = tmp_path / "write_unicode_no_encoding.nc"
655+ msg = (
656+ "String data written to netcdf character variable 'unknown' "
657+ "could not be represented in encoding 'ascii'"
658+ )
659+ with pytest .raises (ValueError , match = msg ):
660+ iris .save (cube , filepath )
661+
662+ def test_write_encoded_overlength__fail (self , tmp_path ):
663+ cube = Cube (np .array ("éclair" ), attributes = {"_Encoding" : "utf8" })
664+ filepath = tmp_path / "write_encoded_overlength.nc"
665+ msg = (
666+ "String 'éclair' written into netcdf variable 'unknown' "
667+ "with encoding 'utf-8' is 7 bytes long, which exceeds the "
668+ "string dimension length, 6. "
669+ r"This can be fixed by converting the data to a \"wider\" string dtype, "
670+ r"e.g. cube.data = cube.data.astype\(\"U7\"\)"
671+ )
672+ with pytest .raises (iris .exceptions .TranslationError , match = msg ):
673+ iris .save (cube , filepath )
674+
675+ @pytest .skip ("Under development" )
676+ def test_write_multibytes__fail (self , tmp_path ):
677+ encoded_bytes = "éclair" .encode ("utf8" )
678+ byte_array = np .array (encoded_bytes )
679+ cube = Cube (byte_array , attributes = {"_Encoding" : "utf8" })
680+ filepath = tmp_path / "write_multibyte_Sxx.nc"
681+ msg = (
682+ r"Variable 'unknown' has unexpected string/character type, dtype\('S7'\) "
683+ r"-- the data array must contain either single-bytes \(dtype 'S1'\), "
684+ r"or unicode strings \(dtype 'U<n>'\)"
685+ )
686+ with pytest .raises (ValueError , match = msg ):
687+ iris .save (cube , filepath )
0 commit comments