@@ -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,94 @@ 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 TestReadParticularCases :
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+ # However, other encodings will FAIL to decode.
617+ filepath = tmp_path / "utf8_no_encoding.nc"
618+ testdata = make_testfile (
619+ testfile_path = filepath ,
620+ encoding_str = data_encoding ,
621+ encoding_attr = None ,
622+ )
623+ cube = iris .load_cube (filepath )
624+ assert "_Encoding" not in cube .attributes
625+
626+ if data_encoding == "utf8" :
627+ assert np .all (cube .data == testdata .datavar_data )
628+ else :
629+ msg = "Character data .* could not be decoded with the 'utf-8' encoding"
630+ with pytest .raises (ValueError , match = msg ):
631+ cube .data
632+
633+ def test_read_wrong_encoding__fail (self , tmp_path ):
634+ filepath = tmp_path / "missing_encoding.nc"
635+ testdata = make_testfile (
636+ testfile_path = filepath ,
637+ encoding_str = "utf-16" ,
638+ encoding_attr = "utf-8" ,
639+ )
640+
641+ from pp_utils import ncdump
642+
643+ ncdump (filepath , opts = "-s" )
644+
645+ cube = iris .load_cube (filepath )
646+ # NOTE: error only occurs when you attempt to fetch + translate the content.
647+ msg = "Character data .* could not be decoded with the 'utf-8' encoding."
648+ with pytest .raises (ValueError , match = msg ):
649+ data = cube .data
650+
651+
652+ class TestWriteParticularCases :
653+ def test_write_unicode_no_encoding__fail (self , tmp_path ):
654+ cube = Cube (np .array ("éclair" ))
655+ filepath = tmp_path / "write_unicode_no_encoding.nc"
656+ msg = (
657+ "String data written to netcdf character variable 'unknown' "
658+ "could not be represented in encoding 'ascii'"
659+ )
660+ with pytest .raises (ValueError , match = msg ):
661+ iris .save (cube , filepath )
662+
663+ def test_write_encoded_overlength__fail (self , tmp_path ):
664+ cube = Cube (np .array ("éclair" ), attributes = {"_Encoding" : "utf8" })
665+ filepath = tmp_path / "write_encoded_overlength.nc"
666+ msg = (
667+ "String 'éclair' written into netcdf variable 'unknown' "
668+ "with encoding 'utf-8' is 7 bytes long, which exceeds the "
669+ "string dimension length, 6. "
670+ r"This can be fixed by converting the data to a \"wider\" string dtype, "
671+ r"e.g. cube.data = cube.data.astype\(\"U7\"\)"
672+ )
673+ with pytest .raises (iris .exceptions .TranslationError , match = msg ):
674+ iris .save (cube , filepath )
675+
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 dtype, dtype\('S7'\)."
683+ "Data content arrays must be numeric, or contain single-bytes "
684+ r"\(dtype 'S1'\), or unicode strings \(dtype 'U<n>'\)."
685+ )
686+ with pytest .raises (ValueError , match = msg ):
687+ iris .save (cube , filepath )
688+
689+ def test_write_stringobjects__fail (self , tmp_path ):
690+ string_array = np .array (["one" , "four" ], dtype = "O" )
691+ cube = Cube (string_array )
692+ filepath = tmp_path / "write_stringobjects.nc"
693+ msg = (
694+ r"Variable 'unknown' has unexpected dtype, dtype\('O'\)."
695+ "Data content arrays must be numeric, or contain single-bytes "
696+ r"\(dtype 'S1'\), or unicode strings \(dtype 'U<n>'\)."
697+ )
698+ with pytest .raises (ValueError , match = msg ):
699+ iris .save (cube , filepath )
0 commit comments