From 83eee13d045a3a3bc8f8d810aea1e18f379a4d23 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 21 Jul 2023 16:45:04 +0100 Subject: [PATCH 01/38] Add docs and future switch, no function yet. --- lib/iris/__init__.py | 16 +++++++++++++--- lib/iris/fileformats/netcdf/saver.py | 14 +++++++++++--- .../integration/test_netcdf__loadsaveattrs.py | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 0e6670533f..e47ae3f98d 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -142,7 +142,9 @@ def callback(cube, field, filename): class Future(threading.local): """Run-time configuration controller.""" - def __init__(self, datum_support=False, pandas_ndim=False): + def __init__( + self, datum_support=False, pandas_ndim=False, save_split_attrs=False + ): """ A container for run-time options controls. @@ -164,6 +166,11 @@ def __init__(self, datum_support=False, pandas_ndim=False): pandas_ndim : bool, default=False See :func:`iris.pandas.as_data_frame` for details - opts in to the newer n-dimensional behaviour. + save_split_attrs : bool, default=False + Save "global" and "local" cube attributes to netcdf in appropriately + different ways : "global" ones are saved as dataset attributes, where + possible, while "local" ones are saved as data-variable attributes. + See :func:`iris.fileformats.netcdf.saver.save`. """ # The flag 'example_future_flag' is provided as a reference for the @@ -175,12 +182,15 @@ def __init__(self, datum_support=False, pandas_ndim=False): # self.__dict__['example_future_flag'] = example_future_flag self.__dict__["datum_support"] = datum_support self.__dict__["pandas_ndim"] = pandas_ndim + self.__dict__["save_split_attrs"] = pandas_ndim def __repr__(self): # msg = ('Future(example_future_flag={})') # return msg.format(self.example_future_flag) - msg = "Future(datum_support={}, pandas_ndim={})" - return msg.format(self.datum_support, self.pandas_ndim) + msg = "Future(datum_support={}, pandas_ndim={}, save_split_attrs={})" + return msg.format( + self.datum_support, self.pandas_ndim, self.save_split_attrs + ) # deprecated_options = {'example_future_flag': 'warning',} deprecated_options = {} diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 312eea9c43..aae235c563 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2600,9 +2600,15 @@ def save( Save cube(s) to a netCDF file, given the cube and the filename. * Iris will write CF 1.7 compliant NetCDF files. - * The attributes dictionaries on each cube in the saved cube list - will be compared and common attributes saved as NetCDF global - attributes where appropriate. + * If **split-attribute saving is disabled**, i.e. + :attr:`iris.FUTURE.save_split_attrs` is ``False``, then attributes dictionaries + on each cube in the saved cube list will be compared and common attributes saved + as NetCDF global attributes where appropriate. + + Or, **when **split-attribute saving is enabled**, then `cube.attributes.locals`` + are always saved as attributes of data-variables, and ``cube.attributes.globals`` + are saved as global (dataset) attributes, where possible. + Since the 2 types are now distinguished : see :class:`~iris.cube.CubeAttrsDict`. * Keyword arguments specifying how to save the data are applied to each cube. To use different settings for different cubes, use the NetCDF Context manager (:class:`~Saver`) directly. @@ -2635,6 +2641,8 @@ def save( An interable of cube attribute keys. Any cube attributes with matching keys will become attributes on the data variable rather than global attributes. + **NOTE:** this is *ignored* if 'split-attribute saving' is **enabled**, + i.e. when ``iris.FUTURE.save_split_attrs`` is ``True``. * unlimited_dimensions (iterable of strings and/or :class:`iris.coords.Coord` objects): diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index a1cad53336..5f1d8907bc 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -77,7 +77,7 @@ def _calling_testname(): Search up the callstack for a function named "test_*", and return the name for use as a test identifier. - Idea borrowed from :meth:`iris.tests.IrisTest_nometa.result_path`. + Idea borrowed from :meth:`iris.tests.IrisTest.result_path`. Returns ------- From ce5cdefc2a858c9e9f7ef8c110592f797fbc1e1c Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sun, 30 Jul 2023 08:19:19 +0100 Subject: [PATCH 02/38] Typing enables code completion for Cube.attributes. --- lib/iris/cube.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 8bb9d7c00e..d9fd105f77 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1,4 +1,4 @@ -# Copyright Iris contributors + # Copyright Iris contributors # # This file is part of Iris and is released under the LGPL license. # See COPYING and COPYING.LESSER in the root of the repository for full @@ -930,19 +930,19 @@ def _normalise_attrs( return attributes @property - def locals(self): + def locals(self) -> LimitedAttributeDict: return self._locals @locals.setter - def locals(self, attributes): + def locals(self, attributes: Optional[Mapping]): self._locals = self._normalise_attrs(attributes) @property - def globals(self): + def globals(self) -> LimitedAttributeDict: return self._globals @globals.setter - def globals(self, attributes): + def globals(self, attributes: Optional[Mapping]): self._globals = self._normalise_attrs(attributes) # @@ -1335,8 +1335,12 @@ def _names(self): # # Ensure that .attributes is always a :class:`CubeAttrsDict`. # - @CFVariableMixin.attributes.setter - def attributes(self, attributes): + @property + def attributes(self) -> CubeAttrsDict: + return super().attributes + + @attributes.setter + def attributes(self, attributes: Optional[Mapping]): """ An override to CfVariableMixin.attributes.setter, which ensures that Cube attributes are stored in a way which distinguishes global + local ones. From 4d660b655276f8a57c80d842ffb09de571904af6 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sun, 30 Jul 2023 23:15:10 +0100 Subject: [PATCH 03/38] Make roundtrip checking more precise + improve some tests accordingly (cf. https://github.com/SciTools/iris/pull/5403). --- .../integration/test_netcdf__loadsaveattrs.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 5f1d8907bc..1db20400cc 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -39,6 +39,7 @@ # A list of "global-style" attribute names : those which should be global attributes by # default (i.e. file- or group-level, *not* attached to a variable). + _GLOBAL_TEST_ATTRS = set(iris.fileformats.netcdf.saver._CF_GLOBAL_ATTRS) # Remove this one, which has peculiar behaviour + is tested separately # N.B. this is not the same as 'Conventions', but is caught in the crossfire when that @@ -263,16 +264,15 @@ def check_roundtrip_results( else: assert self.attrname in ds.ncattrs() assert ds.getncattr(self.attrname) == global_attr_value - if var_attr_vals: - var_attr_vals = self._default_vars_and_attrvalues(var_attr_vals) - for var_name, value in var_attr_vals.items(): - assert var_name in ds.variables - v = ds.variables[var_name] - if value is None: - assert self.attrname not in v.ncattrs() - else: - assert self.attrname in v.ncattrs() - assert v.getncattr(self.attrname) == value + var_attr_vals = self._default_vars_and_attrvalues(var_attr_vals) + for var_name, value in var_attr_vals.items(): + assert var_name in ds.variables + v = ds.variables[var_name] + if value is None: + assert self.attrname not in v.ncattrs() + else: + assert self.attrname in v.ncattrs() + assert v.getncattr(self.attrname) == value ####################################################### # Tests on "user-style" attributes. @@ -302,7 +302,7 @@ def test_02_userstyle_single_local(self): # It results in a "promoted" global attribute. self.create_roundtrip_testcase( attr_name="myname", # A generic "user" attribute with no special handling - vars_values_file1={"myvar": "single-value"}, + vars_values_file1="single-value", ) self.check_roundtrip_results( global_attr_value="single-value", # local values eclipse the global ones @@ -545,10 +545,12 @@ def test_16_localstyle(self, local_attr, origin_style): attr_name=local_attr, vars_values_file1=attrval ) - if local_attr in iris.fileformats.netcdf.saver._CF_DATA_ATTRS: - # These ones are simply discarded on loading. - # By experiment, this overlap between _CF_ATTRS and _CF_DATA_ATTRS - # currently contains only 'missing_value' and 'standard_error_multiplier'. + if ( + local_attr in ('missing_value', 'standard_error_multiplier') + and origin_style == "input_local" + ): + # These ones are actually discarded by roundtrip. + # Not clear why, but for now this captures the facts. expect_global = None expect_var = None else: From 9a80cd28671336c2e41313ed3eb3adddf4af474e Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sun, 30 Jul 2023 17:58:16 +0100 Subject: [PATCH 04/38] Rework all tests to use common setup + results-checking code. --- lib/iris/cube.py | 2 +- .../integration/test_netcdf__loadsaveattrs.py | 849 +++++++++--------- 2 files changed, 419 insertions(+), 432 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index d9fd105f77..0d1f531bf9 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1,4 +1,4 @@ - # Copyright Iris contributors +# Copyright Iris contributors # # This file is part of Iris and is released under the LGPL license. # See COPYING and COPYING.LESSER in the root of the repository for full diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 1db20400cc..dcdff642f6 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -19,13 +19,15 @@ """ import inspect -from typing import Iterable, Optional, Union +from typing import Optional, Union +import numpy as np import pytest import iris import iris.coord_systems -from iris.cube import Cube, CubeAttrsDict +from iris.coords import DimCoord +from iris.cube import Cube import iris.fileformats.netcdf import iris.fileformats.netcdf._thread_safe_nc as threadsafe_nc4 @@ -129,13 +131,14 @@ def _default_vars_and_attrvalues(vars_and_attrvalues): vars_and_attrvalues = {"var": vars_and_attrvalues} return vars_and_attrvalues - def create_testcase_files( + def create_testcase_files_or_cubes( self, attr_name: str, global_value_file1: Optional[str] = None, var_values_file1: Union[None, str, dict] = None, global_value_file2: Optional[str] = None, var_values_file2: Union[None, str, dict] = None, + cubes=False, ): """ Create temporary input netcdf files with specific content. @@ -148,9 +151,13 @@ def create_testcase_files( created, with an attribute = the dictionary value, *except* that a dictionary value of None means that a local attribute is _not_ created on the variable. """ - # Make some input file paths. - filepath1 = self._testfile_path("testfile") - filepath2 = self._testfile_path("testfile2") + # save attribute on the instance + self.attrname = attr_name + + if not cubes: + # Make some input file paths. + filepath1 = self._testfile_path("testfile") + filepath2 = self._testfile_path("testfile2") def make_file( filepath: str, global_value=None, var_values=None @@ -170,24 +177,155 @@ def make_file( ds.close() return filepath - # Create one input file (always). - filepaths = [ - make_file( - filepath1, - global_value=global_value_file1, - var_values=var_values_file1, - ) - ] - if global_value_file2 is not None or var_values_file2 is not None: - # Make a second testfile and add it to files-to-be-loaded. - filepaths.append( - make_file( - filepath2, - global_value=global_value_file2, - var_values=var_values_file2, - ), + def make_cubes(var_name, global_value=None, var_values=None): + cubes = [] + var_values = self._default_vars_and_attrvalues(var_values) + for varname, local_value in var_values.items(): + cube = Cube(np.arange(3.0), var_name=var_name) + cubes.append(cube) + dimco = DimCoord(np.arange(3.0), var_name="x") + cube.add_dim_coord(dimco, 0) + cube.attributes.globals[attr_name] = global_value + if local_value is not None: + cube.attributes.locals[attr_name] = local_value + return cubes + + if cubes: + results = make_cubes("v1", global_value_file1, var_values_file1) + if global_value_file2 is not None or var_values_file2 is not None: + results.extend( + make_cubes("v2", global_value_file2, var_values_file2) + ) + else: + results = [ + make_file(filepath1, global_value_file1, var_values_file1) + ] + if global_value_file2 is not None or var_values_file2 is not None: + # Make a second testfile and add it to files-to-be-loaded. + results.append( + make_file(filepath2, global_value_file2, var_values_file2) + ) + + # Save results on the instance + if cubes: + self.input_cubes = results + else: + self.input_filepaths = results + return results + + def run_testcase(self, attr_name, values, create_cubes_or_files="files"): + # Save common attribute-name on the instance + self.attrname = attr_name + + # Standardise input to a list-of-lists, each inner list = [global, *locals] + assert isinstance(values, list) + if not isinstance(values[0], list): + values = [values] + assert len(values) in (1, 2) + assert len(values[0]) > 1 + + # Decode into global1, *locals1, and optionally global2, *locals2 + global1 = values[0][0] + vars1 = {} + i_var = 0 + for value in values[0][1:]: + vars1[f"var_{i_var}"] = value + i_var += 1 + if len(values) == 1: + global2 = None + vars2 = None + else: + assert len(values) == 2 + global2 = values[1][0] + vars2 = {} + for value in values[1][1:]: + vars2[f"var_{i_var}"] = value + i_var += 1 + + # Create test files or cubes (and store data on the instance) + assert create_cubes_or_files in ("cubes", "files") + make_cubes = create_cubes_or_files == "cubes" + self.create_testcase_files_or_cubes( + attr_name=attr_name, + global_value_file1=global1, + var_values_file1=vars1, + global_value_file2=global2, + var_values_file2=vars2, + cubes=make_cubes, + ) + + def fetch_results( + self, filepath=None, cubes=None, oldstyle_combined=False + ): + """ + Return testcase results from an output file or cubes in a standardised form. + + Unpick the global+local values of an attribute resulting from an operation. + A file result is always [global_value, *local_values] + A cubes result is [*[global_value, *local_values]] (over different global vals) + + When "oldstyle_combined" simulate the "legacy" result, when each cube had a + single combined attribute dictionary. This enables us to check against former + behaviour (and behaviour of results treated as a single dictionary). + If results are from a *file*, this has no effect. + + """ + attr_name = self.attrname + if filepath is not None: + # Fetch global and local values from a file + try: + ds = threadsafe_nc4.DatasetWrapper(filepath) + global_result = ( + ds.getncattr(attr_name) + if attr_name in ds.ncattrs() + else None + ) + # Fetch local attr value from all data variables (except dimcoord vars) + local_vars_results = [ + ( + var.name, + ( + var.getncattr(attr_name) + if attr_name in var.ncattrs() + else None + ), + ) + for var in ds.variables.values() + if var.name not in ds.dimensions + ] + finally: + ds.close() + # This version always returns a single result set [global, local1[, local2]] + # Return global, plus locals sorted by varname + local_vars_results = sorted(local_vars_results, key=lambda x: x[0]) + results = [global_result] + [val for _, val in local_vars_results] + else: + assert cubes is not None + # Sort result cubes according to a standard ordering. + cubes = sorted(cubes, key=lambda cube: cube.name()) + # Fetch globals and locals from cubes. + if oldstyle_combined: + # Replace cubes attributes with all-combined dictionaries + cubes = [cube.copy() for cube in cubes] + for cube in cubes: + combined = dict(cube.attributes) + cube.attributes.clear() + cube.attributes.locals = combined + global_values = set( + cube.attributes.globals.get(attr_name, None) for cube in cubes ) - return filepaths + # This way returns *multiple* result 'sets', one for each global value + results = [ + [globalval] + + [ + cube.attributes.locals.get(attr_name, None) + for cube in cubes + if cube.attributes.globals.get(attr_name, None) + == globalval + ] + for globalval in sorted(global_values) + ] + return results class TestRoundtrip(MixinAttrsTesting): @@ -206,73 +344,37 @@ class TestRoundtrip(MixinAttrsTesting): """ - def _roundtrip_load_and_save( - self, input_filepaths: Union[str, Iterable[str]], output_filepath: str - ) -> None: - """ - Load netcdf input file(s) and re-write all to a given output file. - """ - # Do a load+save to produce a testable output result in a new file. - cubes = iris.load(input_filepaths) - iris.save(cubes, output_filepath) - - def create_roundtrip_testcase( - self, - attr_name, - global_value_file1=None, - vars_values_file1=None, - global_value_file2=None, - vars_values_file2=None, - ): + def run_roundtrip_testcase(self, attr_name, values): """ Initialise the testcase from the passed-in controls, configure the input files and run a save-load roundtrip to produce the output file. The name of the attribute, and the input and output temporary filepaths are - stored on the instance, where "self.check_roundtrip_results()" can get them. + stored on the instance, where "self.check_roundtrip_results_OLDSTYLE()" can get them. """ - self.attrname = attr_name - self.input_filepaths = self.create_testcase_files( - attr_name=attr_name, - global_value_file1=global_value_file1, - var_values_file1=vars_values_file1, - global_value_file2=global_value_file2, - var_values_file2=vars_values_file2, + self.run_testcase( + attr_name=attr_name, values=values, create_cubes_or_files="files" ) self.result_filepath = self._testfile_path("result") - self._roundtrip_load_and_save( - self.input_filepaths, self.result_filepath - ) + # Do a load+save to produce a testable output result in a new file. + cubes = iris.load(self.input_filepaths) + iris.save(cubes, self.result_filepath) - def check_roundtrip_results( - self, global_attr_value=None, var_attr_vals=None - ): + def check_roundtrip_results(self, expected): """ Run checks on the generated output file. - The counterpart to create_testcase, with similar control arguments. - Check existence (or not) of : a global attribute, named variables, and their - local attributes. Values of 'None' mean to check that the relevant global/local - attribute does *not* exist. + The counterpart to create_roundtrip_testcase_OLDSTYLE, with similar control arguments. + Check existence (or not) of a global attribute, and a number of local + (variable) attributes. + Values of 'None' mean to check that the relevant global/local attribute does + *not* exist. """ # N.B. there is only ever one result-file, but it can contain various variables # which came from different input files. - ds = threadsafe_nc4.DatasetWrapper(self.result_filepath) - if global_attr_value is None: - assert self.attrname not in ds.ncattrs() - else: - assert self.attrname in ds.ncattrs() - assert ds.getncattr(self.attrname) == global_attr_value - var_attr_vals = self._default_vars_and_attrvalues(var_attr_vals) - for var_name, value in var_attr_vals.items(): - assert var_name in ds.variables - v = ds.variables[var_name] - if value is None: - assert self.attrname not in v.ncattrs() - else: - assert self.attrname in v.ncattrs() - assert v.getncattr(self.attrname) == value + results = self.fetch_results(filepath=self.result_filepath) + assert results == expected ####################################################### # Tests on "user-style" attributes. @@ -281,94 +383,60 @@ def check_roundtrip_results( # def test_01_userstyle_single_global(self): - self.create_roundtrip_testcase( - attr_name="myname", # A generic "user" attribute with no special handling - global_value_file1="single-value", - vars_values_file1={ - "myvar": None - }, # the variable has no such attribute + self.run_roundtrip_testcase( + attr_name="myname", values=["single-value", None] ) # Default behaviour for a general global user-attribute. # It simply remains global. - self.check_roundtrip_results( - global_attr_value="single-value", # local values eclipse the global ones - var_attr_vals={ - "myvar": None - }, # the variable has no such attribute - ) + self.check_roundtrip_results(["single-value", None]) def test_02_userstyle_single_local(self): # Default behaviour for a general local user-attribute. # It results in a "promoted" global attribute. - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name="myname", # A generic "user" attribute with no special handling - vars_values_file1="single-value", - ) - self.check_roundtrip_results( - global_attr_value="single-value", # local values eclipse the global ones - # N.B. the output var has NO such attribute + values=[None, "single-value"], ) + self.check_roundtrip_results(["single-value", None]) def test_03_userstyle_multiple_different(self): # Default behaviour for general user-attributes. # The global attribute is lost because there are local ones. - vars1 = {"f1_v1": "f1v1", "f1_v2": "f2v2"} - vars2 = {"f2_v1": "x1", "f2_v2": "x2"} - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name="random", # A generic "user" attribute with no special handling - global_value_file1="global_file1", - vars_values_file1=vars1, - global_value_file2="global_file2", - vars_values_file2=vars2, - ) - # combine all 4 vars in one dict - all_vars_and_attrs = vars1.copy() - all_vars_and_attrs.update(vars2) - # TODO: replace with "|", when we drop Python 3.8 - # see: https://peps.python.org/pep-0584/ - # just check they are all there and distinct - assert len(all_vars_and_attrs) == len(vars1) + len(vars2) - self.check_roundtrip_results( - global_attr_value=None, # local values eclipse the global ones - var_attr_vals=all_vars_and_attrs, + values=[ + ["common_global", "f1v1", "f1v2"], + ["common_global", "x1", "x2"], + ], ) + # just check they are all there and distinct + self.check_roundtrip_results([None, "f1v1", "f1v2", "x1", "x2"]) def test_04_userstyle_matching_promoted(self): # matching local user-attributes are "promoted" to a global one. - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name="random", - global_value_file1="global_file1", - vars_values_file1={"v1": "same-value", "v2": "same-value"}, - ) - self.check_roundtrip_results( - global_attr_value="same-value", - var_attr_vals={"v1": None, "v2": None}, + values=["global_file1", "same-value", "same-value"], ) + self.check_roundtrip_results(["same-value", None, None]) def test_05_userstyle_matching_crossfile_promoted(self): # matching user-attributes are promoted, even across input files. - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name="random", - global_value_file1="global_file1", - vars_values_file1={"v1": "same-value", "v2": "same-value"}, - vars_values_file2={"f2_v1": "same-value", "f2_v2": "same-value"}, - ) - self.check_roundtrip_results( - global_attr_value="same-value", - var_attr_vals={x: None for x in ("v1", "v2", "f2_v1", "f2_v2")}, + values=[ + ["global_file1", "same-value", "same-value"], + [None, "same-value", "same-value"], + ], ) + self.check_roundtrip_results(["same-value", None, None, None, None]) def test_06_userstyle_nonmatching_remainlocal(self): # Non-matching user attributes remain 'local' to the individual variables. - self.create_roundtrip_testcase( - attr_name="random", - global_value_file1="global_file1", - vars_values_file1={"v1": "value-1", "v2": "value-2"}, - ) - self.check_roundtrip_results( - global_attr_value=None, # NB it still destroys the global one !! - var_attr_vals={"v1": "value-1", "v2": "value-2"}, + self.run_roundtrip_testcase( + attr_name="random", values=["global_file1", "value-1", "value-2"] ) + self.check_roundtrip_results([None, "value-1", "value-2"]) ####################################################### # Tests on "Conventions" attribute. @@ -383,63 +451,50 @@ def test_06_userstyle_nonmatching_remainlocal(self): def test_07_conventions_var_local(self): # What happens if 'Conventions' appears as a variable-local attribute. # N.B. this is not good CF, but we'll see what happens anyway. - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name="Conventions", - global_value_file1=None, - vars_values_file1="user_set", - ) - self.check_roundtrip_results( - global_attr_value="CF-1.7", # standard content from Iris save - var_attr_vals=None, + values=[None, "user_set"], ) + self.check_roundtrip_results(["CF-1.7", None]) def test_08_conventions_var_both(self): # What happens if 'Conventions' appears as both global + local attribute. - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name="Conventions", - global_value_file1="global-setting", - vars_values_file1="local-setting", - ) - self.check_roundtrip_results( - global_attr_value="CF-1.7", # standard content from Iris save - var_attr_vals=None, + values=["global-setting", "local-setting"], ) + # standard content from Iris save + self.check_roundtrip_results(["CF-1.7", None]) ####################################################### # Tests on "global" style attributes # = those specific ones which 'ought' only to be global (except on collisions) # - def test_09_globalstyle__global(self, global_attr): attr_content = f"Global tracked {global_attr}" - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name=global_attr, - global_value_file1=attr_content, + values=[attr_content, None], ) - self.check_roundtrip_results(global_attr_value=attr_content) + self.check_roundtrip_results([attr_content, None]) def test_10_globalstyle__local(self, global_attr): # Strictly, not correct CF, but let's see what it does with it. attr_content = f"Local tracked {global_attr}" - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name=global_attr, - vars_values_file1=attr_content, + values=[None, attr_content], ) - self.check_roundtrip_results( - global_attr_value=attr_content - ) # "promoted" + self.check_roundtrip_results([attr_content, None]) def test_11_globalstyle__both(self, global_attr): attr_global = f"Global-{global_attr}" attr_local = f"Local-{global_attr}" - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name=global_attr, - global_value_file1=attr_global, - vars_values_file1=attr_local, - ) - self.check_roundtrip_results( - global_attr_value=attr_local # promoted local setting "wins" + values=[attr_global, attr_local], ) + self.check_roundtrip_results([attr_local, None]) def test_12_globalstyle__multivar_different(self, global_attr): # Multiple *different* local settings are retained, not promoted @@ -449,26 +504,20 @@ def test_12_globalstyle__multivar_different(self, global_attr): UserWarning, match="should only be a CF global attribute" ): # A warning should be raised when writing the result. - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name=global_attr, - vars_values_file1={"v1": attr_1, "v2": attr_2}, + values=[None, attr_1, attr_2], ) - self.check_roundtrip_results( - global_attr_value=None, - var_attr_vals={"v1": attr_1, "v2": attr_2}, - ) + self.check_roundtrip_results([None, attr_1, attr_2]) def test_13_globalstyle__multivar_same(self, global_attr): # Multiple *same* local settings are promoted to a common global one attrval = f"Locally-defined-{global_attr}" - self.create_roundtrip_testcase( + self.run_roundtrip_testcase( attr_name=global_attr, - vars_values_file1={"v1": attrval, "v2": attrval}, - ) - self.check_roundtrip_results( - global_attr_value=attrval, - var_attr_vals={"v1": None, "v2": None}, + values=[None, attrval, attrval], ) + self.check_roundtrip_results([attrval, None, None]) def test_14_globalstyle__multifile_different(self, global_attr): # Different global attributes from multiple files are retained as local ones @@ -478,34 +527,23 @@ def test_14_globalstyle__multifile_different(self, global_attr): UserWarning, match="should only be a CF global attribute" ): # A warning should be raised when writing the result. - self.create_roundtrip_testcase( - attr_name=global_attr, - global_value_file1=attr_1, - vars_values_file1={"v1": None}, - global_value_file2=attr_2, - vars_values_file2={"v2": None}, + self.run_roundtrip_testcase( + attr_name=global_attr, values=[[attr_1, None], [attr_2, None]] ) - self.check_roundtrip_results( - # Combining them "demotes" the common global attributes to local ones - var_attr_vals={"v1": attr_1, "v2": attr_2} - ) + self.check_roundtrip_results([None, attr_1, attr_2]) def test_15_globalstyle__multifile_same(self, global_attr): # Matching global-type attributes in multiple files are retained as global attrval = f"Global-{global_attr}" - self.create_roundtrip_testcase( - attr_name=global_attr, - global_value_file1=attrval, - vars_values_file1={"v1": None}, - global_value_file2=attrval, - vars_values_file2={"v2": None}, - ) - self.check_roundtrip_results( - # The attribute remains as a common global setting - global_attr_value=attrval, - # The individual variables do *not* have an attribute of this name - var_attr_vals={"v1": None, "v2": None}, + self.run_roundtrip_testcase( + attr_name=global_attr, values=[[attrval, None], [attrval, None]] ) + # # The attribute remains as a common global setting + # global_attr_value=attrval, + # # The individual variables do *not* have an attribute of this name + # var_attr_vals={"v1": None, "v2": None}, + # ) + self.check_roundtrip_results([attrval, None, None]) ####################################################### # Tests on "local" style attributes @@ -535,19 +573,16 @@ def test_16_localstyle(self, local_attr, origin_style): # as global or a variable attribute if origin_style == "input_global": # Record in source as a global attribute - self.create_roundtrip_testcase( - attr_name=local_attr, global_value_file1=attrval - ) + values = [attrval, None] else: assert origin_style == "input_local" # Record in source as a variable-local attribute - self.create_roundtrip_testcase( - attr_name=local_attr, vars_values_file1=attrval - ) + values = [None, attrval] + self.run_roundtrip_testcase(attr_name=local_attr, values=values) if ( - local_attr in ('missing_value', 'standard_error_multiplier') - and origin_style == "input_local" + local_attr in ("missing_value", "standard_error_multiplier") + and origin_style == "input_local" ): # These ones are actually discarded by roundtrip. # Not clear why, but for now this captures the facts. @@ -569,11 +604,7 @@ def test_16_localstyle(self, local_attr, origin_style): if local_attr == "STASH": # A special case, output translates this to a different attribute name. self.attrname = "um_stash_source" - - self.check_roundtrip_results( - global_attr_value=expect_global, - var_attr_vals=expect_var, - ) + self.check_roundtrip_results([expect_global, expect_var]) class TestLoad(MixinAttrsTesting): @@ -590,33 +621,20 @@ class TestLoad(MixinAttrsTesting): """ - def create_load_testcase( - self, - attr_name, - global_value_file1=None, - vars_values_file1=None, - global_value_file2=None, - vars_values_file2=None, - ) -> iris.cube.CubeList: - """ - Initialise the testcase from the passed-in controls, configure the input - files and run a save-load roundtrip to produce the output file. - - The name of the tested attribute and all the temporary filepaths are stored - on the instance, from where "self.check_load_results()" can get them. - - """ - self.attrname = attr_name - self.input_filepaths = self.create_testcase_files( - attr_name=attr_name, - global_value_file1=global_value_file1, - var_values_file1=vars_values_file1, - global_value_file2=global_value_file2, - var_values_file2=vars_values_file2, + def run_load_testcase(self, attr_name, values): + self.run_testcase( + attr_name=attr_name, values=values, create_cubes_or_files="files" ) + + def check_load_results(self, expected, oldstyle_combined=False): result_cubes = iris.load(self.input_filepaths) - result_cubes = sorted(result_cubes, key=lambda cube: cube.name()) - return result_cubes + results = self.fetch_results( + cubes=result_cubes, oldstyle_combined=oldstyle_combined + ) + assert isinstance(expected, list) + if not isinstance(expected[0], list): + expected = [expected] + assert results == expected ####################################################### # Tests on "user-style" attributes. @@ -625,83 +643,58 @@ def create_load_testcase( # def test_01_userstyle_single_global(self): - cube1, cube2 = self.create_load_testcase( - attr_name="myname", # A generic "user" attribute with no special handling - global_value_file1="single-value", - vars_values_file1={ - "myvar": None, - "myvar2": None, - }, # the variable has no such attribute + self.run_load_testcase( + attr_name="myname", values=["single_value", None, None] ) - # Default behaviour for a general global user-attribute. - # It is attached to all loaded cubes. - - 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) + # Legacy-equivalent result check (single attributes dict per cube) + self.check_load_results( + [None, "single_value", "single_value"], + oldstyle_combined=True, + ) + # Full new-style results check + self.check_load_results(["single_value", None, None]) def test_02_userstyle_single_local(self): # Default behaviour for a general local user-attribute. # It is attached to only the specific cube. - cube1, cube2 = self.create_load_testcase( + self.run_load_testcase( attr_name="myname", # A generic "user" attribute with no special handling - vars_values_file1={"myvar1": "single-value", "myvar2": None}, + values=[None, "single-value", None], ) - assert cube1.attributes == {"myname": "single-value"} - assert cube2.attributes == {} + self.check_load_results( + [None, "single-value", None], oldstyle_combined=True + ) + self.check_load_results([None, "single-value", None]) def test_03_userstyle_multiple_different(self): # Default behaviour for differing local user-attributes. # The global attribute is simply lost, because there are local ones. - vars1 = {"f1_v1": "f1v1", "f1_v2": "f1v2"} - vars2 = {"f2_v1": "x1", "f2_v2": "x2"} - cube1, cube2, cube3, cube4 = self.create_load_testcase( + self.run_load_testcase( attr_name="random", # A generic "user" attribute with no special handling - global_value_file1="global_file1", - vars_values_file1=vars1, - global_value_file2="global_file2", - vars_values_file2=vars2, - ) - - # (#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"} + values=[ + ["global_file1", "f1v1", "f1v2"], + ["global_file2", "x1", "x2"], + ], ) - assert cube2.attributes == CubeAttrsDict( - globals={"random": "global_file1"}, locals={"random": "f1v2"} + self.check_load_results( + [None, "f1v1", "f1v2", "x1", "x2"], + oldstyle_combined=True, ) - assert cube3.attributes == CubeAttrsDict( - globals={"random": "global_file2"}, locals={"random": "x1"} - ) - assert cube4.attributes == CubeAttrsDict( - globals={"random": "global_file2"}, locals={"random": "x2"} + self.check_load_results( + [["global_file1", "f1v1", "f1v2"], ["global_file2", "x1", "x2"]] ) def test_04_userstyle_multiple_same(self): # Nothing special to note in this case # TODO: ??remove?? - cube1, cube2 = self.create_load_testcase( + self.run_load_testcase( attr_name="random", - global_value_file1="global_file1", - vars_values_file1={"v1": "same-value", "v2": "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"}, - ) + values=["global_file1", "same-value", "same-value"], + ) + self.check_load_results( + oldstyle_combined=True, expected=[None, "same-value", "same-value"] + ) + self.check_load_results(["global_file1", "same-value", "same-value"]) ####################################################### # Tests on "Conventions" attribute. @@ -716,28 +709,27 @@ def test_04_userstyle_multiple_same(self): def test_07_conventions_var_local(self): # What happens if 'Conventions' appears as a variable-local attribute. # N.B. this is not good CF, but we'll see what happens anyway. - (cube,) = self.create_load_testcase( + self.run_load_testcase( attr_name="Conventions", - global_value_file1=None, - vars_values_file1="user_set", + values=[None, "user_set"], ) - assert cube.attributes == {"Conventions": "user_set"} + # Legacy result + self.check_load_results([None, "user_set"], oldstyle_combined=True) + # Newstyle result + self.check_load_results([None, "user_set"]) def test_08_conventions_var_both(self): # What happens if 'Conventions' appears as both global + local attribute. - # = the global version gets lost. - (cube,) = self.create_load_testcase( + self.run_load_testcase( attr_name="Conventions", - global_value_file1="global-setting", - vars_values_file1="local-setting", + values=["global-setting", "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"}, + # (#1): legacy result : the global version gets lost. + self.check_load_results( + [None, "local-setting"], oldstyle_combined=True ) + # (#2): newstyle results : retain both. + self.check_load_results(["global-setting", "local-setting"]) ####################################################### # Tests on "global" style attributes @@ -746,74 +738,67 @@ def test_08_conventions_var_both(self): def test_09_globalstyle__global(self, global_attr): attr_content = f"Global tracked {global_attr}" - (cube,) = self.create_load_testcase( - attr_name=global_attr, - global_value_file1=attr_content, + self.run_load_testcase( + attr_name=global_attr, values=[attr_content, None] ) - assert cube.attributes == {global_attr: attr_content} + # (#1) legacy + self.check_load_results([None, attr_content], oldstyle_combined=True) + # (#2) newstyle : global status preserved. + self.check_load_results([attr_content, None]) def test_10_globalstyle__local(self, global_attr): # Strictly, not correct CF, but let's see what it does with it. - # = treated the same as a global setting attr_content = f"Local tracked {global_attr}" - (cube,) = self.create_load_testcase( + self.run_load_testcase( attr_name=global_attr, - vars_values_file1=attr_content, + values=[None, 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} + # (#1): legacy result = treated the same as a global setting + self.check_load_results([None, attr_content], oldstyle_combined=True) + # (#2): newstyle result : remains local + self.check_load_results( + [None, attr_content], ) def test_11_globalstyle__both(self, global_attr): attr_global = f"Global-{global_attr}" attr_local = f"Local-{global_attr}" - (cube,) = self.create_load_testcase( + self.run_load_testcase( attr_name=global_attr, - global_value_file1=attr_global, - vars_values_file1=attr_local, - ) - # promoted local setting "wins" - # (#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}, + values=[attr_global, attr_local], ) + # (#1) legacy result : promoted local setting "wins" + self.check_load_results([None, attr_local], oldstyle_combined=True) + # (#2) newstyle result : both retained + self.check_load_results([attr_global, attr_local]) def test_12_globalstyle__multivar_different(self, global_attr): # Multiple *different* local settings are retained attr_1 = f"Local-{global_attr}-1" attr_2 = f"Local-{global_attr}-2" - cube1, cube2 = self.create_load_testcase( + self.run_load_testcase( attr_name=global_attr, - vars_values_file1={"v1": attr_1, "v2": attr_2}, + values=[None, attr_1, 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} + self.check_load_results([None, attr_1, attr_2], oldstyle_combined=True) # (#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}) + self.check_load_results([None, attr_1, attr_2]) def test_14_globalstyle__multifile_different(self, global_attr): - # Different global attributes from multiple files are retained as local ones + # Different global attributes from multiple files attr_1 = f"Global-{global_attr}-1" attr_2 = f"Global-{global_attr}-2" - cube1, cube2, cube3, cube4 = self.create_load_testcase( + self.run_load_testcase( attr_name=global_attr, - global_value_file1=attr_1, - vars_values_file1={"f1v1": None, "f1v2": None}, - global_value_file2=attr_2, - vars_values_file2={"f2v1": None, "f2v2": None}, + values=[[attr_1, None, None], [attr_2, None, None]], ) - assert cube1.attributes == {global_attr: attr_1} - assert cube2.attributes == {global_attr: attr_1} - assert cube3.attributes == {global_attr: attr_2} - assert cube4.attributes == {global_attr: attr_2} + # (#1) legacy : multiple globals retained as local ones + self.check_load_results( + [None, attr_1, attr_1, attr_2, attr_2], oldstyle_combined=True + ) + # (#1) newstyle : result same as input + self.check_load_results([[attr_1, None, None], [attr_2, None, None]]) ####################################################### # Tests on "local" style attributes @@ -839,38 +824,45 @@ def test_16_localstyle(self, local_attr, origin_style): # Create testfiles and load them, which should always produce a single cube. if origin_style == "input_global": # Record in source as a global attribute - (cube,) = self.create_load_testcase( - attr_name=local_attr, global_value_file1=attrval - ) + values = [attrval, None] + # (cube,) = self.create_load_testcase_OLDSTYLE( + # attr_name=local_attr, global_value_file1=attrval + # ) else: assert origin_style == "input_local" # Record in source as a variable-local attribute - (cube,) = self.create_load_testcase( - attr_name=local_attr, vars_values_file1=attrval - ) + values = [None, attrval] + # (cube,) = self.create_load_testcase_OLDSTYLE( + # attr_name=local_attr, vars_values_file1=attrval + # ) + + self.run_load_testcase(attr_name=local_attr, values=values) # Work out the expected result. - # NOTE: generally, result will be the same whether the original attribute is - # provided as a global or variable attribute ... - expected_result = {local_attr: attrval} - # ... but there are some special cases + result_value = attrval + # ... there are some special cases if origin_style == "input_local": if local_attr == "ukmo__process_flags": # Some odd special behaviour here. - expected_result = {local_attr: ("process",)} + result_value = (result_value,) elif local_attr in ("standard_error_multiplier", "missing_value"): # For some reason, these ones never appear on the cube - expected_result = {} + result_value = None + # NOTE: **legacy** result is the same, whether the original attribute was + # provided as a global or local attribute ... + expected_result_legacy = [None, result_value] + + # While 'newstyle' results preserve the input type local/global. if origin_style == "input_local": - expected_result_newstyle = CubeAttrsDict(expected_result) + expected_result_newstyle = [None, result_value] else: - expected_result_newstyle = CubeAttrsDict(globals=expected_result) + expected_result_newstyle = [result_value, None] # (#1): legacy values, for cube.attributes viewed as a single dict - assert dict(cube.attributes) == expected_result + self.check_load_results(expected_result_legacy, oldstyle_combined=True) # (#2): exact results, with newstyle "split" cube attrs - assert cube.attributes == expected_result_newstyle + self.check_load_results(expected_result_newstyle) class TestSave(MixinAttrsTesting): @@ -879,120 +871,108 @@ class TestSave(MixinAttrsTesting): """ - def create_save_testcase(self, attr_name, value1, value2=None): + def run_save_testcase(self, attr_name, values): + self.run_testcase( + attr_name=attr_name, values=values, create_cubes_or_files="cubes" + ) + self.result_filepath = self._testfile_path("result") + iris.save(self.input_cubes, self.result_filepath) + + def run_save_testcase_legacytype(self, attr_name: str, values: list): """ - Test attribute saving for cube(s) with given value(s). + Legacy-type means : before cubes had split attributes. - Create cubes(s) and save to temporary file, then return the global and all - variable-local attributes of that name (or None-s) from the file. + This just means we have only one "set" of cubes, with ***no*** distinct global + attribute. """ - self.attrname = ( - attr_name # Required for common testfile-naming function. - ) - if value2 is None: - n_cubes = 1 - values = [value1] - else: - n_cubes = 2 - values = [value1, value2] - cube_names = [f"cube_{i_cube}" for i_cube in range(n_cubes)] - cubes = [ - Cube([0], long_name=cube_name, attributes={attr_name: attr_value}) - for cube_name, attr_value in zip(cube_names, values) - ] - self.result_filepath = self._testfile_path("result") - iris.save(cubes, self.result_filepath) - # Get the global+local attribute values directly from the file with netCDF4 - if attr_name == "STASH": - # A special case : the stored name is different - attr_name = "um_stash_source" - try: - ds = threadsafe_nc4.DatasetWrapper(self.result_filepath) - global_result = ( - ds.getncattr(attr_name) if attr_name in ds.ncattrs() else None - ) - local_results = [ - ( - var.getncattr(attr_name) - if attr_name in var.ncattrs() - else None - ) - for var in ds.variables.values() - ] - finally: - ds.close() - return [global_result] + local_results + if not isinstance(values, list): + # Translate single input value to list-of-1 + values = [values] + self.run_save_testcase(attr_name, [None] + values) + + def check_save_results(self, expected: list): + results = self.fetch_results(filepath=self.result_filepath) + assert results == expected def test_01_userstyle__single(self): - results = self.create_save_testcase("random", "value-x") + self.run_save_testcase_legacytype("random", "value-x") # It is stored as a *global* by default. - assert results == ["value-x", None] + self.check_save_results(["value-x", None]) - def test_02_userstyle__multiple_same(self): - results = self.create_save_testcase("random", "value-x", "value-x") - # As above. - assert results == ["value-x", None, None] + def test_02_userstyle__multiple_same_NEWSTYLE(self): + self.run_save_testcase_legacytype("random", ["value-x", "value-x"]) + self.check_save_results(["value-x", None, None]) def test_03_userstyle__multiple_different(self): - results = self.create_save_testcase("random", "value-A", "value-B") # Clashing values are stored as locals on the individual variables. - assert results == [None, "value-A", "value-B"] + self.run_save_testcase_legacytype("random", ["value-A", "value-B"]) + self.check_save_results([None, "value-A", "value-B"]) def test_04_Conventions__single(self): - results = self.create_save_testcase("Conventions", "x") + self.run_save_testcase_legacytype("Conventions", "x") # Always discarded + replaced by a single global setting. - assert results == ["CF-1.7", None] + self.check_save_results(["CF-1.7", None]) def test_05_Conventions__multiple_same(self): - results = self.create_save_testcase( - "Conventions", "same-value", "same-value" + self.run_save_testcase_legacytype( + "Conventions", ["same-value", "same-value"] ) # Always discarded + replaced by a single global setting. - assert results == ["CF-1.7", None, None] + self.check_save_results(["CF-1.7", None, None]) def test_06_Conventions__multiple_different(self): - results = self.create_save_testcase( - "Conventions", "value-A", "value-B" + self.run_save_testcase_legacytype( + "Conventions", ["value-A", "value-B"] ) # Always discarded + replaced by a single global setting. - assert results == ["CF-1.7", None, None] + self.check_save_results(["CF-1.7", None, None]) def test_07_globalstyle__single(self, global_attr): - results = self.create_save_testcase(global_attr, "value") + self.run_save_testcase_legacytype(global_attr, ["value"]) # Defaults to global - assert results == ["value", None] + self.check_save_results(["value", None]) def test_08_globalstyle__multiple_same(self, global_attr): - results = self.create_save_testcase( - global_attr, "value-same", "value-same" + # Multiple user-type with same values are promoted to global. + self.run_save_testcase_legacytype( + global_attr, ["value-same", "value-same"] ) - assert results == ["value-same", None, None] + self.check_save_results(["value-same", None, None]) def test_09_globalstyle__multiple_different(self, global_attr): + # Multiple user-type with different values remain local. msg_regexp = ( f"'{global_attr}' is being added as CF data variable attribute," f".* should only be a CF global attribute." ) with pytest.warns(UserWarning, match=msg_regexp): - results = self.create_save_testcase( - global_attr, "value-A", "value-B" + self.run_save_testcase_legacytype( + global_attr, ["value-A", "value-B"] ) # *Only* stored as locals when there are differing values. - assert results == [None, "value-A", "value-B"] + # assert results == [None, "value-A", "value-B"] + self.check_save_results([None, "value-A", "value-B"]) def test_10_localstyle__single(self, local_attr): - results = self.create_save_testcase(local_attr, "value") + self.run_save_testcase_legacytype(local_attr, ["value"]) + # Defaults to local expected_results = [None, "value"] + # .. but a couple of special cases if local_attr == "ukmo__process_flags": # A particular, really weird case expected_results = [None, "v a l u e"] - assert results == expected_results + elif local_attr == "STASH": + # A special case : the stored name is different + self.attrname = "um_stash_source" + + self.check_save_results(expected_results) def test_11_localstyle__multiple_same(self, local_attr): - results = self.create_save_testcase( - local_attr, "value-same", "value-same" + self.run_save_testcase_legacytype( + local_attr, ["value-same", "value-same"] ) + # They remain separate + local expected_results = [None, "value-same", "value-same"] if local_attr == "ukmo__process_flags": @@ -1002,10 +982,14 @@ def test_11_localstyle__multiple_same(self, local_attr): "v a l u e - s a m e", "v a l u e - s a m e", ] - assert results == expected_results + elif local_attr == "STASH": + # A special case : the stored name is different + self.attrname = "um_stash_source" + + self.check_save_results(expected_results) def test_12_localstyle__multiple_different(self, local_attr): - results = self.create_save_testcase(local_attr, "value-A", "value-B") + self.run_save_testcase_legacytype(local_attr, ["value-A", "value-B"]) # Different values are treated just the same as matching ones. expected_results = [None, "value-A", "value-B"] if local_attr == "ukmo__process_flags": @@ -1015,4 +999,7 @@ def test_12_localstyle__multiple_different(self, local_attr): "v a l u e - A", "v a l u e - B", ] - assert results == expected_results + elif local_attr == "STASH": + # A special case : the stored name is different + self.attrname = "um_stash_source" + self.check_save_results(expected_results) From 4ecef965ca92f87b7fbf8ab3b2d0bc7f5c1f8147 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 21 Jul 2023 17:02:15 +0100 Subject: [PATCH 05/38] Saver supports split-attributes saving (no tests yet). --- lib/iris/fileformats/netcdf/saver.py | 196 +++++++++++++++++++++------ 1 file changed, 155 insertions(+), 41 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index aae235c563..7b43020796 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -540,6 +540,8 @@ def write( An interable of cube attribute keys. Any cube attributes with matching keys will become attributes on the data variable rather than global attributes. + .. note: + Has no effect if :attr:`iris.FUTURE.save_split_attrs` is ``True``. * unlimited_dimensions (iterable of strings and/or :class:`iris.coords.Coord` objects): @@ -709,20 +711,27 @@ def write( # aux factory in the cube. self._add_aux_factories(cube, cf_var_cube, cube_dimensions) - # Add data variable-only attribute names to local_keys. - if local_keys is None: - local_keys = set() - else: - local_keys = set(local_keys) - local_keys.update(_CF_DATA_ATTRS, _UKMO_DATA_ATTRS) - - # Add global attributes taking into account local_keys. - global_attributes = { - k: v - for k, v in cube.attributes.items() - if (k not in local_keys and k.lower() != "conventions") - } - self.update_global_attributes(global_attributes) + if not iris.FUTURE.save_split_attrs: + # In the "old" way, we update global attributes as we go. + # Add data variable-only attribute names to local_keys. + if local_keys is None: + local_keys = set() + else: + local_keys = set(local_keys) + local_keys.update(_CF_DATA_ATTRS, _UKMO_DATA_ATTRS) + + # Add global attributes taking into account local_keys. + cube_attributes = cube.attributes + if iris.FUTURE.save_split_attrs: + # In this case, do *not* promote any 'local' attributes to global ones, + # only "global" cube attrs may be written as global file attributes + cube_attributes = cube_attributes.globals + global_attributes = { + k: v + for k, v in cube_attributes.items() + if (k not in local_keys and k.lower() != "conventions") + } + self.update_global_attributes(global_attributes) if cf_profile_available: cf_patch = iris.site_configuration.get("cf_patch") @@ -778,6 +787,9 @@ def update_global_attributes(self, attributes=None, **kwargs): CF global attributes to be updated. """ + # TODO: when we no longer support combined attribute saving, this routine will + # only be called once: it can reasonably be renamed "_set_global_attributes", + # and the 'kwargs' argument can be removed. if attributes is not None: # Handle sequence e.g. [('fruit', 'apple'), ...]. if not hasattr(attributes, "keys"): @@ -2219,6 +2231,8 @@ def _create_cf_data_variable( The newly created CF-netCDF data variable. """ + # TODO: when iris.FUTURE.save_split_attrs is removed, the 'local_keys' arg can + # be removed. # Get the values in a form which is valid for the file format. data = self._ensure_valid_dtype(cube.core_data(), "cube", cube) @@ -2307,16 +2321,20 @@ def set_packing_ncattrs(cfvar): if cube.units.calendar: _setncattr(cf_var, "calendar", cube.units.calendar) - # Add data variable-only attribute names to local_keys. - if local_keys is None: - local_keys = set() + if iris.FUTURE.save_split_attrs: + attr_names = cube.attributes.locals.keys() else: - local_keys = set(local_keys) - local_keys.update(_CF_DATA_ATTRS, _UKMO_DATA_ATTRS) + # Add data variable-only attribute names to local_keys. + if local_keys is None: + local_keys = set() + else: + local_keys = set(local_keys) + local_keys.update(_CF_DATA_ATTRS, _UKMO_DATA_ATTRS) + + # Add any cube attributes whose keys are in local_keys as + # CF-netCDF data variable attributes. + attr_names = set(cube.attributes).intersection(local_keys) - # Add any cube attributes whose keys are in local_keys as - # CF-netCDF data variable attributes. - attr_names = set(cube.attributes).intersection(local_keys) for attr_name in sorted(attr_names): # Do not output 'conventions' attribute. if attr_name.lower() == "conventions": @@ -2781,26 +2799,117 @@ def save( else: cubes = cube - if local_keys is None: + if iris.FUTURE.save_split_attrs: + # We don't actually use 'local_keys' in this case. + # TODO: can remove this when the iris.FUTURE.save_split_attrs is removed. local_keys = set() + + # Find any collisions in the cube global attributes and "demote" all those to + # local attributes (where possible, else warn they are lost). + # N.B. "collision" includes when not all cubes *have* that attribute. + global_names = set() + for cube in cubes: + global_names |= set(cube.attributes.globals.keys()) + + # Fnd any global attributes which are not the same on *all* cubes. + def attr_values_equal(val1, val2): + # An equality test which also works when some values are numpy arrays (!) + # As done in :meth:`iris.common.mixin.LimitedAttributeDict.__eq__`. + match = val1 == val2 + try: + match = bool(match) + except ValueError: + match = match.all() + return match + + cube0 = cubes[0] + invalid_globals = [ + attrname + for attrname in global_names + if not all( + attr_values_equal( + cube.attributes[attrname], cube0.attributes[attrname] + ) + for cube in cubes[1:] + ) + ] + + # Establish all the global attributes which we will write to the file (at end). + global_attributes = { + attr: cube0.attributes.globals[attr] + for attr in global_names + if attr not in invalid_globals + } + if invalid_globals: + # Some cubes have different global attributes: modify cubes as required. + warnings.warn( + f"Saving the cube global attributes {invalid_globals} as local" + "(i.e. data-variable) attributes, where possible, since they are not '" + "the same on all input cubes." + ) + cubes = list(cubes) # avoiding modifying the actual input arg. + for i_cube in range(len(cubes)): + # We iterate over cube *index*, so we can replace the list entries with + # with cube *copies* -- just to avoid changing our call args. + cube = cubes[i_cube] + demote_attrs = [ + attr + for attr in cube.attributes.globals + if attr in invalid_globals + ] + if any(demote_attrs): + # This cube contains some 'demoted' global attributes. + # Replace the input cube with a copy, so we can modify attributes. + cube = cube.copy() + cubes[i_cube] = cube + # Catch any demoted attrs where there is already a local version + blocked_attrs = [ + attrname + for attrname in demote_attrs + if attrname in cube.attributes.locals + ] + if blocked_attrs: + warnings.warn( + f"Global cube attributes {blocked_attrs} " + f'of cube "{cube.name()}" have been lost, overlaid ' + "by existing local attributes with the same names." + ) + for attr in demote_attrs: + if attr not in blocked_attrs: + cube.attributes.locals[ + attr + ] = cube.attributes.globals[attr] + cube.attributes.globals.pop(attr) + else: - local_keys = set(local_keys) - - # Determine the attribute keys that are common across all cubes and - # thereby extend the collection of local_keys for attributes - # that should be attributes on data variables. - attributes = cubes[0].attributes - common_keys = set(attributes) - for cube in cubes[1:]: - keys = set(cube.attributes) - local_keys.update(keys.symmetric_difference(common_keys)) - common_keys.intersection_update(keys) - different_value_keys = [] - for key in common_keys: - if np.any(attributes[key] != cube.attributes[key]): - different_value_keys.append(key) - common_keys.difference_update(different_value_keys) - local_keys.update(different_value_keys) + # Determine the attribute keys that are common across all cubes and + # thereby extend the collection of local_keys for attributes + # that should be attributes on data variables. + # NOTE: in 'legacy' mode, this code derives a common value for 'local_keys', which + # is employed in saving each cube. + # However, in `split_attrs` mode, this considers ONLY global attributes, and the + # resulting 'common_keys' is the fixed result : each cube is then saved like ... + # "sman.write(... localkeys=list(cube.attributes) - common_keys, ...)" + if local_keys is None: + local_keys = set() + else: + local_keys = set(local_keys) + + common_attr_values = None + for cube in cubes: + cube_attributes = cube.attributes + keys = set(cube_attributes) + if common_attr_values is None: + common_attr_values = cube_attributes.copy() + common_keys = keys.copy() + local_keys.update(keys.symmetric_difference(common_keys)) + common_keys.intersection_update(keys) + different_value_keys = [] + for key in common_keys: + if np.any(common_attr_values[key] != cube_attributes[key]): + different_value_keys.append(key) + common_keys.difference_update(different_value_keys) + local_keys.update(different_value_keys) def is_valid_packspec(p): """Only checks that the datatype is valid.""" @@ -2902,7 +3011,12 @@ def is_valid_packspec(p): warnings.warn(msg) # Add conventions attribute. - sman.update_global_attributes(Conventions=conventions) + if iris.FUTURE.save_split_attrs: + # In the "new way", we just create all the global attributes at once. + global_attributes["Conventions"] = conventions + sman.update_global_attributes(global_attributes) + else: + sman.update_global_attributes(Conventions=conventions) if compute: # No more to do, since we used Saver(compute=True). From 78aaebbf94fdc65ed964a527d38055608830ffd5 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 3 Aug 2023 00:20:04 +0100 Subject: [PATCH 06/38] Tiny docs fix. --- lib/iris/fileformats/netcdf/saver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 7b43020796..1fcb92294b 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -540,7 +540,9 @@ def write( An interable of cube attribute keys. Any cube attributes with matching keys will become attributes on the data variable rather than global attributes. - .. note: + + .. Note:: + Has no effect if :attr:`iris.FUTURE.save_split_attrs` is ``True``. * unlimited_dimensions (iterable of strings and/or From 54f344c3f204ae7c352f7649edab4d8ef166cd32 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 3 Aug 2023 17:09:16 +0100 Subject: [PATCH 07/38] Explain test routines better. --- .../integration/test_netcdf__loadsaveattrs.py | 61 +++++++++++++++---- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index dcdff642f6..a45f2cabdb 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -19,7 +19,7 @@ """ import inspect -from typing import Optional, Union +from typing import Iterable, List, Optional, Union import numpy as np import pytest @@ -138,14 +138,15 @@ def create_testcase_files_or_cubes( var_values_file1: Union[None, str, dict] = None, global_value_file2: Optional[str] = None, var_values_file2: Union[None, str, dict] = None, - cubes=False, + cubes: bool = False, ): """ - Create temporary input netcdf files with specific content. + Create temporary input netcdf files, or cubes, with specific content. Creates a temporary netcdf test file (or two) with the given global and - variable-local attributes. - The file(s) are used to test the behaviour of the attribute. + variable-local attributes. Or build cubes, similarly. + If ``cubes`` is ``True``, save cubes in ``self.input_cubes``. + Else save filepaths in ``self.input_filepaths``. Note: 'var_values_file' args are dictionaries. The named variables are created, with an attribute = the dictionary value, *except* that a dictionary @@ -213,7 +214,32 @@ def make_cubes(var_name, global_value=None, var_values=None): self.input_filepaths = results return results - def run_testcase(self, attr_name, values, create_cubes_or_files="files"): + def run_testcase( + self, + attr_name: str, + values: Union[List, List[List]], + create_cubes_or_files: str = "files", + ) -> None: + """ + Create testcase inputs (files or cubes) with specified attributes. + + Parameters + ---------- + attr_name : str + name for all attributes created in this testcase. + Also saved as ``self.attrname``, as used by ``fetch_results``. + values : list + list, or lists, of values for created attributes, each containing one global + and one-or-more local attribute values as [global, local1, local2...] + create_cubes_or_files : str, default "files" + create either cubes or testfiles. + + If ``create_cubes_or_files`` == "files", create one temporary netCDF file per + values-list, and record in ``self.input_filepaths``. + Else if ``create_cubes_or_files`` == "cubes", create sets of cubes with common + global values and store all of them to ``self.input_cubes``. + + """ # Save common attribute-name on the instance self.attrname = attr_name @@ -255,19 +281,25 @@ def run_testcase(self, attr_name, values, create_cubes_or_files="files"): ) def fetch_results( - self, filepath=None, cubes=None, oldstyle_combined=False + self, + filepath: str = None, + cubes: Iterable[Cube] = None, + oldstyle_combined: bool = False, ): """ Return testcase results from an output file or cubes in a standardised form. - Unpick the global+local values of an attribute resulting from an operation. + Unpick the global+local values of the attribute ``self.attrname``, resulting + from a test operation. A file result is always [global_value, *local_values] A cubes result is [*[global_value, *local_values]] (over different global vals) - When "oldstyle_combined" simulate the "legacy" result, when each cube had a - single combined attribute dictionary. This enables us to check against former - behaviour (and behaviour of results treated as a single dictionary). - If results are from a *file*, this has no effect. + When ``oldstyle_combined`` is ``True``, simulate the "legacy" style results, + that is when each cube had a single combined attribute dictionary. + This enables us to check against former behaviour, by combining results into a + single dictionary. N.B. per-cube single results are then returned in the form: + [None, cube1, cube2...]. + N.B. if results are from a *file*, this key has **no effect**. """ attr_name = self.attrname @@ -365,7 +397,7 @@ def check_roundtrip_results(self, expected): """ Run checks on the generated output file. - The counterpart to create_roundtrip_testcase_OLDSTYLE, with similar control arguments. + The counterpart to :meth:`run_roundtrip_testcase`, with similar arguments. Check existence (or not) of a global attribute, and a number of local (variable) attributes. Values of 'None' mean to check that the relevant global/local attribute does @@ -631,6 +663,7 @@ def check_load_results(self, expected, oldstyle_combined=False): results = self.fetch_results( cubes=result_cubes, oldstyle_combined=oldstyle_combined ) + # Standardise expected form to list(lists). assert isinstance(expected, list) if not isinstance(expected[0], list): expected = [expected] @@ -872,9 +905,11 @@ class TestSave(MixinAttrsTesting): """ def run_save_testcase(self, attr_name, values): + # Create input cubes. self.run_testcase( attr_name=attr_name, values=values, create_cubes_or_files="cubes" ) + # Save input cubes to a temporary result file. self.result_filepath = self._testfile_path("result") iris.save(self.input_cubes, self.result_filepath) From edc989915749093fe031b913ce0e973d391ea23c Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 3 Aug 2023 17:32:24 +0100 Subject: [PATCH 08/38] Fix init of FUTURE object. --- lib/iris/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index e47ae3f98d..74889e5066 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -182,7 +182,7 @@ def __init__( # self.__dict__['example_future_flag'] = example_future_flag self.__dict__["datum_support"] = datum_support self.__dict__["pandas_ndim"] = pandas_ndim - self.__dict__["save_split_attrs"] = pandas_ndim + self.__dict__["save_split_attrs"] = save_split_attrs def __repr__(self): # msg = ('Future(example_future_flag={})') From d24250683f7b8a481fae5bc6198e553c2cda8a64 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 3 Aug 2023 17:35:32 +0100 Subject: [PATCH 09/38] Remove spurious re-test of FUTURE.save_split_attrs. --- lib/iris/fileformats/netcdf/saver.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 1fcb92294b..4d0d09be4c 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -724,10 +724,6 @@ def write( # Add global attributes taking into account local_keys. cube_attributes = cube.attributes - if iris.FUTURE.save_split_attrs: - # In this case, do *not* promote any 'local' attributes to global ones, - # only "global" cube attrs may be written as global file attributes - cube_attributes = cube_attributes.globals global_attributes = { k: v for k, v in cube_attributes.items() From 8d7ad2afcaaee17c12d12ccae5a39e5527295697 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 4 Aug 2023 12:18:11 +0100 Subject: [PATCH 10/38] Don't create Cube attrs of 'None' (n.b. but no effect as currently used). --- lib/iris/tests/integration/test_netcdf__loadsaveattrs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index a45f2cabdb..5f52658173 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -186,7 +186,8 @@ def make_cubes(var_name, global_value=None, var_values=None): cubes.append(cube) dimco = DimCoord(np.arange(3.0), var_name="x") cube.add_dim_coord(dimco, 0) - cube.attributes.globals[attr_name] = global_value + if global_value is not None: + cube.attributes.globals[attr_name] = global_value if local_value is not None: cube.attributes.locals[attr_name] = local_value return cubes From 8aa5311bb9a5562b81e6d5be8a3060a6d1a95f03 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 4 Aug 2023 12:22:36 +0100 Subject: [PATCH 11/38] Remove/repair refs to obsolete routines. --- .../tests/integration/test_netcdf__loadsaveattrs.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 5f52658173..5435a8a6d2 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -383,7 +383,7 @@ def run_roundtrip_testcase(self, attr_name, values): files and run a save-load roundtrip to produce the output file. The name of the attribute, and the input and output temporary filepaths are - stored on the instance, where "self.check_roundtrip_results_OLDSTYLE()" can get them. + stored on the instance, where "self.check_roundtrip_results()" can get them. """ self.run_testcase( @@ -859,16 +859,10 @@ def test_16_localstyle(self, local_attr, origin_style): if origin_style == "input_global": # Record in source as a global attribute values = [attrval, None] - # (cube,) = self.create_load_testcase_OLDSTYLE( - # attr_name=local_attr, global_value_file1=attrval - # ) else: assert origin_style == "input_local" # Record in source as a variable-local attribute values = [None, attrval] - # (cube,) = self.create_load_testcase_OLDSTYLE( - # attr_name=local_attr, vars_values_file1=attrval - # ) self.run_load_testcase(attr_name=local_attr, values=values) @@ -935,7 +929,7 @@ def test_01_userstyle__single(self): # It is stored as a *global* by default. self.check_save_results(["value-x", None]) - def test_02_userstyle__multiple_same_NEWSTYLE(self): + def test_02_userstyle__multiple_same(self): self.run_save_testcase_legacytype("random", ["value-x", "value-x"]) self.check_save_results(["value-x", None, None]) From f87550bd26270a14dc81ce463251fd01bdcade7e Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sat, 5 Aug 2023 00:03:26 +0100 Subject: [PATCH 12/38] Check all warnings from save operations. --- .../integration/test_netcdf__loadsaveattrs.py | 98 +++++++++++++------ 1 file changed, 68 insertions(+), 30 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 5435a8a6d2..0656e206e4 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -19,7 +19,9 @@ """ import inspect +import re from typing import Iterable, List, Optional, Union +import warnings import numpy as np import pytest @@ -73,6 +75,30 @@ def local_attr(request): return request.param # Return the name of the attribute to test. +def check_captured_warnings( + expected_keys: List[str], captured_warnings: List[warnings] +): + if expected_keys is None: + expected_keys = [] + elif hasattr(expected_keys, "upper"): + # Handle a single string + expected_keys = [expected_keys] + expected_keys = [re.compile(key) for key in expected_keys] + found_results = [str(warning.message) for warning in captured_warnings] + remaining_keys = expected_keys.copy() + for i_message, message in enumerate(found_results.copy()): + i_found = None + for i_key, key in enumerate(remaining_keys): + if key.search(message): + # Hit : remove one + only one matching warning from the list + i_found = i_message + break + if i_found is not None: + found_results[i_found] = key + remaining_keys.remove(key) + assert found_results == expected_keys + + class MixinAttrsTesting: @staticmethod def _calling_testname(): @@ -377,7 +403,7 @@ class TestRoundtrip(MixinAttrsTesting): """ - def run_roundtrip_testcase(self, attr_name, values): + def run_roundtrip_testcase(self, attr_name, values, expect_warnings=None): """ Initialise the testcase from the passed-in controls, configure the input files and run a save-load roundtrip to produce the output file. @@ -390,9 +416,13 @@ def run_roundtrip_testcase(self, attr_name, values): attr_name=attr_name, values=values, create_cubes_or_files="files" ) self.result_filepath = self._testfile_path("result") - # Do a load+save to produce a testable output result in a new file. - cubes = iris.load(self.input_filepaths) - iris.save(cubes, self.result_filepath) + + with warnings.catch_warnings(record=True) as captured_warnings: + # Do a load+save to produce a testable output result in a new file. + cubes = iris.load(self.input_filepaths) + iris.save(cubes, self.result_filepath) + + check_captured_warnings(expect_warnings, captured_warnings) def check_roundtrip_results(self, expected): """ @@ -533,14 +563,13 @@ def test_12_globalstyle__multivar_different(self, global_attr): # Multiple *different* local settings are retained, not promoted attr_1 = f"Local-{global_attr}-1" attr_2 = f"Local-{global_attr}-2" - with pytest.warns( - UserWarning, match="should only be a CF global attribute" - ): - # A warning should be raised when writing the result. - self.run_roundtrip_testcase( - attr_name=global_attr, - values=[None, attr_1, attr_2], - ) + expect_warning = "should only be a CF global attribute" + # A warning should be raised when writing the result. + self.run_roundtrip_testcase( + attr_name=global_attr, + values=[None, attr_1, attr_2], + expect_warnings=expect_warning, + ) self.check_roundtrip_results([None, attr_1, attr_2]) def test_13_globalstyle__multivar_same(self, global_attr): @@ -556,13 +585,13 @@ def test_14_globalstyle__multifile_different(self, global_attr): # Different global attributes from multiple files are retained as local ones attr_1 = f"Global-{global_attr}-1" attr_2 = f"Global-{global_attr}-2" - with pytest.warns( - UserWarning, match="should only be a CF global attribute" - ): - # A warning should be raised when writing the result. - self.run_roundtrip_testcase( - attr_name=global_attr, values=[[attr_1, None], [attr_2, None]] - ) + expect_warning = "should only be a CF global attribute" + # A warning should be raised when writing the result. + self.run_roundtrip_testcase( + attr_name=global_attr, + values=[[attr_1, None], [attr_2, None]], + expect_warnings=expect_warning, + ) self.check_roundtrip_results([None, attr_1, attr_2]) def test_15_globalstyle__multifile_same(self, global_attr): @@ -899,16 +928,26 @@ class TestSave(MixinAttrsTesting): """ - def run_save_testcase(self, attr_name, values): + def run_save_testcase( + self, attr_name: str, values: list, expect_warnings: List[str] = None + ): # Create input cubes. self.run_testcase( - attr_name=attr_name, values=values, create_cubes_or_files="cubes" + attr_name=attr_name, + values=values, + create_cubes_or_files="cubes", ) + # Save input cubes to a temporary result file. - self.result_filepath = self._testfile_path("result") - iris.save(self.input_cubes, self.result_filepath) + with warnings.catch_warnings(record=True) as captured_warnings: + self.result_filepath = self._testfile_path("result") + iris.save(self.input_cubes, self.result_filepath) - def run_save_testcase_legacytype(self, attr_name: str, values: list): + check_captured_warnings(expect_warnings, captured_warnings) + + def run_save_testcase_legacytype( + self, attr_name: str, values: list, expect_warnings: List[str] = None + ): """ Legacy-type means : before cubes had split attributes. @@ -918,7 +957,8 @@ def run_save_testcase_legacytype(self, attr_name: str, values: list): if not isinstance(values, list): # Translate single input value to list-of-1 values = [values] - self.run_save_testcase(attr_name, [None] + values) + + self.run_save_testcase(attr_name, [None] + values, expect_warnings) def check_save_results(self, expected: list): results = self.fetch_results(filepath=self.result_filepath) @@ -975,12 +1015,10 @@ def test_09_globalstyle__multiple_different(self, global_attr): f"'{global_attr}' is being added as CF data variable attribute," f".* should only be a CF global attribute." ) - with pytest.warns(UserWarning, match=msg_regexp): - self.run_save_testcase_legacytype( - global_attr, ["value-A", "value-B"] - ) + self.run_save_testcase_legacytype( + global_attr, ["value-A", "value-B"], expect_warnings=msg_regexp + ) # *Only* stored as locals when there are differing values. - # assert results == [None, "value-A", "value-B"] self.check_save_results([None, "value-A", "value-B"]) def test_10_localstyle__single(self, local_attr): From 30d62c2849b940d8ac2e6947eb5e6cb9184e0d88 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sat, 5 Aug 2023 00:08:16 +0100 Subject: [PATCH 13/38] Remove TestSave test numbers. --- .../integration/test_netcdf__loadsaveattrs.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 0656e206e4..5bfc23167c 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -964,52 +964,52 @@ def check_save_results(self, expected: list): results = self.fetch_results(filepath=self.result_filepath) assert results == expected - def test_01_userstyle__single(self): + def test_userstyle__single(self): self.run_save_testcase_legacytype("random", "value-x") # It is stored as a *global* by default. self.check_save_results(["value-x", None]) - def test_02_userstyle__multiple_same(self): + def test_userstyle__multiple_same(self): self.run_save_testcase_legacytype("random", ["value-x", "value-x"]) self.check_save_results(["value-x", None, None]) - def test_03_userstyle__multiple_different(self): + def test_userstyle__multiple_different(self): # Clashing values are stored as locals on the individual variables. self.run_save_testcase_legacytype("random", ["value-A", "value-B"]) self.check_save_results([None, "value-A", "value-B"]) - def test_04_Conventions__single(self): + def test_Conventions__single(self): self.run_save_testcase_legacytype("Conventions", "x") # Always discarded + replaced by a single global setting. self.check_save_results(["CF-1.7", None]) - def test_05_Conventions__multiple_same(self): + def test_Conventions__multiple_same(self): self.run_save_testcase_legacytype( "Conventions", ["same-value", "same-value"] ) # Always discarded + replaced by a single global setting. self.check_save_results(["CF-1.7", None, None]) - def test_06_Conventions__multiple_different(self): + def test_Conventions__multiple_different(self): self.run_save_testcase_legacytype( "Conventions", ["value-A", "value-B"] ) # Always discarded + replaced by a single global setting. self.check_save_results(["CF-1.7", None, None]) - def test_07_globalstyle__single(self, global_attr): + def test_globalstyle__single(self, global_attr): self.run_save_testcase_legacytype(global_attr, ["value"]) # Defaults to global self.check_save_results(["value", None]) - def test_08_globalstyle__multiple_same(self, global_attr): + def test_globalstyle__multiple_same(self, global_attr): # Multiple user-type with same values are promoted to global. self.run_save_testcase_legacytype( global_attr, ["value-same", "value-same"] ) self.check_save_results(["value-same", None, None]) - def test_09_globalstyle__multiple_different(self, global_attr): + def test_globalstyle__multiple_different(self, global_attr): # Multiple user-type with different values remain local. msg_regexp = ( f"'{global_attr}' is being added as CF data variable attribute," @@ -1021,7 +1021,7 @@ def test_09_globalstyle__multiple_different(self, global_attr): # *Only* stored as locals when there are differing values. self.check_save_results([None, "value-A", "value-B"]) - def test_10_localstyle__single(self, local_attr): + def test_localstyle__single(self, local_attr): self.run_save_testcase_legacytype(local_attr, ["value"]) # Defaults to local @@ -1036,7 +1036,7 @@ def test_10_localstyle__single(self, local_attr): self.check_save_results(expected_results) - def test_11_localstyle__multiple_same(self, local_attr): + def test_localstyle__multiple_same(self, local_attr): self.run_save_testcase_legacytype( local_attr, ["value-same", "value-same"] ) @@ -1056,7 +1056,7 @@ def test_11_localstyle__multiple_same(self, local_attr): self.check_save_results(expected_results) - def test_12_localstyle__multiple_different(self, local_attr): + def test_localstyle__multiple_different(self, local_attr): self.run_save_testcase_legacytype(local_attr, ["value-A", "value-B"]) # Different values are treated just the same as matching ones. expected_results = [None, "value-A", "value-B"] From fb1377035fed0ee66cffb226297af49a4cb5a842 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sat, 5 Aug 2023 00:50:46 +0100 Subject: [PATCH 14/38] More save cases: no match with missing, and different cube attribute types. --- .../integration/test_netcdf__loadsaveattrs.py | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 5bfc23167c..c572cf8d7f 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -978,6 +978,16 @@ def test_userstyle__multiple_different(self): self.run_save_testcase_legacytype("random", ["value-A", "value-B"]) self.check_save_results([None, "value-A", "value-B"]) + def test_userstyle__multiple_onemissing(self, global_attr): + # Multiple user-type, with one missing, behave like different values. + self.run_save_testcase_legacytype( + global_attr, + ["value", None], + expect_warnings="should only be a CF global attribute", + ) + # Stored as locals when there are differing values. + self.check_save_results([None, "value", None]) + def test_Conventions__single(self): self.run_save_testcase_legacytype("Conventions", "x") # Always discarded + replaced by a single global setting. @@ -1003,14 +1013,14 @@ def test_globalstyle__single(self, global_attr): self.check_save_results(["value", None]) def test_globalstyle__multiple_same(self, global_attr): - # Multiple user-type with same values are promoted to global. + # Multiple global-type with same values are made global. self.run_save_testcase_legacytype( global_attr, ["value-same", "value-same"] ) self.check_save_results(["value-same", None, None]) def test_globalstyle__multiple_different(self, global_attr): - # Multiple user-type with different values remain local. + # Multiple global-type with different values become local, with warning. msg_regexp = ( f"'{global_attr}' is being added as CF data variable attribute," f".* should only be a CF global attribute." @@ -1021,6 +1031,18 @@ def test_globalstyle__multiple_different(self, global_attr): # *Only* stored as locals when there are differing values. self.check_save_results([None, "value-A", "value-B"]) + def test_globalstyle__multiple_onemissing(self, global_attr): + # Multiple global-type, with one missing, behave like different values. + msg_regexp = ( + f"'{global_attr}' is being added as CF data variable attribute," + f".* should only be a CF global attribute." + ) + self.run_save_testcase_legacytype( + global_attr, ["value", "value", None], expect_warnings=msg_regexp + ) + # Stored as locals when there are differing values. + self.check_save_results([None, "value", "value", None]) + def test_localstyle__single(self, local_attr): self.run_save_testcase_legacytype(local_attr, ["value"]) @@ -1071,3 +1093,52 @@ def test_localstyle__multiple_different(self, local_attr): # A special case : the stored name is different self.attrname = "um_stash_source" self.check_save_results(expected_results) + + # + # Test handling of newstyle independent global+local cube attributes. + # + def test_globallocal_clashing(self): + # A cube has clashing local + global attrs. + self.run_save_testcase( + "userattr", ["valueA", "valueB"], expect_warnings=[] + ) + # N.B. legacy code sees only the local value, and promotes it. + self.check_save_results(["valueB", None]) + + def test_globallocal_oneeach_same(self): + # One cube with global attr, another with identical local one. + self.run_save_testcase( + "userattr", [[None, "value"], ["value", None]], expect_warnings=[] + ) + # N.B. legacy code sees only two equal values (and promotes). + self.check_save_results(["value", None, None]) + + def test_globallocal_oneeach_different(self): + # One cube with global attr, another with different local one. + self.run_save_testcase( + "userattr", + [[None, "valueA"], ["valueB", None]], + expect_warnings=[], + ) + # N.B. legacy code does not warn of global-to-local "demotion". + self.check_save_results([None, "valueA", "valueB"]) + + def test_globallocal_one_other_clashingglobals(self): + # Two cubes with both, second cube has a clashing global attribute. + self.run_save_testcase( + "userattr", + [["valueA", "valueB"], ["valueXXX", "valueB"]], + expect_warnings=[], + ) + # N.B. legacy code sees only the locals, and promotes them. + self.check_save_results(["valueB", None, None]) + + def test_globallocal_one_other_clashinglocals(self): + # Two cubes with both, second cube has a clashing local attribute. + self.run_save_testcase( + "userattr", + [["valueA", "valueB"], ["valueA", "valueXXX"]], + expect_warnings=[], + ) + # N.B. legacy code sees only the locals. + self.check_save_results([None, "valueB", "valueXXX"]) From 5a429d0c7fcfe9928dc7c1c8a0428db38a2466bf Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sat, 5 Aug 2023 02:23:03 +0100 Subject: [PATCH 15/38] Run save/roundtrip tests both with+without split saves. --- lib/iris/fileformats/netcdf/saver.py | 9 +- .../integration/test_netcdf__loadsaveattrs.py | 324 +++++++++++++----- 2 files changed, 250 insertions(+), 83 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 4d0d09be4c..171f32f073 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2826,7 +2826,8 @@ def attr_values_equal(val1, val2): for attrname in global_names if not all( attr_values_equal( - cube.attributes[attrname], cube0.attributes[attrname] + cube.attributes.globals.get(attrname), + cube0.attributes.globals.get(attrname), ) for cube in cubes[1:] ) @@ -2834,15 +2835,15 @@ def attr_values_equal(val1, val2): # Establish all the global attributes which we will write to the file (at end). global_attributes = { - attr: cube0.attributes.globals[attr] + attr: cube0.attributes.globals.get(attr) for attr in global_names if attr not in invalid_globals } if invalid_globals: # Some cubes have different global attributes: modify cubes as required. warnings.warn( - f"Saving the cube global attributes {invalid_globals} as local" - "(i.e. data-variable) attributes, where possible, since they are not '" + f"Saving the cube global attributes {invalid_globals} as local " + "(i.e. data-variable) attributes, where possible, since they are not " "the same on all input cubes." ) cubes = list(cubes) # avoiding modifying the actual input arg. diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index c572cf8d7f..12324cc232 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -403,6 +403,15 @@ class TestRoundtrip(MixinAttrsTesting): """ + # Parametrise all tests over split/unsplit saving. + @pytest.fixture( + params=[False, True], ids=["nosplit", "split"], autouse=True + ) + def do_split(self, request): + do_split = request.param + self.save_split_attrs = do_split + return do_split + def run_roundtrip_testcase(self, attr_name, values, expect_warnings=None): """ Initialise the testcase from the passed-in controls, configure the input @@ -420,7 +429,11 @@ def run_roundtrip_testcase(self, attr_name, values, expect_warnings=None): with warnings.catch_warnings(record=True) as captured_warnings: # Do a load+save to produce a testable output result in a new file. cubes = iris.load(self.input_filepaths) - iris.save(cubes, self.result_filepath) + # Ensure stable result order. + cubes = sorted(cubes, key=lambda cube: cube.name()) + do_split = getattr(self, "save_split_attrs", False) + with iris.FUTURE.context(save_split_attrs=do_split): + iris.save(cubes, self.result_filepath) check_captured_warnings(expect_warnings, captured_warnings) @@ -453,16 +466,20 @@ def test_01_userstyle_single_global(self): # It simply remains global. self.check_roundtrip_results(["single-value", None]) - def test_02_userstyle_single_local(self): + def test_02_userstyle_single_local(self, do_split): # Default behaviour for a general local user-attribute. # It results in a "promoted" global attribute. self.run_roundtrip_testcase( attr_name="myname", # A generic "user" attribute with no special handling values=[None, "single-value"], ) - self.check_roundtrip_results(["single-value", None]) + if do_split: + expected = [None, "single-value"] + else: + expected = ["single-value", None] + self.check_roundtrip_results(expected) - def test_03_userstyle_multiple_different(self): + def test_03_userstyle_multiple_different(self, do_split): # Default behaviour for general user-attributes. # The global attribute is lost because there are local ones. self.run_roundtrip_testcase( @@ -472,34 +489,72 @@ def test_03_userstyle_multiple_different(self): ["common_global", "x1", "x2"], ], ) + expected_result = ["common_global", "f1v1", "f1v2", "x1", "x2"] + if not do_split: + # in legacy mode, global is lost + expected_result[0] = None # just check they are all there and distinct - self.check_roundtrip_results([None, "f1v1", "f1v2", "x1", "x2"]) + self.check_roundtrip_results(expected_result) - def test_04_userstyle_matching_promoted(self): + def test_04_userstyle_matching_promoted(self, do_split): # matching local user-attributes are "promoted" to a global one. + # (but not when saving split attributes) + input_values = ["global_file1", "same-value", "same-value"] self.run_roundtrip_testcase( attr_name="random", - values=["global_file1", "same-value", "same-value"], + values=input_values, ) - self.check_roundtrip_results(["same-value", None, None]) + if do_split: + expected = input_values + else: + expected = ["same-value", None, None] + self.check_roundtrip_results(expected) - def test_05_userstyle_matching_crossfile_promoted(self): + def test_05_userstyle_matching_crossfile_promoted(self, do_split): # matching user-attributes are promoted, even across input files. + # (but not when saving split attributes) + input_values = [ + ["global_file1", "same-value", "same-value"], + [None, "same-value", "same-value"], + ] + if do_split: + # newstyle saves: locals are preserved, mismathced global is *lost* + expected_result = [ + None, + "same-value", + "same-value", + "same-value", + "same-value", + ] + # warnings about the clash + expected_warnings = [ + "Saving.* global attributes.* as local", + 'attributes.* of cube "var_0" have been lost', + 'attributes.* of cube "var_1" have been lost', + ] + else: + # oldstyle saves: matching locals promoted, override original global + expected_result = ["same-value", None, None, None, None] + expected_warnings = None + self.run_roundtrip_testcase( attr_name="random", - values=[ - ["global_file1", "same-value", "same-value"], - [None, "same-value", "same-value"], - ], + values=input_values, + expect_warnings=expected_warnings, ) - self.check_roundtrip_results(["same-value", None, None, None, None]) + self.check_roundtrip_results(expected_result) - def test_06_userstyle_nonmatching_remainlocal(self): + def test_06_userstyle_nonmatching_remainlocal(self, do_split): # Non-matching user attributes remain 'local' to the individual variables. - self.run_roundtrip_testcase( - attr_name="random", values=["global_file1", "value-1", "value-2"] - ) - self.check_roundtrip_results([None, "value-1", "value-2"]) + input_values = ["global_file1", "value-1", "value-2"] + if do_split: + # originals are preserved + expected_result = input_values + else: + # global is lost + expected_result = [None, "value-1", "value-2"] + self.run_roundtrip_testcase(attr_name="random", values=input_values) + self.check_roundtrip_results(expected_result) ####################################################### # Tests on "Conventions" attribute. @@ -541,23 +596,43 @@ def test_09_globalstyle__global(self, global_attr): ) self.check_roundtrip_results([attr_content, None]) - def test_10_globalstyle__local(self, global_attr): + def test_10_globalstyle__local(self, global_attr, do_split): # Strictly, not correct CF, but let's see what it does with it. attr_content = f"Local tracked {global_attr}" + input_values = [None, attr_content] + if do_split: + # remains local as supplied, but there is a warning + expected_result = input_values + expected_warning = f"'{global_attr}'.* should only be a CF global" + else: + # promoted to global + expected_result = [attr_content, None] + expected_warning = None self.run_roundtrip_testcase( attr_name=global_attr, - values=[None, attr_content], + values=input_values, + expect_warnings=expected_warning, ) - self.check_roundtrip_results([attr_content, None]) + self.check_roundtrip_results(expected_result) - def test_11_globalstyle__both(self, global_attr): + def test_11_globalstyle__both(self, global_attr, do_split): attr_global = f"Global-{global_attr}" attr_local = f"Local-{global_attr}" + input_values = [attr_global, attr_local] + if do_split: + # remains local as supplied, but there is a warning + expected_result = input_values + expected_warning = "should only be a CF global" + else: + # promoted to global, no local value, original global lost + expected_result = [attr_local, None] + expected_warning = None self.run_roundtrip_testcase( attr_name=global_attr, - values=[attr_global, attr_local], + values=input_values, + expect_warnings=expected_warning, ) - self.check_roundtrip_results([attr_local, None]) + self.check_roundtrip_results(expected_result) def test_12_globalstyle__multivar_different(self, global_attr): # Multiple *different* local settings are retained, not promoted @@ -572,25 +647,38 @@ def test_12_globalstyle__multivar_different(self, global_attr): ) self.check_roundtrip_results([None, attr_1, attr_2]) - def test_13_globalstyle__multivar_same(self, global_attr): + def test_13_globalstyle__multivar_same(self, global_attr, do_split): # Multiple *same* local settings are promoted to a common global one attrval = f"Locally-defined-{global_attr}" + input_values = [None, attrval, attrval] + if do_split: + # remains local, but with a warning + expected_warning = "should only be a CF global" + expected_result = input_values + else: + # promoted to global + expected_warning = None + expected_result = [attrval, None, None] self.run_roundtrip_testcase( attr_name=global_attr, - values=[None, attrval, attrval], + values=input_values, + expect_warnings=expected_warning, ) - self.check_roundtrip_results([attrval, None, None]) + self.check_roundtrip_results(expected_result) - def test_14_globalstyle__multifile_different(self, global_attr): + def test_14_globalstyle__multifile_different(self, global_attr, do_split): # Different global attributes from multiple files are retained as local ones attr_1 = f"Global-{global_attr}-1" attr_2 = f"Global-{global_attr}-2" - expect_warning = "should only be a CF global attribute" # A warning should be raised when writing the result. + expect_warnings = ["should only be a CF global attribute"] + if do_split: + # An extra warning, only when saving with split-attributes. + expect_warnings = ["Saving.* as local"] + expect_warnings self.run_roundtrip_testcase( attr_name=global_attr, values=[[attr_1, None], [attr_2, None]], - expect_warnings=expect_warning, + expect_warnings=expect_warnings, ) self.check_roundtrip_results([None, attr_1, attr_2]) @@ -614,7 +702,7 @@ def test_15_globalstyle__multifile_same(self, global_attr): # @pytest.mark.parametrize("origin_style", ["input_global", "input_local"]) - def test_16_localstyle(self, local_attr, origin_style): + def test_16_localstyle(self, local_attr, origin_style, do_split): # local-style attributes should *not* get 'promoted' to global ones # Set the name extension to avoid tests with different 'style' params having # collisions over identical testfile names @@ -655,6 +743,7 @@ def test_16_localstyle(self, local_attr, origin_style): if ( local_attr == "ukmo__process_flags" and origin_style == "input_global" + and not do_split ): # This is very odd behaviour + surely unintended. # It's supposed to handle vector values (which we are not checking). @@ -663,10 +752,17 @@ def test_16_localstyle(self, local_attr, origin_style): attrval = "p r o c e s s" expect_var = attrval - if local_attr == "STASH": + if local_attr == "STASH" and ( + origin_style == "input_local" or not do_split + ): # A special case, output translates this to a different attribute name. self.attrname = "um_stash_source" - self.check_roundtrip_results([expect_global, expect_var]) + + expected_result = [expect_global, expect_var] + if do_split and origin_style == "input_global": + # The result is simply the "other way around" + expected_result = expected_result[::-1] + self.check_roundtrip_results(expected_result) class TestLoad(MixinAttrsTesting): @@ -928,6 +1024,15 @@ class TestSave(MixinAttrsTesting): """ + # Parametrise all tests over split/unsplit saving. + @pytest.fixture( + params=[False, True], ids=["nosplit", "split"], autouse=True + ) + def do_split(self, request): + do_split = request.param + self.save_split_attrs = do_split + return do_split + def run_save_testcase( self, attr_name: str, values: list, expect_warnings: List[str] = None ): @@ -941,7 +1046,9 @@ def run_save_testcase( # Save input cubes to a temporary result file. with warnings.catch_warnings(record=True) as captured_warnings: self.result_filepath = self._testfile_path("result") - iris.save(self.input_cubes, self.result_filepath) + do_split = getattr(self, "save_split_attrs", False) + with iris.FUTURE.context(save_split_attrs=do_split): + iris.save(self.input_cubes, self.result_filepath) check_captured_warnings(expect_warnings, captured_warnings) @@ -964,14 +1071,25 @@ def check_save_results(self, expected: list): results = self.fetch_results(filepath=self.result_filepath) assert results == expected - def test_userstyle__single(self): + def test_userstyle__single(self, do_split): self.run_save_testcase_legacytype("random", "value-x") - # It is stored as a *global* by default. - self.check_save_results(["value-x", None]) + if do_split: + # result as input values + expected_result = [None, "value-x"] + else: + # in legacy mode, promoted = stored as a *global* by default. + expected_result = ["value-x", None] + self.check_save_results(expected_result) - def test_userstyle__multiple_same(self): + def test_userstyle__multiple_same(self, do_split): self.run_save_testcase_legacytype("random", ["value-x", "value-x"]) - self.check_save_results(["value-x", None, None]) + if do_split: + # result as input values + expected_result = [None, "value-x", "value-x"] + else: + # in legacy mode, promoted = stored as a *global* by default. + expected_result = ["value-x", None, None] + self.check_save_results(expected_result) def test_userstyle__multiple_different(self): # Clashing values are stored as locals on the individual variables. @@ -1007,17 +1125,37 @@ def test_Conventions__multiple_different(self): # Always discarded + replaced by a single global setting. self.check_save_results(["CF-1.7", None, None]) - def test_globalstyle__single(self, global_attr): - self.run_save_testcase_legacytype(global_attr, ["value"]) - # Defaults to global - self.check_save_results(["value", None]) + def test_globalstyle__single(self, global_attr, do_split): + if do_split: + # result as input values + expected_warning = "should only be a CF global" + expected_result = [None, "value"] + else: + # in legacy mode, promoted + expected_warning = None + expected_result = ["value", None] - def test_globalstyle__multiple_same(self, global_attr): + self.run_save_testcase_legacytype( + global_attr, ["value"], expect_warnings=expected_warning + ) + self.check_save_results(expected_result) + + def test_globalstyle__multiple_same(self, global_attr, do_split): # Multiple global-type with same values are made global. + if do_split: + # result as input values + expected_warning = "should only be a CF global attribute" + expected_result = [None, "value-same", "value-same"] + else: + # in legacy mode, promoted + expected_warning = None + expected_result = ["value-same", None, None] self.run_save_testcase_legacytype( - global_attr, ["value-same", "value-same"] + global_attr, + ["value-same", "value-same"], + expect_warnings=expected_warning, ) - self.check_save_results(["value-same", None, None]) + self.check_save_results(expected_result) def test_globalstyle__multiple_different(self, global_attr): # Multiple global-type with different values become local, with warning. @@ -1097,48 +1235,76 @@ def test_localstyle__multiple_different(self, local_attr): # # Test handling of newstyle independent global+local cube attributes. # - def test_globallocal_clashing(self): + def test_globallocal_clashing(self, do_split): # A cube has clashing local + global attrs. - self.run_save_testcase( - "userattr", ["valueA", "valueB"], expect_warnings=[] - ) - # N.B. legacy code sees only the local value, and promotes it. - self.check_save_results(["valueB", None]) - - def test_globallocal_oneeach_same(self): + original_values = ["valueA", "valueB"] + self.run_save_testcase("userattr", original_values, expect_warnings=[]) + expected_result = original_values.copy() + if not do_split: + # in legacy mode, "promote" = lose the local one + expected_result[0] = expected_result[1] + expected_result[1] = None + self.check_save_results(expected_result) + + def test_globallocal_oneeach_same(self, do_split): # One cube with global attr, another with identical local one. - self.run_save_testcase( - "userattr", [[None, "value"], ["value", None]], expect_warnings=[] - ) - # N.B. legacy code sees only two equal values (and promotes). - self.check_save_results(["value", None, None]) + inputs = [[None, "value"], ["value", None]] + if do_split: + expected = [None, "value", "value"] + warning = ( + "Saving the cube global attributes \\['userattr'\\] as local" + ) + else: + # N.B. legacy code sees only two equal values (and promotes). + expected = ["value", None, None] + warning = None - def test_globallocal_oneeach_different(self): - # One cube with global attr, another with different local one. self.run_save_testcase( - "userattr", - [[None, "valueA"], ["valueB", None]], - expect_warnings=[], + "userattr", values=inputs, expect_warnings=warning ) - # N.B. legacy code does not warn of global-to-local "demotion". + self.check_save_results(expected) + + def test_globallocal_oneeach_different(self, do_split): + # One cube with global attr, another with a *different* local one. + inputs = [[None, "valueA"], ["valueB", None]] + if do_split: + warning = ( + "Saving the cube global attributes \\['userattr'\\] as local" + ) + else: + # N.B. legacy code does not warn of global-to-local "demotion". + warning = None + self.run_save_testcase("userattr", inputs, expect_warnings=warning) self.check_save_results([None, "valueA", "valueB"]) - def test_globallocal_one_other_clashingglobals(self): + def test_globallocal_one_other_clashingglobals(self, do_split): # Two cubes with both, second cube has a clashing global attribute. + inputs = [["valueA", "valueB"], ["valueXXX", "valueB"]] + if do_split: + expected = [None, "valueB", "valueB"] + expected_warnings = [ + "Saving.* global attributes.* as local", + 'attributes.* of cube "v1" have been lost', + 'attributes.* of cube "v2" have been lost', + ] + else: + # N.B. legacy code sees only the locals, and promotes them. + expected = ["valueB", None, None] + expected_warnings = None self.run_save_testcase( "userattr", - [["valueA", "valueB"], ["valueXXX", "valueB"]], - expect_warnings=[], + values=inputs, + expect_warnings=expected_warnings, ) - # N.B. legacy code sees only the locals, and promotes them. - self.check_save_results(["valueB", None, None]) + self.check_save_results(expected) - def test_globallocal_one_other_clashinglocals(self): + def test_globallocal_one_other_clashinglocals(self, do_split): # Two cubes with both, second cube has a clashing local attribute. - self.run_save_testcase( - "userattr", - [["valueA", "valueB"], ["valueA", "valueXXX"]], - expect_warnings=[], - ) - # N.B. legacy code sees only the locals. - self.check_save_results([None, "valueB", "valueXXX"]) + inputs = [["valueA", "valueB"], ["valueA", "valueXXX"]] + if do_split: + expected = ["valueA", "valueB", "valueXXX"] + else: + # N.B. legacy code sees only the locals. + expected = [None, "valueB", "valueXXX"] + self.run_save_testcase("userattr", values=inputs) + self.check_save_results(expected) From dd53275e55bc7bea3f6ff858a0e4a5e7f639f92e Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 7 Aug 2023 00:02:12 +0100 Subject: [PATCH 16/38] Fix. --- lib/iris/tests/integration/test_netcdf__loadsaveattrs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 12324cc232..087ad72444 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -76,7 +76,7 @@ def local_attr(request): def check_captured_warnings( - expected_keys: List[str], captured_warnings: List[warnings] + expected_keys: List[str], captured_warnings: List[warnings.WarningMessage] ): if expected_keys is None: expected_keys = [] From 80a4039647e469c2d055be215910202820cf6476 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 7 Aug 2023 10:23:55 +0100 Subject: [PATCH 17/38] Review changes. --- lib/iris/fileformats/netcdf/saver.py | 51 ++++++++++++++----- .../integration/test_netcdf__loadsaveattrs.py | 8 +-- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 171f32f073..213e2e0368 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -637,6 +637,9 @@ def write( 3 files that do not use HDF5. """ + # TODO: when iris.FUTURE.save_split_attrs defaults to True, we can deprecate the + # "local_keys" arg, and finally remove it when we finally remove the + # save_split_attrs switch. if unlimited_dimensions is None: unlimited_dimensions = [] @@ -785,7 +788,7 @@ def update_global_attributes(self, attributes=None, **kwargs): CF global attributes to be updated. """ - # TODO: when we no longer support combined attribute saving, this routine will + # TODO: when when iris.FUTURE.save_split_attrs is removed, this routine will # only be called once: it can reasonably be renamed "_set_global_attributes", # and the 'kwargs' argument can be removed. if attributes is not None: @@ -2205,6 +2208,8 @@ def _create_cf_data_variable( """ Create CF-netCDF data variable for the cube and any associated grid mapping. + # TODO: when iris.FUTURE.save_split_attrs is removed, the 'local_keys' arg can + # be removed. Args: @@ -2616,12 +2621,12 @@ def save( Save cube(s) to a netCDF file, given the cube and the filename. * Iris will write CF 1.7 compliant NetCDF files. - * If **split-attribute saving is disabled**, i.e. - :attr:`iris.FUTURE.save_split_attrs` is ``False``, then attributes dictionaries - on each cube in the saved cube list will be compared and common attributes saved - as NetCDF global attributes where appropriate. + * **If split-attribute saving is disabled**, i.e. + :data:`iris.FUTURE`\\ ``.save_split_attrs`` is ``False``, then attributes + dictionaries on each cube in the saved cube list will be compared, and common + attributes saved as NetCDF global attributes where appropriate. - Or, **when **split-attribute saving is enabled**, then `cube.attributes.locals`` + Or, **when split-attribute saving is enabled**, then `cube.attributes.locals`` are always saved as attributes of data-variables, and ``cube.attributes.globals`` are saved as global (dataset) attributes, where possible. Since the 2 types are now distinguished : see :class:`~iris.cube.CubeAttrsDict`. @@ -2797,6 +2802,15 @@ def save( else: cubes = cube + # Decide which cube attributes will be saved as "global" attributes + # NOTE: in 'legacy' mode, when iris.FUTURE.save_split_attrs == False, this code + # section derives a common value for 'local_keys', which is passed to 'Saver.write' + # when saving each input cube. The global attributes are then created by a call + # to "Saver.update_global_attributes" within each 'Saver.write' call (which is + # obviously a bit redundant!), plus an extra one to add 'Conventions'. + # HOWEVER, in `split_attrs` mode (iris.FUTURE.save_split_attrs == False), this code + # instead constructs a 'global_attributes' dictionary, and outputs that just once, + # after writing all the input cubes. if iris.FUTURE.save_split_attrs: # We don't actually use 'local_keys' in this case. # TODO: can remove this when the iris.FUTURE.save_split_attrs is removed. @@ -2846,7 +2860,7 @@ def attr_values_equal(val1, val2): "(i.e. data-variable) attributes, where possible, since they are not " "the same on all input cubes." ) - cubes = list(cubes) # avoiding modifying the actual input arg. + cubes = cubes.copy() # avoiding modifying the actual input arg. for i_cube in range(len(cubes)): # We iterate over cube *index*, so we can replace the list entries with # with cube *copies* -- just to avoid changing our call args. @@ -2870,7 +2884,7 @@ def attr_values_equal(val1, val2): if blocked_attrs: warnings.warn( f"Global cube attributes {blocked_attrs} " - f'of cube "{cube.name()}" have been lost, overlaid ' + f'of cube "{cube.name()}" were not saved, overlaid ' "by existing local attributes with the same names." ) for attr in demote_attrs: @@ -2884,16 +2898,27 @@ def attr_values_equal(val1, val2): # Determine the attribute keys that are common across all cubes and # thereby extend the collection of local_keys for attributes # that should be attributes on data variables. - # NOTE: in 'legacy' mode, this code derives a common value for 'local_keys', which - # is employed in saving each cube. - # However, in `split_attrs` mode, this considers ONLY global attributes, and the - # resulting 'common_keys' is the fixed result : each cube is then saved like ... - # "sman.write(... localkeys=list(cube.attributes) - common_keys, ...)" if local_keys is None: local_keys = set() else: local_keys = set(local_keys) + # Determine the attribute keys that are common across all cubes and + # thereby extend the collection of local_keys for attributes + # that should be attributes on data variables. + attributes = cubes[0].attributes + common_keys = set(attributes) + for cube in cubes[1:]: + keys = set(cube.attributes) + local_keys.update(keys.symmetric_difference(common_keys)) + common_keys.intersection_update(keys) + different_value_keys = [] + for key in common_keys: + if np.any(attributes[key] != cube.attributes[key]): + different_value_keys.append(key) + common_keys.difference_update(different_value_keys) + local_keys.update(different_value_keys) + common_attr_values = None for cube in cubes: cube_attributes = cube.attributes diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 087ad72444..1d36dc55a8 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -339,7 +339,8 @@ def fetch_results( if attr_name in ds.ncattrs() else None ) - # Fetch local attr value from all data variables (except dimcoord vars) + # Fetch local attr value from all data variables : In our testcases, + # that is all *except* dimcoords (ones named after dimensions). local_vars_results = [ ( var.name, @@ -688,11 +689,6 @@ def test_15_globalstyle__multifile_same(self, global_attr): self.run_roundtrip_testcase( attr_name=global_attr, values=[[attrval, None], [attrval, None]] ) - # # The attribute remains as a common global setting - # global_attr_value=attrval, - # # The individual variables do *not* have an attribute of this name - # var_attr_vals={"v1": None, "v2": None}, - # ) self.check_roundtrip_results([attrval, None, None]) ####################################################### From fb343aeb96ebe95a802e6e926918039526fc39db Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 8 Aug 2023 10:52:51 +0100 Subject: [PATCH 18/38] Fix changed warning messages. --- lib/iris/tests/integration/test_netcdf__loadsaveattrs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 1d36dc55a8..bd5ff72207 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -530,8 +530,8 @@ def test_05_userstyle_matching_crossfile_promoted(self, do_split): # warnings about the clash expected_warnings = [ "Saving.* global attributes.* as local", - 'attributes.* of cube "var_0" have been lost', - 'attributes.* of cube "var_1" have been lost', + 'attributes.* of cube "var_0" were not saved', + 'attributes.* of cube "var_1" were not saved', ] else: # oldstyle saves: matching locals promoted, override original global @@ -1280,8 +1280,8 @@ def test_globallocal_one_other_clashingglobals(self, do_split): expected = [None, "valueB", "valueB"] expected_warnings = [ "Saving.* global attributes.* as local", - 'attributes.* of cube "v1" have been lost', - 'attributes.* of cube "v2" have been lost', + 'attributes.* of cube "v1" were not saved', + 'attributes.* of cube "v2" were not saved', ] else: # N.B. legacy code sees only the locals, and promotes them. From b1778c68e65a1d439055b16d1a8236d9b94a3399 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 8 Aug 2023 11:15:41 +0100 Subject: [PATCH 19/38] Move warnings checking from 'run' to 'check' phase. --- .../integration/test_netcdf__loadsaveattrs.py | 180 +++++++++--------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index bd5ff72207..d65ab215a5 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -78,6 +78,13 @@ def local_attr(request): def check_captured_warnings( expected_keys: List[str], captured_warnings: List[warnings.WarningMessage] ): + """ + Compare captured warning messages with a list of regexp-matches. + + We allow them to occur in any order, and replace each actual result in the list + with the matching regexp as this makes failure reports much easier to comprehend. + + """ if expected_keys is None: expected_keys = [] elif hasattr(expected_keys, "upper"): @@ -413,7 +420,7 @@ def do_split(self, request): self.save_split_attrs = do_split return do_split - def run_roundtrip_testcase(self, attr_name, values, expect_warnings=None): + def run_roundtrip_testcase(self, attr_name, values): """ Initialise the testcase from the passed-in controls, configure the input files and run a save-load roundtrip to produce the output file. @@ -436,9 +443,9 @@ def run_roundtrip_testcase(self, attr_name, values, expect_warnings=None): with iris.FUTURE.context(save_split_attrs=do_split): iris.save(cubes, self.result_filepath) - check_captured_warnings(expect_warnings, captured_warnings) + self.captured_warnings = captured_warnings - def check_roundtrip_results(self, expected): + def check_roundtrip_results(self, expected, expected_warnings=None): """ Run checks on the generated output file. @@ -447,11 +454,14 @@ def check_roundtrip_results(self, expected): (variable) attributes. Values of 'None' mean to check that the relevant global/local attribute does *not* exist. + + Also check the warnings captured during the testcase run. """ # N.B. there is only ever one result-file, but it can contain various variables # which came from different input files. results = self.fetch_results(filepath=self.result_filepath) assert results == expected + check_captured_warnings(expected_warnings, self.captured_warnings) ####################################################### # Tests on "user-style" attributes. @@ -514,10 +524,13 @@ def test_04_userstyle_matching_promoted(self, do_split): def test_05_userstyle_matching_crossfile_promoted(self, do_split): # matching user-attributes are promoted, even across input files. # (but not when saving split attributes) - input_values = [ - ["global_file1", "same-value", "same-value"], - [None, "same-value", "same-value"], - ] + self.run_roundtrip_testcase( + attr_name="random", + values=[ + ["global_file1", "same-value", "same-value"], + [None, "same-value", "same-value"], + ], + ) if do_split: # newstyle saves: locals are preserved, mismathced global is *lost* expected_result = [ @@ -538,12 +551,7 @@ def test_05_userstyle_matching_crossfile_promoted(self, do_split): expected_result = ["same-value", None, None, None, None] expected_warnings = None - self.run_roundtrip_testcase( - attr_name="random", - values=input_values, - expect_warnings=expected_warnings, - ) - self.check_roundtrip_results(expected_result) + self.check_roundtrip_results(expected_result, expected_warnings) def test_06_userstyle_nonmatching_remainlocal(self, do_split): # Non-matching user attributes remain 'local' to the individual variables. @@ -601,6 +609,10 @@ def test_10_globalstyle__local(self, global_attr, do_split): # Strictly, not correct CF, but let's see what it does with it. attr_content = f"Local tracked {global_attr}" input_values = [None, attr_content] + self.run_roundtrip_testcase( + attr_name=global_attr, + values=input_values, + ) if do_split: # remains local as supplied, but there is a warning expected_result = input_values @@ -609,17 +621,16 @@ def test_10_globalstyle__local(self, global_attr, do_split): # promoted to global expected_result = [attr_content, None] expected_warning = None - self.run_roundtrip_testcase( - attr_name=global_attr, - values=input_values, - expect_warnings=expected_warning, - ) - self.check_roundtrip_results(expected_result) + self.check_roundtrip_results(expected_result, expected_warning) def test_11_globalstyle__both(self, global_attr, do_split): attr_global = f"Global-{global_attr}" attr_local = f"Local-{global_attr}" input_values = [attr_global, attr_local] + self.run_roundtrip_testcase( + attr_name=global_attr, + values=input_values, + ) if do_split: # remains local as supplied, but there is a warning expected_result = input_values @@ -628,12 +639,7 @@ def test_11_globalstyle__both(self, global_attr, do_split): # promoted to global, no local value, original global lost expected_result = [attr_local, None] expected_warning = None - self.run_roundtrip_testcase( - attr_name=global_attr, - values=input_values, - expect_warnings=expected_warning, - ) - self.check_roundtrip_results(expected_result) + self.check_roundtrip_results(expected_result, expected_warning) def test_12_globalstyle__multivar_different(self, global_attr): # Multiple *different* local settings are retained, not promoted @@ -644,14 +650,17 @@ def test_12_globalstyle__multivar_different(self, global_attr): self.run_roundtrip_testcase( attr_name=global_attr, values=[None, attr_1, attr_2], - expect_warnings=expect_warning, ) - self.check_roundtrip_results([None, attr_1, attr_2]) + self.check_roundtrip_results([None, attr_1, attr_2], expect_warning) def test_13_globalstyle__multivar_same(self, global_attr, do_split): # Multiple *same* local settings are promoted to a common global one attrval = f"Locally-defined-{global_attr}" input_values = [None, attrval, attrval] + self.run_roundtrip_testcase( + attr_name=global_attr, + values=input_values, + ) if do_split: # remains local, but with a warning expected_warning = "should only be a CF global" @@ -660,28 +669,22 @@ def test_13_globalstyle__multivar_same(self, global_attr, do_split): # promoted to global expected_warning = None expected_result = [attrval, None, None] - self.run_roundtrip_testcase( - attr_name=global_attr, - values=input_values, - expect_warnings=expected_warning, - ) - self.check_roundtrip_results(expected_result) + self.check_roundtrip_results(expected_result, expected_warning) def test_14_globalstyle__multifile_different(self, global_attr, do_split): # Different global attributes from multiple files are retained as local ones attr_1 = f"Global-{global_attr}-1" attr_2 = f"Global-{global_attr}-2" - # A warning should be raised when writing the result. - expect_warnings = ["should only be a CF global attribute"] - if do_split: - # An extra warning, only when saving with split-attributes. - expect_warnings = ["Saving.* as local"] + expect_warnings self.run_roundtrip_testcase( attr_name=global_attr, values=[[attr_1, None], [attr_2, None]], - expect_warnings=expect_warnings, ) - self.check_roundtrip_results([None, attr_1, attr_2]) + # A warning should be raised when writing the result. + expected_warnings = ["should only be a CF global attribute"] + if do_split: + # An extra warning, only when saving with split-attributes. + expected_warnings = ["Saving.* as local"] + expected_warnings + self.check_roundtrip_results([None, attr_1, attr_2], expected_warnings) def test_15_globalstyle__multifile_same(self, global_attr): # Matching global-type attributes in multiple files are retained as global @@ -1029,9 +1032,7 @@ def do_split(self, request): self.save_split_attrs = do_split return do_split - def run_save_testcase( - self, attr_name: str, values: list, expect_warnings: List[str] = None - ): + def run_save_testcase(self, attr_name: str, values: list): # Create input cubes. self.run_testcase( attr_name=attr_name, @@ -1046,11 +1047,9 @@ def run_save_testcase( with iris.FUTURE.context(save_split_attrs=do_split): iris.save(self.input_cubes, self.result_filepath) - check_captured_warnings(expect_warnings, captured_warnings) + self.captured_warnings = captured_warnings - def run_save_testcase_legacytype( - self, attr_name: str, values: list, expect_warnings: List[str] = None - ): + def run_save_testcase_legacytype(self, attr_name: str, values: list): """ Legacy-type means : before cubes had split attributes. @@ -1061,11 +1060,14 @@ def run_save_testcase_legacytype( # Translate single input value to list-of-1 values = [values] - self.run_save_testcase(attr_name, [None] + values, expect_warnings) + self.run_save_testcase(attr_name, [None] + values) - def check_save_results(self, expected: list): + def check_save_results( + self, expected: list, expected_warnings: List[str] = None + ): results = self.fetch_results(filepath=self.result_filepath) assert results == expected + check_captured_warnings(expected_warnings, self.captured_warnings) def test_userstyle__single(self, do_split): self.run_save_testcase_legacytype("random", "value-x") @@ -1097,10 +1099,12 @@ def test_userstyle__multiple_onemissing(self, global_attr): self.run_save_testcase_legacytype( global_attr, ["value", None], - expect_warnings="should only be a CF global attribute", ) # Stored as locals when there are differing values. - self.check_save_results([None, "value", None]) + self.check_save_results( + [None, "value", None], + expected_warnings="should only be a CF global attribute", + ) def test_Conventions__single(self): self.run_save_testcase_legacytype("Conventions", "x") @@ -1122,6 +1126,7 @@ def test_Conventions__multiple_different(self): self.check_save_results(["CF-1.7", None, None]) def test_globalstyle__single(self, global_attr, do_split): + self.run_save_testcase_legacytype(global_attr, ["value"]) if do_split: # result as input values expected_warning = "should only be a CF global" @@ -1130,52 +1135,49 @@ def test_globalstyle__single(self, global_attr, do_split): # in legacy mode, promoted expected_warning = None expected_result = ["value", None] - - self.run_save_testcase_legacytype( - global_attr, ["value"], expect_warnings=expected_warning - ) - self.check_save_results(expected_result) + self.check_save_results(expected_result, expected_warning) def test_globalstyle__multiple_same(self, global_attr, do_split): # Multiple global-type with same values are made global. + self.run_save_testcase_legacytype( + global_attr, + ["value-same", "value-same"], + ) if do_split: # result as input values - expected_warning = "should only be a CF global attribute" expected_result = [None, "value-same", "value-same"] + expected_warning = "should only be a CF global attribute" else: # in legacy mode, promoted - expected_warning = None expected_result = ["value-same", None, None] - self.run_save_testcase_legacytype( - global_attr, - ["value-same", "value-same"], - expect_warnings=expected_warning, - ) - self.check_save_results(expected_result) + expected_warning = None + self.check_save_results(expected_result, expected_warning) def test_globalstyle__multiple_different(self, global_attr): # Multiple global-type with different values become local, with warning. + self.run_save_testcase_legacytype(global_attr, ["value-A", "value-B"]) + # *Only* stored as locals when there are differing values. msg_regexp = ( f"'{global_attr}' is being added as CF data variable attribute," f".* should only be a CF global attribute." ) - self.run_save_testcase_legacytype( - global_attr, ["value-A", "value-B"], expect_warnings=msg_regexp + self.check_save_results( + [None, "value-A", "value-B"], expected_warnings=msg_regexp ) - # *Only* stored as locals when there are differing values. - self.check_save_results([None, "value-A", "value-B"]) def test_globalstyle__multiple_onemissing(self, global_attr): # Multiple global-type, with one missing, behave like different values. + self.run_save_testcase_legacytype( + global_attr, ["value", "value", None] + ) + # Stored as locals when there are differing values. msg_regexp = ( f"'{global_attr}' is being added as CF data variable attribute," f".* should only be a CF global attribute." ) - self.run_save_testcase_legacytype( - global_attr, ["value", "value", None], expect_warnings=msg_regexp + self.check_save_results( + [None, "value", "value", None], expected_warnings=msg_regexp ) - # Stored as locals when there are differing values. - self.check_save_results([None, "value", "value", None]) def test_localstyle__single(self, local_attr): self.run_save_testcase_legacytype(local_attr, ["value"]) @@ -1234,7 +1236,7 @@ def test_localstyle__multiple_different(self, local_attr): def test_globallocal_clashing(self, do_split): # A cube has clashing local + global attrs. original_values = ["valueA", "valueB"] - self.run_save_testcase("userattr", original_values, expect_warnings=[]) + self.run_save_testcase("userattr", original_values) expected_result = original_values.copy() if not do_split: # in legacy mode, "promote" = lose the local one @@ -1244,25 +1246,26 @@ def test_globallocal_clashing(self, do_split): def test_globallocal_oneeach_same(self, do_split): # One cube with global attr, another with identical local one. - inputs = [[None, "value"], ["value", None]] + self.run_save_testcase( + "userattr", values=[[None, "value"], ["value", None]] + ) if do_split: expected = [None, "value", "value"] - warning = ( + expected_warning = ( "Saving the cube global attributes \\['userattr'\\] as local" ) else: # N.B. legacy code sees only two equal values (and promotes). expected = ["value", None, None] - warning = None + expected_warning = None - self.run_save_testcase( - "userattr", values=inputs, expect_warnings=warning - ) - self.check_save_results(expected) + self.check_save_results(expected, expected_warning) def test_globallocal_oneeach_different(self, do_split): # One cube with global attr, another with a *different* local one. - inputs = [[None, "valueA"], ["valueB", None]] + self.run_save_testcase( + "userattr", [[None, "valueA"], ["valueB", None]] + ) if do_split: warning = ( "Saving the cube global attributes \\['userattr'\\] as local" @@ -1270,12 +1273,14 @@ def test_globallocal_oneeach_different(self, do_split): else: # N.B. legacy code does not warn of global-to-local "demotion". warning = None - self.run_save_testcase("userattr", inputs, expect_warnings=warning) - self.check_save_results([None, "valueA", "valueB"]) + self.check_save_results([None, "valueA", "valueB"], warning) def test_globallocal_one_other_clashingglobals(self, do_split): # Two cubes with both, second cube has a clashing global attribute. - inputs = [["valueA", "valueB"], ["valueXXX", "valueB"]] + self.run_save_testcase( + "userattr", + values=[["valueA", "valueB"], ["valueXXX", "valueB"]], + ) if do_split: expected = [None, "valueB", "valueB"] expected_warnings = [ @@ -1287,12 +1292,7 @@ def test_globallocal_one_other_clashingglobals(self, do_split): # N.B. legacy code sees only the locals, and promotes them. expected = ["valueB", None, None] expected_warnings = None - self.run_save_testcase( - "userattr", - values=inputs, - expect_warnings=expected_warnings, - ) - self.check_save_results(expected) + self.check_save_results(expected, expected_warnings) def test_globallocal_one_other_clashinglocals(self, do_split): # Two cubes with both, second cube has a clashing local attribute. From 067f07ddd459d6d54f1e6d569b69d19a79db07af Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 8 Aug 2023 11:30:56 +0100 Subject: [PATCH 20/38] Simplify and improve warnings checking code. --- .../integration/test_netcdf__loadsaveattrs.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index d65ab215a5..822702b0db 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -82,7 +82,8 @@ def check_captured_warnings( Compare captured warning messages with a list of regexp-matches. We allow them to occur in any order, and replace each actual result in the list - with the matching regexp as this makes failure reports much easier to comprehend. + with its matching regexp, if any, as this makes failure results much easier to + comprehend. """ if expected_keys is None: @@ -94,16 +95,16 @@ def check_captured_warnings( found_results = [str(warning.message) for warning in captured_warnings] remaining_keys = expected_keys.copy() for i_message, message in enumerate(found_results.copy()): - i_found = None - for i_key, key in enumerate(remaining_keys): + for key in remaining_keys: if key.search(message): - # Hit : remove one + only one matching warning from the list - i_found = i_message + # Hit : replace one message in the list with its matching "key" + found_results[i_message] = key + # remove the matching key + remaining_keys.remove(key) + # skip on to next message break - if i_found is not None: - found_results[i_found] = key - remaining_keys.remove(key) - assert found_results == expected_keys + + assert set(found_results) == set(expected_keys) class MixinAttrsTesting: From 891da48fb54e63a959e06545a94bfe710dffe333 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 8 Aug 2023 11:54:17 +0100 Subject: [PATCH 21/38] Fix wrong testcase. --- lib/iris/tests/integration/test_netcdf__loadsaveattrs.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 822702b0db..143ce08119 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -1095,17 +1095,14 @@ def test_userstyle__multiple_different(self): self.run_save_testcase_legacytype("random", ["value-A", "value-B"]) self.check_save_results([None, "value-A", "value-B"]) - def test_userstyle__multiple_onemissing(self, global_attr): + def test_userstyle__multiple_onemissing(self): # Multiple user-type, with one missing, behave like different values. self.run_save_testcase_legacytype( - global_attr, + "random", ["value", None], ) # Stored as locals when there are differing values. - self.check_save_results( - [None, "value", None], - expected_warnings="should only be a CF global attribute", - ) + self.check_save_results([None, "value", None]) def test_Conventions__single(self): self.run_save_testcase_legacytype("Conventions", "x") From 8ecadcab22ed937a3d150387b43a60963da1694e Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 8 Aug 2023 12:04:32 +0100 Subject: [PATCH 22/38] Minor review changes. --- lib/iris/fileformats/netcdf/saver.py | 12 +++++------- .../tests/integration/test_netcdf__loadsaveattrs.py | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 213e2e0368..da85097597 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2626,7 +2626,7 @@ def save( dictionaries on each cube in the saved cube list will be compared, and common attributes saved as NetCDF global attributes where appropriate. - Or, **when split-attribute saving is enabled**, then `cube.attributes.locals`` + Or, **when split-attribute saving is enabled**, then ``cube.attributes.locals`` are always saved as attributes of data-variables, and ``cube.attributes.globals`` are saved as global (dataset) attributes, where possible. Since the 2 types are now distinguished : see :class:`~iris.cube.CubeAttrsDict`. @@ -2887,12 +2887,10 @@ def attr_values_equal(val1, val2): f'of cube "{cube.name()}" were not saved, overlaid ' "by existing local attributes with the same names." ) - for attr in demote_attrs: - if attr not in blocked_attrs: - cube.attributes.locals[ - attr - ] = cube.attributes.globals[attr] - cube.attributes.globals.pop(attr) + for attr in set(demote_attrs) - set(blocked_attrs): + # move global to local + value = cube.attributes.globals.pop(attr) + cube.attributes.locals[attr] = value else: # Determine the attribute keys that are common across all cubes and diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 143ce08119..9520fb9231 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -1250,7 +1250,7 @@ def test_globallocal_oneeach_same(self, do_split): if do_split: expected = [None, "value", "value"] expected_warning = ( - "Saving the cube global attributes \\['userattr'\\] as local" + r"Saving the cube global attributes \['userattr'\] as local" ) else: # N.B. legacy code sees only two equal values (and promotes). @@ -1266,7 +1266,7 @@ def test_globallocal_oneeach_different(self, do_split): ) if do_split: warning = ( - "Saving the cube global attributes \\['userattr'\\] as local" + r"Saving the cube global attributes \['userattr'\] as local" ) else: # N.B. legacy code does not warn of global-to-local "demotion". From 4edf778cea40a761fac43caed92ba2f8cbfd67af Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 8 Aug 2023 12:29:58 +0100 Subject: [PATCH 23/38] Fix reverted code. --- lib/iris/fileformats/netcdf/saver.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index da85097597..9096eda5a2 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2893,9 +2893,8 @@ def attr_values_equal(val1, val2): cube.attributes.locals[attr] = value else: - # Determine the attribute keys that are common across all cubes and - # thereby extend the collection of local_keys for attributes - # that should be attributes on data variables. + # Legacy mode: calculate "local_keys" to control which attributes are local + # and which global. if local_keys is None: local_keys = set() else: @@ -2917,22 +2916,6 @@ def attr_values_equal(val1, val2): common_keys.difference_update(different_value_keys) local_keys.update(different_value_keys) - common_attr_values = None - for cube in cubes: - cube_attributes = cube.attributes - keys = set(cube_attributes) - if common_attr_values is None: - common_attr_values = cube_attributes.copy() - common_keys = keys.copy() - local_keys.update(keys.symmetric_difference(common_keys)) - common_keys.intersection_update(keys) - different_value_keys = [] - for key in common_keys: - if np.any(common_attr_values[key] != cube_attributes[key]): - different_value_keys.append(key) - common_keys.difference_update(different_value_keys) - local_keys.update(different_value_keys) - def is_valid_packspec(p): """Only checks that the datatype is valid.""" if isinstance(p, dict): From 159914cd243630b453a185ba41d90d4d4ed37cc3 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 8 Aug 2023 12:40:03 +0100 Subject: [PATCH 24/38] Use sets to simplify demoted-attributes code. --- lib/iris/fileformats/netcdf/saver.py | 59 +++++++++++++--------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 9096eda5a2..552eadb070 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2835,28 +2835,29 @@ def attr_values_equal(val1, val2): return match cube0 = cubes[0] - invalid_globals = [ - attrname - for attrname in global_names - if not all( - attr_values_equal( - cube.attributes.globals.get(attrname), - cube0.attributes.globals.get(attrname), + invalid_globals = set( + [ + attrname + for attrname in global_names + if not all( + attr_values_equal( + cube.attributes.globals.get(attrname), + cube0.attributes.globals.get(attrname), + ) + for cube in cubes[1:] ) - for cube in cubes[1:] - ) - ] + ] + ) # Establish all the global attributes which we will write to the file (at end). global_attributes = { attr: cube0.attributes.globals.get(attr) - for attr in global_names - if attr not in invalid_globals + for attr in global_names - invalid_globals } if invalid_globals: # Some cubes have different global attributes: modify cubes as required. warnings.warn( - f"Saving the cube global attributes {invalid_globals} as local " + f"Saving the cube global attributes {sorted(invalid_globals)} as local " "(i.e. data-variable) attributes, where possible, since they are not " "the same on all input cubes." ) @@ -2865,32 +2866,26 @@ def attr_values_equal(val1, val2): # We iterate over cube *index*, so we can replace the list entries with # with cube *copies* -- just to avoid changing our call args. cube = cubes[i_cube] - demote_attrs = [ - attr - for attr in cube.attributes.globals - if attr in invalid_globals - ] + demote_attrs = set(cube.attributes.globals) & invalid_globals if any(demote_attrs): - # This cube contains some 'demoted' global attributes. - # Replace the input cube with a copy, so we can modify attributes. - cube = cube.copy() - cubes[i_cube] = cube # Catch any demoted attrs where there is already a local version - blocked_attrs = [ - attrname - for attrname in demote_attrs - if attrname in cube.attributes.locals - ] + blocked_attrs = demote_attrs & set(cube.attributes.locals) if blocked_attrs: warnings.warn( - f"Global cube attributes {blocked_attrs} " + f"Global cube attributes {sorted(blocked_attrs)} " f'of cube "{cube.name()}" were not saved, overlaid ' "by existing local attributes with the same names." ) - for attr in set(demote_attrs) - set(blocked_attrs): - # move global to local - value = cube.attributes.globals.pop(attr) - cube.attributes.locals[attr] = value + demote_attrs -= blocked_attrs + if demote_attrs: + # This cube contains some 'demoted' global attributes. + # Replace input cube with a copy, so we can modify attributes. + cube = cube.copy() + cubes[i_cube] = cube + for attr in demote_attrs: + # move global to local + value = cube.attributes.globals.pop(attr) + cube.attributes.locals[attr] = value else: # Legacy mode: calculate "local_keys" to control which attributes are local From deb1db30c58bcdc5e0978e0100b7b569f2855006 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 18 Aug 2023 15:13:57 +0100 Subject: [PATCH 25/38] WIP --- .../integration/test_netcdf__loadsaveattrs.py | 92 +++++++++++++------ 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 9520fb9231..2ac396f28c 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -75,6 +75,14 @@ def local_attr(request): return request.param # Return the name of the attribute to test. +# Define whether to parametrise over split-attribute saving +# Just for now, so that we can run against legacy code. +_SPLIT_SAVE_SUPPORTED = hasattr(iris.FUTURE, "save_split_attrs") +_SPLIT_OPTS = ["nosplit", "split"] +if not _SPLIT_SAVE_SUPPORTED: + _SPLIT_OPTS.remove("split") + + def check_captured_warnings( expected_keys: List[str], captured_warnings: List[warnings.WarningMessage] ): @@ -220,10 +228,20 @@ def make_cubes(var_name, global_value=None, var_values=None): cubes.append(cube) dimco = DimCoord(np.arange(3.0), var_name="x") cube.add_dim_coord(dimco, 0) - if global_value is not None: - cube.attributes.globals[attr_name] = global_value - if local_value is not None: - cube.attributes.locals[attr_name] = local_value + if not hasattr(cube.attributes, "globals"): + # N.B. For now, also support oldstyle "single" cube attribute + # dictionaries, so that we can generate legacy results to compore + # with the "new world" results. + single_value = global_value + if local_value is not None: + single_value = local_value + if single_value is not None: + cube.attributes[attr_name] = single_value + else: + if global_value is not None: + cube.attributes.globals[attr_name] = global_value + if local_value is not None: + cube.attributes.locals[attr_name] = local_value return cubes if cubes: @@ -372,27 +390,35 @@ def fetch_results( # Sort result cubes according to a standard ordering. cubes = sorted(cubes, key=lambda cube: cube.name()) # Fetch globals and locals from cubes. - if oldstyle_combined: - # Replace cubes attributes with all-combined dictionaries - cubes = [cube.copy() for cube in cubes] - for cube in cubes: - combined = dict(cube.attributes) - cube.attributes.clear() - cube.attributes.locals = combined - global_values = set( - cube.attributes.globals.get(attr_name, None) for cube in cubes - ) # This way returns *multiple* result 'sets', one for each global value - results = [ - [globalval] - + [ - cube.attributes.locals.get(attr_name, None) + if oldstyle_combined: + # Use all-combined dictionaries in place of actual cubes' attributes + cube_attr_dicts = [dict(cube.attributes) for cube in cubes] + # Return results as if all cubes had global=None + results = [ + [None] + + [ + cube_attr_dict.get(attr_name, None) + for cube_attr_dict in cube_attr_dicts + ] + ] + else: + # Return a result-set for each occurring global value (possibly + # including a 'None'). + global_values = set( + cube.attributes.globals.get(attr_name, None) for cube in cubes - if cube.attributes.globals.get(attr_name, None) - == globalval + ) + results = [ + [globalval] + + [ + cube.attributes.locals.get(attr_name, None) + for cube in cubes + if cube.attributes.globals.get(attr_name, None) + == globalval + ] + for globalval in sorted(global_values) ] - for globalval in sorted(global_values) - ] return results @@ -413,9 +439,7 @@ class TestRoundtrip(MixinAttrsTesting): """ # Parametrise all tests over split/unsplit saving. - @pytest.fixture( - params=[False, True], ids=["nosplit", "split"], autouse=True - ) + @pytest.fixture(params=[False, True], ids=_SPLIT_OPTS, autouse=True) def do_split(self, request): do_split = request.param self.save_split_attrs = do_split @@ -441,7 +465,12 @@ def run_roundtrip_testcase(self, attr_name, values): # Ensure stable result order. cubes = sorted(cubes, key=lambda cube: cube.name()) do_split = getattr(self, "save_split_attrs", False) - with iris.FUTURE.context(save_split_attrs=do_split): + kwargs = ( + dict(save_split_attrs=do_split) + if _SPLIT_SAVE_SUPPORTED + else dict() + ) + with iris.FUTURE.context(**kwargs): iris.save(cubes, self.result_filepath) self.captured_warnings = captured_warnings @@ -1025,9 +1054,7 @@ class TestSave(MixinAttrsTesting): """ # Parametrise all tests over split/unsplit saving. - @pytest.fixture( - params=[False, True], ids=["nosplit", "split"], autouse=True - ) + @pytest.fixture(params=[False, True], ids=_SPLIT_OPTS, autouse=True) def do_split(self, request): do_split = request.param self.save_split_attrs = do_split @@ -1045,7 +1072,12 @@ def run_save_testcase(self, attr_name: str, values: list): with warnings.catch_warnings(record=True) as captured_warnings: self.result_filepath = self._testfile_path("result") do_split = getattr(self, "save_split_attrs", False) - with iris.FUTURE.context(save_split_attrs=do_split): + kwargs = ( + dict(save_split_attrs=do_split) + if _SPLIT_SAVE_SUPPORTED + else dict() + ) + with iris.FUTURE.context(**kwargs): iris.save(self.input_cubes, self.result_filepath) self.captured_warnings = captured_warnings From e5d5ff9fd0feca9f93cb13364efd2bee8ad9444a Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 18 Aug 2023 15:53:48 +0100 Subject: [PATCH 26/38] Working with iris 3.6.1, no errors TestSave or TestRoundtrip. --- .../integration/test_netcdf__loadsaveattrs.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 2ac396f28c..ff492711df 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -78,9 +78,11 @@ def local_attr(request): # Define whether to parametrise over split-attribute saving # Just for now, so that we can run against legacy code. _SPLIT_SAVE_SUPPORTED = hasattr(iris.FUTURE, "save_split_attrs") -_SPLIT_OPTS = ["nosplit", "split"] +_SPLIT_PARAM_VALUES = [False, True] +_SPLIT_PARAM_IDS = ["nosplit", "split"] if not _SPLIT_SAVE_SUPPORTED: - _SPLIT_OPTS.remove("split") + _SPLIT_PARAM_VALUES.remove(True) + _SPLIT_PARAM_IDS.remove("split") def check_captured_warnings( @@ -391,7 +393,7 @@ def fetch_results( cubes = sorted(cubes, key=lambda cube: cube.name()) # Fetch globals and locals from cubes. # This way returns *multiple* result 'sets', one for each global value - if oldstyle_combined: + if oldstyle_combined or not _SPLIT_SAVE_SUPPORTED: # Use all-combined dictionaries in place of actual cubes' attributes cube_attr_dicts = [dict(cube.attributes) for cube in cubes] # Return results as if all cubes had global=None @@ -439,7 +441,9 @@ class TestRoundtrip(MixinAttrsTesting): """ # Parametrise all tests over split/unsplit saving. - @pytest.fixture(params=[False, True], ids=_SPLIT_OPTS, autouse=True) + @pytest.fixture( + params=_SPLIT_PARAM_VALUES, ids=_SPLIT_PARAM_IDS, autouse=True + ) def do_split(self, request): do_split = request.param self.save_split_attrs = do_split @@ -1054,7 +1058,9 @@ class TestSave(MixinAttrsTesting): """ # Parametrise all tests over split/unsplit saving. - @pytest.fixture(params=[False, True], ids=_SPLIT_OPTS, autouse=True) + @pytest.fixture( + params=_SPLIT_PARAM_VALUES, ids=_SPLIT_PARAM_IDS, autouse=True + ) def do_split(self, request): do_split = request.param self.save_split_attrs = do_split From 42fce92307313763d4978e1f38ece4f367996d8d Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 21 Aug 2023 11:48:52 +0100 Subject: [PATCH 27/38] Interim save (incomplete?). --- .../integration/test_netcdf__loadsaveattrs.py | 186 +++++++++++++++++- 1 file changed, 184 insertions(+), 2 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index ff492711df..21f653bca0 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -19,6 +19,9 @@ """ import inspect +import json +import os +from pathlib import Path import re from typing import Iterable, List, Optional, Union import warnings @@ -85,6 +88,9 @@ def local_attr(request): _SPLIT_PARAM_IDS.remove("split") +_SKIP_WARNCHECK = "_no_warnings_check" + + def check_captured_warnings( expected_keys: List[str], captured_warnings: List[warnings.WarningMessage] ): @@ -96,11 +102,15 @@ def check_captured_warnings( comprehend. """ - if expected_keys is None: - expected_keys = [] + if expected_keys == _SKIP_WARNCHECK: + return elif hasattr(expected_keys, "upper"): # Handle a single string + if expected_keys == _SKIP_WARNCHECK: + # No check at all in this case + return expected_keys = [expected_keys] + expected_keys = [re.compile(key) for key in expected_keys] found_results = [str(warning.message) for warning in captured_warnings] remaining_keys = expected_keys.copy() @@ -424,6 +434,156 @@ def fetch_results( return results +# Define all the testcases for different parameter input structures : +# - combinations of matching+differing, global+local params +# - these are interpreted differently for the 3 main test types : Load/Save/Roundtrip +_MATRIX_TESTCASE_INPUTS = { + "case_single_localonly": "G-La", + "case_single_globalonly": "GaL-", + "case_single_glsame": "GaLa", + "case_single_gldiffer": "GaLb", + "case_multivar_same_noglobal": "G-Laa", + "case_multivar_same_sameglobal": "GaLaa", + "case_multivar_same_diffglobal": "GaLbb", + "case_multivar_differ_noglobal": "G-Lab", + "case_multivar_differ_diffglobal": "GaLbc", + "case_multivar_differ_sameglobal": "GaLab", + "case_multivar_1none_noglobal": "G-La-", + "case_multivar_1none_diffglobal": "GaLb-", + "case_multivar_1none_sameglobal": "GaLa-", + # Note: the multi-set input cases are more complex. + # These are encoded as *pairs* of specs, for 2 different files, or cubes with + # independent global values. + # We assume that there can be nothing "special" about a var's interaction with + # another one from the *same file* ?? + "case_multisource_gsame_lnone": ("GaL-", "GaL-"), + "case_multisource_gsame_lallsame": ("GaLa", "GaLa"), + "case_multisource_gsame_l1same1none": ("GaLa", "GaL-"), + "case_multisource_gsame_l1same1other": ("GaLa", "GaLb"), + "case_multisource_gsame_lallother": ("GaLb", "GaLb"), + "case_multisource_gsame_lalldiffer": ("GaLb", "GaLc"), + "case_multisource_gnone_l1same1none": ("G-La", "G-L-"), + "case_multisource_gnone_l1same1same": ("G-La", "G-La"), + "case_multisource_gnone_l1same1other": ("G-La", "G-Lb"), + "case_multisource_gdiff_lnone": ("GaL-", "GbL-"), + "case_multisource_gdiff_l1same1none": ("GaLa", "GbL-"), + "case_multisource_gdiff_l1diff1none": ("GaLb", "GcL-"), + "case_multisource_gdiff_lallsame": ("GaLa", "GbLb"), + "case_multisource_gdiff_lallother": ("GaLc", "GbLc"), +} +_MATRIX_TESTCASES = list(_MATRIX_TESTCASE_INPUTS.keys()) + +# +# Define the attrs against which all matrix tests are run +# +_MATRIX_ATTRNAMES = _LOCAL_TEST_ATTRS + list(_GLOBAL_TEST_ATTRS) + ["user"] +# remove special-cases, for now +_SPECIAL_ATTRS = [ + "Conventions", + "ukmo__process_flags", + "missing_value", + "standard_error_multiplier", +] +_MATRIX_ATTRNAMES = [ + attr for attr in _MATRIX_ATTRNAMES if attr not in _SPECIAL_ATTRS +] + + +# +# A routine to work "backwards" from am attribute name to its "style", i.e. type category. +# Possible ones are "globalstyle", "localstyle", "userstyle". +# +_ATTR_STYLES = ["localstyle", "globalstyle", "userstyle"] + + +def deduce_attr_style(attrname: str) -> str: + # Extract the attribute "style type" from an attr_param name + if attrname in _LOCAL_TEST_ATTRS: + style = "localstyle" + elif attrname in _GLOBAL_TEST_ATTRS: + style = "globalstyle" + else: + assert attrname == "user" + style = "userstyle" + return style + + +# +# Decode a matrix "input spec" to codes for global + local values. +# + + +def decode_matrix_input(input_spec): + def decode_specstring(spec: str) -> List[Union[str, None]]: + # Decode an input spec-string to input/output attribute values + assert spec[0] == "G" and spec[2] == "L" + allvals = spec[1] + spec[3:] + result = [None if valchar == "-" else valchar for valchar in allvals] + return result + + if isinstance(input_spec, str): + # Single-source spec (one cube or one file) + gA, vA = decode_specstring(input_spec) + result = [[gA, vA]] + else: + # Dual-source spec (two files, or sets of cubes with common global) + gA, vA = decode_specstring(input_spec[0]) + gB, vB = decode_specstring(input_spec[1]) + result = [[gA, vA], [gB, vB]] + + return result + + +def encode_matrix_result(results: List[List[str]]): + # result + assert isinstance(results, Iterable) and len(results) >= 1 + if isinstance(results[0], str): + results = [results] + assert all( + all(val is None or len(val) == 1 for val in vals) for vals in results + ) + + def valrep(val): + return "-" if val is None else val + + return list( + "".join(["G", valrep(vals[0]), "L"] + list(map(valrep, vals[1:]))) + for vals in results + ) + + +# +# All the matrix test results are stored in a JSON file. +# We have the technology to save the found results also. +# + + +@pytest.fixture(autouse=True, scope="session") +def matrix_results(): + matrix_filepath = Path(__file__).parent / "_testattrs_matrix_results.json" + save_matrix_results = os.environ.get("SAVEALL_MATRIX_RESULTS", False) + + if matrix_filepath.exists(): + # use json ... + matrix_results = json.load(matrix_filepath) + else: + # Initialise empty matrix results content + matrix_results = {} + for testtype in ("load", "save", "roundtrip"): + test_specs = matrix_results.setdefault(testtype, {}) + for testcase in _MATRIX_TESTCASES: + test_case_spec = test_specs.setdefault(testcase, {}) + test_case_spec["input"] = _MATRIX_TESTCASE_INPUTS[testcase] + for attrstyle in _ATTR_STYLES: + test_case_spec[attrstyle] = None # empty + + # Pass through to all the tests : they can also update it, if enabled. + yield save_matrix_results, matrix_results + + if save_matrix_results: + json.dump(matrix_results, matrix_filepath) + + class TestRoundtrip(MixinAttrsTesting): """ Test handling of attributes in roundtrip netcdf-iris-netcdf. @@ -797,6 +957,25 @@ def test_16_localstyle(self, local_attr, origin_style, do_split): expected_result = expected_result[::-1] self.check_roundtrip_results(expected_result) + @pytest.mark.parametrize("testcase", _MATRIX_TESTCASES) + @pytest.mark.parametrize("attrname", _MATRIX_ATTRNAMES) + def test_matrix(self, testcase, attrname, matrix_results): + do_saves, matrix_results = matrix_results + test_spec = matrix_results["roundtrip"][testcase] + input_spec = test_spec["input"] + values = decode_matrix_input(input_spec) + + self.run_roundtrip_testcase(attrname, values) + results = self.fetch_results(filepath=self.result_filepath) + result_spec = encode_matrix_result(results) + + attr_style = deduce_attr_style(attrname) + expected = test_spec[attr_style] + + if do_saves: + test_spec[attr_style] = result_spec + assert result_spec == expected + class TestLoad(MixinAttrsTesting): """ @@ -818,6 +997,9 @@ def run_load_testcase(self, attr_name, values): ) def check_load_results(self, expected, oldstyle_combined=False): + if not _SPLIT_SAVE_SUPPORTED and not oldstyle_combined: + # Don't check "newstyle" in the old world -- just skip it. + return result_cubes = iris.load(self.input_filepaths) results = self.fetch_results( cubes=result_cubes, oldstyle_combined=oldstyle_combined From 19a2956a9b8313991140dc09d1e43f781736e9d5 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 21 Aug 2023 11:54:02 +0100 Subject: [PATCH 28/38] Different results form for split tests; working for roundtrip. --- .../integration/test_netcdf__loadsaveattrs.py | 118 ++++++++++++------ 1 file changed, 81 insertions(+), 37 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 21f653bca0..58a7b6c723 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -102,8 +102,8 @@ def check_captured_warnings( comprehend. """ - if expected_keys == _SKIP_WARNCHECK: - return + if expected_keys is None: + expected_keys = [] elif hasattr(expected_keys, "upper"): # Handle a single string if expected_keys == _SKIP_WARNCHECK: @@ -476,13 +476,20 @@ def fetch_results( # # Define the attrs against which all matrix tests are run # -_MATRIX_ATTRNAMES = _LOCAL_TEST_ATTRS + list(_GLOBAL_TEST_ATTRS) + ["user"] +max_param_attrs = -1 +# max_param_attrs = 5 + +_MATRIX_ATTRNAMES = _LOCAL_TEST_ATTRS[:max_param_attrs] +_MATRIX_ATTRNAMES += list(_GLOBAL_TEST_ATTRS)[:max_param_attrs] +_MATRIX_ATTRNAMES += ["user"] # remove special-cases, for now _SPECIAL_ATTRS = [ "Conventions", "ukmo__process_flags", "missing_value", "standard_error_multiplier", + "STASH", + "um_stash_source", ] _MATRIX_ATTRNAMES = [ attr for attr in _MATRIX_ATTRNAMES if attr not in _SPECIAL_ATTRS @@ -490,8 +497,8 @@ def fetch_results( # -# A routine to work "backwards" from am attribute name to its "style", i.e. type category. -# Possible ones are "globalstyle", "localstyle", "userstyle". +# A routine to work "backwards" from an attribute name to its "style", i.e. type category. +# Possible styles are "globalstyle", "localstyle", "userstyle". # _ATTR_STYLES = ["localstyle", "globalstyle", "userstyle"] @@ -511,9 +518,11 @@ def deduce_attr_style(attrname: str) -> str: # # Decode a matrix "input spec" to codes for global + local values. # - - def decode_matrix_input(input_spec): + # Decode a matrix-test input specifications, like "GaLbc" into lists of values. + # E.G. "GaLbc" -> ["a", "b", "c"] + # ["GaLbc", "GbLbc"] -> [["a", "b", "c"], ["b", "b", c"]] + # N.B. in this form "values" are all one-character strings. def decode_specstring(spec: str) -> List[Union[str, None]]: # Decode an input spec-string to input/output attribute values assert spec[0] == "G" and spec[2] == "L" @@ -523,65 +532,98 @@ def decode_specstring(spec: str) -> List[Union[str, None]]: if isinstance(input_spec, str): # Single-source spec (one cube or one file) - gA, vA = decode_specstring(input_spec) - result = [[gA, vA]] + vals = decode_specstring(input_spec) + result = [vals] else: - # Dual-source spec (two files, or sets of cubes with common global) - gA, vA = decode_specstring(input_spec[0]) - gB, vB = decode_specstring(input_spec[1]) - result = [[gA, vA], [gB, vB]] + # Dual-source spec (two files, or sets of cubes with a common global value) + vals_A = decode_specstring(input_spec[0]) + vals_B = decode_specstring(input_spec[1]) + result = [vals_A, vals_B] return result -def encode_matrix_result(results: List[List[str]]): - # result +def encode_matrix_result(results: List[List[str]]) -> List[str]: + # Re-code a set of output results, [*[global-value, *local-values]] as a list of + # strings, like ["GaL-b"] or ["GaLabc", "GbLabc"]. + # N.B. again assuming that all values are just one-character strings, or None. assert isinstance(results, Iterable) and len(results) >= 1 - if isinstance(results[0], str): + if not isinstance(results[0], list): results = [results] assert all( all(val is None or len(val) == 1 for val in vals) for vals in results ) + # Translate "None" values to "-" def valrep(val): return "-" if val is None else val - return list( + results = list( "".join(["G", valrep(vals[0]), "L"] + list(map(valrep, vals[1:]))) for vals in results ) + return results # -# All the matrix test results are stored in a JSON file. -# We have the technology to save the found results also. +# The "expected" matrix test results are stored in JSON files (one for each test-type). +# We can also save the found results. # +_MATRIX_TESTTYPES = ("load", "save", "roundtrip") @pytest.fixture(autouse=True, scope="session") def matrix_results(): - matrix_filepath = Path(__file__).parent / "_testattrs_matrix_results.json" + matrix_filepaths = { + testtype: Path(__file__).parent + / f"attrs_matrix_results_{testtype}.json" + for testtype in _MATRIX_TESTTYPES + } save_matrix_results = os.environ.get("SAVEALL_MATRIX_RESULTS", False) - if matrix_filepath.exists(): - # use json ... - matrix_results = json.load(matrix_filepath) - else: - # Initialise empty matrix results content - matrix_results = {} - for testtype in ("load", "save", "roundtrip"): - test_specs = matrix_results.setdefault(testtype, {}) + matrix_results = {} + for testtype in _MATRIX_TESTTYPES: + # Either fetch from file, or initialise, a results matrix for each test type + # (load/save/roundtrip). + input_path = matrix_filepaths[testtype] + if input_path.exists(): + # Load from file with json. + with open(input_path) as file_in: + testtype_results = json.load(file_in) + else: + # Create empty matrix results content (for one test-type) + testtype_results = {} for testcase in _MATRIX_TESTCASES: - test_case_spec = test_specs.setdefault(testcase, {}) - test_case_spec["input"] = _MATRIX_TESTCASE_INPUTS[testcase] + test_case_results = {} + testtype_results[testcase] = test_case_results + # Every testcase dict has an "input" slot with the test input spec, + # basically just to help human readability. + test_case_results["input"] = _MATRIX_TESTCASE_INPUTS[testcase] for attrstyle in _ATTR_STYLES: - test_case_spec[attrstyle] = None # empty + # "Load"-type results have a single result per attribute-style + if testtype == "load": + test_case_results[attrstyle] = None # empty + else: + # "save"/"roundtrip"-type results record 2 result sets, + # (unsplit/split) for each attribute-style + # - i.e. when saved without/with split_attrs_saving enabled. + test_case_results[attrstyle] = { + "unsplit": None, + "split": None, + } + + matrix_results[testtype] = testtype_results + # Overall structure, matrix_results[TESTTYPES][TESTCASES][ATTR_STYLES] # Pass through to all the tests : they can also update it, if enabled. yield save_matrix_results, matrix_results if save_matrix_results: - json.dump(matrix_results, matrix_filepath) + for testtype in _MATRIX_TESTTYPES: + output_path = matrix_filepaths[testtype] + results = matrix_results[testtype] + with open(output_path, "w") as file_out: + json.dump(results, file_out, indent=2) class TestRoundtrip(MixinAttrsTesting): @@ -957,10 +999,11 @@ def test_16_localstyle(self, local_attr, origin_style, do_split): expected_result = expected_result[::-1] self.check_roundtrip_results(expected_result) - @pytest.mark.parametrize("testcase", _MATRIX_TESTCASES) + @pytest.mark.parametrize("testcase", _MATRIX_TESTCASES[:max_param_attrs]) @pytest.mark.parametrize("attrname", _MATRIX_ATTRNAMES) - def test_matrix(self, testcase, attrname, matrix_results): + def test_matrix(self, testcase, attrname, matrix_results, do_split): do_saves, matrix_results = matrix_results + split_param = "split" if do_split else "unsplit" test_spec = matrix_results["roundtrip"][testcase] input_spec = test_spec["input"] values = decode_matrix_input(input_spec) @@ -970,11 +1013,12 @@ def test_matrix(self, testcase, attrname, matrix_results): result_spec = encode_matrix_result(results) attr_style = deduce_attr_style(attrname) - expected = test_spec[attr_style] + expected = test_spec[attr_style][split_param] if do_saves: - test_spec[attr_style] = result_spec - assert result_spec == expected + test_spec[attr_style][split_param] = result_spec + if expected is not None: + assert result_spec == expected class TestLoad(MixinAttrsTesting): From 59d05dc4e03d45d7565b7c43467594e6f4858b7c Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 21 Aug 2023 12:12:26 +0100 Subject: [PATCH 29/38] Check that all param lists are sorted. --- lib/iris/tests/integration/test_netcdf__loadsaveattrs.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 58a7b6c723..1a86f48012 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -52,11 +52,12 @@ # N.B. this is not the same as 'Conventions', but is caught in the crossfire when that # one is processed. _GLOBAL_TEST_ATTRS -= set(["conventions"]) +_GLOBAL_TEST_ATTRS = sorted(_GLOBAL_TEST_ATTRS) # Define a fixture to parametrise tests over the 'global-style' test attributes. # This just provides a more concise way of writing parametrised tests. -@pytest.fixture(params=sorted(_GLOBAL_TEST_ATTRS)) +@pytest.fixture(params=_GLOBAL_TEST_ATTRS) def global_attr(request): # N.B. "request" is a standard PyTest fixture return request.param # Return the name of the attribute to test. @@ -72,7 +73,7 @@ def global_attr(request): # Define a fixture to parametrise over the 'local-style' test attributes. # This just provides a more concise way of writing parametrised tests. -@pytest.fixture(params=sorted(_LOCAL_TEST_ATTRS)) +@pytest.fixture(params=_LOCAL_TEST_ATTRS) def local_attr(request): # N.B. "request" is a standard PyTest fixture return request.param # Return the name of the attribute to test. @@ -480,7 +481,7 @@ def fetch_results( # max_param_attrs = 5 _MATRIX_ATTRNAMES = _LOCAL_TEST_ATTRS[:max_param_attrs] -_MATRIX_ATTRNAMES += list(_GLOBAL_TEST_ATTRS)[:max_param_attrs] +_MATRIX_ATTRNAMES += _GLOBAL_TEST_ATTRS[:max_param_attrs] _MATRIX_ATTRNAMES += ["user"] # remove special-cases, for now _SPECIAL_ATTRS = [ From 1770d97509161fa62659eb75f7a21e1c4b70bb46 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 21 Aug 2023 12:35:13 +0100 Subject: [PATCH 30/38] Check matrix result-files compatibility; add test_save_matrix. --- .../integration/test_netcdf__loadsaveattrs.py | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 1a86f48012..46a65ec5ca 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -457,20 +457,24 @@ def fetch_results( # independent global values. # We assume that there can be nothing "special" about a var's interaction with # another one from the *same file* ?? - "case_multisource_gsame_lnone": ("GaL-", "GaL-"), - "case_multisource_gsame_lallsame": ("GaLa", "GaLa"), - "case_multisource_gsame_l1same1none": ("GaLa", "GaL-"), - "case_multisource_gsame_l1same1other": ("GaLa", "GaLb"), - "case_multisource_gsame_lallother": ("GaLb", "GaLb"), - "case_multisource_gsame_lalldiffer": ("GaLb", "GaLc"), - "case_multisource_gnone_l1same1none": ("G-La", "G-L-"), - "case_multisource_gnone_l1same1same": ("G-La", "G-La"), - "case_multisource_gnone_l1same1other": ("G-La", "G-Lb"), - "case_multisource_gdiff_lnone": ("GaL-", "GbL-"), - "case_multisource_gdiff_l1same1none": ("GaLa", "GbL-"), - "case_multisource_gdiff_l1diff1none": ("GaLb", "GcL-"), - "case_multisource_gdiff_lallsame": ("GaLa", "GbLb"), - "case_multisource_gdiff_lallother": ("GaLc", "GbLc"), + "case_multisource_gsame_lnone": ["GaL-", "GaL-"], + "case_multisource_gsame_lallsame": ["GaLa", "GaLa"], + "case_multisource_gsame_l1same1none": ["GaLa", "GaL-"], + "case_multisource_gsame_l1same1other": ["GaLa", "GaLb"], + "case_multisource_gsame_lallother": ["GaLb", "GaLb"], + "case_multisource_gsame_lalldiffer": ["GaLb", "GaLc"], + "case_multisource_gnone_l1same1none": ["G-La", "G-L-"], + "case_multisource_gnone_l1same1same": ["G-La", "G-La"], + "case_multisource_gnone_l1same1other": ["G-La", "G-Lb"], + "case_multisource_gdiff_lnone": ["GaL-", "GbL-"], + "case_multisource_gdiff_l1same1none": ["GaLa", "GbL-"], + "case_multisource_gdiff_l1diff1none": ["GaLb", "GcL-"], + "case_multisource_gdiff_lallsame": ["GaLa", "GbLb"], + "case_multisource_gdiff_lallother": ["GaLc", "GbLc"], + # + # TODO: improvements needed here -- ordering can be more consistent, missed checking + # "gdiff" multi-cases where one global is None, rather than a different value. + # } _MATRIX_TESTCASES = list(_MATRIX_TESTCASE_INPUTS.keys()) @@ -591,6 +595,12 @@ def matrix_results(): # Load from file with json. with open(input_path) as file_in: testtype_results = json.load(file_in) + # Check compatibility (in case we changed the test-specs list) + assert set(testtype_results.keys()) == set(_MATRIX_TESTCASES) + assert all( + testtype_results[key]["input"] == _MATRIX_TESTCASE_INPUTS[key] + for key in _MATRIX_TESTCASES + ) else: # Create empty matrix results content (for one test-type) testtype_results = {} @@ -1002,11 +1012,13 @@ def test_16_localstyle(self, local_attr, origin_style, do_split): @pytest.mark.parametrize("testcase", _MATRIX_TESTCASES[:max_param_attrs]) @pytest.mark.parametrize("attrname", _MATRIX_ATTRNAMES) - def test_matrix(self, testcase, attrname, matrix_results, do_split): + def test_roundtrip_matrix( + self, testcase, attrname, matrix_results, do_split + ): do_saves, matrix_results = matrix_results split_param = "split" if do_split else "unsplit" - test_spec = matrix_results["roundtrip"][testcase] - input_spec = test_spec["input"] + testcase_spec = matrix_results["roundtrip"][testcase] + input_spec = testcase_spec["input"] values = decode_matrix_input(input_spec) self.run_roundtrip_testcase(attrname, values) @@ -1014,10 +1026,10 @@ def test_matrix(self, testcase, attrname, matrix_results, do_split): result_spec = encode_matrix_result(results) attr_style = deduce_attr_style(attrname) - expected = test_spec[attr_style][split_param] + expected = testcase_spec[attr_style][split_param] if do_saves: - test_spec[attr_style][split_param] = result_spec + testcase_spec[attr_style][split_param] = result_spec if expected is not None: assert result_spec == expected @@ -1567,3 +1579,24 @@ def test_globallocal_one_other_clashinglocals(self, do_split): expected = [None, "valueB", "valueXXX"] self.run_save_testcase("userattr", values=inputs) self.check_save_results(expected) + + @pytest.mark.parametrize("testcase", _MATRIX_TESTCASES[:max_param_attrs]) + @pytest.mark.parametrize("attrname", _MATRIX_ATTRNAMES) + def test_save_matrix(self, testcase, attrname, matrix_results, do_split): + do_saves, matrix_results = matrix_results + split_param = "split" if do_split else "unsplit" + testcase_spec = matrix_results["save"][testcase] + input_spec = testcase_spec["input"] + values = decode_matrix_input(input_spec) + + self.run_save_testcase(attrname, values) + results = self.fetch_results(filepath=self.result_filepath) + result_spec = encode_matrix_result(results) + + attr_style = deduce_attr_style(attrname) + expected = testcase_spec[attr_style][split_param] + + if do_saves: + testcase_spec[attr_style][split_param] = result_spec + if expected is not None: + assert result_spec == expected From b67f510e91506031fd5340113794ac8c188100c5 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 21 Aug 2023 15:00:59 +0100 Subject: [PATCH 31/38] test_load_matrix added; two types of load result. --- .../integration/test_netcdf__loadsaveattrs.py | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 46a65ec5ca..0a8870502a 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -84,9 +84,11 @@ def local_attr(request): _SPLIT_SAVE_SUPPORTED = hasattr(iris.FUTURE, "save_split_attrs") _SPLIT_PARAM_VALUES = [False, True] _SPLIT_PARAM_IDS = ["nosplit", "split"] +_MATRIX_LOAD_RESULTSTYLES = ["legacy", "newstyle"] if not _SPLIT_SAVE_SUPPORTED: _SPLIT_PARAM_VALUES.remove(True) _SPLIT_PARAM_IDS.remove("split") + _MATRIX_LOAD_RESULTSTYLES.remove("newstyle") _SKIP_WARNCHECK = "_no_warnings_check" @@ -584,7 +586,9 @@ def matrix_results(): / f"attrs_matrix_results_{testtype}.json" for testtype in _MATRIX_TESTTYPES } - save_matrix_results = os.environ.get("SAVEALL_MATRIX_RESULTS", False) + save_matrix_results = bool( + int(os.environ.get("SAVEALL_MATRIX_RESULTS", "0")) + ) matrix_results = {} for testtype in _MATRIX_TESTTYPES: @@ -611,9 +615,13 @@ def matrix_results(): # basically just to help human readability. test_case_results["input"] = _MATRIX_TESTCASE_INPUTS[testcase] for attrstyle in _ATTR_STYLES: - # "Load"-type results have a single result per attribute-style if testtype == "load": - test_case_results[attrstyle] = None # empty + # "Load" results record a "legacy" result, and a "newstyle" + # result. + test_case_results[attrstyle] = { + "legacy": None, + "newstyle": None, + } else: # "save"/"roundtrip"-type results record 2 result sets, # (unsplit/split) for each attribute-style @@ -1289,6 +1297,34 @@ def test_16_localstyle(self, local_attr, origin_style): # (#2): exact results, with newstyle "split" cube attrs self.check_load_results(expected_result_newstyle) + @pytest.mark.parametrize("testcase", _MATRIX_TESTCASES[:max_param_attrs]) + @pytest.mark.parametrize("attrname", _MATRIX_ATTRNAMES) + @pytest.mark.parametrize("resultstyle", _MATRIX_LOAD_RESULTSTYLES) + def test_load_matrix( + self, testcase, attrname, matrix_results, resultstyle + ): + do_saves, matrix_results = matrix_results + testcase_spec = matrix_results["load"][testcase] + input_spec = testcase_spec["input"] + values = decode_matrix_input(input_spec) + + self.run_load_testcase(attrname, values) + + result_cubes = iris.load(self.input_filepaths) + do_combined = resultstyle == "legacy" + results = self.fetch_results( + cubes=result_cubes, oldstyle_combined=do_combined + ) + result_spec = encode_matrix_result(results) + + attr_style = deduce_attr_style(attrname) + expected = testcase_spec[attr_style][resultstyle] + + if do_saves: + testcase_spec[attr_style][resultstyle] = result_spec + if expected is not None: + assert result_spec == expected + class TestSave(MixinAttrsTesting): """ From 2576826695f59c14a12b2c97acdc9e3bda7fcda4 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 21 Aug 2023 15:46:00 +0100 Subject: [PATCH 32/38] Finalise special-case attributes. --- .../tests/integration/test_netcdf__loadsaveattrs.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 0a8870502a..0d4db64cbd 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -483,15 +483,19 @@ def fetch_results( # # Define the attrs against which all matrix tests are run # -max_param_attrs = -1 +max_param_attrs = None # max_param_attrs = 5 _MATRIX_ATTRNAMES = _LOCAL_TEST_ATTRS[:max_param_attrs] _MATRIX_ATTRNAMES += _GLOBAL_TEST_ATTRS[:max_param_attrs] _MATRIX_ATTRNAMES += ["user"] -# remove special-cases, for now + +# remove special-cases, for now : all these behave irregularly (i.e. unlike the known +# "globalstyle", or "localstyle" generic cases). +# N.B. not including "Conventions", which is not in the globals list, so won't be +# matrix-tested unless we add it specifically. +# TODO: decide if any of these need to be tested, with their own testcases. _SPECIAL_ATTRS = [ - "Conventions", "ukmo__process_flags", "missing_value", "standard_error_multiplier", From e201a8e55cfa9fd71bc157cba05cffc47c5b4997 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 22 Aug 2023 00:02:23 +0100 Subject: [PATCH 33/38] Small docs tweaks. --- .../integration/test_netcdf__loadsaveattrs.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 0d4db64cbd..307ad4030e 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -494,7 +494,7 @@ def fetch_results( # "globalstyle", or "localstyle" generic cases). # N.B. not including "Conventions", which is not in the globals list, so won't be # matrix-tested unless we add it specifically. -# TODO: decide if any of these need to be tested, with their own testcases. +# TODO: decide if any of these need to be tested, as separate test-styles. _SPECIAL_ATTRS = [ "ukmo__process_flags", "missing_value", @@ -530,7 +530,7 @@ def deduce_attr_style(attrname: str) -> str: # Decode a matrix "input spec" to codes for global + local values. # def decode_matrix_input(input_spec): - # Decode a matrix-test input specifications, like "GaLbc" into lists of values. + # Decode a matrix-test input specification, like "GaLbc", into lists of values. # E.G. "GaLbc" -> ["a", "b", "c"] # ["GaLbc", "GbLbc"] -> [["a", "b", "c"], ["b", "b", c"]] # N.B. in this form "values" are all one-character strings. @@ -586,10 +586,12 @@ def valrep(val): @pytest.fixture(autouse=True, scope="session") def matrix_results(): matrix_filepaths = { - testtype: Path(__file__).parent - / f"attrs_matrix_results_{testtype}.json" + testtype: ( + Path(__file__).parent / f"attrs_matrix_results_{testtype}.json" + ) for testtype in _MATRIX_TESTTYPES } + # An environment variable can trigger saving of the results. save_matrix_results = bool( int(os.environ.get("SAVEALL_MATRIX_RESULTS", "0")) ) @@ -620,8 +622,9 @@ def matrix_results(): test_case_results["input"] = _MATRIX_TESTCASE_INPUTS[testcase] for attrstyle in _ATTR_STYLES: if testtype == "load": - # "Load" results record a "legacy" result, and a "newstyle" - # result. + # "load" test results have a "legacy" result (as for a single + # combined attrs dictionary), and a "newstyle" result (with + # the new split dictionary). test_case_results[attrstyle] = { "legacy": None, "newstyle": None, @@ -635,8 +638,8 @@ def matrix_results(): "split": None, } + # Build complete data: matrix_results[TESTTYPES][TESTCASES][ATTR_STYLES] matrix_results[testtype] = testtype_results - # Overall structure, matrix_results[TESTTYPES][TESTCASES][ATTR_STYLES] # Pass through to all the tests : they can also update it, if enabled. yield save_matrix_results, matrix_results From 4602f06ac74c9caf32e5e76a39348d19b6ad7295 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 22 Aug 2023 00:20:33 +0100 Subject: [PATCH 34/38] Add some more testcases, --- .../integration/test_netcdf__loadsaveattrs.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 307ad4030e..901b84c4f4 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -441,10 +441,10 @@ def fetch_results( # - combinations of matching+differing, global+local params # - these are interpreted differently for the 3 main test types : Load/Save/Roundtrip _MATRIX_TESTCASE_INPUTS = { - "case_single_localonly": "G-La", - "case_single_globalonly": "GaL-", - "case_single_glsame": "GaLa", - "case_single_gldiffer": "GaLb", + "case_singlevar_localonly": "G-La", + "case_singlevar_globalonly": "GaL-", + "case_singlevar_glsame": "GaLa", + "case_singlevar_gldiffer": "GaLb", "case_multivar_same_noglobal": "G-Laa", "case_multivar_same_sameglobal": "GaLaa", "case_multivar_same_diffglobal": "GaLbb", @@ -458,7 +458,7 @@ def fetch_results( # These are encoded as *pairs* of specs, for 2 different files, or cubes with # independent global values. # We assume that there can be nothing "special" about a var's interaction with - # another one from the *same file* ?? + # another one from the same (as opposed to the "other") file. "case_multisource_gsame_lnone": ["GaL-", "GaL-"], "case_multisource_gsame_lallsame": ["GaLa", "GaLa"], "case_multisource_gsame_l1same1none": ["GaLa", "GaL-"], @@ -468,15 +468,18 @@ def fetch_results( "case_multisource_gnone_l1same1none": ["G-La", "G-L-"], "case_multisource_gnone_l1same1same": ["G-La", "G-La"], "case_multisource_gnone_l1same1other": ["G-La", "G-Lb"], + "case_multisource_g1none_lnone": ["GaL-", "G-L-"], + "case_multisource_g1none_l1same1none": ["GaLa", "G-L-"], + "case_multisource_g1none_l1none1same": ["GaL-", "G-La"], + "case_multisource_g1none_l1diff1none": ["GaLb", "G-L-"], + "case_multisource_g1none_l1none1diff": ["GaL-", "G-Lb"], + "case_multisource_g1none_lallsame": ["GaLa", "G-La"], + "case_multisource_g1none_lallother": ["GaLc", "G-Lc"], "case_multisource_gdiff_lnone": ["GaL-", "GbL-"], "case_multisource_gdiff_l1same1none": ["GaLa", "GbL-"], "case_multisource_gdiff_l1diff1none": ["GaLb", "GcL-"], "case_multisource_gdiff_lallsame": ["GaLa", "GbLb"], "case_multisource_gdiff_lallother": ["GaLc", "GbLc"], - # - # TODO: improvements needed here -- ordering can be more consistent, missed checking - # "gdiff" multi-cases where one global is None, rather than a different value. - # } _MATRIX_TESTCASES = list(_MATRIX_TESTCASE_INPUTS.keys()) From 2377c89dc16d679f5078a0d640b1bc6975344a69 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 22 Aug 2023 00:31:39 +0100 Subject: [PATCH 35/38] Ensure valid sort-order for globals of possibly different types. --- lib/iris/tests/integration/test_netcdf__loadsaveattrs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 901b84c4f4..1a4b985b3d 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -432,7 +432,7 @@ def fetch_results( if cube.attributes.globals.get(attr_name, None) == globalval ] - for globalval in sorted(global_values) + for globalval in sorted(global_values, key=str) ] return results From 8987a2661d14a081f0dd53d4370270a121fcea22 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 22 Aug 2023 00:35:20 +0100 Subject: [PATCH 36/38] Initialise matrix results with legacy values from v3.6.1 -- all matching. --- .../attrs_matrix_results_load.json | 779 ++++++++++++++++++ .../attrs_matrix_results_roundtrip.json | 779 ++++++++++++++++++ .../attrs_matrix_results_save.json | 779 ++++++++++++++++++ 3 files changed, 2337 insertions(+) create mode 100644 lib/iris/tests/integration/attrs_matrix_results_load.json create mode 100644 lib/iris/tests/integration/attrs_matrix_results_roundtrip.json create mode 100644 lib/iris/tests/integration/attrs_matrix_results_save.json diff --git a/lib/iris/tests/integration/attrs_matrix_results_load.json b/lib/iris/tests/integration/attrs_matrix_results_load.json new file mode 100644 index 0000000000..1377e295b2 --- /dev/null +++ b/lib/iris/tests/integration/attrs_matrix_results_load.json @@ -0,0 +1,779 @@ +{ + "case_singlevar_localonly": { + "input": "G-La", + "localstyle": { + "legacy": [ + "G-La" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-La" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-La" + ], + "newstyle": null + } + }, + "case_singlevar_globalonly": { + "input": "GaL-", + "localstyle": { + "legacy": [ + "G-La" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-La" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-La" + ], + "newstyle": null + } + }, + "case_singlevar_glsame": { + "input": "GaLa", + "localstyle": { + "legacy": [ + "G-La" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-La" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-La" + ], + "newstyle": null + } + }, + "case_singlevar_gldiffer": { + "input": "GaLb", + "localstyle": { + "legacy": [ + "G-Lb" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lb" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lb" + ], + "newstyle": null + } + }, + "case_multivar_same_noglobal": { + "input": "G-Laa", + "localstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + } + }, + "case_multivar_same_sameglobal": { + "input": "GaLaa", + "localstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + } + }, + "case_multivar_same_diffglobal": { + "input": "GaLbb", + "localstyle": { + "legacy": [ + "G-Lbb" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lbb" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lbb" + ], + "newstyle": null + } + }, + "case_multivar_differ_noglobal": { + "input": "G-Lab", + "localstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + } + }, + "case_multivar_differ_diffglobal": { + "input": "GaLbc", + "localstyle": { + "legacy": [ + "G-Lbc" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lbc" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lbc" + ], + "newstyle": null + } + }, + "case_multivar_differ_sameglobal": { + "input": "GaLab", + "localstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + } + }, + "case_multivar_1none_noglobal": { + "input": "G-La-", + "localstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + } + }, + "case_multivar_1none_diffglobal": { + "input": "GaLb-", + "localstyle": { + "legacy": [ + "G-Lba" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lba" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lba" + ], + "newstyle": null + } + }, + "case_multivar_1none_sameglobal": { + "input": "GaLa-", + "localstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + } + }, + "case_multisource_gsame_lnone": { + "input": [ + "GaL-", + "GaL-" + ], + "localstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + } + }, + "case_multisource_gsame_lallsame": { + "input": [ + "GaLa", + "GaLa" + ], + "localstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + } + }, + "case_multisource_gsame_l1same1none": { + "input": [ + "GaLa", + "GaL-" + ], + "localstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + } + }, + "case_multisource_gsame_l1same1other": { + "input": [ + "GaLa", + "GaLb" + ], + "localstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + } + }, + "case_multisource_gsame_lallother": { + "input": [ + "GaLb", + "GaLb" + ], + "localstyle": { + "legacy": [ + "G-Lbb" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lbb" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lbb" + ], + "newstyle": null + } + }, + "case_multisource_gsame_lalldiffer": { + "input": [ + "GaLb", + "GaLc" + ], + "localstyle": { + "legacy": [ + "G-Lbc" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lbc" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lbc" + ], + "newstyle": null + } + }, + "case_multisource_gnone_l1same1none": { + "input": [ + "G-La", + "G-L-" + ], + "localstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + } + }, + "case_multisource_gnone_l1same1same": { + "input": [ + "G-La", + "G-La" + ], + "localstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + } + }, + "case_multisource_gnone_l1same1other": { + "input": [ + "G-La", + "G-Lb" + ], + "localstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + } + }, + "case_multisource_g1none_lnone": { + "input": [ + "GaL-", + "G-L-" + ], + "localstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + } + }, + "case_multisource_g1none_l1same1none": { + "input": [ + "GaLa", + "G-L-" + ], + "localstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-La-" + ], + "newstyle": null + } + }, + "case_multisource_g1none_l1none1same": { + "input": [ + "GaL-", + "G-La" + ], + "localstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + } + }, + "case_multisource_g1none_l1diff1none": { + "input": [ + "GaLb", + "G-L-" + ], + "localstyle": { + "legacy": [ + "G-Lb-" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lb-" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lb-" + ], + "newstyle": null + } + }, + "case_multisource_g1none_l1none1diff": { + "input": [ + "GaL-", + "G-Lb" + ], + "localstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + } + }, + "case_multisource_g1none_lallsame": { + "input": [ + "GaLa", + "G-La" + ], + "localstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Laa" + ], + "newstyle": null + } + }, + "case_multisource_g1none_lallother": { + "input": [ + "GaLc", + "G-Lc" + ], + "localstyle": { + "legacy": [ + "G-Lcc" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lcc" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lcc" + ], + "newstyle": null + } + }, + "case_multisource_gdiff_lnone": { + "input": [ + "GaL-", + "GbL-" + ], + "localstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + } + }, + "case_multisource_gdiff_l1same1none": { + "input": [ + "GaLa", + "GbL-" + ], + "localstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + } + }, + "case_multisource_gdiff_l1diff1none": { + "input": [ + "GaLb", + "GcL-" + ], + "localstyle": { + "legacy": [ + "G-Lbc" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lbc" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lbc" + ], + "newstyle": null + } + }, + "case_multisource_gdiff_lallsame": { + "input": [ + "GaLa", + "GbLb" + ], + "localstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lab" + ], + "newstyle": null + } + }, + "case_multisource_gdiff_lallother": { + "input": [ + "GaLc", + "GbLc" + ], + "localstyle": { + "legacy": [ + "G-Lcc" + ], + "newstyle": null + }, + "globalstyle": { + "legacy": [ + "G-Lcc" + ], + "newstyle": null + }, + "userstyle": { + "legacy": [ + "G-Lcc" + ], + "newstyle": null + } + } +} \ No newline at end of file diff --git a/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json b/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json new file mode 100644 index 0000000000..b131ee69d4 --- /dev/null +++ b/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json @@ -0,0 +1,779 @@ +{ + "case_singlevar_localonly": { + "input": "G-La", + "localstyle": { + "unsplit": [ + "G-La" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + } + }, + "case_singlevar_globalonly": { + "input": "GaL-", + "localstyle": { + "unsplit": [ + "G-La" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + } + }, + "case_singlevar_glsame": { + "input": "GaLa", + "localstyle": { + "unsplit": [ + "G-La" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + } + }, + "case_singlevar_gldiffer": { + "input": "GaLb", + "localstyle": { + "unsplit": [ + "G-Lb" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GbL-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GbL-" + ], + "split": null + } + }, + "case_multivar_same_noglobal": { + "input": "G-Laa", + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multivar_same_sameglobal": { + "input": "GaLaa", + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multivar_same_diffglobal": { + "input": "GaLbb", + "localstyle": { + "unsplit": [ + "G-Lbb" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GbL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GbL--" + ], + "split": null + } + }, + "case_multivar_differ_noglobal": { + "input": "G-Lab", + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multivar_differ_diffglobal": { + "input": "GaLbc", + "localstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + } + }, + "case_multivar_differ_sameglobal": { + "input": "GaLab", + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multivar_1none_noglobal": { + "input": "G-La-", + "localstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + } + }, + "case_multivar_1none_diffglobal": { + "input": "GaLb-", + "localstyle": { + "unsplit": [ + "G-Lba" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lba" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lba" + ], + "split": null + } + }, + "case_multivar_1none_sameglobal": { + "input": "GaLa-", + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gsame_lnone": { + "input": [ + "GaL-", + "GaL-" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gsame_lallsame": { + "input": [ + "GaLa", + "GaLa" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gsame_l1same1none": { + "input": [ + "GaLa", + "GaL-" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gsame_l1same1other": { + "input": [ + "GaLa", + "GaLb" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_gsame_lallother": { + "input": [ + "GaLb", + "GaLb" + ], + "localstyle": { + "unsplit": [ + "G-Lbb" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GbL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GbL--" + ], + "split": null + } + }, + "case_multisource_gsame_lalldiffer": { + "input": [ + "GaLb", + "GaLc" + ], + "localstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + } + }, + "case_multisource_gnone_l1same1none": { + "input": [ + "G-La", + "G-L-" + ], + "localstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + } + }, + "case_multisource_gnone_l1same1same": { + "input": [ + "G-La", + "G-La" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gnone_l1same1other": { + "input": [ + "G-La", + "G-Lb" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_g1none_lnone": { + "input": [ + "GaL-", + "G-L-" + ], + "localstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + } + }, + "case_multisource_g1none_l1same1none": { + "input": [ + "GaLa", + "G-L-" + ], + "localstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + } + }, + "case_multisource_g1none_l1none1same": { + "input": [ + "GaL-", + "G-La" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_g1none_l1diff1none": { + "input": [ + "GaLb", + "G-L-" + ], + "localstyle": { + "unsplit": [ + "G-Lb-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lb-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lb-" + ], + "split": null + } + }, + "case_multisource_g1none_l1none1diff": { + "input": [ + "GaL-", + "G-Lb" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_g1none_lallsame": { + "input": [ + "GaLa", + "G-La" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_g1none_lallother": { + "input": [ + "GaLc", + "G-Lc" + ], + "localstyle": { + "unsplit": [ + "G-Lcc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GcL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GcL--" + ], + "split": null + } + }, + "case_multisource_gdiff_lnone": { + "input": [ + "GaL-", + "GbL-" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_gdiff_l1same1none": { + "input": [ + "GaLa", + "GbL-" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_gdiff_l1diff1none": { + "input": [ + "GaLb", + "GcL-" + ], + "localstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + } + }, + "case_multisource_gdiff_lallsame": { + "input": [ + "GaLa", + "GbLb" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_gdiff_lallother": { + "input": [ + "GaLc", + "GbLc" + ], + "localstyle": { + "unsplit": [ + "G-Lcc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GcL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GcL--" + ], + "split": null + } + } +} \ No newline at end of file diff --git a/lib/iris/tests/integration/attrs_matrix_results_save.json b/lib/iris/tests/integration/attrs_matrix_results_save.json new file mode 100644 index 0000000000..b131ee69d4 --- /dev/null +++ b/lib/iris/tests/integration/attrs_matrix_results_save.json @@ -0,0 +1,779 @@ +{ + "case_singlevar_localonly": { + "input": "G-La", + "localstyle": { + "unsplit": [ + "G-La" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + } + }, + "case_singlevar_globalonly": { + "input": "GaL-", + "localstyle": { + "unsplit": [ + "G-La" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + } + }, + "case_singlevar_glsame": { + "input": "GaLa", + "localstyle": { + "unsplit": [ + "G-La" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL-" + ], + "split": null + } + }, + "case_singlevar_gldiffer": { + "input": "GaLb", + "localstyle": { + "unsplit": [ + "G-Lb" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GbL-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GbL-" + ], + "split": null + } + }, + "case_multivar_same_noglobal": { + "input": "G-Laa", + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multivar_same_sameglobal": { + "input": "GaLaa", + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multivar_same_diffglobal": { + "input": "GaLbb", + "localstyle": { + "unsplit": [ + "G-Lbb" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GbL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GbL--" + ], + "split": null + } + }, + "case_multivar_differ_noglobal": { + "input": "G-Lab", + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multivar_differ_diffglobal": { + "input": "GaLbc", + "localstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + } + }, + "case_multivar_differ_sameglobal": { + "input": "GaLab", + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multivar_1none_noglobal": { + "input": "G-La-", + "localstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + } + }, + "case_multivar_1none_diffglobal": { + "input": "GaLb-", + "localstyle": { + "unsplit": [ + "G-Lba" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lba" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lba" + ], + "split": null + } + }, + "case_multivar_1none_sameglobal": { + "input": "GaLa-", + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gsame_lnone": { + "input": [ + "GaL-", + "GaL-" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gsame_lallsame": { + "input": [ + "GaLa", + "GaLa" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gsame_l1same1none": { + "input": [ + "GaLa", + "GaL-" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gsame_l1same1other": { + "input": [ + "GaLa", + "GaLb" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_gsame_lallother": { + "input": [ + "GaLb", + "GaLb" + ], + "localstyle": { + "unsplit": [ + "G-Lbb" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GbL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GbL--" + ], + "split": null + } + }, + "case_multisource_gsame_lalldiffer": { + "input": [ + "GaLb", + "GaLc" + ], + "localstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + } + }, + "case_multisource_gnone_l1same1none": { + "input": [ + "G-La", + "G-L-" + ], + "localstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + } + }, + "case_multisource_gnone_l1same1same": { + "input": [ + "G-La", + "G-La" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_gnone_l1same1other": { + "input": [ + "G-La", + "G-Lb" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_g1none_lnone": { + "input": [ + "GaL-", + "G-L-" + ], + "localstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + } + }, + "case_multisource_g1none_l1same1none": { + "input": [ + "GaLa", + "G-L-" + ], + "localstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-La-" + ], + "split": null + } + }, + "case_multisource_g1none_l1none1same": { + "input": [ + "GaL-", + "G-La" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_g1none_l1diff1none": { + "input": [ + "GaLb", + "G-L-" + ], + "localstyle": { + "unsplit": [ + "G-Lb-" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lb-" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lb-" + ], + "split": null + } + }, + "case_multisource_g1none_l1none1diff": { + "input": [ + "GaL-", + "G-Lb" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_g1none_lallsame": { + "input": [ + "GaLa", + "G-La" + ], + "localstyle": { + "unsplit": [ + "G-Laa" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GaL--" + ], + "split": null + } + }, + "case_multisource_g1none_lallother": { + "input": [ + "GaLc", + "G-Lc" + ], + "localstyle": { + "unsplit": [ + "G-Lcc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GcL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GcL--" + ], + "split": null + } + }, + "case_multisource_gdiff_lnone": { + "input": [ + "GaL-", + "GbL-" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_gdiff_l1same1none": { + "input": [ + "GaLa", + "GbL-" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_gdiff_l1diff1none": { + "input": [ + "GaLb", + "GcL-" + ], + "localstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lbc" + ], + "split": null + } + }, + "case_multisource_gdiff_lallsame": { + "input": [ + "GaLa", + "GbLb" + ], + "localstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "G-Lab" + ], + "split": null + } + }, + "case_multisource_gdiff_lallother": { + "input": [ + "GaLc", + "GbLc" + ], + "localstyle": { + "unsplit": [ + "G-Lcc" + ], + "split": null + }, + "globalstyle": { + "unsplit": [ + "GcL--" + ], + "split": null + }, + "userstyle": { + "unsplit": [ + "GcL--" + ], + "split": null + } + } +} \ No newline at end of file From cbd7167540d1e823f18e62805b5eb0fc2eecf3dc Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 22 Aug 2023 00:38:02 +0100 Subject: [PATCH 37/38] Add full current matrix results, i.e. snapshot current behaviours. --- .../attrs_matrix_results_load.json | 444 ++++++++++++++---- .../attrs_matrix_results_roundtrip.json | 408 ++++++++++++---- .../attrs_matrix_results_save.json | 408 ++++++++++++---- 3 files changed, 954 insertions(+), 306 deletions(-) diff --git a/lib/iris/tests/integration/attrs_matrix_results_load.json b/lib/iris/tests/integration/attrs_matrix_results_load.json index 1377e295b2..111ca530a9 100644 --- a/lib/iris/tests/integration/attrs_matrix_results_load.json +++ b/lib/iris/tests/integration/attrs_matrix_results_load.json @@ -5,19 +5,25 @@ "legacy": [ "G-La" ], - "newstyle": null + "newstyle": [ + "G-La" + ] }, "globalstyle": { "legacy": [ "G-La" ], - "newstyle": null + "newstyle": [ + "G-La" + ] }, "userstyle": { "legacy": [ "G-La" ], - "newstyle": null + "newstyle": [ + "G-La" + ] } }, "case_singlevar_globalonly": { @@ -26,19 +32,25 @@ "legacy": [ "G-La" ], - "newstyle": null + "newstyle": [ + "GaL-" + ] }, "globalstyle": { "legacy": [ "G-La" ], - "newstyle": null + "newstyle": [ + "GaL-" + ] }, "userstyle": { "legacy": [ "G-La" ], - "newstyle": null + "newstyle": [ + "GaL-" + ] } }, "case_singlevar_glsame": { @@ -47,19 +59,25 @@ "legacy": [ "G-La" ], - "newstyle": null + "newstyle": [ + "GaLa" + ] }, "globalstyle": { "legacy": [ "G-La" ], - "newstyle": null + "newstyle": [ + "GaLa" + ] }, "userstyle": { "legacy": [ "G-La" ], - "newstyle": null + "newstyle": [ + "GaLa" + ] } }, "case_singlevar_gldiffer": { @@ -68,19 +86,25 @@ "legacy": [ "G-Lb" ], - "newstyle": null + "newstyle": [ + "GaLb" + ] }, "globalstyle": { "legacy": [ "G-Lb" ], - "newstyle": null + "newstyle": [ + "GaLb" + ] }, "userstyle": { "legacy": [ "G-Lb" ], - "newstyle": null + "newstyle": [ + "GaLb" + ] } }, "case_multivar_same_noglobal": { @@ -89,19 +113,25 @@ "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-Laa" + ] }, "globalstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-Laa" + ] }, "userstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-Laa" + ] } }, "case_multivar_same_sameglobal": { @@ -110,19 +140,25 @@ "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLaa" + ] }, "globalstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLaa" + ] }, "userstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLaa" + ] } }, "case_multivar_same_diffglobal": { @@ -131,19 +167,25 @@ "legacy": [ "G-Lbb" ], - "newstyle": null + "newstyle": [ + "GaLbb" + ] }, "globalstyle": { "legacy": [ "G-Lbb" ], - "newstyle": null + "newstyle": [ + "GaLbb" + ] }, "userstyle": { "legacy": [ "G-Lbb" ], - "newstyle": null + "newstyle": [ + "GaLbb" + ] } }, "case_multivar_differ_noglobal": { @@ -152,19 +194,25 @@ "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "G-Lab" + ] }, "globalstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "G-Lab" + ] }, "userstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "G-Lab" + ] } }, "case_multivar_differ_diffglobal": { @@ -173,19 +221,25 @@ "legacy": [ "G-Lbc" ], - "newstyle": null + "newstyle": [ + "GaLbc" + ] }, "globalstyle": { "legacy": [ "G-Lbc" ], - "newstyle": null + "newstyle": [ + "GaLbc" + ] }, "userstyle": { "legacy": [ "G-Lbc" ], - "newstyle": null + "newstyle": [ + "GaLbc" + ] } }, "case_multivar_differ_sameglobal": { @@ -194,19 +248,25 @@ "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLab" + ] }, "globalstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLab" + ] }, "userstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLab" + ] } }, "case_multivar_1none_noglobal": { @@ -215,19 +275,25 @@ "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-La-" + ] }, "globalstyle": { "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-La-" + ] }, "userstyle": { "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-La-" + ] } }, "case_multivar_1none_diffglobal": { @@ -236,19 +302,25 @@ "legacy": [ "G-Lba" ], - "newstyle": null + "newstyle": [ + "GaLb-" + ] }, "globalstyle": { "legacy": [ "G-Lba" ], - "newstyle": null + "newstyle": [ + "GaLb-" + ] }, "userstyle": { "legacy": [ "G-Lba" ], - "newstyle": null + "newstyle": [ + "GaLb-" + ] } }, "case_multivar_1none_sameglobal": { @@ -257,19 +329,25 @@ "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLa-" + ] }, "globalstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLa-" + ] }, "userstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLa-" + ] } }, "case_multisource_gsame_lnone": { @@ -281,19 +359,25 @@ "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaL--" + ] }, "globalstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaL--" + ] }, "userstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaL--" + ] } }, "case_multisource_gsame_lallsame": { @@ -305,19 +389,25 @@ "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLaa" + ] }, "globalstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLaa" + ] }, "userstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLaa" + ] } }, "case_multisource_gsame_l1same1none": { @@ -329,19 +419,25 @@ "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLa-" + ] }, "globalstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLa-" + ] }, "userstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "GaLa-" + ] } }, "case_multisource_gsame_l1same1other": { @@ -353,19 +449,25 @@ "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLab" + ] }, "globalstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLab" + ] }, "userstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLab" + ] } }, "case_multisource_gsame_lallother": { @@ -377,19 +479,25 @@ "legacy": [ "G-Lbb" ], - "newstyle": null + "newstyle": [ + "GaLbb" + ] }, "globalstyle": { "legacy": [ "G-Lbb" ], - "newstyle": null + "newstyle": [ + "GaLbb" + ] }, "userstyle": { "legacy": [ "G-Lbb" ], - "newstyle": null + "newstyle": [ + "GaLbb" + ] } }, "case_multisource_gsame_lalldiffer": { @@ -401,19 +509,25 @@ "legacy": [ "G-Lbc" ], - "newstyle": null + "newstyle": [ + "GaLbc" + ] }, "globalstyle": { "legacy": [ "G-Lbc" ], - "newstyle": null + "newstyle": [ + "GaLbc" + ] }, "userstyle": { "legacy": [ "G-Lbc" ], - "newstyle": null + "newstyle": [ + "GaLbc" + ] } }, "case_multisource_gnone_l1same1none": { @@ -425,19 +539,25 @@ "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-La-" + ] }, "globalstyle": { "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-La-" + ] }, "userstyle": { "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-La-" + ] } }, "case_multisource_gnone_l1same1same": { @@ -449,19 +569,25 @@ "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-Laa" + ] }, "globalstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-Laa" + ] }, "userstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-Laa" + ] } }, "case_multisource_gnone_l1same1other": { @@ -473,19 +599,25 @@ "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "G-Lab" + ] }, "globalstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "G-Lab" + ] }, "userstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "G-Lab" + ] } }, "case_multisource_g1none_lnone": { @@ -497,19 +629,28 @@ "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-L-", + "GaL-" + ] }, "globalstyle": { "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-L-", + "GaL-" + ] }, "userstyle": { "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-L-", + "GaL-" + ] } }, "case_multisource_g1none_l1same1none": { @@ -521,19 +662,28 @@ "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-L-", + "GaLa" + ] }, "globalstyle": { "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-L-", + "GaLa" + ] }, "userstyle": { "legacy": [ "G-La-" ], - "newstyle": null + "newstyle": [ + "G-L-", + "GaLa" + ] } }, "case_multisource_g1none_l1none1same": { @@ -545,19 +695,28 @@ "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-La", + "GaL-" + ] }, "globalstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-La", + "GaL-" + ] }, "userstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-La", + "GaL-" + ] } }, "case_multisource_g1none_l1diff1none": { @@ -569,19 +728,28 @@ "legacy": [ "G-Lb-" ], - "newstyle": null + "newstyle": [ + "G-L-", + "GaLb" + ] }, "globalstyle": { "legacy": [ "G-Lb-" ], - "newstyle": null + "newstyle": [ + "G-L-", + "GaLb" + ] }, "userstyle": { "legacy": [ "G-Lb-" ], - "newstyle": null + "newstyle": [ + "G-L-", + "GaLb" + ] } }, "case_multisource_g1none_l1none1diff": { @@ -593,19 +761,28 @@ "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "G-Lb", + "GaL-" + ] }, "globalstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "G-Lb", + "GaL-" + ] }, "userstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "G-Lb", + "GaL-" + ] } }, "case_multisource_g1none_lallsame": { @@ -617,19 +794,28 @@ "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-La", + "GaLa" + ] }, "globalstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-La", + "GaLa" + ] }, "userstyle": { "legacy": [ "G-Laa" ], - "newstyle": null + "newstyle": [ + "G-La", + "GaLa" + ] } }, "case_multisource_g1none_lallother": { @@ -641,19 +827,28 @@ "legacy": [ "G-Lcc" ], - "newstyle": null + "newstyle": [ + "G-Lc", + "GaLc" + ] }, "globalstyle": { "legacy": [ "G-Lcc" ], - "newstyle": null + "newstyle": [ + "G-Lc", + "GaLc" + ] }, "userstyle": { "legacy": [ "G-Lcc" ], - "newstyle": null + "newstyle": [ + "G-Lc", + "GaLc" + ] } }, "case_multisource_gdiff_lnone": { @@ -665,19 +860,28 @@ "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaL-", + "GbL-" + ] }, "globalstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaL-", + "GbL-" + ] }, "userstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaL-", + "GbL-" + ] } }, "case_multisource_gdiff_l1same1none": { @@ -689,19 +893,28 @@ "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLa", + "GbL-" + ] }, "globalstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLa", + "GbL-" + ] }, "userstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLa", + "GbL-" + ] } }, "case_multisource_gdiff_l1diff1none": { @@ -713,19 +926,28 @@ "legacy": [ "G-Lbc" ], - "newstyle": null + "newstyle": [ + "GaLb", + "GcL-" + ] }, "globalstyle": { "legacy": [ "G-Lbc" ], - "newstyle": null + "newstyle": [ + "GaLb", + "GcL-" + ] }, "userstyle": { "legacy": [ "G-Lbc" ], - "newstyle": null + "newstyle": [ + "GaLb", + "GcL-" + ] } }, "case_multisource_gdiff_lallsame": { @@ -737,19 +959,28 @@ "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLa", + "GbLb" + ] }, "globalstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLa", + "GbLb" + ] }, "userstyle": { "legacy": [ "G-Lab" ], - "newstyle": null + "newstyle": [ + "GaLa", + "GbLb" + ] } }, "case_multisource_gdiff_lallother": { @@ -761,19 +992,28 @@ "legacy": [ "G-Lcc" ], - "newstyle": null + "newstyle": [ + "GaLc", + "GbLc" + ] }, "globalstyle": { "legacy": [ "G-Lcc" ], - "newstyle": null + "newstyle": [ + "GaLc", + "GbLc" + ] }, "userstyle": { "legacy": [ "G-Lcc" ], - "newstyle": null + "newstyle": [ + "GaLc", + "GbLc" + ] } } } \ No newline at end of file diff --git a/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json b/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json index b131ee69d4..0f0968abf8 100644 --- a/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json +++ b/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json @@ -5,19 +5,25 @@ "unsplit": [ "G-La" ], - "split": null + "split": [ + "G-La" + ] }, "globalstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "G-La" + ] }, "userstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "G-La" + ] } }, "case_singlevar_globalonly": { @@ -26,19 +32,25 @@ "unsplit": [ "G-La" ], - "split": null + "split": [ + "GaL-" + ] }, "globalstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "GaL-" + ] }, "userstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "GaL-" + ] } }, "case_singlevar_glsame": { @@ -47,19 +59,25 @@ "unsplit": [ "G-La" ], - "split": null + "split": [ + "GaLa" + ] }, "globalstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "GaLa" + ] }, "userstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "GaLa" + ] } }, "case_singlevar_gldiffer": { @@ -68,19 +86,25 @@ "unsplit": [ "G-Lb" ], - "split": null + "split": [ + "GaLb" + ] }, "globalstyle": { "unsplit": [ "GbL-" ], - "split": null + "split": [ + "GaLb" + ] }, "userstyle": { "unsplit": [ "GbL-" ], - "split": null + "split": [ + "GaLb" + ] } }, "case_multivar_same_noglobal": { @@ -89,19 +113,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "G-Laa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] } }, "case_multivar_same_sameglobal": { @@ -110,19 +140,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaLaa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLaa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLaa" + ] } }, "case_multivar_same_diffglobal": { @@ -131,19 +167,25 @@ "unsplit": [ "G-Lbb" ], - "split": null + "split": [ + "GaLbb" + ] }, "globalstyle": { "unsplit": [ "GbL--" ], - "split": null + "split": [ + "GaLbb" + ] }, "userstyle": { "unsplit": [ "GbL--" ], - "split": null + "split": [ + "GaLbb" + ] } }, "case_multivar_differ_noglobal": { @@ -152,19 +194,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multivar_differ_diffglobal": { @@ -173,19 +221,25 @@ "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] }, "globalstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] }, "userstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] } }, "case_multivar_differ_sameglobal": { @@ -194,19 +248,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] } }, "case_multivar_1none_noglobal": { @@ -215,19 +275,25 @@ "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "globalstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "userstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] } }, "case_multivar_1none_diffglobal": { @@ -236,19 +302,25 @@ "unsplit": [ "G-Lba" ], - "split": null + "split": [ + "GaLb-" + ] }, "globalstyle": { "unsplit": [ "G-Lba" ], - "split": null + "split": [ + "GaLb-" + ] }, "userstyle": { "unsplit": [ "G-Lba" ], - "split": null + "split": [ + "GaLb-" + ] } }, "case_multivar_1none_sameglobal": { @@ -257,19 +329,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaLa-" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLa-" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLa-" + ] } }, "case_multisource_gsame_lnone": { @@ -281,19 +359,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaL--" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaL--" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaL--" + ] } }, "case_multisource_gsame_lallsame": { @@ -305,19 +389,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaLaa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLaa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLaa" + ] } }, "case_multisource_gsame_l1same1none": { @@ -329,19 +419,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaLa-" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLa-" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLa-" + ] } }, "case_multisource_gsame_l1same1other": { @@ -353,19 +449,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] } }, "case_multisource_gsame_lallother": { @@ -377,19 +479,25 @@ "unsplit": [ "G-Lbb" ], - "split": null + "split": [ + "GaLbb" + ] }, "globalstyle": { "unsplit": [ "GbL--" ], - "split": null + "split": [ + "GaLbb" + ] }, "userstyle": { "unsplit": [ "GbL--" ], - "split": null + "split": [ + "GaLbb" + ] } }, "case_multisource_gsame_lalldiffer": { @@ -401,19 +509,25 @@ "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] }, "globalstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] }, "userstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] } }, "case_multisource_gnone_l1same1none": { @@ -425,19 +539,25 @@ "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "globalstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "userstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] } }, "case_multisource_gnone_l1same1same": { @@ -449,19 +569,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "G-Laa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] } }, "case_multisource_gnone_l1same1other": { @@ -473,19 +599,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_g1none_lnone": { @@ -497,19 +629,25 @@ "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "globalstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "userstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] } }, "case_multisource_g1none_l1same1none": { @@ -521,19 +659,25 @@ "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "globalstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "userstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] } }, "case_multisource_g1none_l1none1same": { @@ -545,19 +689,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "G-Laa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] } }, "case_multisource_g1none_l1diff1none": { @@ -569,19 +719,25 @@ "unsplit": [ "G-Lb-" ], - "split": null + "split": [ + "G-Lb-" + ] }, "globalstyle": { "unsplit": [ "G-Lb-" ], - "split": null + "split": [ + "G-Lb-" + ] }, "userstyle": { "unsplit": [ "G-Lb-" ], - "split": null + "split": [ + "G-Lb-" + ] } }, "case_multisource_g1none_l1none1diff": { @@ -593,19 +749,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_g1none_lallsame": { @@ -617,19 +779,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "G-Laa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] } }, "case_multisource_g1none_lallother": { @@ -641,19 +809,25 @@ "unsplit": [ "G-Lcc" ], - "split": null + "split": [ + "G-Lcc" + ] }, "globalstyle": { "unsplit": [ "GcL--" ], - "split": null + "split": [ + "G-Lcc" + ] }, "userstyle": { "unsplit": [ "GcL--" ], - "split": null + "split": [ + "G-Lcc" + ] } }, "case_multisource_gdiff_lnone": { @@ -665,19 +839,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_gdiff_l1same1none": { @@ -689,19 +869,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_gdiff_l1diff1none": { @@ -713,19 +899,25 @@ "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "G-Lbc" + ] }, "globalstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "G-Lbc" + ] }, "userstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "G-Lbc" + ] } }, "case_multisource_gdiff_lallsame": { @@ -737,19 +929,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_gdiff_lallother": { @@ -761,19 +959,25 @@ "unsplit": [ "G-Lcc" ], - "split": null + "split": [ + "G-Lcc" + ] }, "globalstyle": { "unsplit": [ "GcL--" ], - "split": null + "split": [ + "G-Lcc" + ] }, "userstyle": { "unsplit": [ "GcL--" ], - "split": null + "split": [ + "G-Lcc" + ] } } } \ No newline at end of file diff --git a/lib/iris/tests/integration/attrs_matrix_results_save.json b/lib/iris/tests/integration/attrs_matrix_results_save.json index b131ee69d4..0f0968abf8 100644 --- a/lib/iris/tests/integration/attrs_matrix_results_save.json +++ b/lib/iris/tests/integration/attrs_matrix_results_save.json @@ -5,19 +5,25 @@ "unsplit": [ "G-La" ], - "split": null + "split": [ + "G-La" + ] }, "globalstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "G-La" + ] }, "userstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "G-La" + ] } }, "case_singlevar_globalonly": { @@ -26,19 +32,25 @@ "unsplit": [ "G-La" ], - "split": null + "split": [ + "GaL-" + ] }, "globalstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "GaL-" + ] }, "userstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "GaL-" + ] } }, "case_singlevar_glsame": { @@ -47,19 +59,25 @@ "unsplit": [ "G-La" ], - "split": null + "split": [ + "GaLa" + ] }, "globalstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "GaLa" + ] }, "userstyle": { "unsplit": [ "GaL-" ], - "split": null + "split": [ + "GaLa" + ] } }, "case_singlevar_gldiffer": { @@ -68,19 +86,25 @@ "unsplit": [ "G-Lb" ], - "split": null + "split": [ + "GaLb" + ] }, "globalstyle": { "unsplit": [ "GbL-" ], - "split": null + "split": [ + "GaLb" + ] }, "userstyle": { "unsplit": [ "GbL-" ], - "split": null + "split": [ + "GaLb" + ] } }, "case_multivar_same_noglobal": { @@ -89,19 +113,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "G-Laa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] } }, "case_multivar_same_sameglobal": { @@ -110,19 +140,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaLaa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLaa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLaa" + ] } }, "case_multivar_same_diffglobal": { @@ -131,19 +167,25 @@ "unsplit": [ "G-Lbb" ], - "split": null + "split": [ + "GaLbb" + ] }, "globalstyle": { "unsplit": [ "GbL--" ], - "split": null + "split": [ + "GaLbb" + ] }, "userstyle": { "unsplit": [ "GbL--" ], - "split": null + "split": [ + "GaLbb" + ] } }, "case_multivar_differ_noglobal": { @@ -152,19 +194,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multivar_differ_diffglobal": { @@ -173,19 +221,25 @@ "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] }, "globalstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] }, "userstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] } }, "case_multivar_differ_sameglobal": { @@ -194,19 +248,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] } }, "case_multivar_1none_noglobal": { @@ -215,19 +275,25 @@ "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "globalstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "userstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] } }, "case_multivar_1none_diffglobal": { @@ -236,19 +302,25 @@ "unsplit": [ "G-Lba" ], - "split": null + "split": [ + "GaLb-" + ] }, "globalstyle": { "unsplit": [ "G-Lba" ], - "split": null + "split": [ + "GaLb-" + ] }, "userstyle": { "unsplit": [ "G-Lba" ], - "split": null + "split": [ + "GaLb-" + ] } }, "case_multivar_1none_sameglobal": { @@ -257,19 +329,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaLa-" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLa-" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLa-" + ] } }, "case_multisource_gsame_lnone": { @@ -281,19 +359,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaL--" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaL--" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaL--" + ] } }, "case_multisource_gsame_lallsame": { @@ -305,19 +389,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaLaa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLaa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLaa" + ] } }, "case_multisource_gsame_l1same1none": { @@ -329,19 +419,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "GaLa-" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLa-" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "GaLa-" + ] } }, "case_multisource_gsame_l1same1other": { @@ -353,19 +449,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "GaLab" + ] } }, "case_multisource_gsame_lallother": { @@ -377,19 +479,25 @@ "unsplit": [ "G-Lbb" ], - "split": null + "split": [ + "GaLbb" + ] }, "globalstyle": { "unsplit": [ "GbL--" ], - "split": null + "split": [ + "GaLbb" + ] }, "userstyle": { "unsplit": [ "GbL--" ], - "split": null + "split": [ + "GaLbb" + ] } }, "case_multisource_gsame_lalldiffer": { @@ -401,19 +509,25 @@ "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] }, "globalstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] }, "userstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "GaLbc" + ] } }, "case_multisource_gnone_l1same1none": { @@ -425,19 +539,25 @@ "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "globalstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "userstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] } }, "case_multisource_gnone_l1same1same": { @@ -449,19 +569,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "G-Laa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] } }, "case_multisource_gnone_l1same1other": { @@ -473,19 +599,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_g1none_lnone": { @@ -497,19 +629,25 @@ "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "globalstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "userstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] } }, "case_multisource_g1none_l1same1none": { @@ -521,19 +659,25 @@ "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "globalstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] }, "userstyle": { "unsplit": [ "G-La-" ], - "split": null + "split": [ + "G-La-" + ] } }, "case_multisource_g1none_l1none1same": { @@ -545,19 +689,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "G-Laa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] } }, "case_multisource_g1none_l1diff1none": { @@ -569,19 +719,25 @@ "unsplit": [ "G-Lb-" ], - "split": null + "split": [ + "G-Lb-" + ] }, "globalstyle": { "unsplit": [ "G-Lb-" ], - "split": null + "split": [ + "G-Lb-" + ] }, "userstyle": { "unsplit": [ "G-Lb-" ], - "split": null + "split": [ + "G-Lb-" + ] } }, "case_multisource_g1none_l1none1diff": { @@ -593,19 +749,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_g1none_lallsame": { @@ -617,19 +779,25 @@ "unsplit": [ "G-Laa" ], - "split": null + "split": [ + "G-Laa" + ] }, "globalstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] }, "userstyle": { "unsplit": [ "GaL--" ], - "split": null + "split": [ + "G-Laa" + ] } }, "case_multisource_g1none_lallother": { @@ -641,19 +809,25 @@ "unsplit": [ "G-Lcc" ], - "split": null + "split": [ + "G-Lcc" + ] }, "globalstyle": { "unsplit": [ "GcL--" ], - "split": null + "split": [ + "G-Lcc" + ] }, "userstyle": { "unsplit": [ "GcL--" ], - "split": null + "split": [ + "G-Lcc" + ] } }, "case_multisource_gdiff_lnone": { @@ -665,19 +839,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_gdiff_l1same1none": { @@ -689,19 +869,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_gdiff_l1diff1none": { @@ -713,19 +899,25 @@ "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "G-Lbc" + ] }, "globalstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "G-Lbc" + ] }, "userstyle": { "unsplit": [ "G-Lbc" ], - "split": null + "split": [ + "G-Lbc" + ] } }, "case_multisource_gdiff_lallsame": { @@ -737,19 +929,25 @@ "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "globalstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] }, "userstyle": { "unsplit": [ "G-Lab" ], - "split": null + "split": [ + "G-Lab" + ] } }, "case_multisource_gdiff_lallother": { @@ -761,19 +959,25 @@ "unsplit": [ "G-Lcc" ], - "split": null + "split": [ + "G-Lcc" + ] }, "globalstyle": { "unsplit": [ "GcL--" ], - "split": null + "split": [ + "G-Lcc" + ] }, "userstyle": { "unsplit": [ "GcL--" ], - "split": null + "split": [ + "G-Lcc" + ] } } } \ No newline at end of file From eea99d1d060cc64d6354021572f6fb89da4b2860 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 10 Oct 2023 10:52:34 +0100 Subject: [PATCH 38/38] Review changes : rename some matrix testcases, for clarity. --- lib/iris/tests/integration/attrs_matrix_results_load.json | 6 +++--- .../tests/integration/attrs_matrix_results_roundtrip.json | 6 +++--- lib/iris/tests/integration/attrs_matrix_results_save.json | 6 +++--- lib/iris/tests/integration/test_netcdf__loadsaveattrs.py | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/iris/tests/integration/attrs_matrix_results_load.json b/lib/iris/tests/integration/attrs_matrix_results_load.json index 111ca530a9..a1d37708a9 100644 --- a/lib/iris/tests/integration/attrs_matrix_results_load.json +++ b/lib/iris/tests/integration/attrs_matrix_results_load.json @@ -530,7 +530,7 @@ ] } }, - "case_multisource_gnone_l1same1none": { + "case_multisource_gnone_l1one1none": { "input": [ "G-La", "G-L-" @@ -560,7 +560,7 @@ ] } }, - "case_multisource_gnone_l1same1same": { + "case_multisource_gnone_l1one1same": { "input": [ "G-La", "G-La" @@ -590,7 +590,7 @@ ] } }, - "case_multisource_gnone_l1same1other": { + "case_multisource_gnone_l1one1other": { "input": [ "G-La", "G-Lb" diff --git a/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json b/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json index 0f0968abf8..3446c7f312 100644 --- a/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json +++ b/lib/iris/tests/integration/attrs_matrix_results_roundtrip.json @@ -530,7 +530,7 @@ ] } }, - "case_multisource_gnone_l1same1none": { + "case_multisource_gnone_l1one1none": { "input": [ "G-La", "G-L-" @@ -560,7 +560,7 @@ ] } }, - "case_multisource_gnone_l1same1same": { + "case_multisource_gnone_l1one1same": { "input": [ "G-La", "G-La" @@ -590,7 +590,7 @@ ] } }, - "case_multisource_gnone_l1same1other": { + "case_multisource_gnone_l1one1other": { "input": [ "G-La", "G-Lb" diff --git a/lib/iris/tests/integration/attrs_matrix_results_save.json b/lib/iris/tests/integration/attrs_matrix_results_save.json index 0f0968abf8..3446c7f312 100644 --- a/lib/iris/tests/integration/attrs_matrix_results_save.json +++ b/lib/iris/tests/integration/attrs_matrix_results_save.json @@ -530,7 +530,7 @@ ] } }, - "case_multisource_gnone_l1same1none": { + "case_multisource_gnone_l1one1none": { "input": [ "G-La", "G-L-" @@ -560,7 +560,7 @@ ] } }, - "case_multisource_gnone_l1same1same": { + "case_multisource_gnone_l1one1same": { "input": [ "G-La", "G-La" @@ -590,7 +590,7 @@ ] } }, - "case_multisource_gnone_l1same1other": { + "case_multisource_gnone_l1one1other": { "input": [ "G-La", "G-Lb" diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index 1a4b985b3d..9bd996312c 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -465,9 +465,9 @@ def fetch_results( "case_multisource_gsame_l1same1other": ["GaLa", "GaLb"], "case_multisource_gsame_lallother": ["GaLb", "GaLb"], "case_multisource_gsame_lalldiffer": ["GaLb", "GaLc"], - "case_multisource_gnone_l1same1none": ["G-La", "G-L-"], - "case_multisource_gnone_l1same1same": ["G-La", "G-La"], - "case_multisource_gnone_l1same1other": ["G-La", "G-Lb"], + "case_multisource_gnone_l1one1none": ["G-La", "G-L-"], + "case_multisource_gnone_l1one1same": ["G-La", "G-La"], + "case_multisource_gnone_l1one1other": ["G-La", "G-Lb"], "case_multisource_g1none_lnone": ["GaL-", "G-L-"], "case_multisource_g1none_l1same1none": ["GaLa", "G-L-"], "case_multisource_g1none_l1none1same": ["GaL-", "G-La"],