Skip to content

Commit 4e956e0

Browse files
committed
Tests for saving scalar strings, and a mix of encodings and lengths.
1 parent 713f631 commit 4e956e0

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

lib/iris/tests/integration/netcdf/test_stringdata.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import iris
2020
from iris.coords import AuxCoord, DimCoord
2121
from iris.cube import Cube
22+
import iris.exceptions
2223
from iris.fileformats.netcdf import (
2324
DECODE_TO_STRINGS_ON_READ,
2425
SUPPORTED_ENCODINGS,
@@ -46,6 +47,13 @@ def all_lazy_auxcoords():
4647
TEST_ENCODINGS = [NO_ENCODING_STR, ALIAS_UTF8_STR] + SUPPORTED_ENCODINGS
4748

4849

50+
# Common fixture to save with split-attrs ONLY in these tests
51+
@pytest.fixture(scope="module", autouse=True)
52+
def all_split_attrs():
53+
with iris.FUTURE.context(save_split_attrs=True):
54+
yield
55+
56+
4957
#
5058
# Routines to convert between byte and string arrays.
5159
# Independently defined here, to avoid relying on any code we are testing.
@@ -476,3 +484,119 @@ def test_valid_encodings(self, encoding, writetest_data, write_bytes):
476484
)
477485
assert np.all(data_main == vararray)
478486
assert np.all(data_co == coordarray)
487+
488+
489+
class TestStringCubeBehaviour:
490+
def test_create(self):
491+
cube = Cube(["this", "that", "cliché"])
492+
assert isinstance(cube.core_data(), np.ndarray)
493+
assert cube.shape == (3,)
494+
assert cube.dtype == np.dtype("U6")
495+
496+
def test_scalar_extract(self):
497+
cube = Cube(["one", "two", "thirteen"])
498+
cube = cube[0]
499+
assert isinstance(cube.core_data(), np.ndarray)
500+
assert cube.shape == ()
501+
assert cube.dtype == np.dtype("U3")
502+
503+
def test_scalar_create(self):
504+
cube = Cube("éclair")
505+
assert isinstance(cube.core_data(), np.ndarray)
506+
assert cube.shape == ()
507+
assert cube.dtype == np.dtype("U6")
508+
509+
510+
class TestWriteReadMixedEncodings:
511+
"""Check saving of different types of string data, in cubes.
512+
513+
Checks that encodings are preserved through save/load.
514+
Checks that scalar cubes save.
515+
Checks that multiple cubes with different encodings save correctly.
516+
"""
517+
518+
def test_mixed(self, tmp_path):
519+
# Save a mixture of string + numeric cubes, 1-D and scalar
520+
# Ensure that they save, and read back correctly.
521+
c1 = Cube(["test-string"], var_name="c1")
522+
c2 = Cube(["test=éclair"], var_name="c2", attributes={"_Encoding": "utf16"})
523+
c3 = Cube(4.5, var_name="c3")
524+
c4 = Cube(np.array("q"), var_name="c4") # a SCALAR character-type cube
525+
cubes = [c1, c2, c3, c4]
526+
originals = [c.copy() for c in cubes]
527+
528+
# Check they save OK
529+
filepath = tmp_path / "tst.nc"
530+
iris.save(cubes, filepath)
531+
532+
# Check they also read back the same (except for Conventions attribute)
533+
results = iris.load_cubes(filepath, ["c1", "c2", "c3", "c4"])
534+
for cube in results:
535+
cube.attributes.pop("Conventions", None)
536+
assert all(orig == result for orig, result in zip(originals, results))
537+
538+
539+
class TestWriteReadScalarStringCubes:
540+
"""Check how scalar string-typed cubes are saved.
541+
NB all these gain a string dimension, even when only a single byte character,
542+
so they are not actually "scalar" in the file.
543+
"""
544+
545+
def test_save_scalar_ascii__ok(self, tmp_path):
546+
# We can save a scalar cube containing a *single ascii character*
547+
scalar_char_cube = Cube(
548+
np.array("x"),
549+
var_name="c1",
550+
attributes={"_Encoding": "utf8"}, # NB no encoding is *needed* here.
551+
)
552+
assert scalar_char_cube.shape == ()
553+
filepath = tmp_path / "tst.nc"
554+
iris.save(scalar_char_cube, filepath)
555+
556+
# Check dims in file
557+
ds = _thread_safe_nc.DatasetWrapper(filepath)
558+
assert ds.variables["c1"].dimensions == ("string1",)
559+
assert ds.dimensions["string1"].size == 1
560+
ds.close()
561+
562+
# check read-back result
563+
result = iris.load_cube(filepath)
564+
result.attributes.pop("Conventions", None)
565+
assert result == scalar_char_cube
566+
567+
def test_save_scalar_unicode__fail(self, tmp_path):
568+
# You *can't* save a scalar cube containing a non-ascii character
569+
# *without an explicitly lengthened dtype*,
570+
# because it doesn't convert to a single "char".
571+
scalar_char_bad = Cube(
572+
np.array("ü"), var_name="c1", attributes={"_Encoding": "utf8"}
573+
)
574+
assert scalar_char_bad.shape == ()
575+
filepath = tmp_path / "tst.nc"
576+
msg = (
577+
"String 'ü' written .* is 2 bytes long, "
578+
"which exceeds the string dimension length"
579+
)
580+
with pytest.raises(iris.exceptions.TranslationError, match=msg):
581+
iris.save(scalar_char_bad, filepath)
582+
583+
def test_save_single_unicode__okay(self, tmp_path):
584+
# You *can* save a scalar cube containing a non-ascii character,
585+
# *if* the dtype is extended to allow for multiple encoded bytes.
586+
scalar_char_cube = Cube(
587+
np.array("ü", dtype="U2"), var_name="c1", attributes={"_Encoding": "utf8"}
588+
)
589+
assert scalar_char_cube.shape == ()
590+
filepath = tmp_path / "tst.nc"
591+
iris.save(scalar_char_cube, filepath)
592+
593+
# Check dims in file
594+
ds = _thread_safe_nc.DatasetWrapper(filepath)
595+
assert ds.variables["c1"].dimensions == ("string2",)
596+
assert ds.dimensions["string2"].size == 2
597+
ds.close()
598+
599+
# check read-back result
600+
result = iris.load_cube(filepath)
601+
result.attributes.pop("Conventions", None)
602+
assert result == scalar_char_cube

0 commit comments

Comments
 (0)