Skip to content

Commit 5b93be6

Browse files
pp-mopre-commit-ci[bot]trexfeathers
authored
Mergeback of "Feature _split_attrs" branch (#5152)
* Split-attrs: Cube metadata refactortests (#4993) * Convert Test___eq__ to pytest. * Convert Test_combine to pytest. * Convert Test_difference to pytest. * Review changes. * Split attrs - tests for status quo (#4960) * Tests for attribute handling in netcdf load/save. * Tidy test functions. * Fix import order exception. * Add cf-global attributes test. * Towards more pytest-y implemenation. * Replace 'create_testcase' with fixture which also handles temporary directory. * Much tidy; use fixtures to parametrise over multiple attributes. * Fix warnings; begin data-style attrs tests. * Tests for data-style attributes. * Simplify setup fixture + improve docstring. * No parallel test runner, to avoid error for Python>3.8. * Fixed for new-style netcdf module. * Small review changes. * Rename attributes set 'data-style' as 'local-style'. * Simplify use of fixtures; clarify docstrings/comments and improve argument names. * Clarify testing sections for different attribute 'styles'. * Re-enable parallel testing. * Sorted params to avoid parallel testing bug - pytest#432. * Rename test functions to make alpha-order match order in class. * Split netcdf load/save attribute testing into separate sourcefile. * Add tests for loaded cube attributes; refactor to share code between Load and Roundtrip tests. * Add tests for attribute saving. * Fix method names in comments. * Clarify source of Conventions attributes. * Explain the test numbering in TestRoundtrip/TestLoad. * Remove obsolete test helper method. * Fix small typo; Fix numbering of testcases in TestSave. * 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]> * Splitattrs ncload (#5384) * Distinguish local+global attributes in netcdf loads. * Small test fixes. * Small doctest fix. * Fix attribute load-save tests for new behaviour, and old-behaviour equivalence. * Split attrs docs (#5418) * Clarification in CubeAttrsDict examples. * CubeAttrsDict fix docstring typo. * Raise awareness of split attributes in user guide. * What's New entry. * Changes to metadata documentation. * Splitattrs ncsave redo (#5410) * Add docs and future switch, no function yet. * Typing enables code completion for Cube.attributes. * Make roundtrip checking more precise + improve some tests accordingly (cf. #5403). * Rework all tests to use common setup + results-checking code. * Saver supports split-attributes saving (no tests yet). * Tiny docs fix. * Explain test routines better. * Fix init of FUTURE object. * Remove spurious re-test of FUTURE.save_split_attrs. * Don't create Cube attrs of 'None' (n.b. but no effect as currently used). * Remove/repair refs to obsolete routines. * Check all warnings from save operations. * Remove TestSave test numbers. * More save cases: no match with missing, and different cube attribute types. * Run save/roundtrip tests both with+without split saves. * Fix. * Review changes. * Fix changed warning messages. * Move warnings checking from 'run' to 'check' phase. * Simplify and improve warnings checking code. * Fix wrong testcase. * Minor review changes. * Fix reverted code. * Use sets to simplify demoted-attributes code. * WIP * Working with iris 3.6.1, no errors TestSave or TestRoundtrip. * Interim save (incomplete?). * Different results form for split tests; working for roundtrip. * Check that all param lists are sorted. * Check matrix result-files compatibility; add test_save_matrix. * test_load_matrix added; two types of load result. * Finalise special-case attributes. * Small docs tweaks. * Add some more testcases, * Ensure valid sort-order for globals of possibly different types. * Initialise matrix results with legacy values from v3.6.1 -- all matching. * Add full current matrix results, i.e. snapshot current behaviours. * Review changes : rename some matrix testcases, for clarity. * Splitattrs ncsave redo commonmeta (#5538) * Define common-metadata operartions on split attribute dictionaries. * Tests for split-attributes handling in CubeMetadata operations. * Small tidy and clarify. * Common metadata ops support mixed split/unsplit attribute dicts. * Clarify with better naming, comments, docstrings. * Remove split-attrs handling to own sourcefile, and implement as a decorator. * Remove redundant tests duplicated by matrix testcases. * Newstyle split-attrs matrix testing, with fewer testcases. * Small improvements to comments + docstrings. * Fix logic for equals expectation; expand primary/secondary independence test. * Clarify result testing in metadata operations decorator. * Splitattrs equalise (#5586) * Add tests in advance for split-attributes handling cases. * Move dict conversion inside utility, for use elsewhere. * Add support for split-attributes to equalise_attributes. * Update lib/iris/util.py Co-authored-by: Martin Yeo <[email protected]> * Update lib/iris/tests/unit/util/test_equalise_attributes.py Co-authored-by: Martin Yeo <[email protected]> * Simplify and clarify equalise_attributes code. --------- Co-authored-by: Martin Yeo <[email protected]> * Fix merge-fail messaging for attribute mismatches. (#5590) * Extra CubeAttrsDict methods to emulate dictionary behaviours. (#5592) * Extra CubeAttrsDict methods to emulate dictionary behaviours. * Don't use staticmethod on fixture. * Add Iris warning categories to saver warnings. * Type equality fixes for new flake8. * Licence header fixes. * Splitattrs ncsave deprecation (#5595) * Small improvement to split-attrs whatsnew. * Emit deprecation warning when saving without split-attrs enabled. * Stop legacy-split-attribute warnings from upsetting delayed-saving tests. --------- 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 96a69e4 commit 5b93be6

25 files changed

+6945
-524
lines changed

docs/src/further_topics/metadata.rst

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ actual `data attribute`_ names of the metadata members on the Iris class.
9191
metadata members are Iris specific terms, rather than recognised `CF Conventions`_
9292
terms.
9393

94+
.. note::
95+
96+
:class:`~iris.cube.Cube` :attr:`~iris.cube.Cube.attributes` implement the
97+
concept of dataset-level and variable-level attributes, to enable correct
98+
NetCDF loading and saving (see :class:`~iris.cube.CubeAttrsDict` and NetCDF
99+
:func:`~iris.fileformats.netcdf.saver.save` for more). ``attributes`` on
100+
the other classes do not have this distinction, but the ``attributes``
101+
members of ALL the classes still have the same interface, and can be
102+
compared.
103+
94104

95105
Common Metadata API
96106
===================
@@ -128,10 +138,12 @@ For example, given the following :class:`~iris.cube.Cube`,
128138
source 'Data from Met Office Unified Model 6.05'
129139

130140
We can easily get all of the associated metadata of the :class:`~iris.cube.Cube`
131-
using the ``metadata`` property:
141+
using the ``metadata`` property (note the specialised
142+
:class:`~iris.cube.CubeAttrsDict` for the :attr:`~iris.cube.Cube.attributes`,
143+
as mentioned earlier):
132144

133145
>>> 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=()),))
146+
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=()),))
135147

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

676688
.. doctest:: metadata-combine
677689

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=()),))
690+
>>> cube.metadata
691+
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=()),))
680692

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

@@ -701,7 +713,7 @@ which is replaced with a **different value**,
701713
>>> metadata != cube.metadata
702714
True
703715
>>> 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=()),))
716+
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=()),))
705717

706718
The ``combine`` method combines metadata by performing a **strict** comparison
707719
between each of the associated metadata member values,
@@ -724,7 +736,7 @@ Let's reinforce this behaviour, but this time by combining metadata where the
724736
>>> metadata != cube.metadata
725737
True
726738
>>> metadata.combine(cube.metadata).attributes
727-
{'Model scenario': 'A1B'}
739+
CubeAttrsDict(globals={}, locals={'Model scenario': 'A1B'})
728740

729741
The combined result for the ``attributes`` member only contains those
730742
**common keys** with **common values**.
@@ -810,16 +822,17 @@ the ``from_metadata`` class method. For example, given the following
810822

811823
.. doctest:: metadata-convert
812824

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=()),))
825+
>>> cube.metadata
826+
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=()),))
815827

816828
We can easily convert it to a :class:`~iris.common.metadata.DimCoordMetadata` instance
817829
using ``from_metadata``,
818830

819831
.. doctest:: metadata-convert
820832

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)
833+
>>> newmeta = DimCoordMetadata.from_metadata(cube.metadata)
834+
>>> print(newmeta)
835+
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'})
823836

824837
By examining :numref:`metadata members table`, we can see that the
825838
:class:`~iris.cube.Cube` and :class:`~iris.coords.DimCoord` container
@@ -849,9 +862,9 @@ class instance,
849862

850863
.. doctest:: metadata-convert
851864

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-
865+
>>> newmeta = longitude.metadata.from_metadata(cube.metadata)
866+
>>> print(newmeta)
867+
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'})
855868

856869
.. _metadata assignment:
857870

@@ -978,7 +991,7 @@ Indeed, it's also possible to assign to the ``metadata`` property with a
978991
>>> longitude.metadata
979992
DimCoordMetadata(standard_name='longitude', long_name=None, var_name='longitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False)
980993
>>> longitude.metadata = cube.metadata
981-
>>> longitude.metadata # doctest: +SKIP
994+
>>> longitude.metadata
982995
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)
983996

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

docs/src/userguide/iris_cubes.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ A cube consists of:
8585
data dimensions as the coordinate has dimensions.
8686

8787
* an attributes dictionary which, other than some protected CF names, can
88-
hold arbitrary extra metadata.
88+
hold arbitrary extra metadata. This implements the concept of dataset-level
89+
and variable-level attributes when loading and and saving NetCDF files (see
90+
:class:`~iris.cube.CubeAttrsDict` and NetCDF
91+
:func:`~iris.fileformats.netcdf.saver.save` for more).
8992
* a list of cell methods to represent operations which have already been
9093
applied to the data (e.g. "mean over time")
9194
* a list of coordinate "factories" used for deriving coordinates from the

docs/src/whatsnew/latest.rst

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ This document explains the changes made to Iris for this release
2929

3030
✨ Features
3131
===========
32+
#. `@pp-mo`_, `@lbdreyer`_ and `@trexfeathers`_ improved
33+
:class:`~iris.cube.Cube` :attr:`~iris.cube.Cube.attributes` handling to
34+
better preserve the distinction between dataset-level and variable-level
35+
attributes, allowing file-Cube-file round-tripping of NetCDF attributes. See
36+
:class:`~iris.cube.CubeAttrsDict`, NetCDF
37+
:func:`~iris.fileformats.netcdf.saver.save` and :data:`~iris.Future` for more.
38+
(:pull:`5152`, `split attributes project`_)
39+
40+
#. `@rcomer`_ rewrote :func:`~iris.util.broadcast_to_shape` so it now handles
41+
lazy data. (:pull:`5307`)
3242

3343
#. `@trexfeathers`_ and `@HGWright`_ (reviewer) sub-categorised all Iris'
3444
:class:`UserWarning`\s for richer filtering. The full index of
@@ -45,7 +55,7 @@ This document explains the changes made to Iris for this release
4555
the year of December) instead of the following year (the default behaviour).
4656
(:pull:`5573`)
4757

48-
#. `@HGWright`_ added :attr:`~iris.coords.Coord.ignore_axis` to allow manual
58+
#. `@HGWright`_ added :attr:`~iris.coords.Coord.ignore_axis` to allow manual
4959
intervention preventing :func:`~iris.util.guess_coord_axis` from acting on a
5060
coordinate. (:pull:`5551`)
5161

@@ -152,4 +162,4 @@ This document explains the changes made to Iris for this release
152162
153163
.. _NEP29 Drop Schedule: https://numpy.org/neps/nep-0029-deprecation_policy.html#drop-schedule
154164
.. _codespell: https://github.com/codespell-project/codespell
155-
165+
.. _split attributes project: https://github.com/orgs/SciTools/projects/5?pane=info

lib/iris/__init__.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ def callback(cube, field, filename):
141141
class Future(threading.local):
142142
"""Run-time configuration controller."""
143143

144-
def __init__(self, datum_support=False, pandas_ndim=False):
144+
def __init__(
145+
self, datum_support=False, pandas_ndim=False, save_split_attrs=False
146+
):
145147
"""
146148
A container for run-time options controls.
147149
@@ -163,6 +165,11 @@ def __init__(self, datum_support=False, pandas_ndim=False):
163165
pandas_ndim : bool, default=False
164166
See :func:`iris.pandas.as_data_frame` for details - opts in to the
165167
newer n-dimensional behaviour.
168+
save_split_attrs : bool, default=False
169+
Save "global" and "local" cube attributes to netcdf in appropriately
170+
different ways : "global" ones are saved as dataset attributes, where
171+
possible, while "local" ones are saved as data-variable attributes.
172+
See :func:`iris.fileformats.netcdf.saver.save`.
166173
167174
"""
168175
# The flag 'example_future_flag' is provided as a reference for the
@@ -174,14 +181,18 @@ def __init__(self, datum_support=False, pandas_ndim=False):
174181
# self.__dict__['example_future_flag'] = example_future_flag
175182
self.__dict__["datum_support"] = datum_support
176183
self.__dict__["pandas_ndim"] = pandas_ndim
184+
self.__dict__["save_split_attrs"] = save_split_attrs
185+
177186
# TODO: next major release: set IrisDeprecation to subclass
178187
# DeprecationWarning instead of UserWarning.
179188

180189
def __repr__(self):
181190
# msg = ('Future(example_future_flag={})')
182191
# return msg.format(self.example_future_flag)
183-
msg = "Future(datum_support={}, pandas_ndim={})"
184-
return msg.format(self.datum_support, self.pandas_ndim)
192+
msg = "Future(datum_support={}, pandas_ndim={}, save_split_attrs={})"
193+
return msg.format(
194+
self.datum_support, self.pandas_ndim, self.save_split_attrs
195+
)
185196

186197
# deprecated_options = {'example_future_flag': 'warning',}
187198
deprecated_options = {}

lib/iris/_merge.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
multidim_lazy_stack,
2323
)
2424
from iris.common import CoordMetadata, CubeMetadata
25+
from iris.common._split_attribute_dicts import (
26+
_convert_splitattrs_to_pairedkeys_dict as convert_splitattrs_to_pairedkeys_dict,
27+
)
2528
import iris.coords
2629
import iris.cube
2730
import iris.exceptions
@@ -390,23 +393,27 @@ def _defn_msgs(self, other_defn):
390393
)
391394
)
392395
if self_defn.attributes != other_defn.attributes:
393-
diff_keys = set(self_defn.attributes.keys()) ^ set(
394-
other_defn.attributes.keys()
396+
attrs_1, attrs_2 = self_defn.attributes, other_defn.attributes
397+
diff_keys = sorted(
398+
set(attrs_1.globals) ^ set(attrs_2.globals)
399+
| set(attrs_1.locals) ^ set(attrs_2.locals)
395400
)
396401
if diff_keys:
397402
msgs.append(
398403
"cube.attributes keys differ: "
399404
+ ", ".join(repr(key) for key in diff_keys)
400405
)
401406
else:
407+
attrs_1, attrs_2 = [
408+
convert_splitattrs_to_pairedkeys_dict(dic)
409+
for dic in (attrs_1, attrs_2)
410+
]
402411
diff_attrs = [
403-
repr(key)
404-
for key in self_defn.attributes
405-
if np.all(
406-
self_defn.attributes[key] != other_defn.attributes[key]
407-
)
412+
repr(key[1])
413+
for key in attrs_1
414+
if np.all(attrs_1[key] != attrs_2[key])
408415
]
409-
diff_attrs = ", ".join(diff_attrs)
416+
diff_attrs = ", ".join(sorted(diff_attrs))
410417
msgs.append(
411418
"cube.attributes values differ for keys: {}".format(
412419
diff_attrs
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright Iris contributors
2+
#
3+
# This file is part of Iris and is released under the BSD license.
4+
# See LICENSE in the root of the repository for full licensing details.
5+
"""
6+
Dictionary operations for dealing with the CubeAttrsDict "split"-style attribute
7+
dictionaries.
8+
9+
The idea here is to convert a split-dictionary into a "plain" one for calculations,
10+
whose keys are all pairs of the form ('global', <keyname>) or ('local', <keyname>).
11+
And to convert back again after the operation, if the result is a dictionary.
12+
13+
For "strict" operations this clearly does all that is needed. For lenient ones,
14+
we _might_ want for local+global attributes of the same name to interact.
15+
However, on careful consideration, it seems that this is not actually desirable for
16+
any of the common-metadata operations.
17+
So, we simply treat "global" and "local" attributes of the same name as entirely
18+
independent. Which happily is also the easiest to code, and to explain.
19+
"""
20+
from collections.abc import Mapping, Sequence
21+
from functools import wraps
22+
23+
24+
def _convert_splitattrs_to_pairedkeys_dict(dic):
25+
"""
26+
Convert a split-attributes dictionary to a "normal" dict.
27+
28+
Transform a :class:`~iris.cube.CubeAttributesDict` "split" attributes dictionary
29+
into a 'normal' :class:`dict`, with paired keys of the form ('global', name) or
30+
('local', name).
31+
32+
If the input is *not* a split-attrs dict, it is converted to one before
33+
transforming it. This will assign its keys to global/local depending on a standard
34+
set of choices (see :class:`~iris.cube.CubeAttributesDict`).
35+
"""
36+
from iris.cube import CubeAttrsDict
37+
38+
# Convert input to CubeAttrsDict
39+
if not hasattr(dic, "globals") or not hasattr(dic, "locals"):
40+
dic = CubeAttrsDict(dic)
41+
42+
def _global_then_local_items(dic):
43+
# Routine to produce global, then local 'items' in order, and with all keys
44+
# "labelled" as local or global type, to ensure they are all unique.
45+
for key, value in dic.globals.items():
46+
yield ("global", key), value
47+
for key, value in dic.locals.items():
48+
yield ("local", key), value
49+
50+
return dict(_global_then_local_items(dic))
51+
52+
53+
def _convert_pairedkeys_dict_to_splitattrs(dic):
54+
"""
55+
Convert an input with global/local paired keys back into a split-attrs dict.
56+
57+
For now, this is always and only a :class:`iris.cube.CubeAttrsDict`.
58+
"""
59+
from iris.cube import CubeAttrsDict
60+
61+
result = CubeAttrsDict()
62+
for key, value in dic.items():
63+
keytype, keyname = key
64+
if keytype == "global":
65+
result.globals[keyname] = value
66+
else:
67+
assert keytype == "local"
68+
result.locals[keyname] = value
69+
return result
70+
71+
72+
def adjust_for_split_attribute_dictionaries(operation):
73+
"""
74+
Decorator to make a function of attribute-dictionaries work with split attributes.
75+
76+
The wrapped function of attribute-dictionaries is currently always one of "equals",
77+
"combine" or "difference", with signatures like :
78+
equals(left: dict, right: dict) -> bool
79+
combine(left: dict, right: dict) -> dict
80+
difference(left: dict, right: dict) -> None | (dict, dict)
81+
82+
The results of the wrapped operation are either :
83+
* for "equals" (or "__eq__") : a boolean
84+
* for "combine" : a (converted) attributes-dictionary
85+
* for "difference" : a list of (None or "pair"), where a pair contains two
86+
dictionaries
87+
88+
Before calling the wrapped operation, its inputs (left, right) are modified by
89+
converting any "split" dictionaries to a form where the keys are pairs
90+
of the form ("global", name) or ("local", name).
91+
92+
After calling the wrapped operation, for "combine" or "difference", the result can
93+
contain a dictionary or dictionaries. These are then transformed back from the
94+
'converted' form to split-attribute dictionaries, before returning.
95+
96+
"Split" dictionaries are all of class :class:`~iris.cube.CubeAttrsDict`, since
97+
the only usage of 'split' attribute dictionaries is in Cubes (i.e. they are not
98+
used for cube components).
99+
"""
100+
101+
@wraps(operation)
102+
def _inner_function(*args, **kwargs):
103+
# Convert all inputs into 'pairedkeys' type dicts
104+
args = [_convert_splitattrs_to_pairedkeys_dict(arg) for arg in args]
105+
106+
result = operation(*args, **kwargs)
107+
108+
# Convert known specific cases of 'pairedkeys' dicts in the result, and convert
109+
# those back into split-attribute dictionaries.
110+
if isinstance(result, Mapping):
111+
# Fix a result which is a single dictionary -- for "combine"
112+
result = _convert_pairedkeys_dict_to_splitattrs(result)
113+
elif isinstance(result, Sequence) and len(result) == 2:
114+
# Fix a result which is a pair of dictionaries -- for "difference"
115+
left, right = result
116+
left, right = (
117+
_convert_pairedkeys_dict_to_splitattrs(left),
118+
_convert_pairedkeys_dict_to_splitattrs(right),
119+
)
120+
result = result.__class__([left, right])
121+
# ELSE: leave other types of result unchanged. E.G. None, bool
122+
123+
return result
124+
125+
return _inner_function

0 commit comments

Comments
 (0)