Skip to content

Commit eaf35e9

Browse files
authored
Chardata moretests (SciTools#7101)
* Test with DECODE_TO_STRINGS off: WIP problem with cube contents. * Resolve load problems with non-string read loading by skipping problem testcases. * Make cf chartostring disable work for non-owned regular datasets. * Checks for alternate encoding name. * Fix bug where nc save was removing _Encoding attributes from saved cubes. * Fix bug: stop cube equality failing with char/string data. * Remove ban on string data for cubes: Extracting a scalar string cube then works. * Tests for saving scalar strings, and a mix of encodings and lengths. * Tests for some specific save/load usecases. * Test save/load of a 'bad' unicode sequence as a byte array. * Remove various obsolete comments, and debug code. * Review changes.
1 parent a113c93 commit eaf35e9

9 files changed

Lines changed: 415 additions & 117 deletions

File tree

lib/iris/cube.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,10 +1280,6 @@ def __init__(
12801280
... (longitude, 1)])
12811281
12821282
"""
1283-
# Temporary error while we transition the API.
1284-
if isinstance(data, str):
1285-
raise TypeError("Invalid data type: {!r}.".format(data))
1286-
12871283
# Configure the metadata manager.
12881284
self._metadata_manager = metadata_manager_factory(CubeMetadata)
12891285

@@ -4468,15 +4464,20 @@ def __eq__(self, other):
44684464

44694465
# Having checked everything else, check approximate data equality.
44704466
if result and not dataless_equality:
4471-
# TODO: why do we use allclose() here, but strict equality in
4472-
# _DimensionalMetadata (via util.array_equal())?
4473-
result = bool(
4474-
np.allclose(
4475-
self.core_data(),
4476-
other.core_data(),
4477-
equal_nan=True,
4467+
if self.dtype.kind in "if":
4468+
# numbers
4469+
# TODO: why do we use allclose() here, but strict equality in
4470+
# _DimensionalMetadata (via util.array_equal())?
4471+
result = bool(
4472+
np.allclose(
4473+
self.core_data(),
4474+
other.core_data(),
4475+
equal_nan=True,
4476+
)
44784477
)
4479-
)
4478+
else:
4479+
# non-numeric: use exact equality
4480+
result = bool(np.all(self.core_data() == other.core_data()))
44804481
return result
44814482

44824483
# Must supply __ne__, Python does not defer to __eq__ for negative equality

lib/iris/fileformats/_nc_load_rules/helpers.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,11 +1637,7 @@ def _add_auxiliary_coordinate(
16371637
# Determine the name of the dimension/s shared between the CF-netCDF data variable
16381638
# and the coordinate being built.
16391639
coord_dims = cf_coord_var.dimensions
1640-
# if cf._is_str_dtype(cf_coord_var):
1641-
# coord_dims = coord_dims[:-1]
16421640
datavar_dims = engine.cf_var.dimensions
1643-
# if cf._is_str_dtype(engine.cf_var):
1644-
# datavar_dims = datavar_dims[:-1]
16451641
common_dims = [dim for dim in coord_dims if dim in datavar_dims]
16461642
data_dims = None
16471643
if common_dims:

lib/iris/fileformats/cf.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,12 @@ def __init__(self, file_source, warn=False, monotonic=False):
13441344
self._with_ugrid = False
13451345

13461346
# Read the variables in the dataset only once to reduce runtime.
1347+
# Turn off *any* automatic decoding in the underlying netCDF4 dataset
1348+
ds = self._dataset
1349+
if isinstance(ds, _thread_safe_nc.DatasetWrapper):
1350+
ds._contained_instance.set_auto_chartostring(False)
1351+
else:
1352+
ds.set_auto_chartostring(False)
13471353
variables = self._dataset.variables
13481354
self._translate(variables)
13491355
self._build_cf_groups(variables)

lib/iris/fileformats/netcdf/_bytecoding_datasets.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ def encode_stringarray_as_bytearray(
116116
raise ValueError(msg) from err
117117

118118
n_bytes = len(bytes)
119-
# TODO: may want to issue warning or error if we overflow the length?
120119
if n_bytes > string_dimension_length:
121120
from iris.exceptions import TranslationError
122121

lib/iris/fileformats/netcdf/_thread_safe_nc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ class GroupWrapper(_ThreadSafeWrapper):
160160
# Note: will also accept a whole Dataset object, but that is OK.
161161
_DUCKTYPE_CHECK_PROPERTIES = ["createVariable"]
162162
# Class to use when creating variable wrappers (default=VariableWrapper).
163-
# - needed to support _byte_encoded_data.EncodedDataset.
163+
# - needed to support _bytecoding_datasets.EncodedDataset.
164164
VAR_WRAPPER_CLS = VariableWrapper
165165
GRP_WRAPPER_CLS: typing.Any | None = None # self-reference : fill in later
166166

lib/iris/fileformats/netcdf/saver.py

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,20 +1719,23 @@ def add_names_attrs():
17191719
if element.units.calendar:
17201720
_setncattr(cf_var, "calendar", str(element.units.calendar))
17211721

1722+
# Take a copy so we can remove things
1723+
element_attrs = element.attributes.copy()
1724+
17221725
# Note: when writing UGRID, "element" can be a Mesh which has no "dtype",
17231726
# and for dataless cubes it will have a 'None' dtype.
17241727
if getattr(element, "dtype", None) is not None:
17251728
# Most attributes are dealt with later. But _Encoding needs to be defined
17261729
# *before* we can write to a character variable.
1727-
if element.dtype.kind in "SU" and "_Encoding" in element.attributes:
1728-
encoding = element.attributes.pop("_Encoding")
1730+
if element.dtype.kind in "SU" and "_Encoding" in element_attrs:
1731+
encoding = element_attrs.pop("_Encoding")
17291732
_setncattr(cf_var, "_Encoding", encoding)
17301733

17311734
if not isinstance(element, Cube):
17321735
# Add any other custom coordinate attributes.
17331736
# N.B. not Cube, which has specific handling in _create_cf_data_variable
1734-
for name in sorted(element.attributes):
1735-
value = element.attributes[name]
1737+
for name in sorted(element_attrs):
1738+
value = element_attrs[name]
17361739

17371740
if name == "STASH":
17381741
# Adopting provisional Metadata Conventions for representing MO
@@ -1830,8 +1833,8 @@ def _create_generic_cf_array_var(
18301833
if cube is not None and data is not None and cube.shape != data.shape:
18311834
compression_kwargs = {}
18321835

1833-
if not is_dataless and np.issubdtype(data.dtype, np.str_):
1834-
# Deal with string-type variables.
1836+
if not is_dataless and data.dtype.kind == "U":
1837+
# Deal with unicode-string-type variables.
18351838
# Typically CF label variables, but also possibly ancil-vars ?
18361839

18371840
# NOTE: all we are doing here is to calculate the byte dimension length,
@@ -1840,37 +1843,26 @@ def _create_generic_cf_array_var(
18401843
# being a _bytecoding_datasets.EncodedVariable.
18411844
string_dimension_depth = data.dtype.itemsize
18421845

1843-
if data.dtype.kind == "U":
1844-
# String content (U) instead of bytes (S).
1845-
# For numpy strings, itemsize is **always** a multiple of 4
1846-
if string_dimension_depth % 4 != 0:
1847-
msg = (
1848-
"Unexpected numpy string 'itemsize' for element "
1849-
f"{cube_or_mesh.name()}: "
1850-
f"'dtype.itemsize = {string_dimension_depth}, expected "
1851-
"a multiple of four (always)."
1852-
)
1853-
raise ValueError(msg)
1854-
nchars = string_dimension_depth // 4
1855-
1856-
encoding_attr = element.attributes.get("_Encoding", "ascii")
1857-
# Look this up + return a supported encoding name
1858-
# NB implements defaults and raises a warning if given not recognised.
1859-
encoding = bytecoding_datasets._identify_encoding(
1860-
encoding=encoding_attr, var_name=cf_name, writing=True
1846+
# String content (U) instead of bytes (S).
1847+
# For numpy strings, itemsize is **always** a multiple of 4
1848+
if string_dimension_depth % 4 != 0:
1849+
msg = (
1850+
"Unexpected numpy string 'itemsize' for element "
1851+
f"{cube_or_mesh.name()}: "
1852+
f"'dtype.itemsize = {string_dimension_depth}, expected "
1853+
"a multiple of four (always)."
18611854
)
1862-
width_fns = bytecoding_datasets._ENCODING_WIDTH_TRANSLATIONS[encoding]
1863-
string_dimension_depth = width_fns.nchars_2_nbytes(nchars)
1864-
else:
1865-
if data.dtype.kind != "S" or data.dtype.itemsize != 1:
1866-
# Some type of data we don't "understand".
1867-
# NB this includes "Sxx" types other than "S1" : It seems that
1868-
# netCDF4 saves Sxx as variable-length strings. But we don't support that type in Iris.
1869-
msg = (
1870-
f"Variable {cf_name!r} has unexpected string/character dtype, "
1871-
f"{data.dtype} -- should be either 'S' or 'U' type."
1872-
)
1873-
raise ValueError(msg)
1855+
raise ValueError(msg)
1856+
nchars = string_dimension_depth // 4
1857+
1858+
encoding_attr = element.attributes.get("_Encoding", "ascii")
1859+
# Look this up + return a supported encoding name
1860+
# NB implements defaults and raises a warning if given not recognised.
1861+
encoding = bytecoding_datasets._identify_encoding(
1862+
encoding=encoding_attr, var_name=cf_name, writing=True
1863+
)
1864+
width_fns = bytecoding_datasets._ENCODING_WIDTH_TRANSLATIONS[encoding]
1865+
string_dimension_depth = width_fns.nchars_2_nbytes(nchars)
18741866

18751867
string_dimension_name = "string%d" % string_dimension_depth
18761868

@@ -1890,12 +1882,32 @@ def _create_generic_cf_array_var(
18901882
# Create the label coordinate variable.
18911883
cf_var = self._dataset.createVariable(cf_name, "|S1", element_dims)
18921884
else:
1893-
# A normal (numeric) variable.
1885+
# A non-string variable.
18941886
# ensure a valid datatype for the file format.
18951887
if is_dataless:
18961888
dtype = self._DATALESS_DTYPE
18971889
fill_value = self._DATALESS_FILLVALUE
18981890
else:
1891+
# Normal non-string data.
1892+
# NOTE: this includes byte-arrays (S1 only) : however these must
1893+
# use an actual cube dimension for the 'string dimension', which
1894+
# seriously limits the utility of DECODE_TO_STRINGS_ON_READ.
1895+
# TODO: also support netCDF variable-length strings ("string" type).
1896+
# Currently hit a **write error here**, being numpy object dtype ("O").
1897+
if data.dtype.kind not in "iufSU" or (
1898+
data.dtype.kind == "S" and data.dtype.itemsize != 1
1899+
):
1900+
# This is a type of data we don't "understand".
1901+
# NB this includes "Sxx" types other than "S1" : It seems that
1902+
# netCDF4 saves Sxx as variable-length strings.
1903+
# But we don't support that type in Iris.
1904+
msg = (
1905+
f"Variable {cf_name!r} has unexpected dtype, {data.dtype!r}."
1906+
f"Data content arrays must be numeric, or contain "
1907+
"single-bytes (dtype 'S1'), or unicode strings (dtype 'U<n>')."
1908+
)
1909+
raise ValueError(msg)
1910+
18991911
element_type = type(element).__name__
19001912
data = self._ensure_valid_dtype(data, element_type, element)
19011913
if not packing_controls:

0 commit comments

Comments
 (0)