diff --git a/docs/src/further_topics/metadata.rst b/docs/src/further_topics/metadata.rst index 1c140e7ccd..bba879a707 100644 --- a/docs/src/further_topics/metadata.rst +++ b/docs/src/further_topics/metadata.rst @@ -91,6 +91,16 @@ actual `data attribute`_ names of the metadata members on the Iris class. metadata members are Iris specific terms, rather than recognised `CF Conventions`_ terms. +.. note:: + + :class:`~iris.cube.Cube` :attr:`~iris.cube.Cube.attributes` implement the + concept of dataset-level and variable-level attributes, to enable correct + NetCDF loading and saving (see :class:`~iris.cube.CubeAttrsDict` and NetCDF + :func:`~iris.fileformats.netcdf.saver.save` for more). ``attributes`` on + the other classes do not have this distinction, but the ``attributes`` + members of ALL the classes still have the same interface, and can be + compared. + Common Metadata API =================== @@ -128,7 +138,9 @@ For example, given the following :class:`~iris.cube.Cube`, source 'Data from Met Office Unified Model 6.05' We can easily get all of the associated metadata of the :class:`~iris.cube.Cube` -using the ``metadata`` property: +using the ``metadata`` property (note the specialised +:class:`~iris.cube.CubeAttrsDict` for the :attr:`~iris.cube.Cube.attributes`, +as mentioned earlier): >>> cube.metadata CubeMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes=CubeAttrsDict(globals={'Conventions': 'CF-1.5'}, locals={'STASH': STASH(model=1, section=3, item=236), 'Model scenario': 'A1B', 'source': 'Data from Met Office Unified Model 6.05'}), cell_methods=(CellMethod(method='mean', coord_names=('time',), intervals=('6 hour',), comments=()),)) @@ -701,7 +713,7 @@ which is replaced with a **different value**, >>> metadata != cube.metadata True >>> metadata.combine(cube.metadata) # doctest: +SKIP - CubeMetadata(standard_name=None, long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'Conventions': 'CF-1.5', 'Model scenario': 'A1B', 'STASH': STASH(model=1, section=3, item=236), 'source': 'Data from Met Office Unified Model 6.05'}, cell_methods=(CellMethod(method='mean', coord_names=('time',), intervals=('6 hour',), comments=()),)) + CubeMetadata(standard_name=None, long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'STASH': STASH(model=1, section=3, item=236), 'Model scenario': 'A1B', 'source': 'Data from Met Office Unified Model 6.05', 'Conventions': 'CF-1.5'}, cell_methods=(CellMethod(method='mean', coord_names=('time',), intervals=('6 hour',), comments=()),)) The ``combine`` method combines metadata by performing a **strict** comparison between each of the associated metadata member values, diff --git a/docs/src/userguide/iris_cubes.rst b/docs/src/userguide/iris_cubes.rst index 267f97b0fc..03b5093efc 100644 --- a/docs/src/userguide/iris_cubes.rst +++ b/docs/src/userguide/iris_cubes.rst @@ -85,7 +85,10 @@ A cube consists of: data dimensions as the coordinate has dimensions. * an attributes dictionary which, other than some protected CF names, can - hold arbitrary extra metadata. + hold arbitrary extra metadata. This implements the concept of dataset-level + and variable-level attributes when loading and and saving NetCDF files (see + :class:`~iris.cube.CubeAttrsDict` and NetCDF + :func:`~iris.fileformats.netcdf.saver.save` for more). * a list of cell methods to represent operations which have already been applied to the data (e.g. "mean over time") * a list of coordinate "factories" used for deriving coordinates from the diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 9b62715be6..280ca9a333 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -30,6 +30,14 @@ This document explains the changes made to Iris for this release ✨ Features =========== +#. `@pp-mo`_, `@lbdreyer`_ and `@trexfeathers`_ improved + :class:`~iris.cube.Cube` :attr:`~iris.cube.Cube.attributes` handling to + better preserve the distinction between dataset-level and variable-level + attributes, allowing file-Cube-file round-tripping of NetCDF attributes. See + :class:`~iris.cube.CubeAttrsDict` and NetCDF + :func:`~iris.fileformats.netcdf.saver.save` for more. (:pull:`5152`, + `split attributes project`_) + #. `@rcomer`_ rewrote :func:`~iris.util.broadcast_to_shape` so it now handles lazy data. (:pull:`5307`) @@ -94,3 +102,4 @@ This document explains the changes made to Iris for this release .. comment Whatsnew resources in alphabetical order: +.. _split attributes project: https://github.com/orgs/SciTools/projects/5?pane=info diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 8bb9d7c00e..49669dd4df 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -825,6 +825,7 @@ class CubeAttrsDict(MutableMapping): >>> from iris.cube import Cube >>> cube = Cube([0]) + >>> # CF defines 'history' as global by default. >>> cube.attributes.update({"history": "from test-123", "mycode": 3}) >>> print(cube.attributes) {'history': 'from test-123', 'mycode': 3} @@ -843,6 +844,9 @@ class CubeAttrsDict(MutableMapping): """ + # TODO: Create a 'further topic' / 'tech paper' on NetCDF I/O, including + # discussion of attribute handling. + def __init__( self, combined: Optional[Union[Mapping, str]] = "__unspecified", @@ -853,7 +857,7 @@ def __init__( Create a cube attributes dictionary. We support initialisation from a single generic mapping input, using the default - global/local assignment rules explained at :meth:`__setatrr__`, or from + global/local assignment rules explained at :meth:`__setattr__`, or from two separate mappings. Two separate dicts can be passed in the ``locals`` and ``globals`` args, **or** via a ``combined`` arg which has its own ``.globals`` and ``.locals`` properties -- so this allows passing an existing @@ -878,6 +882,7 @@ def __init__( -------- >>> from iris.cube import CubeAttrsDict + >>> # CF defines 'history' as global by default. >>> CubeAttrsDict({'history': 'data-story', 'comment': 'this-cube'}) CubeAttrsDict(globals={'history': 'data-story'}, locals={'comment': 'this-cube'})