Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/src/further_topics/metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ We can easily get all of the associated metadata of the :class:`~iris.cube.Cube`
using the ``metadata`` property:

>>> cube.metadata
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=()),))
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=()),))

We can also inspect the ``metadata`` of the ``longitude``
:class:`~iris.coords.DimCoord` attached to the :class:`~iris.cube.Cube` in the same way:
Expand Down Expand Up @@ -676,7 +676,7 @@ For example, consider the following :class:`~iris.common.metadata.CubeMetadata`,
.. doctest:: metadata-combine

>>> cube.metadata
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=()),))
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=()),))

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

Expand Down Expand Up @@ -811,7 +811,7 @@ the ``from_metadata`` class method. For example, given the following
.. doctest:: metadata-convert

>>> cube.metadata
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=()),))
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=()),))

We can easily convert it to a :class:`~iris.common.metadata.DimCoordMetadata` instance
using ``from_metadata``,
Expand Down
4 changes: 2 additions & 2 deletions lib/iris/fileformats/_nc_load_rules/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,9 +433,9 @@ def build_cube_metadata(engine):
# Set the cube global attributes.
for attr_name, attr_value in cf_var.cf_group.global_attributes.items():
try:
cube.attributes[str(attr_name)] = attr_value
cube.attributes.globals[str(attr_name)] = attr_value
except ValueError as e:
msg = "Skipping global attribute {!r}: {}"
msg = "Skipping disallowed global attribute {!r}: {}"
warnings.warn(msg.format(attr_name, str(e)))


Expand Down
7 changes: 6 additions & 1 deletion lib/iris/fileformats/netcdf/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,13 @@ def attribute_predicate(item):
return item[0] not in _CF_ATTRS

tmpvar = filter(attribute_predicate, cf_var.cf_attrs_unused())
attrs_dict = iris_object.attributes
if hasattr(attrs_dict, "locals"):
# Treat cube attributes (i.e. a CubeAttrsDict) as a special case.
# These attrs are "local" (i.e. on the variable), so record them as such.
attrs_dict = attrs_dict.locals
for attr_name, attr_value in tmpvar:
_set_attributes(iris_object.attributes, attr_name, attr_value)
_set_attributes(attrs_dict, attr_name, attr_value)


def _get_actual_dtype(cf_var):
Expand Down
86 changes: 71 additions & 15 deletions lib/iris/tests/integration/test_netcdf__loadsaveattrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import iris
import iris.coord_systems
from iris.cube import Cube
from iris.cube import Cube, CubeAttrsDict
import iris.fileformats.netcdf
import iris.fileformats.netcdf._thread_safe_nc as threadsafe_nc4

Expand Down Expand Up @@ -633,8 +633,13 @@ def test_01_userstyle_single_global(self):
)
# Default behaviour for a general global user-attribute.
# It is attached to all loaded cubes.
assert cube1.attributes == {"myname": "single-value"}
assert cube2.attributes == {"myname": "single-value"}

expected_dict = {"myname": "single-value"}
for cube in (cube1, cube2):
# #1 : legacy results, for cube.attributes **viewed as a plain dictionary**.
assert dict(cube1.attributes) == expected_dict
# #2 : exact expected result, viewed as newstyle split-attributes
assert cube1.attributes == CubeAttrsDict(globals=expected_dict)

def test_02_userstyle_single_local(self):
# Default behaviour for a general local user-attribute.
Expand All @@ -658,10 +663,26 @@ def test_03_userstyle_multiple_different(self):
global_value_file2="global_file2",
vars_values_file2=vars2,
)
assert cube1.attributes == {"random": "f1v1"}
assert cube2.attributes == {"random": "f1v2"}
assert cube3.attributes == {"random": "x1"}
assert cube4.attributes == {"random": "x2"}

# (#1) : legacy equivalence : for cube.attributes viewed as a plain 'dict'
assert dict(cube1.attributes) == {"random": "f1v1"}
assert dict(cube2.attributes) == {"random": "f1v2"}
assert dict(cube3.attributes) == {"random": "x1"}
assert dict(cube4.attributes) == {"random": "x2"}

# (#1) : exact results check, for newstyle "split" cube attrs
assert cube1.attributes == CubeAttrsDict(
globals={"random": "global_file1"}, locals={"random": "f1v1"}
)
assert cube2.attributes == CubeAttrsDict(
globals={"random": "global_file1"}, locals={"random": "f1v2"}
)
assert cube3.attributes == CubeAttrsDict(
globals={"random": "global_file2"}, locals={"random": "x1"}
)
assert cube4.attributes == CubeAttrsDict(
globals={"random": "global_file2"}, locals={"random": "x2"}
)

def test_04_userstyle_multiple_same(self):
# Nothing special to note in this case
Expand All @@ -671,8 +692,14 @@ def test_04_userstyle_multiple_same(self):
global_value_file1="global_file1",
vars_values_file1={"v1": "same-value", "v2": "same-value"},
)
assert cube1.attributes == {"random": "same-value"}
assert cube2.attributes == {"random": "same-value"}
for cube in (cube1, cube2):
# (#1): legacy values, for cube.attributes viewed as a single dict
assert dict(cube.attributes) == {"random": "same-value"}
# (#2): exact results, with newstyle "split" cube attrs
assert cube2.attributes == CubeAttrsDict(
globals={"random": "global_file1"},
locals={"random": "same-value"},
)

#######################################################
# Tests on "Conventions" attribute.
Expand Down Expand Up @@ -702,7 +729,13 @@ def test_08_conventions_var_both(self):
global_value_file1="global-setting",
vars_values_file1="local-setting",
)
assert cube.attributes == {"Conventions": "local-setting"}
# (#1): legacy values, for cube.attributes viewed as a single dict
assert dict(cube.attributes) == {"Conventions": "local-setting"}
# (#2): exact results, with newstyle "split" cube attrs
assert cube.attributes == CubeAttrsDict(
globals={"Conventions": "global-setting"},
locals={"Conventions": "local-setting"},
)

#######################################################
# Tests on "global" style attributes
Expand All @@ -725,7 +758,12 @@ def test_10_globalstyle__local(self, global_attr):
attr_name=global_attr,
vars_values_file1=attr_content,
)
assert cube.attributes == {global_attr: attr_content}
# (#1): legacy values, for cube.attributes viewed as a single dict
assert dict(cube.attributes) == {global_attr: attr_content}
# (#2): exact results, with newstyle "split" cube attrs
assert cube.attributes == CubeAttrsDict(
locals={global_attr: attr_content}
)

def test_11_globalstyle__both(self, global_attr):
attr_global = f"Global-{global_attr}"
Expand All @@ -736,7 +774,13 @@ def test_11_globalstyle__both(self, global_attr):
vars_values_file1=attr_local,
)
# promoted local setting "wins"
assert cube.attributes == {global_attr: attr_local}
# (#1): legacy values, for cube.attributes viewed as a single dict
assert dict(cube.attributes) == {global_attr: attr_local}
# (#2): exact results, with newstyle "split" cube attrs
assert cube.attributes == CubeAttrsDict(
globals={global_attr: attr_global},
locals={global_attr: attr_local},
)

def test_12_globalstyle__multivar_different(self, global_attr):
# Multiple *different* local settings are retained
Expand All @@ -746,8 +790,12 @@ def test_12_globalstyle__multivar_different(self, global_attr):
attr_name=global_attr,
vars_values_file1={"v1": attr_1, "v2": attr_2},
)
assert cube1.attributes == {global_attr: attr_1}
assert cube2.attributes == {global_attr: attr_2}
# (#1): legacy values, for cube.attributes viewed as a single dict
assert dict(cube1.attributes) == {global_attr: attr_1}
assert dict(cube2.attributes) == {global_attr: attr_2}
# (#2): exact results, with newstyle "split" cube attrs
assert cube1.attributes == CubeAttrsDict(locals={global_attr: attr_1})
assert cube2.attributes == CubeAttrsDict(locals={global_attr: attr_2})

def test_14_globalstyle__multifile_different(self, global_attr):
# Different global attributes from multiple files are retained as local ones
Expand Down Expand Up @@ -812,7 +860,15 @@ def test_16_localstyle(self, local_attr, origin_style):
# For some reason, these ones never appear on the cube
expected_result = {}

assert cube.attributes == expected_result
if origin_style == "input_local":
expected_result_newstyle = CubeAttrsDict(expected_result)
else:
expected_result_newstyle = CubeAttrsDict(globals=expected_result)

# (#1): legacy values, for cube.attributes viewed as a single dict
assert dict(cube.attributes) == expected_result
# (#2): exact results, with newstyle "split" cube attrs
assert cube.attributes == expected_result_newstyle


class TestSave(MixinAttrsTesting):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def _make_engine(global_attributes=None, standard_name=None, long_name=None):
return engine


class TestInvalidGlobalAttributes(tests.IrisTest):
class TestGlobalAttributes(tests.IrisTest):
def test_valid(self):
global_attributes = {
"Conventions": "CF-1.5",
Expand All @@ -51,7 +51,7 @@ def test_valid(self):
engine = _make_engine(global_attributes)
build_cube_metadata(engine)
expected = global_attributes
self.assertEqual(engine.cube.attributes, expected)
self.assertEqual(engine.cube.attributes.globals, expected)

def test_invalid(self):
global_attributes = {
Expand All @@ -65,13 +65,14 @@ def test_invalid(self):
# Check for a warning.
self.assertEqual(warn.call_count, 1)
self.assertIn(
"Skipping global attribute 'calendar'", warn.call_args[0][0]
"Skipping disallowed global attribute 'calendar'",
warn.call_args[0][0],
)
# Check resulting attributes. The invalid entry 'calendar'
# should be filtered out.
global_attributes.pop("calendar")
expected = global_attributes
self.assertEqual(engine.cube.attributes, expected)
self.assertEqual(engine.cube.attributes.globals, expected)


class TestCubeName(tests.IrisTest):
Expand Down