Skip to content

Commit 8b751c5

Browse files
pp-mopre-commit-ci[bot]trexfeathers
authored
Implement split cube attributes. (#5040)
* Implement split cube attributes. * Test fixes. * Modify examples for simpler metadata printouts. * Added tests, small behaviour fixes. * Simplify copy. * Fix doctests. * Skip doctests with non-replicable outputs (from use of sets). * Tidy test comments, and add extra test. * Tiny typo. * Remove redundant redefinition of Cube.attributes. * Add CubeAttrsDict in module __all__ + improve docs coverage. * Review changes - small test changes. * More review changes. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix CubeAttrsDict example docstrings. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Odd small fixes. * Improved docstrings and comments; fix doctests. * Don't sidestep netcdf4 thread-safety. * Publicise LimitedAttributeDict, so CubeAttrsDict can refer to it. * Fix various internal + external links. * Update lib/iris/cube.py Co-authored-by: Martin Yeo <[email protected]> * Update lib/iris/cube.py Co-authored-by: Martin Yeo <[email protected]> * Update lib/iris/cube.py Co-authored-by: Martin Yeo <[email protected]> * Update lib/iris/cube.py Co-authored-by: Martin Yeo <[email protected]> * Streamline docs. * Review changes. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Martin Yeo <[email protected]>
1 parent bba4650 commit 8b751c5

File tree

8 files changed

+705
-29
lines changed

8 files changed

+705
-29
lines changed

docs/src/further_topics/metadata.rst

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ We can easily get all of the associated metadata of the :class:`~iris.cube.Cube`
131131
using the ``metadata`` property:
132132

133133
>>> cube.metadata
134-
CubeMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'Conventions': 'CF-1.5', '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=()),))
134+
CubeMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes=CubeAttrsDict(globals={}, locals={'Conventions': 'CF-1.5', '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=()),))
135135

136136
We can also inspect the ``metadata`` of the ``longitude``
137137
:class:`~iris.coords.DimCoord` attached to the :class:`~iris.cube.Cube` in the same way:
@@ -675,8 +675,8 @@ For example, consider the following :class:`~iris.common.metadata.CubeMetadata`,
675675

676676
.. doctest:: metadata-combine
677677

678-
>>> cube.metadata # doctest: +SKIP
679-
CubeMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'Conventions': 'CF-1.5', '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=()),))
678+
>>> cube.metadata
679+
CubeMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes=CubeAttrsDict(globals={}, locals={'Conventions': 'CF-1.5', '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=()),))
680680

681681
We can perform the **identity function** by comparing the metadata with itself,
682682

@@ -701,7 +701,7 @@ which is replaced with a **different value**,
701701
>>> metadata != cube.metadata
702702
True
703703
>>> metadata.combine(cube.metadata) # doctest: +SKIP
704-
CubeMetadata(standard_name=None, long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'STASH': STASH(model=1, section=3, item=236), 'source': 'Data from Met Office Unified Model 6.05', 'Model scenario': 'A1B', 'Conventions': 'CF-1.5'}, cell_methods=(CellMethod(method='mean', coord_names=('time',), intervals=('6 hour',), comments=()),))
704+
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=()),))
705705

706706
The ``combine`` method combines metadata by performing a **strict** comparison
707707
between each of the associated metadata member values,
@@ -810,16 +810,17 @@ the ``from_metadata`` class method. For example, given the following
810810

811811
.. doctest:: metadata-convert
812812

813-
>>> cube.metadata # doctest: +SKIP
814-
CubeMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'Conventions': 'CF-1.5', '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=()),))
813+
>>> cube.metadata
814+
CubeMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes=CubeAttrsDict(globals={}, locals={'Conventions': 'CF-1.5', '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=()),))
815815

816816
We can easily convert it to a :class:`~iris.common.metadata.DimCoordMetadata` instance
817817
using ``from_metadata``,
818818

819819
.. doctest:: metadata-convert
820820

821-
>>> DimCoordMetadata.from_metadata(cube.metadata) # doctest: +SKIP
822-
DimCoordMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'Conventions': 'CF-1.5', 'STASH': STASH(model=1, section=3, item=236), 'Model scenario': 'A1B', 'source': 'Data from Met Office Unified Model 6.05'}, coord_system=None, climatological=None, circular=None)
821+
>>> newmeta = DimCoordMetadata.from_metadata(cube.metadata)
822+
>>> print(newmeta)
823+
DimCoordMetadata(standard_name=air_temperature, var_name=air_temperature, units=K, attributes={'Conventions': 'CF-1.5', 'STASH': STASH(model=1, section=3, item=236), 'Model scenario': 'A1B', 'source': 'Data from Met Office Unified Model 6.05'})
823824

824825
By examining :numref:`metadata members table`, we can see that the
825826
:class:`~iris.cube.Cube` and :class:`~iris.coords.DimCoord` container
@@ -849,9 +850,9 @@ class instance,
849850

850851
.. doctest:: metadata-convert
851852

852-
>>> longitude.metadata.from_metadata(cube.metadata)
853-
DimCoordMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'Conventions': 'CF-1.5', 'STASH': STASH(model=1, section=3, item=236), 'Model scenario': 'A1B', 'source': 'Data from Met Office Unified Model 6.05'}, coord_system=None, climatological=None, circular=None)
854-
853+
>>> newmeta = longitude.metadata.from_metadata(cube.metadata)
854+
>>> print(newmeta)
855+
DimCoordMetadata(standard_name=air_temperature, var_name=air_temperature, units=K, attributes={'Conventions': 'CF-1.5', 'STASH': STASH(model=1, section=3, item=236), 'Model scenario': 'A1B', 'source': 'Data from Met Office Unified Model 6.05'})
855856

856857
.. _metadata assignment:
857858

@@ -978,7 +979,7 @@ Indeed, it's also possible to assign to the ``metadata`` property with a
978979
>>> longitude.metadata
979980
DimCoordMetadata(standard_name='longitude', long_name=None, var_name='longitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False)
980981
>>> longitude.metadata = cube.metadata
981-
>>> longitude.metadata # doctest: +SKIP
982+
>>> longitude.metadata
982983
DimCoordMetadata(standard_name='air_temperature', long_name=None, var_name='air_temperature', units=Unit('K'), attributes={'Conventions': 'CF-1.5', 'STASH': STASH(model=1, section=3, item=236), 'Model scenario': 'A1B', 'source': 'Data from Met Office Unified Model 6.05'}, coord_system=GeogCS(6371229.0), climatological=False, circular=False)
983984

984985
Note that, only **common** metadata members will be assigned new associated

lib/iris/common/metadata.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,11 @@ def __str__(self):
242242
field_strings = []
243243
for field in self._fields:
244244
value = getattr(self, field)
245-
if value is None or isinstance(value, (str, dict)) and not value:
245+
if (
246+
value is None
247+
or isinstance(value, (str, Mapping))
248+
and not value
249+
):
246250
continue
247251
field_strings.append(f"{field}={value}")
248252

lib/iris/common/mixin.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from .metadata import BaseMetadata
1919

20-
__all__ = ["CFVariableMixin"]
20+
__all__ = ["CFVariableMixin", "LimitedAttributeDict"]
2121

2222

2323
def _get_valid_standard_name(name):
@@ -53,7 +53,29 @@ def _get_valid_standard_name(name):
5353

5454

5555
class LimitedAttributeDict(dict):
56-
_forbidden_keys = (
56+
"""
57+
A specialised 'dict' subclass, which forbids (errors) certain attribute names.
58+
59+
Used for the attribute dictionaries of all Iris data objects (that is,
60+
:class:`CFVariableMixin` and its subclasses).
61+
62+
The "excluded" attributes are those which either :mod:`netCDF4` or Iris intpret and
63+
control with special meaning, which therefore should *not* be defined as custom
64+
'user' attributes on Iris data objects such as cubes.
65+
66+
For example : "coordinates", "grid_mapping", "scale_factor".
67+
68+
The 'forbidden' attributes are those listed in
69+
:data:`iris.common.mixin.LimitedAttributeDict.CF_ATTRS_FORBIDDEN` .
70+
71+
All the forbidden attributes are amongst those listed in
72+
`Appendix A of the CF Conventions: <http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#attribute-appendix>`_
73+
-- however, not *all* of them, since not all are interpreted by Iris.
74+
75+
"""
76+
77+
#: Attributes with special CF meaning, forbidden in Iris attribute dictionaries.
78+
CF_ATTRS_FORBIDDEN = (
5779
"standard_name",
5880
"long_name",
5981
"units",
@@ -78,7 +100,7 @@ def __init__(self, *args, **kwargs):
78100
dict.__init__(self, *args, **kwargs)
79101
# Check validity of keys
80102
for key in self.keys():
81-
if key in self._forbidden_keys:
103+
if key in self.CF_ATTRS_FORBIDDEN:
82104
raise ValueError(f"{key!r} is not a permitted attribute")
83105

84106
def __eq__(self, other):
@@ -99,11 +121,12 @@ def __ne__(self, other):
99121
return not self == other
100122

101123
def __setitem__(self, key, value):
102-
if key in self._forbidden_keys:
124+
if key in self.CF_ATTRS_FORBIDDEN:
103125
raise ValueError(f"{key!r} is not a permitted attribute")
104126
dict.__setitem__(self, key, value)
105127

106128
def update(self, other, **kwargs):
129+
"""Standard ``dict.update()`` operation."""
107130
# Gather incoming keys
108131
keys = []
109132
if hasattr(other, "keys"):
@@ -115,7 +138,7 @@ def update(self, other, **kwargs):
115138

116139
# Check validity of keys
117140
for key in keys:
118-
if key in self._forbidden_keys:
141+
if key in self.CF_ATTRS_FORBIDDEN:
119142
raise ValueError(f"{key!r} is not a permitted attribute")
120143

121144
dict.update(self, other, **kwargs)

0 commit comments

Comments
 (0)