|
19 | 19 | import iris |
20 | 20 | from iris.coords import AuxCoord, DimCoord |
21 | 21 | from iris.cube import Cube |
| 22 | +import iris.exceptions |
22 | 23 | from iris.fileformats.netcdf import ( |
23 | 24 | DECODE_TO_STRINGS_ON_READ, |
24 | 25 | SUPPORTED_ENCODINGS, |
@@ -46,6 +47,13 @@ def all_lazy_auxcoords(): |
46 | 47 | TEST_ENCODINGS = [NO_ENCODING_STR, ALIAS_UTF8_STR] + SUPPORTED_ENCODINGS |
47 | 48 |
|
48 | 49 |
|
| 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 | + |
49 | 57 | # |
50 | 58 | # Routines to convert between byte and string arrays. |
51 | 59 | # 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): |
476 | 484 | ) |
477 | 485 | assert np.all(data_main == vararray) |
478 | 486 | 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