@@ -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