From 3fdccacdfd84bcbdd24dd50baa902d9911c596e8 Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Mon, 1 Jun 2020 12:27:23 +0100 Subject: [PATCH 01/32] Change default units to "unknown" for all DimensionalMetadata (#3713) --- .../Meteorology/lagged_ensemble.py | 2 +- docs/iris/src/userguide/navigating_a_cube.rst | 2 +- lib/iris/analysis/__init__.py | 4 +- lib/iris/coords.py | 12 ++--- lib/iris/fileformats/pp_load_rules.py | 18 +++++-- lib/iris/tests/integration/test_netcdf.py | 4 +- lib/iris/tests/integration/test_pp.py | 14 +++-- .../analysis/first_quartile_foo_1d.cml | 2 +- .../first_quartile_foo_1d_fast_percentile.cml | 2 +- .../analysis/first_quartile_foo_2d.cml | 2 +- .../first_quartile_foo_2d_fast_percentile.cml | 2 +- .../analysis/first_quartile_foo_bar_2d.cml | 2 +- ...st_quartile_foo_bar_2d_fast_percentile.cml | 2 +- .../analysis/last_quartile_foo_3d_masked.cml | 2 +- .../last_quartile_foo_3d_notmasked.cml | 2 +- ...rtile_foo_3d_notmasked_fast_percentile.cml | 2 +- .../analysis/third_quartile_foo_1d.cml | 2 +- .../third_quartile_foo_1d_fast_percentile.cml | 2 +- lib/iris/tests/results/coord_api/minimal.xml | 2 +- .../TestClimatology/reference_simpledata.cdl | 4 +- .../results/netcdf/netcdf_save_no_name.cdl | 1 - .../netcdf/Saver/write/with_climatology.cdl | 4 +- lib/iris/tests/stock/__init__.py | 4 +- lib/iris/tests/test_aggregate_by.py | 8 ++- lib/iris/tests/test_concatenate.py | 54 +++++++++++-------- lib/iris/tests/test_coord_api.py | 4 +- lib/iris/tests/test_iterate.py | 13 +++-- lib/iris/tests/test_netcdf.py | 22 ++++---- lib/iris/tests/test_plot.py | 1 + .../test_RectilinearInterpolator.py | 12 +++-- .../regrid/test_RectilinearRegridder.py | 2 +- .../analysis/test_PercentileAggregator.py | 8 +-- .../test_WeightedPercentileAggregator.py | 8 +-- .../aux_factory/test_HybridPressureFactory.py | 4 +- .../coord_categorisation/test_add_hour.py | 2 +- lib/iris/tests/unit/cube/test_Cube.py | 7 ++- lib/iris/tests/unit/cube/test_CubeList.py | 14 ++--- .../unit/fileformats/netcdf/test_Saver.py | 2 +- .../pp_load_rules/test__all_other_rules.py | 2 +- ...est__convert_scalar_pseudo_level_coords.py | 3 +- ...test__convert_scalar_realization_coords.py | 2 +- .../test__convert_vertical_coords.py | 6 +++ .../um/fast_load/test__convert_collation.py | 3 ++ 43 files changed, 170 insertions(+), 100 deletions(-) diff --git a/docs/iris/example_code/Meteorology/lagged_ensemble.py b/docs/iris/example_code/Meteorology/lagged_ensemble.py index 298d178a1e..512d6459ac 100644 --- a/docs/iris/example_code/Meteorology/lagged_ensemble.py +++ b/docs/iris/example_code/Meteorology/lagged_ensemble.py @@ -40,7 +40,7 @@ def realization_metadata(cube, field, fname): import iris.coords realization_coord = iris.coords.AuxCoord( - np.int32(realization_number), "realization" + np.int32(realization_number), "realization", units="1" ) cube.add_aux_coord(realization_coord) diff --git a/docs/iris/src/userguide/navigating_a_cube.rst b/docs/iris/src/userguide/navigating_a_cube.rst index 055617e047..581d1a67cf 100644 --- a/docs/iris/src/userguide/navigating_a_cube.rst +++ b/docs/iris/src/userguide/navigating_a_cube.rst @@ -229,7 +229,7 @@ by field basis *before* they are automatically merged together: # Add our own realization coordinate if it doesn't already exist. if not cube.coords('realization'): realization = np.int32(filename[-6:-3]) - ensemble_coord = icoords.AuxCoord(realization, standard_name='realization') + ensemble_coord = icoords.AuxCoord(realization, standard_name='realization', units="1") cube.add_aux_coord(ensemble_coord) filename = iris.sample_data_path('GloSea4', '*.pp') diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 5b7dff813d..4740d4e342 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -802,7 +802,9 @@ def post_process(self, collapsed_cube, data_result, coords, **kwargs): # order cube. for point in points: cube = collapsed_cube.copy() - coord = iris.coords.AuxCoord(point, long_name=coord_name) + coord = iris.coords.AuxCoord( + point, long_name=coord_name, units="percent" + ) cube.add_aux_coord(coord) cubes.append(cube) diff --git a/lib/iris/coords.py b/lib/iris/coords.py index b5392579c8..3ad5e018f9 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -59,7 +59,7 @@ def __init__( standard_name=None, long_name=None, var_name=None, - units="no-unit", + units=None, attributes=None, ): """ @@ -688,7 +688,7 @@ def __init__( standard_name=None, long_name=None, var_name=None, - units="no-unit", + units=None, attributes=None, ): """ @@ -788,7 +788,7 @@ def __init__( standard_name=None, long_name=None, var_name=None, - units="1", + units=None, attributes=None, measure=None, ): @@ -1440,7 +1440,7 @@ def __init__( standard_name=None, long_name=None, var_name=None, - units="1", + units=None, bounds=None, attributes=None, coord_system=None, @@ -2411,7 +2411,7 @@ def from_regular( standard_name=None, long_name=None, var_name=None, - units="1", + units=None, attributes=None, coord_system=None, circular=False, @@ -2474,7 +2474,7 @@ def __init__( standard_name=None, long_name=None, var_name=None, - units="1", + units=None, bounds=None, attributes=None, coord_system=None, diff --git a/lib/iris/fileformats/pp_load_rules.py b/lib/iris/fileformats/pp_load_rules.py index c0a4081970..bee33927f6 100644 --- a/lib/iris/fileformats/pp_load_rules.py +++ b/lib/iris/fileformats/pp_load_rules.py @@ -147,6 +147,7 @@ def _convert_vertical_coords( model_level_number, standard_name="model_level_number", attributes={"positive": "down"}, + units="1", ) coords_and_dims.append((coord, dim)) @@ -197,6 +198,7 @@ def _convert_vertical_coords( model_level_number, long_name="soil_model_level_number", attributes={"positive": "down"}, + units="1", ) coords_and_dims.append((coord, dim)) elif np.any(brsvd1 != brlev): @@ -235,6 +237,7 @@ def _convert_vertical_coords( model_level_number, standard_name="model_level_number", attributes={"positive": "up"}, + units="1", ) level_pressure = _dim_or_aux( bhlev, @@ -243,7 +246,10 @@ def _convert_vertical_coords( bounds=np.vstack((bhrlev, brsvd2)).T, ) sigma = AuxCoord( - blev, long_name="sigma", bounds=np.vstack((brlev, brsvd1)).T + blev, + long_name="sigma", + bounds=np.vstack((brlev, brsvd1)).T, + units="1", ) coords_and_dims.extend( [(model_level_number, dim), (level_pressure, dim), (sigma, dim)] @@ -265,6 +271,7 @@ def _convert_vertical_coords( model_level_number, standard_name="model_level_number", attributes={"positive": "up"}, + units="1", ) level_height = _dim_or_aux( blev, @@ -274,7 +281,10 @@ def _convert_vertical_coords( attributes={"positive": "up"}, ) sigma = AuxCoord( - bhlev, long_name="sigma", bounds=np.vstack((bhrlev, brsvd2)).T + bhlev, + long_name="sigma", + bounds=np.vstack((bhrlev, brsvd2)).T, + units="1", ) coords_and_dims.extend( [(model_level_number, dim), (level_height, dim), (sigma, dim)] @@ -846,7 +856,7 @@ def _convert_scalar_realization_coords(lbrsvd4): coords_and_dims = [] if lbrsvd4 != 0: coords_and_dims.append( - (DimCoord(lbrsvd4, standard_name="realization"), None) + (DimCoord(lbrsvd4, standard_name="realization", units="1"), None) ) return coords_and_dims @@ -1078,7 +1088,7 @@ def _all_other_rules(f): and f.lbmon == f.lbmond ): aux_coords_and_dims.append( - (AuxCoord(f.lbmon, long_name="month_number"), None) + (AuxCoord(f.lbmon, long_name="month_number", units="1"), None) ) aux_coords_and_dims.append( ( diff --git a/lib/iris/tests/integration/test_netcdf.py b/lib/iris/tests/integration/test_netcdf.py index 8c6e0f6659..267e5beb50 100644 --- a/lib/iris/tests/integration/test_netcdf.py +++ b/lib/iris/tests/integration/test_netcdf.py @@ -81,7 +81,9 @@ def test_hybrid_height_and_pressure(self): 1200.0, long_name="level_pressure", units="hPa" ) ) - cube.add_aux_coord(iris.coords.DimCoord(0.5, long_name="other sigma")) + cube.add_aux_coord( + iris.coords.DimCoord(0.5, long_name="other sigma", units="1") + ) cube.add_aux_coord( iris.coords.DimCoord( 1000.0, long_name="surface_air_pressure", units="hPa" diff --git a/lib/iris/tests/integration/test_pp.py b/lib/iris/tests/integration/test_pp.py index 6fbf180ac5..b9b096d782 100644 --- a/lib/iris/tests/integration/test_pp.py +++ b/lib/iris/tests/integration/test_pp.py @@ -299,7 +299,7 @@ def test_hybrid_height_with_non_standard_coords(self): delta_lower, delta, delta_upper = 150, 200, 250 cube = Cube(np.zeros((ny, nx)), "air_temperature") - level_coord = AuxCoord(0, "model_level_number") + level_coord = AuxCoord(0, "model_level_number", units="1") cube.add_aux_coord(level_coord) delta_coord = AuxCoord( delta, @@ -308,7 +308,10 @@ def test_hybrid_height_with_non_standard_coords(self): units="m", ) sigma_coord = AuxCoord( - sigma, bounds=[[sigma_lower, sigma_upper]], long_name="mavis" + sigma, + bounds=[[sigma_lower, sigma_upper]], + long_name="mavis", + units="1", ) surface_altitude_coord = AuxCoord( np.zeros((ny, nx)), "surface_altitude", units="m" @@ -343,7 +346,7 @@ def test_hybrid_pressure_with_non_standard_coords(self): delta_lower, delta, delta_upper = 0.15, 0.2, 0.25 cube = Cube(np.zeros((ny, nx)), "air_temperature") - level_coord = AuxCoord(0, "model_level_number") + level_coord = AuxCoord(0, "model_level_number", units="1") cube.add_aux_coord(level_coord) delta_coord = AuxCoord( delta, @@ -352,7 +355,10 @@ def test_hybrid_pressure_with_non_standard_coords(self): units="Pa", ) sigma_coord = AuxCoord( - sigma, bounds=[[sigma_lower, sigma_upper]], long_name="mavis" + sigma, + bounds=[[sigma_lower, sigma_upper]], + long_name="mavis", + units="1", ) surface_air_pressure_coord = AuxCoord( np.zeros((ny, nx)), "surface_air_pressure", units="Pa" diff --git a/lib/iris/tests/results/analysis/first_quartile_foo_1d.cml b/lib/iris/tests/results/analysis/first_quartile_foo_1d.cml index a9e69c291e..f027f2d9f8 100644 --- a/lib/iris/tests/results/analysis/first_quartile_foo_1d.cml +++ b/lib/iris/tests/results/analysis/first_quartile_foo_1d.cml @@ -6,7 +6,7 @@ - + diff --git a/lib/iris/tests/results/analysis/first_quartile_foo_1d_fast_percentile.cml b/lib/iris/tests/results/analysis/first_quartile_foo_1d_fast_percentile.cml index a9e69c291e..f027f2d9f8 100644 --- a/lib/iris/tests/results/analysis/first_quartile_foo_1d_fast_percentile.cml +++ b/lib/iris/tests/results/analysis/first_quartile_foo_1d_fast_percentile.cml @@ -6,7 +6,7 @@ - + diff --git a/lib/iris/tests/results/analysis/first_quartile_foo_2d.cml b/lib/iris/tests/results/analysis/first_quartile_foo_2d.cml index 34c9e746f6..1bc809ce63 100644 --- a/lib/iris/tests/results/analysis/first_quartile_foo_2d.cml +++ b/lib/iris/tests/results/analysis/first_quartile_foo_2d.cml @@ -11,7 +11,7 @@ - + diff --git a/lib/iris/tests/results/analysis/first_quartile_foo_2d_fast_percentile.cml b/lib/iris/tests/results/analysis/first_quartile_foo_2d_fast_percentile.cml index 34c9e746f6..1bc809ce63 100644 --- a/lib/iris/tests/results/analysis/first_quartile_foo_2d_fast_percentile.cml +++ b/lib/iris/tests/results/analysis/first_quartile_foo_2d_fast_percentile.cml @@ -11,7 +11,7 @@ - + diff --git a/lib/iris/tests/results/analysis/first_quartile_foo_bar_2d.cml b/lib/iris/tests/results/analysis/first_quartile_foo_bar_2d.cml index b3f135cede..cadd1e8b65 100644 --- a/lib/iris/tests/results/analysis/first_quartile_foo_bar_2d.cml +++ b/lib/iris/tests/results/analysis/first_quartile_foo_bar_2d.cml @@ -9,7 +9,7 @@ - + diff --git a/lib/iris/tests/results/analysis/first_quartile_foo_bar_2d_fast_percentile.cml b/lib/iris/tests/results/analysis/first_quartile_foo_bar_2d_fast_percentile.cml index b3f135cede..cadd1e8b65 100644 --- a/lib/iris/tests/results/analysis/first_quartile_foo_bar_2d_fast_percentile.cml +++ b/lib/iris/tests/results/analysis/first_quartile_foo_bar_2d_fast_percentile.cml @@ -9,7 +9,7 @@ - + diff --git a/lib/iris/tests/results/analysis/last_quartile_foo_3d_masked.cml b/lib/iris/tests/results/analysis/last_quartile_foo_3d_masked.cml index 80fab0e150..059541e208 100644 --- a/lib/iris/tests/results/analysis/last_quartile_foo_3d_masked.cml +++ b/lib/iris/tests/results/analysis/last_quartile_foo_3d_masked.cml @@ -9,7 +9,7 @@ - + diff --git a/lib/iris/tests/results/analysis/last_quartile_foo_3d_notmasked.cml b/lib/iris/tests/results/analysis/last_quartile_foo_3d_notmasked.cml index 80fab0e150..059541e208 100644 --- a/lib/iris/tests/results/analysis/last_quartile_foo_3d_notmasked.cml +++ b/lib/iris/tests/results/analysis/last_quartile_foo_3d_notmasked.cml @@ -9,7 +9,7 @@ - + diff --git a/lib/iris/tests/results/analysis/last_quartile_foo_3d_notmasked_fast_percentile.cml b/lib/iris/tests/results/analysis/last_quartile_foo_3d_notmasked_fast_percentile.cml index 80fab0e150..059541e208 100644 --- a/lib/iris/tests/results/analysis/last_quartile_foo_3d_notmasked_fast_percentile.cml +++ b/lib/iris/tests/results/analysis/last_quartile_foo_3d_notmasked_fast_percentile.cml @@ -9,7 +9,7 @@ - + diff --git a/lib/iris/tests/results/analysis/third_quartile_foo_1d.cml b/lib/iris/tests/results/analysis/third_quartile_foo_1d.cml index b14c51cfb3..038e7c8668 100644 --- a/lib/iris/tests/results/analysis/third_quartile_foo_1d.cml +++ b/lib/iris/tests/results/analysis/third_quartile_foo_1d.cml @@ -6,7 +6,7 @@ - + diff --git a/lib/iris/tests/results/analysis/third_quartile_foo_1d_fast_percentile.cml b/lib/iris/tests/results/analysis/third_quartile_foo_1d_fast_percentile.cml index b14c51cfb3..038e7c8668 100644 --- a/lib/iris/tests/results/analysis/third_quartile_foo_1d_fast_percentile.cml +++ b/lib/iris/tests/results/analysis/third_quartile_foo_1d_fast_percentile.cml @@ -6,7 +6,7 @@ - + diff --git a/lib/iris/tests/results/coord_api/minimal.xml b/lib/iris/tests/results/coord_api/minimal.xml index a35c93dc68..8f93fb6376 100644 --- a/lib/iris/tests/results/coord_api/minimal.xml +++ b/lib/iris/tests/results/coord_api/minimal.xml @@ -1,2 +1,2 @@ - + diff --git a/lib/iris/tests/results/integration/climatology/TestClimatology/reference_simpledata.cdl b/lib/iris/tests/results/integration/climatology/TestClimatology/reference_simpledata.cdl index 1740926645..1f6bc36832 100644 --- a/lib/iris/tests/results/integration/climatology/TestClimatology/reference_simpledata.cdl +++ b/lib/iris/tests/results/integration/climatology/TestClimatology/reference_simpledata.cdl @@ -17,11 +17,11 @@ variables: double time_climatology(time, bnds) ; double latitude(latitude) ; latitude:axis = "Y" ; - latitude:units = "1" ; + latitude:units = "degrees_north" ; latitude:standard_name = "latitude" ; double longitude(longitude) ; longitude:axis = "X" ; - longitude:units = "1" ; + longitude:units = "degrees_east" ; longitude:standard_name = "longitude" ; // global attributes: diff --git a/lib/iris/tests/results/netcdf/netcdf_save_no_name.cdl b/lib/iris/tests/results/netcdf/netcdf_save_no_name.cdl index e67316b2f7..be13f83fc8 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_no_name.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_no_name.cdl @@ -6,7 +6,6 @@ variables: double unknown(dim0, dim1) ; unknown:coordinates = "unknown_scalar" ; double dim0(dim0) ; - dim0:units = "1" ; double dim1(dim1) ; dim1:units = "m" ; char unknown_scalar(string6) ; diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/Saver/write/with_climatology.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/Saver/write/with_climatology.cdl index 3646627746..3c1033c17e 100644 --- a/lib/iris/tests/results/unit/fileformats/netcdf/Saver/write/with_climatology.cdl +++ b/lib/iris/tests/results/unit/fileformats/netcdf/Saver/write/with_climatology.cdl @@ -17,10 +17,10 @@ variables: double time_climatology(time, bnds) ; double latitude(latitude) ; latitude:axis = "Y" ; - latitude:units = "1" ; + latitude:units = "degrees_north" ; latitude:standard_name = "latitude" ; double longitude(longitude) ; longitude:axis = "X" ; - longitude:units = "1" ; + longitude:units = "degrees_east" ; longitude:standard_name = "longitude" ; } diff --git a/lib/iris/tests/stock/__init__.py b/lib/iris/tests/stock/__init__.py index 9bb1c4626f..41979fad94 100644 --- a/lib/iris/tests/stock/__init__.py +++ b/lib/iris/tests/stock/__init__.py @@ -834,8 +834,8 @@ def jan_offset(day, year): units="days since 1970-01-01 00:00:00-00", climatological=True, ) - lon_dim = DimCoord(lon, standard_name="longitude") - lat_dim = DimCoord(lat, standard_name="latitude") + lon_dim = DimCoord(lon, standard_name="longitude", units="degrees") + lat_dim = DimCoord(lat, standard_name="latitude", units="degrees") data_shape = (len(time_points), len(lat), len(lon)) values = np.zeros(shape=data_shape, dtype=np.int8) diff --git a/lib/iris/tests/test_aggregate_by.py b/lib/iris/tests/test_aggregate_by.py index b4e1bad640..bc759f251d 100644 --- a/lib/iris/tests/test_aggregate_by.py +++ b/lib/iris/tests/test_aggregate_by.py @@ -89,7 +89,9 @@ def setUp(self): ) model_level = iris.coords.DimCoord( - np.arange(z_points.size), standard_name="model_level_number" + np.arange(z_points.size), + standard_name="model_level_number", + units="1", ) self.cube_single.add_aux_coord(self.coord_z_single, 0) @@ -124,7 +126,9 @@ def setUp(self): ) model_level = iris.coords.DimCoord( - np.arange(z1_points.size), standard_name="model_level_number" + np.arange(z1_points.size), + standard_name="model_level_number", + units="1", ) self.cube_multi.add_aux_coord(self.coord_z1_multi, 0) diff --git a/lib/iris/tests/test_concatenate.py b/lib/iris/tests/test_concatenate.py index bbe5f5eba2..d45a884a2f 100644 --- a/lib/iris/tests/test_concatenate.py +++ b/lib/iris/tests/test_concatenate.py @@ -66,52 +66,58 @@ def _make_cube( cube_data = np.empty((y_size, x_size), dtype=np.float32) cube_data[:] = data cube = iris.cube.Cube(cube_data) - coord = DimCoord(y_range, long_name="y") + coord = DimCoord(y_range, long_name="y", units="1") coord.guess_bounds() cube.add_dim_coord(coord, 0) - coord = DimCoord(x_range, long_name="x") + coord = DimCoord(x_range, long_name="x", units="1") coord.guess_bounds() cube.add_dim_coord(coord, 1) if aux is not None: aux = aux.split(",") if "y" in aux: - coord = AuxCoord(y_range * 10, long_name="y-aux") + coord = AuxCoord(y_range * 10, long_name="y-aux", units="1") cube.add_aux_coord(coord, (0,)) if "x" in aux: - coord = AuxCoord(x_range * 10, long_name="x-aux") + coord = AuxCoord(x_range * 10, long_name="x-aux", units="1") cube.add_aux_coord(coord, (1,)) if "xy" in aux: payload = np.arange(y_size * x_size, dtype=np.float32).reshape( y_size, x_size ) - coord = AuxCoord(payload * 100 + offset, long_name="xy-aux") + coord = AuxCoord( + payload * 100 + offset, long_name="xy-aux", units="1" + ) cube.add_aux_coord(coord, (0, 1)) if cell_measure is not None: cell_measure = cell_measure.split(",") if "y" in cell_measure: - cm = CellMeasure(y_range * 10, long_name="y-aux") + cm = CellMeasure(y_range * 10, long_name="y-aux", units="1") cube.add_cell_measure(cm, (0,)) if "x" in cell_measure: - cm = CellMeasure(x_range * 10, long_name="x-aux") + cm = CellMeasure(x_range * 10, long_name="x-aux", units="1") cube.add_cell_measure(cm, (1,)) if "xy" in cell_measure: payload = x_range + y_range[:, np.newaxis] - cm = CellMeasure(payload * 100 + offset, long_name="xy-aux") + cm = CellMeasure( + payload * 100 + offset, long_name="xy-aux", units="1" + ) cube.add_cell_measure(cm, (0, 1)) if ancil is not None: ancil = ancil.split(",") if "y" in ancil: - av = AncillaryVariable(y_range * 10, long_name="y-aux") + av = AncillaryVariable(y_range * 10, long_name="y-aux", units="1") cube.add_ancillary_variable(av, (0,)) if "x" in ancil: - av = AncillaryVariable(x_range * 10, long_name="x-aux") + av = AncillaryVariable(x_range * 10, long_name="x-aux", units="1") cube.add_ancillary_variable(av, (1,)) if "xy" in ancil: payload = x_range + y_range[:, np.newaxis] - av = AncillaryVariable(payload * 100 + offset, long_name="xy-aux") + av = AncillaryVariable( + payload * 100 + offset, long_name="xy-aux", units="1" + ) cube.add_ancillary_variable(av, (0, 1)) if scalar is not None: @@ -169,50 +175,56 @@ def _make_cube_3d(x, y, z, data, aux=None, offset=0): cube_data = np.empty((x_size, y_size, z_size), dtype=np.float32) cube_data[:] = data cube = iris.cube.Cube(cube_data) - coord = DimCoord(z_range, long_name="z") + coord = DimCoord(z_range, long_name="z", units="1") coord.guess_bounds() cube.add_dim_coord(coord, 0) - coord = DimCoord(y_range, long_name="y") + coord = DimCoord(y_range, long_name="y", units="1") coord.guess_bounds() cube.add_dim_coord(coord, 1) - coord = DimCoord(x_range, long_name="x") + coord = DimCoord(x_range, long_name="x", units="1") coord.guess_bounds() cube.add_dim_coord(coord, 2) if aux is not None: aux = aux.split(",") if "z" in aux: - coord = AuxCoord(z_range * 10, long_name="z-aux") + coord = AuxCoord(z_range * 10, long_name="z-aux", units="1") cube.add_aux_coord(coord, (0,)) if "y" in aux: - coord = AuxCoord(y_range * 10, long_name="y-aux") + coord = AuxCoord(y_range * 10, long_name="y-aux", units="1") cube.add_aux_coord(coord, (1,)) if "x" in aux: - coord = AuxCoord(x_range * 10, long_name="x-aux") + coord = AuxCoord(x_range * 10, long_name="x-aux", units="1") cube.add_aux_coord(coord, (2,)) if "xy" in aux: payload = np.arange(x_size * y_size, dtype=np.float32).reshape( y_size, x_size ) - coord = AuxCoord(payload + offset, long_name="xy-aux") + coord = AuxCoord(payload + offset, long_name="xy-aux", units="1") cube.add_aux_coord(coord, (1, 2)) if "xz" in aux: payload = np.arange(x_size * z_size, dtype=np.float32).reshape( z_size, x_size ) - coord = AuxCoord(payload * 10 + offset, long_name="xz-aux") + coord = AuxCoord( + payload * 10 + offset, long_name="xz-aux", units="1" + ) cube.add_aux_coord(coord, (0, 2)) if "yz" in aux: payload = np.arange(y_size * z_size, dtype=np.float32).reshape( z_size, y_size ) - coord = AuxCoord(payload * 100 + offset, long_name="yz-aux") + coord = AuxCoord( + payload * 100 + offset, long_name="yz-aux", units="1" + ) cube.add_aux_coord(coord, (0, 1)) if "xyz" in aux: payload = np.arange( x_size * y_size * z_size, dtype=np.float32 ).reshape(z_size, y_size, x_size) - coord = AuxCoord(payload * 1000 + offset, long_name="xyz-aux") + coord = AuxCoord( + payload * 1000 + offset, long_name="xyz-aux", units="1" + ) cube.add_aux_coord(coord, (0, 1, 2)) return cube diff --git a/lib/iris/tests/test_coord_api.py b/lib/iris/tests/test_coord_api.py index 053b6b509b..d98d771210 100644 --- a/lib/iris/tests/test_coord_api.py +++ b/lib/iris/tests/test_coord_api.py @@ -247,7 +247,7 @@ def test_basic(self): "AuxCoord(" "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," " standard_name=None," - " units=Unit('1')," + " units=Unit('unknown')," " attributes={'monty': 'python'})" ) self.assertEqual(result, str(b)) @@ -337,7 +337,7 @@ def test_basic(self): "DimCoord(" "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," " standard_name=None," - " units=Unit('1')," + " units=Unit('unknown')," " attributes={'monty': 'python'})" ) self.assertEqual(result, str(b)) diff --git a/lib/iris/tests/test_iterate.py b/lib/iris/tests/test_iterate.py index 85f5943b8e..e53eede6f4 100644 --- a/lib/iris/tests/test_iterate.py +++ b/lib/iris/tests/test_iterate.py @@ -475,17 +475,24 @@ def test_izip_nd_non_ortho(self): def test_izip_nd_ortho(self): cube1 = iris.cube.Cube(np.zeros((5, 5, 5, 5, 5), dtype="f8")) cube1.add_dim_coord( - iris.coords.DimCoord(np.arange(5, dtype="i8"), long_name="z"), [0] + iris.coords.DimCoord( + np.arange(5, dtype="i8"), long_name="z", units="1" + ), + [0], ) cube1.add_aux_coord( iris.coords.AuxCoord( - np.arange(25, dtype="i8").reshape(5, 5), long_name="y" + np.arange(25, dtype="i8").reshape(5, 5), + long_name="y", + units="1", ), [1, 2], ) cube1.add_aux_coord( iris.coords.AuxCoord( - np.arange(25, dtype="i8").reshape(5, 5), long_name="x" + np.arange(25, dtype="i8").reshape(5, 5), + long_name="x", + units="1", ), [3, 4], ) diff --git a/lib/iris/tests/test_netcdf.py b/lib/iris/tests/test_netcdf.py index a550e1ed4b..e3d23727a6 100644 --- a/lib/iris/tests/test_netcdf.py +++ b/lib/iris/tests/test_netcdf.py @@ -608,10 +608,10 @@ def test_netcdf_multi_with_coords(self): def test_netcdf_multi_wtih_samedimcoord(self): time1 = iris.coords.DimCoord( - np.arange(10), standard_name="time", var_name="time" + np.arange(10), standard_name="time", var_name="time", units="1" ) time2 = iris.coords.DimCoord( - np.arange(20), standard_name="time", var_name="time" + np.arange(20), standard_name="time", var_name="time", units="1" ) self.cube4.add_dim_coord(time1, 0) @@ -630,11 +630,13 @@ def test_netcdf_multi_wtih_samedimcoord(self): def test_netcdf_multi_conflict_name_dup_coord(self): # Duplicate coordinates with modified variable names lookup. latitude1 = iris.coords.DimCoord( - np.arange(10), standard_name="latitude" + np.arange(10), standard_name="latitude", units="1" + ) + time2 = iris.coords.DimCoord( + np.arange(2), standard_name="time", units="1" ) - time2 = iris.coords.DimCoord(np.arange(2), standard_name="time") latitude2 = iris.coords.DimCoord( - np.arange(2), standard_name="latitude" + np.arange(2), standard_name="latitude", units="1" ) self.cube6.add_dim_coord(latitude1, 0) @@ -711,10 +713,10 @@ def test_netcdf_save_conflicting_aux(self): # Test saving CF-netCDF with multi-dimensional auxiliary coordinates, # with conflicts. self.cube4.add_aux_coord( - iris.coords.AuxCoord(np.arange(10), "time"), 0 + iris.coords.AuxCoord(np.arange(10), "time", units="1"), 0 ) self.cube6.add_aux_coord( - iris.coords.AuxCoord(np.arange(10, 20), "time"), 0 + iris.coords.AuxCoord(np.arange(10, 20), "time", units="1"), 0 ) cubes = iris.cube.CubeList([self.cube4, self.cube6]) @@ -811,9 +813,11 @@ def test_netcdf_save_conflicting_names(self): # Test saving CF-netCDF with a dimension name corresponding to # an existing variable name (conflict). self.cube4.add_dim_coord( - iris.coords.DimCoord(np.arange(10), "time"), 0 + iris.coords.DimCoord(np.arange(10), "time", units="1"), 0 + ) + self.cube6.add_aux_coord( + iris.coords.AuxCoord(1, "time", units="1"), None ) - self.cube6.add_aux_coord(iris.coords.AuxCoord(1, "time"), None) cubes = iris.cube.CubeList([self.cube4, self.cube6]) with self.temp_filename(suffix=".nc") as file_out: diff --git a/lib/iris/tests/test_plot.py b/lib/iris/tests/test_plot.py index 04418d8d40..600801312f 100644 --- a/lib/iris/tests/test_plot.py +++ b/lib/iris/tests/test_plot.py @@ -875,6 +875,7 @@ def test_non_cube_coordinate(self): pts, standard_name="model_level_number", attributes={"positive": "up"}, + units="1", ) self.draw("contourf", cube, coords=["grid_latitude", x]) diff --git a/lib/iris/tests/unit/analysis/interpolation/test_RectilinearInterpolator.py b/lib/iris/tests/unit/analysis/interpolation/test_RectilinearInterpolator.py index d1eaf71030..fa4ca8b608 100644 --- a/lib/iris/tests/unit/analysis/interpolation/test_RectilinearInterpolator.py +++ b/lib/iris/tests/unit/analysis/interpolation/test_RectilinearInterpolator.py @@ -34,9 +34,15 @@ class ThreeDimCube(tests.IrisTest): def setUp(self): cube = stock.simple_3d_w_multidim_coords() - cube.add_aux_coord(iris.coords.DimCoord(np.arange(2), "height"), 0) - cube.add_dim_coord(iris.coords.DimCoord(np.arange(3), "latitude"), 1) - cube.add_dim_coord(iris.coords.DimCoord(np.arange(4), "longitude"), 2) + cube.add_aux_coord( + iris.coords.DimCoord(np.arange(2), "height", units="1"), 0 + ) + cube.add_dim_coord( + iris.coords.DimCoord(np.arange(3), "latitude", units="1"), 1 + ) + cube.add_dim_coord( + iris.coords.DimCoord(np.arange(4), "longitude", units="1"), 2 + ) self.data = np.arange(24).reshape(2, 3, 4).astype(np.float32) cube.data = self.data self.cube = cube diff --git a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py index 55fb2f4829..492283f843 100644 --- a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py +++ b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py @@ -1253,7 +1253,7 @@ def setUp(self): units="m", attributes={"positive": "up"}, ) - sigma = AuxCoord(1, long_name="sigma") + sigma = AuxCoord(1, long_name="sigma", units="1") surface_altitude = AuxCoord( (src.data - src.data.min()) * 50, "surface_altitude", units="m" ) diff --git a/lib/iris/tests/unit/analysis/test_PercentileAggregator.py b/lib/iris/tests/unit/analysis/test_PercentileAggregator.py index 2b2524795c..cffac86291 100644 --- a/lib/iris/tests/unit/analysis/test_PercentileAggregator.py +++ b/lib/iris/tests/unit/analysis/test_PercentileAggregator.py @@ -71,7 +71,7 @@ def test_simple_single_point(self): self.assertIs(actual.data, data) name = "percentile_over_time" coord = actual.coord(name) - expected = AuxCoord(percent, long_name=name) + expected = AuxCoord(percent, long_name=name, units="percent") self.assertEqual(coord, expected) def test_simple_multiple_points(self): @@ -89,7 +89,7 @@ def test_simple_multiple_points(self): self.assertArrayEqual(actual.data, expected) name = "percentile_over_time" coord = actual.coord(name) - expected = AuxCoord(percent, long_name=name) + expected = AuxCoord(percent, long_name=name, units="percent") self.assertEqual(coord, expected) def test_multi_single_point(self): @@ -105,7 +105,7 @@ def test_multi_single_point(self): self.assertIs(actual.data, data) name = "percentile_over_time" coord = actual.coord(name) - expected = AuxCoord(percent, long_name=name) + expected = AuxCoord(percent, long_name=name, units="percent") self.assertEqual(coord, expected) def test_multi_multiple_points(self): @@ -123,7 +123,7 @@ def test_multi_multiple_points(self): self.assertArrayEqual(actual.data, expected) name = "percentile_over_time" coord = actual.coord(name) - expected = AuxCoord(percent, long_name=name) + expected = AuxCoord(percent, long_name=name, units="percent") self.assertEqual(coord, expected) diff --git a/lib/iris/tests/unit/analysis/test_WeightedPercentileAggregator.py b/lib/iris/tests/unit/analysis/test_WeightedPercentileAggregator.py index 1c59ded1fc..878708e48a 100644 --- a/lib/iris/tests/unit/analysis/test_WeightedPercentileAggregator.py +++ b/lib/iris/tests/unit/analysis/test_WeightedPercentileAggregator.py @@ -82,7 +82,7 @@ def test_simple_single_point(self): self.assertIs(actual.data, data) name = "weighted_percentile_over_time" coord = actual.coord(name) - expected = AuxCoord(percent, long_name=name) + expected = AuxCoord(percent, long_name=name, units="percent") self.assertEqual(coord, expected) def test_simple_multiple_points(self): @@ -107,7 +107,7 @@ def test_simple_multiple_points(self): self.assertIs(actual[1], total_weights) name = "weighted_percentile_over_time" coord = actual[0].coord(name) - expected = AuxCoord(percent, long_name=name) + expected = AuxCoord(percent, long_name=name, units="percent") self.assertEqual(coord, expected) def test_multi_single_point(self): @@ -123,7 +123,7 @@ def test_multi_single_point(self): self.assertIs(actual.data, data) name = "weighted_percentile_over_time" coord = actual.coord(name) - expected = AuxCoord(percent, long_name=name) + expected = AuxCoord(percent, long_name=name, units="percent") self.assertEqual(coord, expected) def test_multi_multiple_points(self): @@ -141,7 +141,7 @@ def test_multi_multiple_points(self): self.assertArrayEqual(actual.data, expected) name = "weighted_percentile_over_time" coord = actual.coord(name) - expected = AuxCoord(percent, long_name=name) + expected = AuxCoord(percent, long_name=name, units="percent") self.assertEqual(coord, expected) diff --git a/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py b/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py index 789b1d61d5..14944891f2 100644 --- a/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py @@ -144,7 +144,9 @@ def setUp(self): self.delta = iris.coords.DimCoord( [0.0, 1.0, 2.0], long_name="level_pressure", units="Pa" ) - self.sigma = iris.coords.DimCoord([1.0, 0.9, 0.8], long_name="sigma") + self.sigma = iris.coords.DimCoord( + [1.0, 0.9, 0.8], long_name="sigma", units="1" + ) self.surface_air_pressure = iris.coords.AuxCoord( np.arange(4).reshape(2, 2), "surface_air_pressure", units="Pa" ) diff --git a/lib/iris/tests/unit/coord_categorisation/test_add_hour.py b/lib/iris/tests/unit/coord_categorisation/test_add_hour.py index 6965ea7a2f..9b101362a5 100644 --- a/lib/iris/tests/unit/coord_categorisation/test_add_hour.py +++ b/lib/iris/tests/unit/coord_categorisation/test_add_hour.py @@ -70,7 +70,7 @@ def test_basic(self): cube = self.cube time_coord = self.time_coord expected_coord = iris.coords.AuxCoord( - self.hour_numbers % 24, long_name=coord_name + self.hour_numbers % 24, long_name=coord_name, units="1" ) ccat.add_hour(cube, time_coord, coord_name) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 9c03f0f4d4..3b98be6454 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -276,7 +276,7 @@ def test_byteorder_true(self): def test_cell_measures(self): cube = stock.simple_3d_w_multidim_coords() cm_a = iris.coords.CellMeasure( - np.zeros(cube.shape[-2:]), measure="area" + np.zeros(cube.shape[-2:]), measure="area", units="1" ) cube.add_cell_measure(cm_a, (1, 2)) cm_v = iris.coords.CellMeasure( @@ -1077,7 +1077,10 @@ def create_cube(lon_min, lon_max, bounds=False): 0, ) cube.add_aux_coord( - iris.coords.AuxCoord([1.0, 0.9, 0.8, 0.6], long_name="sigma"), 0 + iris.coords.AuxCoord( + [1.0, 0.9, 0.8, 0.6], long_name="sigma", units="1" + ), + 0, ) cube.add_dim_coord( iris.coords.DimCoord([-45, 0, 45], "latitude", units="degrees"), 1 diff --git a/lib/iris/tests/unit/cube/test_CubeList.py b/lib/iris/tests/unit/cube/test_CubeList.py index 6870e2367f..e59775d1c9 100644 --- a/lib/iris/tests/unit/cube/test_CubeList.py +++ b/lib/iris/tests/unit/cube/test_CubeList.py @@ -150,16 +150,18 @@ class Test_merge__time_triple(tests.IrisTest): @staticmethod def _make_cube(fp, rt, t, realization=None): cube = Cube(np.arange(20).reshape(4, 5)) - cube.add_dim_coord(DimCoord(np.arange(5), long_name="x"), 1) - cube.add_dim_coord(DimCoord(np.arange(4), long_name="y"), 0) - cube.add_aux_coord(DimCoord(fp, standard_name="forecast_period")) + cube.add_dim_coord(DimCoord(np.arange(5), long_name="x", units="1"), 1) + cube.add_dim_coord(DimCoord(np.arange(4), long_name="y", units="1"), 0) cube.add_aux_coord( - DimCoord(rt, standard_name="forecast_reference_time") + DimCoord(fp, standard_name="forecast_period", units="1") ) - cube.add_aux_coord(DimCoord(t, standard_name="time")) + cube.add_aux_coord( + DimCoord(rt, standard_name="forecast_reference_time", units="1") + ) + cube.add_aux_coord(DimCoord(t, standard_name="time", units="1")) if realization is not None: cube.add_aux_coord( - DimCoord(realization, standard_name="realization") + DimCoord(realization, standard_name="realization", units="1") ) return cube diff --git a/lib/iris/tests/unit/fileformats/netcdf/test_Saver.py b/lib/iris/tests/unit/fileformats/netcdf/test_Saver.py index 08595ed3f3..acea552fdf 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test_Saver.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test_Saver.py @@ -161,7 +161,7 @@ def _simple_cube(self, dtype): points = np.arange(3, dtype=dtype) bounds = np.arange(6, dtype=dtype).reshape(3, 2) cube = Cube(data, "air_pressure_anomaly") - coord = DimCoord(points, bounds=bounds) + coord = DimCoord(points, bounds=bounds, units="1") cube.add_dim_coord(coord, 0) return cube diff --git a/lib/iris/tests/unit/fileformats/pp_load_rules/test__all_other_rules.py b/lib/iris/tests/unit/fileformats/pp_load_rules/test__all_other_rules.py index d10c1218ab..d44b5a1d54 100644 --- a/lib/iris/tests/unit/fileformats/pp_load_rules/test__all_other_rules.py +++ b/lib/iris/tests/unit/fileformats/pp_load_rules/test__all_other_rules.py @@ -269,7 +269,7 @@ def test_month_coord(self): res = _all_other_rules(field)[AUX_COORDS_INDEX] expected = [ - (AuxCoord(3, long_name="month_number"), None), + (AuxCoord(3, long_name="month_number", units="1"), None), (AuxCoord("Mar", long_name="month", units=Unit("no unit")), None), ( DimCoord( diff --git a/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_scalar_pseudo_level_coords.py b/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_scalar_pseudo_level_coords.py index 70807408d0..b7074f3c00 100644 --- a/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_scalar_pseudo_level_coords.py +++ b/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_scalar_pseudo_level_coords.py @@ -23,7 +23,8 @@ class Test(TestField): def test_valid(self): coords_and_dims = _convert_scalar_pseudo_level_coords(lbuser5=21) self.assertEqual( - coords_and_dims, [(DimCoord([21], long_name="pseudo_level"), None)] + coords_and_dims, + [(DimCoord([21], long_name="pseudo_level", units="1"), None)], ) def test_missing_indicator(self): diff --git a/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_scalar_realization_coords.py b/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_scalar_realization_coords.py index 4a4649c978..929f65c921 100644 --- a/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_scalar_realization_coords.py +++ b/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_scalar_realization_coords.py @@ -24,7 +24,7 @@ def test_valid(self): coords_and_dims = _convert_scalar_realization_coords(lbrsvd4=21) self.assertEqual( coords_and_dims, - [(DimCoord([21], standard_name="realization"), None)], + [(DimCoord([21], standard_name="realization", units="1"), None)], ) def test_missing_indicator(self): diff --git a/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_vertical_coords.py b/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_vertical_coords.py index b3a6e537ac..b9a652c397 100644 --- a/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_vertical_coords.py +++ b/lib/iris/tests/unit/fileformats/pp_load_rules/test__convert_vertical_coords.py @@ -210,6 +210,7 @@ def _check_depth( lblev, standard_name="model_level_number", attributes={"positive": "down"}, + units="1", ), dim, ) @@ -354,6 +355,7 @@ def _check_soil_level( lblev, long_name="soil_model_level_number", attributes={"positive": "down"}, + units="1", ) expect_result = [(coord, dim)] self.assertCoordsAndDimsListsMatch(coords_and_dims, expect_result) @@ -604,6 +606,7 @@ def _check( lblev, standard_name="model_level_number", attributes={"positive": "up"}, + units="1", ), dim, ) @@ -630,6 +633,7 @@ def _check( blev, long_name="sigma", bounds=np.vstack((brlev, brsvd1)).T, + units="1", ), dim, ) @@ -706,6 +710,7 @@ def _check( lblev, standard_name="model_level_number", attributes={"positive": "up"}, + units="1", ), dim, ) @@ -732,6 +737,7 @@ def _check( bhlev, long_name="sigma", bounds=np.vstack((bhrlev, brsvd2)).T, + units="1", ), dim, ) diff --git a/lib/iris/tests/unit/fileformats/um/fast_load/test__convert_collation.py b/lib/iris/tests/unit/fileformats/um/fast_load/test__convert_collation.py index 3dc6f96d48..7ce0573d25 100644 --- a/lib/iris/tests/unit/fileformats/um/fast_load/test__convert_collation.py +++ b/lib/iris/tests/unit/fileformats/um/fast_load/test__convert_collation.py @@ -335,6 +335,7 @@ def test_soil_level(self): points, long_name="soil_model_level_number", attributes={"positive": "down"}, + units="1", ) coords_and_dims = [(LONGITUDE, 2), (LATITUDE, 1), (level, (0,))] self.assertEqual(metadata.dim_coords_and_dims, coords_and_dims) @@ -416,6 +417,7 @@ def test_vertical_hybrid_height(self): [1, 2, 3], "model_level_number", attributes={"positive": "up"}, + units="1", ), (0,), ), @@ -437,6 +439,7 @@ def test_vertical_hybrid_height(self): [0.9994, 0.9979, 0.9957], long_name="sigma", bounds=[[1, 0.9989], [0.9989, 0.9970], [0.9970, 0.9944]], + units="1", ), (0,), ), From 3171b952b607cb581ae770e526347864e15d50fa Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Mon, 1 Jun 2020 16:28:45 +0100 Subject: [PATCH 02/32] Change default loading unit from "1" to "unknown" (correct branch) (#3709) --- ...020-May-15_change_default_unit_loading.txt | 1 + .../fileformats/_pyke_rules/fc_rules_cf.krb | 10 ++-- .../netcdf/int64_auxiliary_coord_netcdf3.cml | 2 +- .../netcdf/int64_dimension_coord_netcdf3.cml | 2 +- .../results/netcdf/netcdf_cell_methods.cml | 56 +++++++++---------- .../netcdf/netcdf_global_xyzt_gems.cml | 8 +-- .../netcdf/netcdf_global_xyzt_gems_iter_0.cml | 4 +- .../netcdf/netcdf_global_xyzt_gems_iter_1.cml | 4 +- .../netcdf/uint32_auxiliary_coord_netcdf3.cml | 2 +- .../netcdf/uint32_dimension_coord_netcdf3.cml | 2 +- ...000.44.101.131200.1920.09.01.00.00.b_0.cml | 2 +- ...000.44.101.000128.1890.09.01.00.00.b_0.cml | 2 +- .../from_netcdf/st0fc699.b_0.cml | 2 +- .../from_netcdf/st0fc942.b_0.cml | 2 +- lib/iris/tests/test_netcdf.py | 39 +++++++++++++ 15 files changed, 90 insertions(+), 48 deletions(-) create mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2020-May-15_change_default_unit_loading.txt diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2020-May-15_change_default_unit_loading.txt b/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2020-May-15_change_default_unit_loading.txt new file mode 100644 index 0000000000..be048990f3 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2020-May-15_change_default_unit_loading.txt @@ -0,0 +1 @@ +* When loading data from netcdf-CF files, where a variable has no "units" property, the corresponding Iris object will have "units='unknown'". Prior to Iris 3.0, these cases defaulted to "units='1'". \ No newline at end of file diff --git a/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb b/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb index 5ecfeb77b1..b26275f4db 100644 --- a/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb +++ b/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb @@ -1195,6 +1195,8 @@ fc_extras UD_UNITS_LON = ['degrees_east', 'degree_east', 'degree_e', 'degrees_e', 'degreee', 'degreese', 'degrees', 'degrees east', 'degree east', 'degree e', 'degrees e'] + UNKNOWN_UNIT_STRING = "?" + NO_UNIT_STRING = "-" # # CF Dimensionless Vertical Coordinates @@ -1651,9 +1653,9 @@ fc_extras ################################################################################ def get_attr_units(cf_var, attributes): - attr_units = getattr(cf_var, CF_ATTR_UNITS, cf_units._UNIT_DIMENSIONLESS) + attr_units = getattr(cf_var, CF_ATTR_UNITS, UNKNOWN_UNIT_STRING) if not attr_units: - attr_units = '1' + attr_units = UNKNOWN_UNIT_STRING # Sanitise lat/lon units. if attr_units in UD_UNITS_LAT or attr_units in UD_UNITS_LON: @@ -1668,10 +1670,10 @@ fc_extras cf_var.cf_name, attr_units) warnings.warn(msg) attributes['invalid_units'] = attr_units - attr_units = cf_units._UNKNOWN_UNIT_STRING + attr_units = UNKNOWN_UNIT_STRING if np.issubdtype(cf_var.dtype, np.str_): - attr_units = cf_units._NO_UNIT_STRING + attr_units = NO_UNIT_STRING # Get any assoicated calendar for a time reference coordinate. if cf_units.as_unit(attr_units).is_time_reference(): diff --git a/lib/iris/tests/results/netcdf/int64_auxiliary_coord_netcdf3.cml b/lib/iris/tests/results/netcdf/int64_auxiliary_coord_netcdf3.cml index 39cb8f2950..e48cf41d2a 100644 --- a/lib/iris/tests/results/netcdf/int64_auxiliary_coord_netcdf3.cml +++ b/lib/iris/tests/results/netcdf/int64_auxiliary_coord_netcdf3.cml @@ -6,7 +6,7 @@ - + diff --git a/lib/iris/tests/results/netcdf/int64_dimension_coord_netcdf3.cml b/lib/iris/tests/results/netcdf/int64_dimension_coord_netcdf3.cml index 1c59fc947e..78fec459e9 100644 --- a/lib/iris/tests/results/netcdf/int64_dimension_coord_netcdf3.cml +++ b/lib/iris/tests/results/netcdf/int64_dimension_coord_netcdf3.cml @@ -6,7 +6,7 @@ - + diff --git a/lib/iris/tests/results/netcdf/netcdf_cell_methods.cml b/lib/iris/tests/results/netcdf/netcdf_cell_methods.cml index 8dd0e43b71..ca4a0eb017 100644 --- a/lib/iris/tests/results/netcdf/netcdf_cell_methods.cml +++ b/lib/iris/tests/results/netcdf/netcdf_cell_methods.cml @@ -1,6 +1,6 @@ - + @@ -20,7 +20,7 @@ - + @@ -41,7 +41,7 @@ - + @@ -66,7 +66,7 @@ - + @@ -89,7 +89,7 @@ - + @@ -112,7 +112,7 @@ - + @@ -131,7 +131,7 @@ - + @@ -150,7 +150,7 @@ - + @@ -170,7 +170,7 @@ - + @@ -190,7 +190,7 @@ - + @@ -213,7 +213,7 @@ - + @@ -232,7 +232,7 @@ - + @@ -252,7 +252,7 @@ - + @@ -272,7 +272,7 @@ - + @@ -295,7 +295,7 @@ - + @@ -320,7 +320,7 @@ - + @@ -333,7 +333,7 @@ - + @@ -346,7 +346,7 @@ - + @@ -359,7 +359,7 @@ - + @@ -372,7 +372,7 @@ - + @@ -385,7 +385,7 @@ - + @@ -404,7 +404,7 @@ - + @@ -424,7 +424,7 @@ - + @@ -447,7 +447,7 @@ - + @@ -460,7 +460,7 @@ - + @@ -473,7 +473,7 @@ - + @@ -486,7 +486,7 @@ - + @@ -499,7 +499,7 @@ - + diff --git a/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems.cml b/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems.cml index 27d4569236..ac41f4a8b8 100644 --- a/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems.cml +++ b/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems.cml @@ -13,11 +13,11 @@ - + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]" shape="(60,)" units="Unit('unknown')" value_type="int32" var_name="levelist"/> @@ -39,11 +39,11 @@ - + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]" shape="(60,)" units="Unit('unknown')" value_type="int32" var_name="levelist"/> diff --git a/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems_iter_0.cml b/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems_iter_0.cml index d677191beb..4234b5cc84 100644 --- a/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems_iter_0.cml +++ b/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems_iter_0.cml @@ -13,11 +13,11 @@ - + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]" shape="(60,)" units="Unit('unknown')" value_type="int32" var_name="levelist"/> diff --git a/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems_iter_1.cml b/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems_iter_1.cml index 775f480c66..17d87a0190 100644 --- a/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems_iter_1.cml +++ b/lib/iris/tests/results/netcdf/netcdf_global_xyzt_gems_iter_1.cml @@ -13,11 +13,11 @@ - + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]" shape="(60,)" units="Unit('unknown')" value_type="int32" var_name="levelist"/> diff --git a/lib/iris/tests/results/netcdf/uint32_auxiliary_coord_netcdf3.cml b/lib/iris/tests/results/netcdf/uint32_auxiliary_coord_netcdf3.cml index 39cb8f2950..e48cf41d2a 100644 --- a/lib/iris/tests/results/netcdf/uint32_auxiliary_coord_netcdf3.cml +++ b/lib/iris/tests/results/netcdf/uint32_auxiliary_coord_netcdf3.cml @@ -6,7 +6,7 @@ - + diff --git a/lib/iris/tests/results/netcdf/uint32_dimension_coord_netcdf3.cml b/lib/iris/tests/results/netcdf/uint32_dimension_coord_netcdf3.cml index 1c59fc947e..78fec459e9 100644 --- a/lib/iris/tests/results/netcdf/uint32_dimension_coord_netcdf3.cml +++ b/lib/iris/tests/results/netcdf/uint32_dimension_coord_netcdf3.cml @@ -6,7 +6,7 @@ - + diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/002000000000.44.101.131200.1920.09.01.00.00.b_0.cml b/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/002000000000.44.101.131200.1920.09.01.00.00.b_0.cml index 3ea688d1fa..0bf359e9c4 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/002000000000.44.101.131200.1920.09.01.00.00.b_0.cml +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/002000000000.44.101.131200.1920.09.01.00.00.b_0.cml @@ -1,6 +1,6 @@ - + diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/008000000000.44.101.000128.1890.09.01.00.00.b_0.cml b/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/008000000000.44.101.000128.1890.09.01.00.00.b_0.cml index 829c7ce38e..e5cec55565 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/008000000000.44.101.000128.1890.09.01.00.00.b_0.cml +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/008000000000.44.101.000128.1890.09.01.00.00.b_0.cml @@ -1,6 +1,6 @@ - + diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/st0fc699.b_0.cml b/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/st0fc699.b_0.cml index 4f84609832..b484ebb305 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/st0fc699.b_0.cml +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/st0fc699.b_0.cml @@ -1,6 +1,6 @@ - + diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/st0fc942.b_0.cml b/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/st0fc942.b_0.cml index caafa5845c..c594c748cd 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/st0fc942.b_0.cml +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/from_netcdf/st0fc942.b_0.cml @@ -1,6 +1,6 @@ - + diff --git a/lib/iris/tests/test_netcdf.py b/lib/iris/tests/test_netcdf.py index e3d23727a6..c69a83edd5 100644 --- a/lib/iris/tests/test_netcdf.py +++ b/lib/iris/tests/test_netcdf.py @@ -16,9 +16,11 @@ import os.path import shutil import stat +from subprocess import check_call import tempfile from unittest import mock +from cf_units import as_unit import netCDF4 as nc import numpy as np import numpy.ma as ma @@ -27,6 +29,7 @@ import iris.analysis.trajectory import iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc as pyke_rules import iris.fileformats.netcdf +from iris.fileformats.netcdf import load_cubes as nc_load_cubes import iris.std_names import iris.util import iris.coord_systems as icoord_systems @@ -292,6 +295,42 @@ def test_deferred_loading(self): cube[0][(0, 2), (1, 3)], ("netcdf", "netcdf_deferred_mix_1.cml") ) + def test_default_units(self): + # Note: using a CDL string as a test data reference, rather than a binary file. + ref_cdl = """ + netcdf cm_attr { + dimensions: + axv = 3 ; + ayv = 2 ; + variables: + int64 qqv(ayv, axv) ; + qqv:long_name = "qq" ; + int64 ayv(ayv) ; + ayv:long_name = "y" ; + int64 axv(axv) ; + axv:units = "1" ; + axv:long_name = "x" ; + data: + axv = 11, 12, 13; + ayv = 21, 22; + } + """ + self.tmpdir = tempfile.mkdtemp() + cdl_path = os.path.join(self.tmpdir, "tst.cdl") + nc_path = os.path.join(self.tmpdir, "tst.nc") + # Write CDL string into a temporary CDL file. + with open(cdl_path, "w") as f_out: + f_out.write(ref_cdl) + # Use ncgen to convert this into an actual (temporary) netCDF file. + command = "ncgen -o {} {}".format(nc_path, cdl_path) + check_call(command, shell=True) + # Load with iris.fileformats.netcdf.load_cubes, and check expected content. + cubes = list(nc_load_cubes(nc_path)) + self.assertEqual(len(cubes), 1) + self.assertEqual(cubes[0].units, as_unit("unknown")) + self.assertEqual(cubes[0].coord("y").units, as_unit("unknown")) + self.assertEqual(cubes[0].coord("x").units, as_unit(1)) + def test_units(self): # Test exercising graceful cube and coordinate units loading. cube0, cube1 = sorted( From 912f500c5a5683c74a7b57de5d8010c216cba89c Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Mon, 1 Jun 2020 17:52:42 +0100 Subject: [PATCH 03/32] Unify saving behaviour of "unknown" and "no_unit" (#3711) --- lib/iris/fileformats/netcdf.py | 7 ++++--- lib/iris/tests/results/netcdf/netcdf_save_no_name.cdl | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 4d7ddedc61..6f17f6cd9d 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -22,6 +22,7 @@ import warnings import dask.array as da +import cf_units import netCDF4 import numpy as np import numpy.ma as ma @@ -1760,7 +1761,7 @@ def _inner_create_cf_cellmeasure_or_ancil_variable( # Add the data to the CF-netCDF variable. cf_var[:] = data - if dimensional_metadata.units != "unknown": + if dimensional_metadata.units.is_udunits(): _setncattr(cf_var, "units", str(dimensional_metadata.units)) if dimensional_metadata.standard_name is not None: @@ -1926,7 +1927,7 @@ def _create_cf_coord_variable(self, cube, dimension_names, coord): # Deal with CF-netCDF units and standard name. standard_name, long_name, units = self._cf_coord_identity(coord) - if units != "unknown": + if cf_units.as_unit(units).is_udunits(): _setncattr(cf_var, "units", units) if standard_name is not None: @@ -2371,7 +2372,7 @@ def store(data, cf_var, fill_value): if cube.long_name: _setncattr(cf_var, "long_name", cube.long_name) - if cube.units != "unknown": + if cube.units.is_udunits(): _setncattr(cf_var, "units", str(cube.units)) # Add the CF-netCDF calendar attribute. diff --git a/lib/iris/tests/results/netcdf/netcdf_save_no_name.cdl b/lib/iris/tests/results/netcdf/netcdf_save_no_name.cdl index be13f83fc8..f1399e88b3 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_no_name.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_no_name.cdl @@ -9,7 +9,6 @@ variables: double dim1(dim1) ; dim1:units = "m" ; char unknown_scalar(string6) ; - unknown_scalar:units = "no_unit" ; // global attributes: :Conventions = "CF-1.7" ; From 94d561945f741b43968912508b333d67f4e4e1b4 Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Wed, 10 Jun 2020 16:28:06 +0100 Subject: [PATCH 04/32] fix test (#3732) --- .../compiled_krb/fc_rules_cf_fc/test_get_attr_units.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_get_attr_units.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_get_attr_units.py index c5e36e8d8e..b752de2370 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_get_attr_units.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_get_attr_units.py @@ -44,7 +44,7 @@ def test_unicode_character(self): expected_attributes = {'invalid_units': u'\u266b'} cf_var = self._make_cf_var() attr_units = get_attr_units(cf_var, attributes) - self.assertEqual(attr_units, 'unknown') + self.assertEqual(attr_units, '?') self.assertEqual(attributes, expected_attributes) From c653280b99d576b43bd761b311e16927247a50ee Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Thu, 11 Jun 2020 11:10:04 +0100 Subject: [PATCH 05/32] explicitly set coords in nimrod load with units "1" (#3736) --- lib/iris/fileformats/nimrod_load_rules.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/iris/fileformats/nimrod_load_rules.py b/lib/iris/fileformats/nimrod_load_rules.py index deb4ac862c..81e1b8cf41 100644 --- a/lib/iris/fileformats/nimrod_load_rules.py +++ b/lib/iris/fileformats/nimrod_load_rules.py @@ -301,7 +301,9 @@ def experiment(cube, field): """Add an 'experiment number' to the cube, if present in the field.""" if not is_missing(field, field.experiment_num): cube.add_aux_coord( - DimCoord(field.experiment_num, long_name="experiment_number") + DimCoord( + field.experiment_num, long_name="experiment_number", units="1" + ) ) @@ -592,7 +594,9 @@ def ensemble_member(cube, field): if not is_missing(field, ensemble_member_value): cube.add_aux_coord( DimCoord( - np.array(ensemble_member_value, dtype=np.int32), "realization" + np.array(ensemble_member_value, dtype=np.int32), + "realization", + units="1", ) ) From 404aeaad124d0f372773509de05af3e6e425cd50 Mon Sep 17 00:00:00 2001 From: Stephen Moseley Date: Fri, 12 Jun 2020 11:14:42 +0100 Subject: [PATCH 06/32] Adds rounding to nearest-second to forecast_reference_time coord generation. (#3737) --- lib/iris/fileformats/nimrod_load_rules.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/iris/fileformats/nimrod_load_rules.py b/lib/iris/fileformats/nimrod_load_rules.py index 81e1b8cf41..7a0fd20fb9 100644 --- a/lib/iris/fileformats/nimrod_load_rules.py +++ b/lib/iris/fileformats/nimrod_load_rules.py @@ -233,9 +233,8 @@ def reference_time(cube, field): field.dt_hour, field.dt_minute, ) - ref_time_coord = DimCoord( - np.array(TIME_UNIT.date2num(data_date), dtype=np.int64), + np.array(np.round(TIME_UNIT.date2num(data_date)), dtype=np.int64), standard_name="forecast_reference_time", units=TIME_UNIT, ) From 3398e871813bf7e8d42a8aa777b315f5f0d906d6 Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Mon, 15 Jun 2020 11:34:43 +0100 Subject: [PATCH 07/32] PI-3637: Allow derived coordinates to be removed (#3641) --- lib/iris/cube.py | 3 + .../results/derived/removed_derived_coord.cml | 119 ++++++++++++++++++ lib/iris/tests/test_hybrid.py | 5 + 3 files changed, 127 insertions(+) create mode 100644 lib/iris/tests/results/derived/removed_derived_coord.cml diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 1b1a4d7b9a..03e942c6c9 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1301,6 +1301,9 @@ def _remove_coord(self, coord): for coord_, dims in self._aux_coords_and_dims if coord_ is not coord ] + for aux_factory in self.aux_factories: + if coord._as_defn() == aux_factory._as_defn(): + self.remove_aux_factory(aux_factory) def remove_coord(self, coord): """ diff --git a/lib/iris/tests/results/derived/removed_derived_coord.cml b/lib/iris/tests/results/derived/removed_derived_coord.cml new file mode 100644 index 0000000000..12feb2b643 --- /dev/null +++ b/lib/iris/tests/results/derived/removed_derived_coord.cml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/test_hybrid.py b/lib/iris/tests/test_hybrid.py index 28a733f7cc..29bad235c7 100644 --- a/lib/iris/tests/test_hybrid.py +++ b/lib/iris/tests/test_hybrid.py @@ -51,6 +51,11 @@ def test_indexing(self): _ = cube.coord("altitude") self.assertCML(cube, ("derived", "column.cml")) + def test_removing_derived_coord(self): + cube = self.cube + cube.remove_coord("altitude") + self.assertCML(cube, ("derived", "removed_derived_coord.cml")) + def test_removing_sigma(self): # Check the cube remains OK when sigma is removed. cube = self.cube From be9e2ff703e4a87f2c2533e86bd06aa402563b80 Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Mon, 15 Jun 2020 17:32:03 +0100 Subject: [PATCH 08/32] add whatsnew (#3740) --- ...ugfix_2020-Jun-15_remove_aux_factories_with_remove_coord.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Jun-15_remove_aux_factories_with_remove_coord.txt diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Jun-15_remove_aux_factories_with_remove_coord.txt b/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Jun-15_remove_aux_factories_with_remove_coord.txt new file mode 100644 index 0000000000..f5d88ab357 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Jun-15_remove_aux_factories_with_remove_coord.txt @@ -0,0 +1,2 @@ +* The method :meth:`~iris.Cube.cube.remove_coord` would fail to remove derived + coordinates, will now remove derived coordinates by removing aux_factories. \ No newline at end of file From a8f90efac29ce197f1bee0ddfbd6690ccc0e2b59 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 25 Jun 2020 09:08:37 +0100 Subject: [PATCH 09/32] Remove iris-grib dependency from Iris tests. (#3742) * Remove file-format interoperability tests (gone to iris-grib). * Don't install iris-grib for 'full' travis tests. * Remove 'stock cube' loaded from grib (unused). * Remove unused CMLs * Remove grib from system filetypes testing. * Remove grib-specific test support. * Install grib support for docs+examples only. * Update example code to use iris-grib. * Remove iris-grib check from test runner. * Remove intersphinx mapping to iris-grib docs (unused). * Remove flake8 excludes for sourcefiles no longer existing. * Reinstate 'stop' code, removed in error. --- .travis.yml | 8 +- docs/iris/example_tests/test_polar_stereo.py | 1 - docs/iris/src/conf.py | 1 - docs/iris/src/userguide/saving_iris_cubes.rst | 9 +- lib/iris/tests/__init__.py | 13 -- .../integration/format_interop/__init__.py | 6 - .../format_interop/test_name_grib.py | 115 ------------------ .../format_interop/test_pp_grib.py | 50 -------- .../NAMEII/0_TRACER_AIR_CONCENTRATION.cml | 37 ------ .../name_grib/NAMEII/1_TRACER_DOSAGE.cml | 37 ------ .../NAMEII/3_TRACER_DRY_DEPOSITION.cml | 34 ------ .../NAMEII/4_TRACER_TOTAL_DEPOSITION.cml | 34 ------ .../NAMEIII/0_TRACER_AIR_CONCENTRATION.cml | 34 ------ .../NAMEIII/1_TRACER_AIR_CONCENTRATION.cml | 34 ------ .../NAMEIII/2_TRACER_DRY_DEPOSITION.cml | 34 ------ .../NAMEIII/3_TRACER_WET_DEPOSITION.cml | 34 ------ .../name_grib/NAMEIII/4_TRACER_DEPOSITION.cml | 34 ------ lib/iris/tests/runner/_runner.py | 5 +- lib/iris/tests/stock/__init__.py | 6 - lib/iris/tests/system_test.py | 2 - setup.cfg | 3 - 21 files changed, 8 insertions(+), 523 deletions(-) delete mode 100644 lib/iris/tests/integration/format_interop/__init__.py delete mode 100644 lib/iris/tests/integration/format_interop/test_name_grib.py delete mode 100644 lib/iris/tests/integration/format_interop/test_pp_grib.py delete mode 100644 lib/iris/tests/results/integration/name_grib/NAMEII/0_TRACER_AIR_CONCENTRATION.cml delete mode 100644 lib/iris/tests/results/integration/name_grib/NAMEII/1_TRACER_DOSAGE.cml delete mode 100644 lib/iris/tests/results/integration/name_grib/NAMEII/3_TRACER_DRY_DEPOSITION.cml delete mode 100644 lib/iris/tests/results/integration/name_grib/NAMEII/4_TRACER_TOTAL_DEPOSITION.cml delete mode 100644 lib/iris/tests/results/integration/name_grib/NAMEIII/0_TRACER_AIR_CONCENTRATION.cml delete mode 100644 lib/iris/tests/results/integration/name_grib/NAMEIII/1_TRACER_AIR_CONCENTRATION.cml delete mode 100644 lib/iris/tests/results/integration/name_grib/NAMEIII/2_TRACER_DRY_DEPOSITION.cml delete mode 100644 lib/iris/tests/results/integration/name_grib/NAMEIII/3_TRACER_WET_DEPOSITION.cml delete mode 100644 lib/iris/tests/results/integration/name_grib/NAMEIII/4_TRACER_DEPOSITION.cml diff --git a/.travis.yml b/.travis.yml index 312914d634..368fe33e31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -100,12 +100,10 @@ install: - python setup.py --quiet install - # TODO : remove when iris doesn't do an integration test requiring iris-grib. - # test against the latest version of python-eccodes. - # Conda-forge versioning is out of order (0.9.* is later than 2.12.*). + # Docs builds and examples tests also need grib, for gallery examples. - > - if [[ "${TEST_MINIMAL}" != true ]]; then - conda install --quiet -n ${ENV_NAME} python-eccodes">=0.9.1, <2"; + if [[ "${TEST_TARGET}" == 'docstest' || "${TEST_TARGET}" == 'example' ]]; then + conda install --quiet -n ${ENV_NAME} python-eccodes; conda install --quiet -n ${ENV_NAME} --no-deps iris-grib; fi diff --git a/docs/iris/example_tests/test_polar_stereo.py b/docs/iris/example_tests/test_polar_stereo.py index 63581e7707..963d5729fe 100644 --- a/docs/iris/example_tests/test_polar_stereo.py +++ b/docs/iris/example_tests/test_polar_stereo.py @@ -15,7 +15,6 @@ ) -@tests.skip_grib class TestPolarStereo(tests.GraphicsTest): """Test the polar_stereo example code.""" diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 98c12d2cb2..0e4c53bccc 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -140,7 +140,6 @@ intersphinx_mapping = { "cartopy": ("http://scitools.org.uk/cartopy/docs/latest/", None), - "iris-grib": ("http://iris-grib.readthedocs.io/en/latest/", None), "matplotlib": ("http://matplotlib.org/", None), "numpy": ("http://docs.scipy.org/doc/numpy/", None), "python": ("http://docs.python.org/2.7", None), diff --git a/docs/iris/src/userguide/saving_iris_cubes.rst b/docs/iris/src/userguide/saving_iris_cubes.rst index ecf2210810..f14f83006e 100644 --- a/docs/iris/src/userguide/saving_iris_cubes.rst +++ b/docs/iris/src/userguide/saving_iris_cubes.rst @@ -6,8 +6,8 @@ Saving Iris cubes Iris supports the saving of cubes and cube lists to: -* CF netCDF (1.5) -* GRIB (edition 2) +* CF netCDF (version 1.6) +* GRIB edition 2 (if `iris-grib `_ is installed) * Met Office PP @@ -57,7 +57,6 @@ The :py:func:`iris.save` function passes all other keywords through to the saver See * :py:func:`iris.fileformats.netcdf.save` -* :py:func:`iris.fileformats.grib.save_grib2` * :py:func:`iris.fileformats.pp.save` for more details on supported arguments for the individual savers. @@ -70,14 +69,14 @@ When saving to GRIB or PP, the save process may be intercepted between the trans For example, a GRIB2 message with a particular known long_name may need to be saved to a specific parameter code and type of statistical process. This can be achieved by:: def tweaked_messages(cube): - for cube, grib_message in iris.fileformats.grib.as_pairs(cube): + for cube, grib_message in iris_grib.save_pairs_from_cube(cube): # post process the GRIB2 message, prior to saving if cube.name() == 'carefully_customised_precipitation_amount': gribapi.grib_set_long(grib_message, "typeOfStatisticalProcess", 1) gribapi.grib_set_long(grib_message, "parameterCategory", 1) gribapi.grib_set_long(grib_message, "parameterNumber", 1) yield grib_message - iris.fileformats.grib.save_messages(tweaked_messages(cubes[0]), '/tmp/agrib2.grib2') + iris_grib.save_messages(tweaked_messages(cubes[0]), '/tmp/agrib2.grib2') Similarly a PP field may need to be written out with a specific value for LBEXP. This can be achieved by:: diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index 66966daaf4..8602defa16 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -76,13 +76,6 @@ else: GDAL_AVAILABLE = True -try: - from iris_grib.message import GribMessage - - GRIB_AVAILABLE = True -except ImportError: - GRIB_AVAILABLE = False - try: import iris_sample_data # noqa except ImportError: @@ -1181,12 +1174,6 @@ class MyPlotTests(test.GraphicsTest): return skip(fn) -skip_grib = unittest.skipIf( - not GRIB_AVAILABLE, - 'Test(s) require "iris-grib" package, ' "which is not available.", -) - - skip_sample_data = unittest.skipIf( not SAMPLE_DATA_AVAILABLE, ('Test(s) require "iris-sample-data", ' "which is not available."), diff --git a/lib/iris/tests/integration/format_interop/__init__.py b/lib/iris/tests/integration/format_interop/__init__.py deleted file mode 100644 index b9024f2f39..0000000000 --- a/lib/iris/tests/integration/format_interop/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# 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 -# licensing details. -"""Integration tests for format interoperability.""" diff --git a/lib/iris/tests/integration/format_interop/test_name_grib.py b/lib/iris/tests/integration/format_interop/test_name_grib.py deleted file mode 100644 index 63889b879d..0000000000 --- a/lib/iris/tests/integration/format_interop/test_name_grib.py +++ /dev/null @@ -1,115 +0,0 @@ -# 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 -# licensing details. -"""Integration tests for NAME to GRIB2 interoperability.""" - -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests - -import numpy as np -import warnings - -import iris - - -def name_cb(cube, field, filename): - # NAME files give the time point at the end of the range but Iris' - # GRIB loader creates it in the middle (the GRIB file itself doesn't - # encode a time point). Here we make them consistent so we can - # easily compare them. - t_coord = cube.coord("time") - t_coord.points = t_coord.bounds[0][1] - fp_coord = cube.coord("forecast_period") - fp_coord.points = fp_coord.bounds[0][1] - # NAME contains extra vertical meta-data. - z_coord = cube.coords("height") - if z_coord: - z_coord[0].standard_name = "height" - z_coord[0].long_name = "height above ground level" - - -@tests.skip_grib -class TestNameToGRIB(tests.IrisTest): - def check_common(self, name_cube, grib_cube): - self.assertTrue(np.allclose(name_cube.data, name_cube.data)) - self.assertTrue( - np.allclose( - name_cube.coord("latitude").points, - grib_cube.coord("latitude").points, - ) - ) - self.assertTrue( - np.allclose( - name_cube.coord("longitude").points, - grib_cube.coord("longitude").points - 360, - ) - ) - - for c in ["height", "time"]: - if name_cube.coords(c): - self.assertEqual(name_cube.coord(c), grib_cube.coord(c)) - - @tests.skip_data - def test_name2_field(self): - filepath = tests.get_data_path(("NAME", "NAMEII_field.txt")) - name_cubes = iris.load(filepath) - - # There is a known load/save problem with numerous - # gribapi/eccodes versions and - # zero only data, where min == max. - # This may be a problem with data scaling. - for i, name_cube in enumerate(name_cubes): - data = name_cube.data - if np.min(data) == np.max(data): - msg = ( - 'NAMEII cube #{}, "{}" has empty data : ' - "SKIPPING test for this cube, as save/load will " - "not currently work." - ) - warnings.warn(msg.format(i, name_cube.name())) - continue - - with self.temp_filename(".grib2") as temp_filename: - iris.save(name_cube, temp_filename) - grib_cube = iris.load_cube(temp_filename, callback=name_cb) - self.check_common(name_cube, grib_cube) - self.assertCML( - grib_cube, - tests.get_result_path( - ( - "integration", - "name_grib", - "NAMEII", - "{}_{}.cml".format(i, name_cube.name()), - ) - ), - ) - - @tests.skip_data - def test_name3_field(self): - filepath = tests.get_data_path(("NAME", "NAMEIII_field.txt")) - name_cubes = iris.load(filepath) - for i, name_cube in enumerate(name_cubes): - with self.temp_filename(".grib2") as temp_filename: - iris.save(name_cube, temp_filename) - grib_cube = iris.load_cube(temp_filename, callback=name_cb) - - self.check_common(name_cube, grib_cube) - self.assertCML( - grib_cube, - tests.get_result_path( - ( - "integration", - "name_grib", - "NAMEIII", - "{}_{}.cml".format(i, name_cube.name()), - ) - ), - ) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/integration/format_interop/test_pp_grib.py b/lib/iris/tests/integration/format_interop/test_pp_grib.py deleted file mode 100644 index 70d89f834a..0000000000 --- a/lib/iris/tests/integration/format_interop/test_pp_grib.py +++ /dev/null @@ -1,50 +0,0 @@ -# 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 -# licensing details. -"""Integration tests for PP/GRIB interoperability.""" - -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests - -import iris - - -@tests.skip_grib -class TestBoundedTime(tests.IrisTest): - @tests.skip_data - def test_time_and_forecast_period_round_trip(self): - pp_path = tests.get_data_path( - ("PP", "meanMaxMin", "200806081200__qwpb.T24.pp") - ) - # Choose the first time-bounded Cube in the PP dataset. - original = [ - cube - for cube in iris.load(pp_path) - if cube.coord("time").has_bounds() - ][0] - # Save it to GRIB2 and re-load. - with self.temp_filename(".grib2") as grib_path: - iris.save(original, grib_path) - from_grib = iris.load_cube(grib_path) - # Avoid the downcasting warning when saving to PP. - from_grib.data = from_grib.data.astype("f4") - # Re-save to PP and re-load. - with self.temp_filename(".pp") as pp_path: - iris.save(from_grib, pp_path) - from_pp = iris.load_cube(pp_path) - self.assertEqual(original.coord("time"), from_grib.coord("time")) - self.assertEqual( - original.coord("forecast_period"), - from_grib.coord("forecast_period"), - ) - self.assertEqual(original.coord("time"), from_pp.coord("time")) - self.assertEqual( - original.coord("forecast_period"), from_pp.coord("forecast_period") - ) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/results/integration/name_grib/NAMEII/0_TRACER_AIR_CONCENTRATION.cml b/lib/iris/tests/results/integration/name_grib/NAMEII/0_TRACER_AIR_CONCENTRATION.cml deleted file mode 100644 index b0daf50907..0000000000 --- a/lib/iris/tests/results/integration/name_grib/NAMEII/0_TRACER_AIR_CONCENTRATION.cml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/results/integration/name_grib/NAMEII/1_TRACER_DOSAGE.cml b/lib/iris/tests/results/integration/name_grib/NAMEII/1_TRACER_DOSAGE.cml deleted file mode 100644 index aef4988ce6..0000000000 --- a/lib/iris/tests/results/integration/name_grib/NAMEII/1_TRACER_DOSAGE.cml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/results/integration/name_grib/NAMEII/3_TRACER_DRY_DEPOSITION.cml b/lib/iris/tests/results/integration/name_grib/NAMEII/3_TRACER_DRY_DEPOSITION.cml deleted file mode 100644 index 5787c19643..0000000000 --- a/lib/iris/tests/results/integration/name_grib/NAMEII/3_TRACER_DRY_DEPOSITION.cml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/results/integration/name_grib/NAMEII/4_TRACER_TOTAL_DEPOSITION.cml b/lib/iris/tests/results/integration/name_grib/NAMEII/4_TRACER_TOTAL_DEPOSITION.cml deleted file mode 100644 index 5787c19643..0000000000 --- a/lib/iris/tests/results/integration/name_grib/NAMEII/4_TRACER_TOTAL_DEPOSITION.cml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/results/integration/name_grib/NAMEIII/0_TRACER_AIR_CONCENTRATION.cml b/lib/iris/tests/results/integration/name_grib/NAMEIII/0_TRACER_AIR_CONCENTRATION.cml deleted file mode 100644 index 1a31427de0..0000000000 --- a/lib/iris/tests/results/integration/name_grib/NAMEIII/0_TRACER_AIR_CONCENTRATION.cml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/results/integration/name_grib/NAMEIII/1_TRACER_AIR_CONCENTRATION.cml b/lib/iris/tests/results/integration/name_grib/NAMEIII/1_TRACER_AIR_CONCENTRATION.cml deleted file mode 100644 index 7007836e62..0000000000 --- a/lib/iris/tests/results/integration/name_grib/NAMEIII/1_TRACER_AIR_CONCENTRATION.cml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/results/integration/name_grib/NAMEIII/2_TRACER_DRY_DEPOSITION.cml b/lib/iris/tests/results/integration/name_grib/NAMEIII/2_TRACER_DRY_DEPOSITION.cml deleted file mode 100644 index 850ef89ed2..0000000000 --- a/lib/iris/tests/results/integration/name_grib/NAMEIII/2_TRACER_DRY_DEPOSITION.cml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/results/integration/name_grib/NAMEIII/3_TRACER_WET_DEPOSITION.cml b/lib/iris/tests/results/integration/name_grib/NAMEIII/3_TRACER_WET_DEPOSITION.cml deleted file mode 100644 index ade4cea92d..0000000000 --- a/lib/iris/tests/results/integration/name_grib/NAMEIII/3_TRACER_WET_DEPOSITION.cml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/results/integration/name_grib/NAMEIII/4_TRACER_DEPOSITION.cml b/lib/iris/tests/results/integration/name_grib/NAMEIII/4_TRACER_DEPOSITION.cml deleted file mode 100644 index 088b622c46..0000000000 --- a/lib/iris/tests/results/integration/name_grib/NAMEIII/4_TRACER_DEPOSITION.cml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/runner/_runner.py b/lib/iris/tests/runner/_runner.py index 41d27bbfe3..8175e7b19f 100644 --- a/lib/iris/tests/runner/_runner.py +++ b/lib/iris/tests/runner/_runner.py @@ -143,10 +143,7 @@ def run(self): regexp_pat, "--process-timeout=180", ] - try: - import gribapi # noqa - except ImportError: - args.append("--exclude=^grib$") + if self.stop: args.append("--stop") diff --git a/lib/iris/tests/stock/__init__.py b/lib/iris/tests/stock/__init__.py index 9bb1c4626f..ea6d03a442 100644 --- a/lib/iris/tests/stock/__init__.py +++ b/lib/iris/tests/stock/__init__.py @@ -721,12 +721,6 @@ def realistic_4d_w_missing_data(): return cube -def global_grib2(): - path = tests.get_data_path(("GRIB", "global_t", "global.grib2")) - cube = iris.load_cube(path) - return cube - - def ocean_sigma_z(): """ Return a sample cube with an diff --git a/lib/iris/tests/system_test.py b/lib/iris/tests/system_test.py index 207bd700a3..a98a83768a 100644 --- a/lib/iris/tests/system_test.py +++ b/lib/iris/tests/system_test.py @@ -65,8 +65,6 @@ def horiz_cs(): ) filetypes = (".nc", ".pp") - if tests.GRIB_AVAILABLE: - filetypes += (".grib2",) for filetype in filetypes: saved_tmpfile = iris.util.create_temp_filename(suffix=filetype) iris.save(cm, saved_tmpfile) diff --git a/setup.cfg b/setup.cfg index 6e8bd69f88..a87902cbfd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,6 @@ exclude = */iris/std_names.py,\ */iris/io/format_picker.py,\ */iris/tests/__init__.py,\ */iris/tests/pp.py,\ - */iris/tests/stock.py,\ */iris/tests/system_test.py,\ */iris/tests/test_analysis.py,\ */iris/tests/test_analysis_calculus.py,\ @@ -28,8 +27,6 @@ exclude = */iris/std_names.py,\ */iris/tests/test_cube_to_pp.py,\ */iris/tests/test_file_load.py,\ */iris/tests/test_file_save.py,\ - */iris/tests/test_grib_save.py,\ - */iris/tests/test_grib_save_rules.py,\ */iris/tests/test_hybrid.py,\ */iris/tests/test_intersect.py,\ */iris/tests/test_io_init.py,\ From dd5f1fe913eae81cc3001cfa6d3cad398be951e4 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 26 Jun 2020 09:45:46 +0100 Subject: [PATCH 10/32] PI-3745: Stereographic plotting example using netcdf instead of GRIB data (#3746) * Replace grib file with netcdf in polar_stereo example. * Remove iris-grib from Travis testing. * New image hash for new version of test_polar_stereo. --- .travis.yml | 7 ------- docs/iris/example_code/General/polar_stereo.py | 2 +- lib/iris/tests/results/imagerepo.json | 3 ++- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 368fe33e31..ca47a73388 100644 --- a/.travis.yml +++ b/.travis.yml @@ -100,13 +100,6 @@ install: - python setup.py --quiet install - # Docs builds and examples tests also need grib, for gallery examples. - - > - if [[ "${TEST_TARGET}" == 'docstest' || "${TEST_TARGET}" == 'example' ]]; then - conda install --quiet -n ${ENV_NAME} python-eccodes; - conda install --quiet -n ${ENV_NAME} --no-deps iris-grib; - fi - script: # Capture install-dir: As a test command must be last for get Travis to check # the RC, so it's best to start each operation with an absolute cd. diff --git a/docs/iris/example_code/General/polar_stereo.py b/docs/iris/example_code/General/polar_stereo.py index ac8c757ed9..bd4a11923d 100644 --- a/docs/iris/example_code/General/polar_stereo.py +++ b/docs/iris/example_code/General/polar_stereo.py @@ -15,7 +15,7 @@ def main(): - file_path = iris.sample_data_path("polar_stereo.grib2") + file_path = iris.sample_data_path("toa_brightness_stereographic.nc") cube = iris.load_cube(file_path) qplt.contourf(cube) ax = plt.gca() diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json index 884e2b875f..e6a225f022 100644 --- a/lib/iris/tests/results/imagerepo.json +++ b/lib/iris/tests/results/imagerepo.json @@ -108,7 +108,8 @@ ], "example_tests.test_polar_stereo.TestPolarStereo.test_polar_stereo.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/e168317a92d36d89c5bb9e94c55e6f0c9a93c15a6ec584763b21716791de3a81.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/b9e16079971e9e93c8ce0f84c31e3b929f92c0ff3ca1c17e39e03961c07e3f80.png" + "https://scitools.github.io/test-iris-imagehash/images/v4/b9e16079971e9e93c8ce0f84c31e3b929f92c0ff3ca1c17e39e03961c07e3f80.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e615ec7e097a9961f9cb190f838e091c2c1e73f07c11f6f386b3cc1783e11.png" ], "example_tests.test_polynomial_fit.TestPolynomialFit.test_polynomial_fit.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abff4a9df26435886520c97f12414695c4b69d23934bc86adc969237d68ccc6f.png", From 1ea17a9d5a91e70c7623eaa0691bfbfa16b385be Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 6 Jul 2020 14:53:53 +0100 Subject: [PATCH 11/32] pin cftime (#3750) --- requirements/core.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/core.txt b/requirements/core.txt index c3f5775d7e..3f2f458595 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -6,7 +6,7 @@ cartopy>=0.12 #conda: proj4<6 cf-units>=2 -cftime +cftime==1.1.3 dask[array]>=2 #conda: dask>=2 matplotlib netcdf4 From 0971869e3c4879a646c8044b5fe6010c49b0abd6 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Wed, 15 Jul 2020 10:45:15 +0100 Subject: [PATCH 12/32] Read The Docs documentation (#3752) overhaul to use readthedocs theme and service and added whatsnew contribution for readthedocs --- .gitignore | 7 +- .readthedocs.yml | 19 + .travis.yml | 25 +- INSTALL | 7 +- README.md | 6 +- ci/requirements/readthedocs.yml | 60 + docs/iris/Makefile | 28 +- docs/iris/gallery_code/README.rst | 26 + docs/iris/gallery_code/general/README.rst | 2 + .../general}/__init__.py | 0 .../general/plot_SOI_filtering.py} | 0 .../general/plot_anomaly_log_colouring.py} | 2 +- .../general/plot_coriolis.py} | 0 .../general/plot_cross_section.py} | 0 .../general/plot_custom_aggregation.py} | 0 .../general/plot_custom_file_loading.py} | 0 .../general/plot_global_map.py} | 0 .../general/plot_inset.py} | 0 .../general/plot_lineplot_with_legend.py} | 0 .../general/plot_orca_projection.py} | 0 .../general/plot_polar_stereo.py} | 0 .../general/plot_polynomial_fit.py} | 0 .../plot_projections_and_annotations.py} | 0 .../general/plot_rotated_pole_mapping.py} | 0 docs/iris/gallery_code/meteorology/README.rst | 3 + .../meteorology}/__init__.py | 0 .../meteorology/plot_COP_1d.py} | 0 .../meteorology/plot_COP_maps.py} | 0 .../meteorology/plot_TEC.py} | 0 .../meteorology/plot_deriving_phenomena.py} | 0 .../meteorology/plot_hovmoller.py} | 0 .../meteorology/plot_lagged_ensemble.py} | 0 .../meteorology/plot_wind_speed.py} | 0 .../iris/gallery_code/oceanography/README.rst | 3 + .../oceanography}/__init__.py | 0 .../oceanography/plot_atlantic_profiles.py} | 0 .../oceanography/plot_load_nemo.py} | 0 .../__init__.py | 0 .../gallerytest_util.py} | 20 +- .../test_plot_COP_1d.py} | 14 +- .../test_plot_COP_maps.py} | 14 +- .../test_plot_SOI_filtering.py} | 14 +- .../test_plot_TEC.py} | 14 +- .../test_plot_anomaly_log_colouring.py} | 14 +- .../test_plot_atlantic_profiles.py} | 14 +- .../test_plot_coriolis.py} | 14 +- .../test_plot_cross_section.py} | 14 +- .../test_plot_custom_aggregation.py} | 14 +- .../test_plot_custom_file_loading.py} | 14 +- .../test_plot_deriving_phenomena.py} | 14 +- .../test_plot_global_map.py} | 14 +- .../test_plot_hovmoller.py} | 14 +- .../test_plot_inset.py} | 14 +- .../test_plot_lagged_ensemble.py} | 14 +- .../test_plot_lineplot_with_legend.py} | 14 +- .../test_plot_load_nemo.py} | 14 +- .../test_plot_orca_projection.py} | 14 +- .../test_plot_polar_stereo.py} | 14 +- .../test_plot_polynomial_fit.py} | 14 +- .../test_plot_projections_and_annotations.py} | 14 +- .../test_plot_rotated_pole_mapping.py} | 14 +- .../test_plot_wind_speed.py} | 14 +- docs/iris/src/Makefile | 34 +- docs/iris/src/_static/copybutton.js | 59 - docs/iris/src/_static/favicon-16x16.png | Bin 1041 -> 0 bytes docs/iris/src/_static/favicon-32x32.png | Bin 2954 -> 0 bytes docs/iris/src/_static/favicon.ico | Bin 0 -> 1150 bytes docs/iris/src/_static/iris-logo-title.png | Bin 0 -> 38785 bytes docs/iris/src/_static/iris-logo-title.svg | 89 ++ .../src/_static/iris_colour_logo_centred.png | Bin 327072 -> 0 bytes .../src/_static/jquery.cycle.all.latest.js | 1331 ----------------- docs/iris/src/_static/logo_banner.png | Bin 31305 -> 0 bytes docs/iris/src/_static/style.css | 99 -- docs/iris/src/_static/theme_override.css | 28 + docs/iris/src/_templates/index.html | 146 -- docs/iris/src/_templates/layout.html | 88 +- docs/iris/src/conf.py | 351 ++--- docs/iris/src/contents.rst | 32 - docs/iris/src/copyright.rst | 4 +- .../iris/src/developers_guide/code_format.rst | 2 +- .../contributing_documentation.rst | 145 ++ .../documenting/docstrings.rst | 7 +- .../documenting/rest_guide.rst | 24 +- .../documenting/whats_new_contributions.rst | 8 +- .../gitwash/development_workflow.rst | 4 +- .../developers_guide/gitwash/forking_hell.rst | 4 +- .../gitwash/git_development.rst | 2 - .../developers_guide/gitwash/git_install.rst | 6 +- .../developers_guide/gitwash/git_links.inc | 1 - .../gitwash/git_resources.rst | 6 +- .../src/developers_guide/gitwash/index.rst | 2 - .../src/developers_guide/graphics_tests.rst | 31 +- docs/iris/src/developers_guide/index.rst | 19 - docs/iris/src/developers_guide/pulls.rst | 12 +- docs/iris/src/developers_guide/release.rst | 14 +- docs/iris/src/developers_guide/tests.rst | 1 + docs/iris/src/index.rst | 87 ++ docs/iris/src/installing.rst | 3 +- docs/iris/src/sphinxext/auto_label_figures.py | 25 - .../src/sphinxext/custom_class_autodoc.py | 7 +- .../iris/src/sphinxext/custom_data_autodoc.py | 2 +- .../src/sphinxext/gen_example_directory.py | 168 --- docs/iris/src/sphinxext/gen_gallery.py | 201 --- .../src/sphinxext/generate_package_rst.py | 286 ++-- .../change_management.rst | 28 +- .../src/{whitepapers => techpapers}/index.rst | 9 +- .../missing_data_handling.rst | 0 .../um_files_loading.rst | 0 docs/iris/src/userguide/citation.rst | 4 +- docs/iris/src/userguide/code_maintenance.rst | 6 +- docs/iris/src/userguide/cube_maths.rst | 2 +- docs/iris/src/userguide/cube_statistics.rst | 5 +- docs/iris/src/userguide/end_of_userguide.rst | 15 - docs/iris/src/userguide/index.rst | 42 +- docs/iris/src/userguide/iris_cubes.rst | 11 +- .../iris/src/userguide/loading_iris_cubes.rst | 2 + docs/iris/src/userguide/merge_and_concat.rst | 2 +- docs/iris/src/userguide/plotting_a_cube.rst | 6 +- .../plotting_examples/1d_quickplot_simple.py | 1 + .../userguide/plotting_examples/1d_simple.py | 1 + .../plotting_examples/1d_with_legend.py | 1 - .../src/userguide/plotting_examples/brewer.py | 39 +- .../plotting_examples/cube_blockplot.py | 1 - .../cube_brewer_cite_contourf.py | 1 - docs/iris/src/userguide/saving_iris_cubes.rst | 2 +- docs/iris/src/whatsnew/1.0.rst | 21 +- docs/iris/src/whatsnew/1.10.rst | 10 +- docs/iris/src/whatsnew/1.11.rst | 8 +- docs/iris/src/whatsnew/1.12.rst | 6 +- docs/iris/src/whatsnew/1.13.rst | 6 +- docs/iris/src/whatsnew/1.4.rst | 4 +- docs/iris/src/whatsnew/1.5.rst | 2 +- docs/iris/src/whatsnew/1.7.rst | 13 +- docs/iris/src/whatsnew/1.8.rst | 2 +- docs/iris/src/whatsnew/1.9.rst | 10 +- docs/iris/src/whatsnew/2.0.rst | 8 +- docs/iris/src/whatsnew/2.1.rst | 10 +- docs/iris/src/whatsnew/2.2.rst | 10 +- docs/iris/src/whatsnew/2.3.rst | 46 +- docs/iris/src/whatsnew/2.4.rst | 8 +- docs/iris/src/whatsnew/aggregate_directory.py | 4 +- ...mentation_using_themes_and_readthedocs.txt | 2 + ..._2020-Jan-31_nimrod_format_enhancement.txt | 4 +- docs/iris/src/whatsnew/index.rst | 4 +- lib/iris/_constraints.py | 4 +- lib/iris/analysis/__init__.py | 13 +- lib/iris/analysis/stats.py | 2 +- lib/iris/cube.py | 2 + lib/iris/experimental/stratify.py | 4 +- lib/iris/fileformats/cf.py | 2 +- lib/iris/fileformats/netcdf.py | 14 +- lib/iris/tests/__init__.py | 2 +- lib/iris/tests/results/imagerepo.json | 68 +- lib/iris/tests/runner/_runner.py | 22 +- lib/iris/tests/test_coding_standards.py | 5 +- requirements/docs.txt | 3 + setup.py | 1 - tools/generate_std_names.py | 9 +- 158 files changed, 1390 insertions(+), 2975 deletions(-) create mode 100644 .readthedocs.yml create mode 100644 ci/requirements/readthedocs.yml create mode 100644 docs/iris/gallery_code/README.rst create mode 100644 docs/iris/gallery_code/general/README.rst rename docs/iris/{example_code/General => gallery_code/general}/__init__.py (100%) rename docs/iris/{example_code/General/SOI_filtering.py => gallery_code/general/plot_SOI_filtering.py} (100%) rename docs/iris/{example_code/General/anomaly_log_colouring.py => gallery_code/general/plot_anomaly_log_colouring.py} (97%) rename docs/iris/{example_code/General/coriolis_plot.py => gallery_code/general/plot_coriolis.py} (100%) rename docs/iris/{example_code/General/cross_section.py => gallery_code/general/plot_cross_section.py} (100%) rename docs/iris/{example_code/General/custom_aggregation.py => gallery_code/general/plot_custom_aggregation.py} (100%) rename docs/iris/{example_code/General/custom_file_loading.py => gallery_code/general/plot_custom_file_loading.py} (100%) rename docs/iris/{example_code/General/global_map.py => gallery_code/general/plot_global_map.py} (100%) rename docs/iris/{example_code/General/inset_plot.py => gallery_code/general/plot_inset.py} (100%) rename docs/iris/{example_code/General/lineplot_with_legend.py => gallery_code/general/plot_lineplot_with_legend.py} (100%) rename docs/iris/{example_code/General/orca_projection.py => gallery_code/general/plot_orca_projection.py} (100%) rename docs/iris/{example_code/General/polar_stereo.py => gallery_code/general/plot_polar_stereo.py} (100%) rename docs/iris/{example_code/General/polynomial_fit.py => gallery_code/general/plot_polynomial_fit.py} (100%) rename docs/iris/{example_code/General/projections_and_annotations.py => gallery_code/general/plot_projections_and_annotations.py} (100%) rename docs/iris/{example_code/General/rotated_pole_mapping.py => gallery_code/general/plot_rotated_pole_mapping.py} (100%) create mode 100644 docs/iris/gallery_code/meteorology/README.rst rename docs/iris/{example_code/Meteorology => gallery_code/meteorology}/__init__.py (100%) rename docs/iris/{example_code/Meteorology/COP_1d_plot.py => gallery_code/meteorology/plot_COP_1d.py} (100%) rename docs/iris/{example_code/Meteorology/COP_maps.py => gallery_code/meteorology/plot_COP_maps.py} (100%) rename docs/iris/{example_code/Meteorology/TEC.py => gallery_code/meteorology/plot_TEC.py} (100%) rename docs/iris/{example_code/Meteorology/deriving_phenomena.py => gallery_code/meteorology/plot_deriving_phenomena.py} (100%) rename docs/iris/{example_code/Meteorology/hovmoller.py => gallery_code/meteorology/plot_hovmoller.py} (100%) rename docs/iris/{example_code/Meteorology/lagged_ensemble.py => gallery_code/meteorology/plot_lagged_ensemble.py} (100%) rename docs/iris/{example_code/Meteorology/wind_speed.py => gallery_code/meteorology/plot_wind_speed.py} (100%) create mode 100644 docs/iris/gallery_code/oceanography/README.rst rename docs/iris/{example_code/Oceanography => gallery_code/oceanography}/__init__.py (100%) rename docs/iris/{example_code/Oceanography/atlantic_profiles.py => gallery_code/oceanography/plot_atlantic_profiles.py} (100%) rename docs/iris/{example_code/Oceanography/load_nemo.py => gallery_code/oceanography/plot_load_nemo.py} (100%) rename docs/iris/{example_tests => gallery_tests}/__init__.py (100%) rename docs/iris/{example_tests/extest_util.py => gallery_tests/gallerytest_util.py} (84%) rename docs/iris/{example_tests/test_COP_1d_plot.py => gallery_tests/test_plot_COP_1d.py} (70%) rename docs/iris/{example_tests/test_COP_maps.py => gallery_tests/test_plot_COP_maps.py} (70%) rename docs/iris/{example_tests/test_SOI_filtering.py => gallery_tests/test_plot_SOI_filtering.py} (68%) rename docs/iris/{example_tests/test_TEC.py => gallery_tests/test_plot_TEC.py} (71%) rename docs/iris/{example_tests/test_anomaly_log_colouring.py => gallery_tests/test_plot_anomaly_log_colouring.py} (66%) rename docs/iris/{example_tests/test_atlantic_profiles.py => gallery_tests/test_plot_atlantic_profiles.py} (67%) rename docs/iris/{example_tests/test_coriolis_plot.py => gallery_tests/test_plot_coriolis.py} (59%) rename docs/iris/{example_tests/test_cross_section.py => gallery_tests/test_plot_cross_section.py} (68%) rename docs/iris/{example_tests/test_custom_aggregation.py => gallery_tests/test_plot_custom_aggregation.py} (67%) rename docs/iris/{example_tests/test_custom_file_loading.py => gallery_tests/test_plot_custom_file_loading.py} (67%) rename docs/iris/{example_tests/test_deriving_phenomena.py => gallery_tests/test_plot_deriving_phenomena.py} (67%) rename docs/iris/{example_tests/test_hovmoller.py => gallery_tests/test_plot_global_map.py} (69%) rename docs/iris/{example_tests/test_global_map.py => gallery_tests/test_plot_hovmoller.py} (69%) rename docs/iris/{example_tests/test_inset_plot.py => gallery_tests/test_plot_inset.py} (70%) rename docs/iris/{example_tests/test_lagged_ensemble.py => gallery_tests/test_plot_lagged_ensemble.py} (68%) rename docs/iris/{example_tests/test_lineplot_with_legend.py => gallery_tests/test_plot_lineplot_with_legend.py} (66%) rename docs/iris/{example_tests/test_load_nemo.py => gallery_tests/test_plot_load_nemo.py} (69%) rename docs/iris/{example_tests/test_orca_projection.py => gallery_tests/test_plot_orca_projection.py} (68%) rename docs/iris/{example_tests/test_polar_stereo.py => gallery_tests/test_plot_polar_stereo.py} (69%) rename docs/iris/{example_tests/test_polynomial_fit.py => gallery_tests/test_plot_polynomial_fit.py} (68%) rename docs/iris/{example_tests/test_projections_and_annotations.py => gallery_tests/test_plot_projections_and_annotations.py} (65%) rename docs/iris/{example_tests/test_rotated_pole_mapping.py => gallery_tests/test_plot_rotated_pole_mapping.py} (66%) rename docs/iris/{example_tests/test_wind_speed.py => gallery_tests/test_plot_wind_speed.py} (69%) delete mode 100644 docs/iris/src/_static/copybutton.js delete mode 100644 docs/iris/src/_static/favicon-16x16.png delete mode 100644 docs/iris/src/_static/favicon-32x32.png create mode 100644 docs/iris/src/_static/favicon.ico create mode 100644 docs/iris/src/_static/iris-logo-title.png create mode 100644 docs/iris/src/_static/iris-logo-title.svg delete mode 100755 docs/iris/src/_static/iris_colour_logo_centred.png delete mode 100644 docs/iris/src/_static/jquery.cycle.all.latest.js delete mode 100644 docs/iris/src/_static/logo_banner.png delete mode 100644 docs/iris/src/_static/style.css create mode 100644 docs/iris/src/_static/theme_override.css delete mode 100644 docs/iris/src/_templates/index.html delete mode 100644 docs/iris/src/contents.rst create mode 100644 docs/iris/src/developers_guide/contributing_documentation.rst delete mode 100644 docs/iris/src/developers_guide/index.rst create mode 100644 docs/iris/src/index.rst delete mode 100644 docs/iris/src/sphinxext/auto_label_figures.py delete mode 100644 docs/iris/src/sphinxext/gen_example_directory.py delete mode 100644 docs/iris/src/sphinxext/gen_gallery.py rename docs/iris/src/{whitepapers => techpapers}/change_management.rst (96%) rename docs/iris/src/{whitepapers => techpapers}/index.rst (54%) rename docs/iris/src/{whitepapers => techpapers}/missing_data_handling.rst (100%) rename docs/iris/src/{whitepapers => techpapers}/um_files_loading.rst (100%) delete mode 100644 docs/iris/src/userguide/end_of_userguide.rst create mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-10_modernise_documentation_using_themes_and_readthedocs.txt diff --git a/.gitignore b/.gitignore index 48cddc53be..d589c306fe 100644 --- a/.gitignore +++ b/.gitignore @@ -55,11 +55,8 @@ lib/iris/tests/results/imagerepo.lock *.cover # Auto generated documentation files -docs/iris/src/_static/random_image.js -docs/iris/src/_templates/gallery.html -docs/iris/src/examples/ -docs/iris/src/iris/ -docs/iris/src/matplotlibrc +docs/iris/src/_build/* +docs/iris/src/generated # Example test results docs/iris/iris_image_test_output/ diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000000..1306c3fc2c --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,19 @@ +version: 2 + +build: + image: latest + +conda: + environment: ci/requirements/readthedocs.yml + +sphinx: + configuration: docs/iris/src/conf.py + fail_on_warning: false + +python: + install: + - method: setuptools + path: . + +formats: + - htmlzip diff --git a/.travis.yml b/.travis.yml index ca47a73388..262e8d7791 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,12 +15,12 @@ env: matrix: - PYTHON_VERSION=3.6 TEST_TARGET=default TEST_MINIMAL=true - PYTHON_VERSION=3.6 TEST_TARGET=default TEST_BLACK=true - - PYTHON_VERSION=3.6 TEST_TARGET=example - + - PYTHON_VERSION=3.6 TEST_TARGET=gallery - PYTHON_VERSION=3.7 TEST_TARGET=default TEST_MINIMAL=true - PYTHON_VERSION=3.7 TEST_TARGET=default TEST_BLACK=true - - PYTHON_VERSION=3.7 TEST_TARGET=example + - PYTHON_VERSION=3.7 TEST_TARGET=gallery - PYTHON_VERSION=3.7 TEST_TARGET=doctest PUSH_BUILT_DOCS=true + - PYTHON_VERSION=3.7 TEST_TARGET=linkcheck git: # We need a deep clone so that we can compute the age of the files using their git history. @@ -61,8 +61,8 @@ install: if [[ "${TEST_MINIMAL}" != true ]]; then CONDA_REQS_GROUPS="${CONDA_REQS_GROUPS} all"; fi; - if [[ "${TEST_TARGET}" == 'doctest' ]]; then - CONDA_REQS_GROUPS="${CONDA_REQS_GROUPS} docs"; + if [[ "${TEST_TARGET}" == 'doctest' || "${TEST_TARGET}" == 'linkcheck' ]]; then + CONDA_REQS_GROUPS="${CONDA_REQS_GROUPS} docs"; fi; CONDA_REQS_FILE="conda-requirements.txt"; python requirements/gen_conda_requirements.py --groups ${CONDA_REQS_GROUPS} > ${CONDA_REQS_FILE}; @@ -118,8 +118,8 @@ script: python -m iris.tests.runner --default-tests --system-tests; fi - - if [[ "${TEST_TARGET}" == 'example' ]]; then - python -m iris.tests.runner --example-tests; + - if [[ "${TEST_TARGET}" == 'gallery' ]]; then + python -m iris.tests.runner --gallery-tests; fi # A call to check "whatsnew" contributions are valid, because the Iris test @@ -152,6 +152,17 @@ script: make clean html && make doctest; fi + # check the links in the docs + - > + if [[ "${TEST_TARGET}" == 'linkcheck' ]]; then + MPL_RC_DIR="${HOME}/.config/matplotlib"; + mkdir -p ${MPL_RC_DIR}; + echo 'backend : agg' > ${MPL_RC_DIR}/matplotlibrc; + echo 'image.cmap : viridis' >> ${MPL_RC_DIR}/matplotlibrc; + cd ${INSTALL_DIR}/docs/iris; + make clean && make linkcheck; + fi + # Split the organisation out of the slug. See https://stackoverflow.com/a/5257398/741316 for description. # NOTE: a *separate* "export" command appears to be necessary here : A command of the # form "export ORG=.." failed to define ORG for the following command (?!) diff --git a/INSTALL b/INSTALL index 9296f97a29..cf4c4d1bae 100644 --- a/INSTALL +++ b/INSTALL @@ -1,9 +1,11 @@ You can either install Iris using the conda package manager or from source. + Installing using conda ---------------------- Iris is available using conda for the following platforms: + * Linux 64-bit, * Mac OSX 64-bit, and * Windows 32-bit and 64-bit. @@ -16,8 +18,7 @@ the following command:: conda install -c conda-forge iris -If you wish to run any of the code examples -(see http://scitools.org.uk/iris/docs/latest/examples/index.html) you will also +If you wish to run any of the code in the gallery you will also need the Iris sample data. This can also be installed using conda:: conda install -c conda-forge iris-sample-data @@ -77,7 +78,7 @@ Hence the commands change to:: conda activate my_iris_env # or whatever other name you gave it pip install -e . -The tests can then be run with +The tests can then be run with:: python setup.py test diff --git a/README.md b/README.md index ee7a170822..126869b267 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@

- - Iris
+ + Iris

+

Iris is a powerful, format-agnostic, community-driven Python library for analysing and visualising Earth science data diff --git a/ci/requirements/readthedocs.yml b/ci/requirements/readthedocs.yml new file mode 100644 index 0000000000..611c69307d --- /dev/null +++ b/ci/requirements/readthedocs.yml @@ -0,0 +1,60 @@ +name: iris-docs + +channels: + - conda-forge + +dependencies: +# Dependencies necessary to run setup.py of iris +# ---------------------------------------------- + - setuptools + - pyke + +# Absolute minimal dependencies for iris +# -------------------------------------- + +# Without these, iris won't even import. + + - cartopy>=0.12 + - proj4<6 + - cf-units>=2 + - cftime==1.1.3 + - dask>=2 + - matplotlib + - netcdf4 + - numpy>=1.14 + - scipy + +# Dependencies needed to run the iris tests +#------------------------------------------ + + - black=19.10b0 + - filelock + - pillow<7 + - imagehash>=4.0 + - nose + - pre-commit + - requests + - asv + +# Dependencies for a feature complete installation +# ------------------------------------------------ + +# esmpy regridding not available through pip. + - esmpy>=7.0 +#gdal : under review -- not tested at present + - mo_pack + - nc-time-axis + - pandas + - python-stratify + - pyugrid + + - graphviz + +# Iris sample data is not available through pip. It can be installed from +# https://github.com/SciTools/iris-sample-data/archive/master.zip + - iris-sample-data + - sphinx + - sphinx_rtd_theme + - sphinx-copybutton + - sphinx-gallery + diff --git a/docs/iris/Makefile b/docs/iris/Makefile index 1a66b03805..a220502028 100644 --- a/docs/iris/Makefile +++ b/docs/iris/Makefile @@ -20,28 +20,36 @@ pdf: all: @for i in $(SUBDIRS); do \ - echo "make all in $$i..."; \ - (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) all); done + echo "make all in $$i..."; \ + (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) all); done + install: @for i in $(SUBDIRS); do \ - echo "Installing in $$i..."; \ - (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) install); done + echo "Installing in $$i..."; \ + (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) install); done + build: @for i in $(SUBDIRS); do \ - echo "Clearing in $$i..."; \ - (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) build); done + echo "Clearing in $$i..."; \ + (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) build); done + clean: @for i in $(SUBDIRS); do \ - echo "Clearing in $$i..."; \ - (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) clean); done + echo "Clearing in $$i..."; \ + (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) clean); done doctest: @for i in $(SUBDIRS); do \ echo "Running doctest in $$i..."; \ (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) doctest); done -extest: +linkcheck: + @for i in $(SUBDIRS); do \ + echo "Running linkcheck in $$i..."; \ + (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) linkcheck); done + +gallerytest: @echo - @echo "Running \"example_code/graphics\" tests..." + @echo "Running \"gallery\" tests..." @echo python -m unittest discover -v -t . diff --git a/docs/iris/gallery_code/README.rst b/docs/iris/gallery_code/README.rst new file mode 100644 index 0000000000..7d8fb60e81 --- /dev/null +++ b/docs/iris/gallery_code/README.rst @@ -0,0 +1,26 @@ +Gallery +======= + +The gallery is divided into sections as described below. All entries +show the code used to produce the example plot. Additionally there are links +to download the code directly as source or as part of a +`jupyter notebook `_, +these links are at the bottom of the page. + +In order to successfuly view the jupyter notebook locally so you may +experiment with the code you will need an environment setup with the +appropriate dependencies, see :ref:`installing_iris` for instructions. +Ensure that ``iris-sample-data`` is installed as it is used in the gallery. +Additionally ensure that you install ``jupyter``. The command to install both +is:: + + conda install -c conda-forge iris-sample-data jupyter + +Once you have downloaded the notebooks (bottom of each gallery page), +you may start the jupyter notebook via:: + + jupyter notebook + +If you wish to contribute to the gallery see the +:ref:`contributing.documentation.gallery` section of the +:ref:`contributing.documentation`. diff --git a/docs/iris/gallery_code/general/README.rst b/docs/iris/gallery_code/general/README.rst new file mode 100644 index 0000000000..c846755f1e --- /dev/null +++ b/docs/iris/gallery_code/general/README.rst @@ -0,0 +1,2 @@ +General +------- diff --git a/docs/iris/example_code/General/__init__.py b/docs/iris/gallery_code/general/__init__.py similarity index 100% rename from docs/iris/example_code/General/__init__.py rename to docs/iris/gallery_code/general/__init__.py diff --git a/docs/iris/example_code/General/SOI_filtering.py b/docs/iris/gallery_code/general/plot_SOI_filtering.py similarity index 100% rename from docs/iris/example_code/General/SOI_filtering.py rename to docs/iris/gallery_code/general/plot_SOI_filtering.py diff --git a/docs/iris/example_code/General/anomaly_log_colouring.py b/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py similarity index 97% rename from docs/iris/example_code/General/anomaly_log_colouring.py rename to docs/iris/gallery_code/general/plot_anomaly_log_colouring.py index 95af1e1f61..28f7ce323b 100644 --- a/docs/iris/example_code/General/anomaly_log_colouring.py +++ b/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py @@ -13,7 +13,7 @@ To do this, we create a custom value mapping function (normalization) using the matplotlib Norm class `matplotlib.colours.SymLogNorm -`_. +`_. We use this to make a cell-filled pseudocolour plot with a colorbar. NOTE: By "pseudocolour", we mean that each data point is drawn as a "cell" diff --git a/docs/iris/example_code/General/coriolis_plot.py b/docs/iris/gallery_code/general/plot_coriolis.py similarity index 100% rename from docs/iris/example_code/General/coriolis_plot.py rename to docs/iris/gallery_code/general/plot_coriolis.py diff --git a/docs/iris/example_code/General/cross_section.py b/docs/iris/gallery_code/general/plot_cross_section.py similarity index 100% rename from docs/iris/example_code/General/cross_section.py rename to docs/iris/gallery_code/general/plot_cross_section.py diff --git a/docs/iris/example_code/General/custom_aggregation.py b/docs/iris/gallery_code/general/plot_custom_aggregation.py similarity index 100% rename from docs/iris/example_code/General/custom_aggregation.py rename to docs/iris/gallery_code/general/plot_custom_aggregation.py diff --git a/docs/iris/example_code/General/custom_file_loading.py b/docs/iris/gallery_code/general/plot_custom_file_loading.py similarity index 100% rename from docs/iris/example_code/General/custom_file_loading.py rename to docs/iris/gallery_code/general/plot_custom_file_loading.py diff --git a/docs/iris/example_code/General/global_map.py b/docs/iris/gallery_code/general/plot_global_map.py similarity index 100% rename from docs/iris/example_code/General/global_map.py rename to docs/iris/gallery_code/general/plot_global_map.py diff --git a/docs/iris/example_code/General/inset_plot.py b/docs/iris/gallery_code/general/plot_inset.py similarity index 100% rename from docs/iris/example_code/General/inset_plot.py rename to docs/iris/gallery_code/general/plot_inset.py diff --git a/docs/iris/example_code/General/lineplot_with_legend.py b/docs/iris/gallery_code/general/plot_lineplot_with_legend.py similarity index 100% rename from docs/iris/example_code/General/lineplot_with_legend.py rename to docs/iris/gallery_code/general/plot_lineplot_with_legend.py diff --git a/docs/iris/example_code/General/orca_projection.py b/docs/iris/gallery_code/general/plot_orca_projection.py similarity index 100% rename from docs/iris/example_code/General/orca_projection.py rename to docs/iris/gallery_code/general/plot_orca_projection.py diff --git a/docs/iris/example_code/General/polar_stereo.py b/docs/iris/gallery_code/general/plot_polar_stereo.py similarity index 100% rename from docs/iris/example_code/General/polar_stereo.py rename to docs/iris/gallery_code/general/plot_polar_stereo.py diff --git a/docs/iris/example_code/General/polynomial_fit.py b/docs/iris/gallery_code/general/plot_polynomial_fit.py similarity index 100% rename from docs/iris/example_code/General/polynomial_fit.py rename to docs/iris/gallery_code/general/plot_polynomial_fit.py diff --git a/docs/iris/example_code/General/projections_and_annotations.py b/docs/iris/gallery_code/general/plot_projections_and_annotations.py similarity index 100% rename from docs/iris/example_code/General/projections_and_annotations.py rename to docs/iris/gallery_code/general/plot_projections_and_annotations.py diff --git a/docs/iris/example_code/General/rotated_pole_mapping.py b/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py similarity index 100% rename from docs/iris/example_code/General/rotated_pole_mapping.py rename to docs/iris/gallery_code/general/plot_rotated_pole_mapping.py diff --git a/docs/iris/gallery_code/meteorology/README.rst b/docs/iris/gallery_code/meteorology/README.rst new file mode 100644 index 0000000000..e8e902b498 --- /dev/null +++ b/docs/iris/gallery_code/meteorology/README.rst @@ -0,0 +1,3 @@ +Meteorology +----------- + diff --git a/docs/iris/example_code/Meteorology/__init__.py b/docs/iris/gallery_code/meteorology/__init__.py similarity index 100% rename from docs/iris/example_code/Meteorology/__init__.py rename to docs/iris/gallery_code/meteorology/__init__.py diff --git a/docs/iris/example_code/Meteorology/COP_1d_plot.py b/docs/iris/gallery_code/meteorology/plot_COP_1d.py similarity index 100% rename from docs/iris/example_code/Meteorology/COP_1d_plot.py rename to docs/iris/gallery_code/meteorology/plot_COP_1d.py diff --git a/docs/iris/example_code/Meteorology/COP_maps.py b/docs/iris/gallery_code/meteorology/plot_COP_maps.py similarity index 100% rename from docs/iris/example_code/Meteorology/COP_maps.py rename to docs/iris/gallery_code/meteorology/plot_COP_maps.py diff --git a/docs/iris/example_code/Meteorology/TEC.py b/docs/iris/gallery_code/meteorology/plot_TEC.py similarity index 100% rename from docs/iris/example_code/Meteorology/TEC.py rename to docs/iris/gallery_code/meteorology/plot_TEC.py diff --git a/docs/iris/example_code/Meteorology/deriving_phenomena.py b/docs/iris/gallery_code/meteorology/plot_deriving_phenomena.py similarity index 100% rename from docs/iris/example_code/Meteorology/deriving_phenomena.py rename to docs/iris/gallery_code/meteorology/plot_deriving_phenomena.py diff --git a/docs/iris/example_code/Meteorology/hovmoller.py b/docs/iris/gallery_code/meteorology/plot_hovmoller.py similarity index 100% rename from docs/iris/example_code/Meteorology/hovmoller.py rename to docs/iris/gallery_code/meteorology/plot_hovmoller.py diff --git a/docs/iris/example_code/Meteorology/lagged_ensemble.py b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py similarity index 100% rename from docs/iris/example_code/Meteorology/lagged_ensemble.py rename to docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py diff --git a/docs/iris/example_code/Meteorology/wind_speed.py b/docs/iris/gallery_code/meteorology/plot_wind_speed.py similarity index 100% rename from docs/iris/example_code/Meteorology/wind_speed.py rename to docs/iris/gallery_code/meteorology/plot_wind_speed.py diff --git a/docs/iris/gallery_code/oceanography/README.rst b/docs/iris/gallery_code/oceanography/README.rst new file mode 100644 index 0000000000..0f3adf906b --- /dev/null +++ b/docs/iris/gallery_code/oceanography/README.rst @@ -0,0 +1,3 @@ +Oceanography +------------ + diff --git a/docs/iris/example_code/Oceanography/__init__.py b/docs/iris/gallery_code/oceanography/__init__.py similarity index 100% rename from docs/iris/example_code/Oceanography/__init__.py rename to docs/iris/gallery_code/oceanography/__init__.py diff --git a/docs/iris/example_code/Oceanography/atlantic_profiles.py b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py similarity index 100% rename from docs/iris/example_code/Oceanography/atlantic_profiles.py rename to docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py diff --git a/docs/iris/example_code/Oceanography/load_nemo.py b/docs/iris/gallery_code/oceanography/plot_load_nemo.py similarity index 100% rename from docs/iris/example_code/Oceanography/load_nemo.py rename to docs/iris/gallery_code/oceanography/plot_load_nemo.py diff --git a/docs/iris/example_tests/__init__.py b/docs/iris/gallery_tests/__init__.py similarity index 100% rename from docs/iris/example_tests/__init__.py rename to docs/iris/gallery_tests/__init__.py diff --git a/docs/iris/example_tests/extest_util.py b/docs/iris/gallery_tests/gallerytest_util.py similarity index 84% rename from docs/iris/example_tests/extest_util.py rename to docs/iris/gallery_tests/gallerytest_util.py index c96f47ae50..38678fdb18 100644 --- a/docs/iris/example_tests/extest_util.py +++ b/docs/iris/gallery_tests/gallerytest_util.py @@ -6,7 +6,7 @@ """ Provides context managers which are fundamental to the ability -to run the example tests. +to run the gallery tests. """ @@ -23,26 +23,26 @@ import iris.quickplot as qplt -EXAMPLE_DIRECTORY = os.path.join( - os.path.dirname(os.path.dirname(__file__)), "example_code" +GALLERY_DIRECTORY = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "gallery_code" ) -EXAMPLE_DIRECTORIES = [ - os.path.join(EXAMPLE_DIRECTORY, the_dir) - for the_dir in os.listdir(EXAMPLE_DIRECTORY) +GALLERY_DIRECTORIES = [ + os.path.join(GALLERY_DIRECTORY, the_dir) + for the_dir in os.listdir(GALLERY_DIRECTORY) ] @contextlib.contextmanager -def add_examples_to_path(): +def add_gallery_to_path(): """ - Creates a context manager which can be used to add the iris examples - to the PYTHONPATH. The examples are only importable throughout the lifetime + Creates a context manager which can be used to add the iris gallery + to the PYTHONPATH. The gallery entries are only importable throughout the lifetime of this context manager. """ orig_sys_path = sys.path sys.path = sys.path[:] - sys.path += EXAMPLE_DIRECTORIES + sys.path += GALLERY_DIRECTORIES yield sys.path = orig_sys_path diff --git a/docs/iris/example_tests/test_COP_1d_plot.py b/docs/iris/gallery_tests/test_plot_COP_1d.py similarity index 70% rename from docs/iris/example_tests/test_COP_1d_plot.py rename to docs/iris/gallery_tests/test_plot_COP_1d.py index d0e989a3f2..7ad59ba10b 100644 --- a/docs/iris/example_tests/test_COP_1d_plot.py +++ b/docs/iris/gallery_tests/test_plot_COP_1d.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestCOP1DPlot(tests.GraphicsTest): - """Test the COP_1d_plot example code.""" + """Test the COP_1d_plot gallery code.""" - def test_COP_1d_plot(self): + def test_plot_COP_1d(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import COP_1d_plot + with add_gallery_to_path(): + import plot_COP_1d with show_replaced_by_check_graphic(self): - COP_1d_plot.main() + plot_COP_1d.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_COP_maps.py b/docs/iris/gallery_tests/test_plot_COP_maps.py similarity index 70% rename from docs/iris/example_tests/test_COP_maps.py rename to docs/iris/gallery_tests/test_plot_COP_maps.py index 9db5060c89..5252ddfe6f 100644 --- a/docs/iris/example_tests/test_COP_maps.py +++ b/docs/iris/gallery_tests/test_plot_COP_maps.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestCOPMaps(tests.GraphicsTest): - """Test the COP_maps example code.""" + """Test the COP_maps gallery code.""" - def test_cop_maps(self): + def test_plot_cop_maps(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import COP_maps + with add_gallery_to_path(): + import plot_COP_maps with show_replaced_by_check_graphic(self): - COP_maps.main() + plot_COP_maps.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_SOI_filtering.py b/docs/iris/gallery_tests/test_plot_SOI_filtering.py similarity index 68% rename from docs/iris/example_tests/test_SOI_filtering.py rename to docs/iris/gallery_tests/test_plot_SOI_filtering.py index 2d791567b0..384a44ebd8 100644 --- a/docs/iris/example_tests/test_SOI_filtering.py +++ b/docs/iris/gallery_tests/test_plot_SOI_filtering.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestSOIFiltering(tests.GraphicsTest): - """Test the SOI_filtering example code.""" + """Test the SOI_filtering gallery code.""" - def test_soi_filtering(self): + def test_plot_soi_filtering(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import SOI_filtering + with add_gallery_to_path(): + import plot_SOI_filtering with show_replaced_by_check_graphic(self): - SOI_filtering.main() + plot_SOI_filtering.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_TEC.py b/docs/iris/gallery_tests/test_plot_TEC.py similarity index 71% rename from docs/iris/example_tests/test_TEC.py rename to docs/iris/gallery_tests/test_plot_TEC.py index 4bcd70f9f5..2852ab06b9 100644 --- a/docs/iris/example_tests/test_TEC.py +++ b/docs/iris/gallery_tests/test_plot_TEC.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestTEC(tests.GraphicsTest): - """Test the TEC example code.""" + """Test the TEC gallery code.""" - def test_TEC(self): + def test_plot_TEC(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import TEC + with add_gallery_to_path(): + import plot_TEC with show_replaced_by_check_graphic(self): - TEC.main() + plot_TEC.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_anomaly_log_colouring.py b/docs/iris/gallery_tests/test_plot_anomaly_log_colouring.py similarity index 66% rename from docs/iris/example_tests/test_anomaly_log_colouring.py rename to docs/iris/gallery_tests/test_plot_anomaly_log_colouring.py index d0f07b02c4..eaae11f6b5 100644 --- a/docs/iris/example_tests/test_anomaly_log_colouring.py +++ b/docs/iris/gallery_tests/test_plot_anomaly_log_colouring.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestAnomalyLogColouring(tests.GraphicsTest): - """Test the anomaly colouring example code.""" + """Test the anomaly colouring gallery code.""" - def test_anomaly_log_colouring(self): + def test_plot_anomaly_log_colouring(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import anomaly_log_colouring + with add_gallery_to_path(): + import plot_anomaly_log_colouring with show_replaced_by_check_graphic(self): - anomaly_log_colouring.main() + plot_anomaly_log_colouring.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_atlantic_profiles.py b/docs/iris/gallery_tests/test_plot_atlantic_profiles.py similarity index 67% rename from docs/iris/example_tests/test_atlantic_profiles.py rename to docs/iris/gallery_tests/test_plot_atlantic_profiles.py index d85dc72c2c..b69408337b 100644 --- a/docs/iris/example_tests/test_atlantic_profiles.py +++ b/docs/iris/gallery_tests/test_plot_atlantic_profiles.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestAtlanticProfiles(tests.GraphicsTest): - """Test the atlantic_profiles example code.""" + """Test the atlantic_profiles gallery code.""" - def test_atlantic_profiles(self): + def test_plot_atlantic_profiles(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import atlantic_profiles + with add_gallery_to_path(): + import plot_atlantic_profiles with show_replaced_by_check_graphic(self): - atlantic_profiles.main() + plot_atlantic_profiles.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_coriolis_plot.py b/docs/iris/gallery_tests/test_plot_coriolis.py similarity index 59% rename from docs/iris/example_tests/test_coriolis_plot.py rename to docs/iris/gallery_tests/test_plot_coriolis.py index e61fdce81d..2e4cea8a74 100644 --- a/docs/iris/example_tests/test_coriolis_plot.py +++ b/docs/iris/gallery_tests/test_plot_coriolis.py @@ -9,18 +9,18 @@ import iris.tests as tests -from . import extest_util +from . import gallerytest_util -with extest_util.add_examples_to_path(): - import coriolis_plot +with gallerytest_util.add_gallery_to_path(): + import plot_coriolis class TestCoriolisPlot(tests.GraphicsTest): - """Test the Coriolis Plot example code.""" + """Test the Coriolis Plot gallery code.""" - def test_coriolis_plot(self): - with extest_util.show_replaced_by_check_graphic(self): - coriolis_plot.main() + def test_plot_coriolis(self): + with gallerytest_util.show_replaced_by_check_graphic(self): + plot_coriolis.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_cross_section.py b/docs/iris/gallery_tests/test_plot_cross_section.py similarity index 68% rename from docs/iris/example_tests/test_cross_section.py rename to docs/iris/gallery_tests/test_plot_cross_section.py index 7fe13d825f..4b92f5f5fe 100644 --- a/docs/iris/example_tests/test_cross_section.py +++ b/docs/iris/gallery_tests/test_plot_cross_section.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestCrossSection(tests.GraphicsTest): - """Test the cross_section example code.""" + """Test the cross_section gallery code.""" - def test_cross_section(self): + def test_plot_cross_section(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import cross_section + with add_gallery_to_path(): + import plot_cross_section with show_replaced_by_check_graphic(self): - cross_section.main() + plot_cross_section.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_custom_aggregation.py b/docs/iris/gallery_tests/test_plot_custom_aggregation.py similarity index 67% rename from docs/iris/example_tests/test_custom_aggregation.py rename to docs/iris/gallery_tests/test_plot_custom_aggregation.py index 130f46d847..b674f401b4 100644 --- a/docs/iris/example_tests/test_custom_aggregation.py +++ b/docs/iris/gallery_tests/test_plot_custom_aggregation.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestCustomAggregation(tests.GraphicsTest): - """Test the custom aggregation example code.""" + """Test the custom aggregation gallery code.""" - def test_custom_aggregation(self): + def test_plot_custom_aggregation(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import custom_aggregation + with add_gallery_to_path(): + import plot_custom_aggregation with show_replaced_by_check_graphic(self): - custom_aggregation.main() + plot_custom_aggregation.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_custom_file_loading.py b/docs/iris/gallery_tests/test_plot_custom_file_loading.py similarity index 67% rename from docs/iris/example_tests/test_custom_file_loading.py rename to docs/iris/gallery_tests/test_plot_custom_file_loading.py index 9c466c53d6..d580ac9d01 100644 --- a/docs/iris/example_tests/test_custom_file_loading.py +++ b/docs/iris/gallery_tests/test_plot_custom_file_loading.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestCustomFileLoading(tests.GraphicsTest): - """Test the custom_file_loading example code.""" + """Test the custom_file_loading gallery code.""" - def test_custom_file_loading(self): + def test_plot_custom_file_loading(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import custom_file_loading + with add_gallery_to_path(): + import plot_custom_file_loading with show_replaced_by_check_graphic(self): - custom_file_loading.main() + plot_custom_file_loading.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_deriving_phenomena.py b/docs/iris/gallery_tests/test_plot_deriving_phenomena.py similarity index 67% rename from docs/iris/example_tests/test_deriving_phenomena.py rename to docs/iris/gallery_tests/test_plot_deriving_phenomena.py index 63cbf40ec0..b7378da9df 100644 --- a/docs/iris/example_tests/test_deriving_phenomena.py +++ b/docs/iris/gallery_tests/test_plot_deriving_phenomena.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestDerivingPhenomena(tests.GraphicsTest): - """Test the deriving_phenomena example code.""" + """Test the deriving_phenomena gallery code.""" - def test_deriving_phenomena(self): + def test_plot_deriving_phenomena(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import deriving_phenomena + with add_gallery_to_path(): + import plot_deriving_phenomena with show_replaced_by_check_graphic(self): - deriving_phenomena.main() + plot_deriving_phenomena.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_hovmoller.py b/docs/iris/gallery_tests/test_plot_global_map.py similarity index 69% rename from docs/iris/example_tests/test_hovmoller.py rename to docs/iris/gallery_tests/test_plot_global_map.py index b492baebbc..ece1c3a361 100644 --- a/docs/iris/example_tests/test_hovmoller.py +++ b/docs/iris/gallery_tests/test_plot_global_map.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestGlobalMap(tests.GraphicsTest): - """Test the hovmoller example code.""" + """Test the global_map gallery code.""" - def test_hovmoller(self): + def test_plot_global_map(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import hovmoller + with add_gallery_to_path(): + import plot_global_map with show_replaced_by_check_graphic(self): - hovmoller.main() + plot_global_map.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_global_map.py b/docs/iris/gallery_tests/test_plot_hovmoller.py similarity index 69% rename from docs/iris/example_tests/test_global_map.py rename to docs/iris/gallery_tests/test_plot_hovmoller.py index 1ec2a47ef6..23fb741e44 100644 --- a/docs/iris/example_tests/test_global_map.py +++ b/docs/iris/gallery_tests/test_plot_hovmoller.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestGlobalMap(tests.GraphicsTest): - """Test the global_map example code.""" + """Test the hovmoller gallery code.""" - def test_global_map(self): + def test_plot_hovmoller(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import global_map + with add_gallery_to_path(): + import plot_hovmoller with show_replaced_by_check_graphic(self): - global_map.main() + plot_hovmoller.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_inset_plot.py b/docs/iris/gallery_tests/test_plot_inset.py similarity index 70% rename from docs/iris/example_tests/test_inset_plot.py rename to docs/iris/gallery_tests/test_plot_inset.py index 58ef63bcac..e77b629c44 100644 --- a/docs/iris/example_tests/test_inset_plot.py +++ b/docs/iris/gallery_tests/test_plot_inset.py @@ -9,22 +9,22 @@ import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestInsetPlot(tests.GraphicsTest): - """Test the inset plot example code.""" + """Test the inset plot gallery code.""" - def test_inset_plot(self): + def test_plot_inset(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import inset_plot + with add_gallery_to_path(): + import plot_inset with show_replaced_by_check_graphic(self): - inset_plot.main() + plot_inset.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_lagged_ensemble.py b/docs/iris/gallery_tests/test_plot_lagged_ensemble.py similarity index 68% rename from docs/iris/example_tests/test_lagged_ensemble.py rename to docs/iris/gallery_tests/test_plot_lagged_ensemble.py index ecce499dc7..386ad7353c 100644 --- a/docs/iris/example_tests/test_lagged_ensemble.py +++ b/docs/iris/gallery_tests/test_plot_lagged_ensemble.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestLaggedEnsemble(tests.GraphicsTest): - """Test the lagged ensemble example code.""" + """Test the lagged ensemble gallery code.""" - def test_lagged_ensemble(self): + def test_plot_lagged_ensemble(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import lagged_ensemble + with add_gallery_to_path(): + import plot_lagged_ensemble with show_replaced_by_check_graphic(self): - lagged_ensemble.main() + plot_lagged_ensemble.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_lineplot_with_legend.py b/docs/iris/gallery_tests/test_plot_lineplot_with_legend.py similarity index 66% rename from docs/iris/example_tests/test_lineplot_with_legend.py rename to docs/iris/gallery_tests/test_plot_lineplot_with_legend.py index ca246b178a..edb4d7d305 100644 --- a/docs/iris/example_tests/test_lineplot_with_legend.py +++ b/docs/iris/gallery_tests/test_plot_lineplot_with_legend.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestLineplotWithLegend(tests.GraphicsTest): - """Test the lineplot_with_legend example code.""" + """Test the lineplot_with_legend gallery code.""" - def test_lineplot_with_legend(self): + def test_plot_lineplot_with_legend(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import lineplot_with_legend + with add_gallery_to_path(): + import plot_lineplot_with_legend with show_replaced_by_check_graphic(self): - lineplot_with_legend.main() + plot_lineplot_with_legend.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_load_nemo.py b/docs/iris/gallery_tests/test_plot_load_nemo.py similarity index 69% rename from docs/iris/example_tests/test_load_nemo.py rename to docs/iris/gallery_tests/test_plot_load_nemo.py index 3d9b5bba23..58a5bbf72a 100644 --- a/docs/iris/example_tests/test_load_nemo.py +++ b/docs/iris/gallery_tests/test_plot_load_nemo.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestLoadNemo(tests.GraphicsTest): - """Test the load_nemo example code.""" + """Test the load_nemo gallery code.""" - def test_load_nemo(self): + def test_plot_load_nemo(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import load_nemo + with add_gallery_to_path(): + import plot_load_nemo with show_replaced_by_check_graphic(self): - load_nemo.main() + plot_load_nemo.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_orca_projection.py b/docs/iris/gallery_tests/test_plot_orca_projection.py similarity index 68% rename from docs/iris/example_tests/test_orca_projection.py rename to docs/iris/gallery_tests/test_plot_orca_projection.py index 1854f68aa6..2b6fae4b1b 100644 --- a/docs/iris/example_tests/test_orca_projection.py +++ b/docs/iris/gallery_tests/test_plot_orca_projection.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestOrcaProjection(tests.GraphicsTest): - """Test the orca projection example code.""" + """Test the orca projection gallery code.""" - def test_orca_projection(self): + def test_plot_orca_projection(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import orca_projection + with add_gallery_to_path(): + import plot_orca_projection with show_replaced_by_check_graphic(self): - orca_projection.main() + plot_orca_projection.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_polar_stereo.py b/docs/iris/gallery_tests/test_plot_polar_stereo.py similarity index 69% rename from docs/iris/example_tests/test_polar_stereo.py rename to docs/iris/gallery_tests/test_plot_polar_stereo.py index 963d5729fe..3cd7dfa482 100644 --- a/docs/iris/example_tests/test_polar_stereo.py +++ b/docs/iris/gallery_tests/test_plot_polar_stereo.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestPolarStereo(tests.GraphicsTest): - """Test the polar_stereo example code.""" + """Test the polar_stereo gallery code.""" - def test_polar_stereo(self): + def test_plot_polar_stereo(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import polar_stereo + with add_gallery_to_path(): + import plot_polar_stereo with show_replaced_by_check_graphic(self): - polar_stereo.main() + plot_polar_stereo.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_polynomial_fit.py b/docs/iris/gallery_tests/test_plot_polynomial_fit.py similarity index 68% rename from docs/iris/example_tests/test_polynomial_fit.py rename to docs/iris/gallery_tests/test_plot_polynomial_fit.py index 6e1b148e19..5b47b46688 100644 --- a/docs/iris/example_tests/test_polynomial_fit.py +++ b/docs/iris/gallery_tests/test_plot_polynomial_fit.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestPolynomialFit(tests.GraphicsTest): - """Test the polynomial_fit example code.""" + """Test the polynomial_fit gallery code.""" - def test_polynomial_fit(self): + def test_plot_polynomial_fit(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import polynomial_fit + with add_gallery_to_path(): + import plot_polynomial_fit with show_replaced_by_check_graphic(self): - polynomial_fit.main() + plot_polynomial_fit.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_projections_and_annotations.py b/docs/iris/gallery_tests/test_plot_projections_and_annotations.py similarity index 65% rename from docs/iris/example_tests/test_projections_and_annotations.py rename to docs/iris/gallery_tests/test_plot_projections_and_annotations.py index f273e040e4..7052414011 100644 --- a/docs/iris/example_tests/test_projections_and_annotations.py +++ b/docs/iris/gallery_tests/test_plot_projections_and_annotations.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestProjectionsAndAnnotations(tests.GraphicsTest): - """Test the atlantic_profiles example code.""" + """Test the atlantic_profiles gallery code.""" - def test_projections_and_annotations(self): + def test_plot_projections_and_annotations(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import projections_and_annotations + with add_gallery_to_path(): + import plot_projections_and_annotations with show_replaced_by_check_graphic(self): - projections_and_annotations.main() + plot_projections_and_annotations.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_rotated_pole_mapping.py b/docs/iris/gallery_tests/test_plot_rotated_pole_mapping.py similarity index 66% rename from docs/iris/example_tests/test_rotated_pole_mapping.py rename to docs/iris/gallery_tests/test_plot_rotated_pole_mapping.py index 4395b0519a..fa11a60a9c 100644 --- a/docs/iris/example_tests/test_rotated_pole_mapping.py +++ b/docs/iris/gallery_tests/test_plot_rotated_pole_mapping.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestRotatedPoleMapping(tests.GraphicsTest): - """Test the rotated_pole_mapping example code.""" + """Test the rotated_pole_mapping gallery code.""" - def test_rotated_pole_mapping(self): + def test_plot_rotated_pole_mapping(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import rotated_pole_mapping + with add_gallery_to_path(): + import plot_rotated_pole_mapping with show_replaced_by_check_graphic(self): - rotated_pole_mapping.main() + plot_rotated_pole_mapping.main() if __name__ == "__main__": diff --git a/docs/iris/example_tests/test_wind_speed.py b/docs/iris/gallery_tests/test_plot_wind_speed.py similarity index 69% rename from docs/iris/example_tests/test_wind_speed.py rename to docs/iris/gallery_tests/test_plot_wind_speed.py index 1cd4402fdb..7a0be601a5 100644 --- a/docs/iris/example_tests/test_wind_speed.py +++ b/docs/iris/gallery_tests/test_plot_wind_speed.py @@ -8,22 +8,22 @@ # importing anything else. import iris.tests as tests -from .extest_util import ( - add_examples_to_path, +from .gallerytest_util import ( + add_gallery_to_path, show_replaced_by_check_graphic, fail_any_deprecation_warnings, ) class TestWindSpeed(tests.GraphicsTest): - """Test the wind_speed example code.""" + """Test the wind_speed gallery code.""" - def test_wind_speed(self): + def test_plot_wind_speed(self): with fail_any_deprecation_warnings(): - with add_examples_to_path(): - import wind_speed + with add_gallery_to_path(): + import plot_wind_speed with show_replaced_by_check_graphic(self): - wind_speed.main() + plot_wind_speed.main() if __name__ == "__main__": diff --git a/docs/iris/src/Makefile b/docs/iris/src/Makefile index 53d224874d..5589cce730 100644 --- a/docs/iris/src/Makefile +++ b/docs/iris/src/Makefile @@ -2,18 +2,18 @@ # # You can set these variables from the command line. -SPHINXOPTS = +SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = -BUILDDIR = ../build -SRCDIR = ./ +BUILDDIR = _build +SRCDIR = . # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest +.PHONY: help clean html html-noplot dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @@ -35,34 +35,38 @@ help: @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: - -rm -rf $(BUILDDIR)/* - -rm -rf $(SRCDIR)/iris - -rm -rf $(SRCDIR)/examples $(SRCDIR)/_templates/gallery.html $(SRCDIR)/_static/random_image.js $(SRCDIR)/_static/random.js + -rm -rf $(BUILDDIR) + -rm -rf $(SRCDIR)/generated html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html" + +html-noplot: + $(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML (no gallery) pages are in $(BUILDDIR)/html" dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml" singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml" pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo - @echo "Build finished; now you can process the pickle files." + @echo "Build finished; now you can process the pickle files" json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo - @echo "Build finished; now you can process the JSON files." + @echo "Build finished; now you can process the JSON files" htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @@ -91,7 +95,7 @@ devhelp: epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + @echo "Build finished. The epub file is in $(BUILDDIR)/epub" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @@ -104,7 +108,7 @@ latexpdf: latex $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex" text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @@ -114,7 +118,7 @@ text: man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + @echo "Build finished. The manual pages are in $(BUILDDIR)/man" changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes diff --git a/docs/iris/src/_static/copybutton.js b/docs/iris/src/_static/copybutton.js deleted file mode 100644 index 6800c3cb93..0000000000 --- a/docs/iris/src/_static/copybutton.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2013 PSF. Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -// File originates from the cpython source found in Doc/tools/sphinxext/static/copybutton.js - -$(document).ready(function() { - /* Add a [>>>] button on the top-right corner of code samples to hide - * the >>> and ... prompts and the output and thus make the code - * copyable. */ - var div = $('.highlight-python .highlight,' + - '.highlight-python3 .highlight') - var pre = div.find('pre'); - - // get the styles from the current theme - pre.parent().parent().css('position', 'relative'); - var hide_text = 'Hide the prompts and output'; - var show_text = 'Show the prompts and output'; - var border_width = pre.css('border-top-width'); - var border_style = pre.css('border-top-style'); - var border_color = pre.css('border-top-color'); - var button_styles = { - 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', - 'border-color': border_color, 'border-style': border_style, - 'border-width': border_width, 'color': border_color, 'text-size': '75%', - 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em' - } - - // create and add the button to all the code blocks that contain >>> - div.each(function(index) { - var jthis = $(this); - if (jthis.find('.gp').length > 0) { - var button = $('>>>'); - button.css(button_styles) - button.attr('title', hide_text); - jthis.prepend(button); - } - // tracebacks (.gt) contain bare text elements that need to be - // wrapped in a span to work with .nextUntil() (see later) - jthis.find('pre:has(.gt)').contents().filter(function() { - return ((this.nodeType == 3) && (this.data.trim().length > 0)); - }).wrap(''); - }); - - // define the behavior of the button when it's clicked - $('.copybutton').toggle( - function() { - var button = $(this); - button.parent().find('.go, .gp, .gt').hide(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden'); - button.css('text-decoration', 'line-through'); - button.attr('title', show_text); - }, - function() { - var button = $(this); - button.parent().find('.go, .gp, .gt').show(); - button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible'); - button.css('text-decoration', 'none'); - button.attr('title', hide_text); - }); -}); - diff --git a/docs/iris/src/_static/favicon-16x16.png b/docs/iris/src/_static/favicon-16x16.png deleted file mode 100644 index e2ea4567703398229924f8b8fcf9a05ab06aec07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1041 zcmV+s1n&EZP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y9E-_;&oJ#-z1A<9JK~y+TeUe*DlLr`vd$$`_T{}i?`CCv@ zka1|ChyMpFJ#Z)|DWkR=iqr-O>1rhv$(S43P>T)_D1sCOD%Os(C;~$^*#Vd_qN1{l zC59|)S+*r!Y{}vd_>Of=mc?vO@?L%4^Im-UX#d5lq@j^X8aA0!$rnt@;9@3~LnwR0Le=H|uoPurQpLYB$l|)d+KaGNrA#KJcM+M!pM(~RC(Kd@!g>N> z)pd}aGDFv(12@YEMsnecC5-+0y*nxG=AHr*9HS~!c}lzmoff!mYQU;(g|V*^-o<_B znBE8Bq0;YzQw|0NR2pqZO5RR7so5a1OJN^LL(6OqiZ0f8q~}@?d8QZPr~9G4a2oCN z+K-&q^LrY`(yzr77CqTPX+Ecu+6Wq*%6;z@nFfW8T1>5oF?}x;j#;xOtZC#oPRk8! zXG__8!BuZdI5Ev4h+SSF(MdIXW#>ohv4zRfQ(AhWp&k@gm_9X@c{ zJ^R&jLlu&;GPqKeSY&JpRv|{GI93MW)!t{ee9GP^0(=^%3c8*}zDGyk0orle*`=hEg z?_9J(Rq2=if}23r3p3=1wjPDCrwp_AWtdw{h4FITV|M+;Mb-)T_CvKJw0{#N6jJ=y z`1hmYg%(kgC{jQsj>U-+zuhYkJmbg2ZSZ3R15vRFiGlTPXtX~8#gCdSFh-3p00000 LNkvXXu0mjf*6rHh diff --git a/docs/iris/src/_static/favicon-32x32.png b/docs/iris/src/_static/favicon-32x32.png deleted file mode 100644 index 2f90c60eb594ee979ed7b0f6647dd19413ac5393..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2954 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 A+5i9m diff --git a/docs/iris/src/_static/favicon.ico b/docs/iris/src/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0e5f0492b47539c4026dd2f78087d010c777b642 GIT binary patch literal 1150 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 + + + + + + + + + image/svg+xml + + + + + + + + Iris + + diff --git a/docs/iris/src/_static/iris_colour_logo_centred.png b/docs/iris/src/_static/iris_colour_logo_centred.png deleted file mode 100755 index 2a1bebc5f3bce9e86f5cd5d49c20d02cd6fed302..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 327072 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

qNk{68!tX`aBH=3P)%P%*$ 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%0A8hlNsY;Q=HJlFUeg#|h_&K;^8NY|F;vPIZ`At>2 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

t{trhVu)t5T$+P=iX8C1BkGA}aMCD7} z8%Cew3)wCo69`|cq1E#Rwk<+Ji;J7qzVv5Wn-xCpLYmxbHY|tEcsFl6ETpNow3&I( zG-#vsIc@IF8|LB3E2JC>qoUd!=KrovzV~S44O#tXcI?7O!h!kAPfTc{FnAC+hAVtM zd@rD^`y19Gfy5YV2TN`Vs9PDII3vj)Scx)##&BQ~n1==ciMc?-pxht74H6q4nAyPB zcl@{@qxDyIb&%udrJWkeBJrT_X>m`NkQi-T8{yDQ+%hlz68;Cs6xP^weLzl$`Uf<9 z@nkpCt}t>wu)RlWc6)znSL{xM6syQZe>iNom`s&2U?#@Zeyr!%&y69;A1`y#!aH{w z|0?FRtXq^mNeSlm{o>4ZOp@&mkPHM;c773u=pvV`$9E&%9x4xq&z>`rr!-Xf~`7JteLd=YWy)kZFtIZGW(A{CHtm0+O1p+ zt$fPvy4!_^6RK`Lw9n*N;-|IIJ-rRQ(B`vcY0sSnM+00T+_exn{cqRg;u=Q#RDIT% z8%K9kCtf`+lO%oH-U^DrY3@7wjUttfsN{U_Mr#fe`GB259no42Z**fp#7Og)O0X8k zNsWc$1VGYCCc~ms8>_+_X zCiJ)9XC-VOv`-nD#SWH9QBU{Q%Y7{7$GU)SQWsGrvl5?+6(?e#i4#gDQi~ys~3Z_>Q_}nR~gDQ?qS-hEj$V7+6ne8 zEKjSU2x!<`<}6Q}a<(4a78*fTS3yv%S{+py9pj8VnHpbQ;B5g+8$?)d zQ4m1PfhI1Z5YsSGsWYD6b!3Q9WYrg6Zb4my6*KXZ;c*2fw9TgYM^d}5TwL0-(}dfk z)X2L@WR+Gkngl2;A=t^|3PZ?Q{ixnnf4BBm{mZuJ6aJJF8sy%6xNzWuRLtG;7OaU3 zTOJ&d#*jq@gh=gW&3_YV0P`RR6;k|p1;dma~!u_HRPN*Z)tqA+HYq)uDG*98D?NE z$*cHrS_5z)P3^P2wBXF1)g9j-N|Cjbv4XQCEmx_++UgQw9!1gCP+ke^Qs%bo6+_<+ z-5aVy@r4MbC|wKMyU037kQM-7Q<1Q^W%bLV+6DeRQ|I!{0}I`wHO}f(Y$V zb#`APZw;aFXJXLEQ$7xCU>#(OX_F&dc!y z!?RJg!#pD#Q<7#uo(a=xLc(B0b7;?Uz%&Jj(3UH1$eEu3i9}*uV3#Yz%dn? z52f}ASE#_EJNxwGwfS(48jQcQDt<=~!n%NQxz~mtL`8lBw9(31Nq@sSZS+LQZr;#v zv71oB61kXPOGj8WHC8tz5(Bbk<8c^rA&2fW#s%gf*aS}vbYtWFm#4)RdTYJ(H|n5U+k5UV3NJWp)WFYP@+0Zp){o16ANR&6PepI5KJ=h9 zCprLLig6B^7(H#rbm!o_lgIlj+&Tpg_5k1w+$RcAzgi98u&VC=rbjrBMGp* zWGAKi87klh{R|=e@+bvb&e#`Vpw7Hb-R7Hrn(dzlZTpJ4cE@&K{ET(cuwKqiM|y2o zG!`%qrJt57kpcQH$7N< zpA^2YK40*3LJT?c_4uD-S&WrC&J!R1 zn4H%)DE?dfXMXJoDYV9Z{Qdln7B9pUH7_hBV9s+>?mslQVw(&AgQ_5 z|Ih`4Q<`pR6)T>$WSTjSclVd2YK5=rlFV=TxRm8|lIqG;#tzaj}X{gGctP4|C9kvsaPUj93rmG@T< z5WUp!qf$nw;PkA#E&HyZ{bXCCO!*!3FLQAQRwBb#+bH9DGE71$Pd_D?q3MVTti-3L zYj$~QxCX};+;co6Mg|1wZ5S2Dk}B1iyUkk(eS@g>s~|`v7AGUlOnur^z>~gOIJJ;L zwld?SA?8U{4KG`DH`x(Y_x-8IF#4NNHB0H;_eT_SbU1~ zJHNk3HzndksC@e;xB4M1++--zehv&uP!CdVFz597y_bf+KH<^S6T4fu0w#UcSn}+g5p4y7p{;2 z>|%g|Czu*t2M0(4_lS`=wAaZ6$lVi0XYG2vs2YcH)s)Afcr$=$1`yWri@DK;_jHQ2 z37%tQR#^@1n3zUF+&qh}qdD)(H_?ys1Km8REB~_G|eOIy&E24=vt=}F7=uO$EA)5higDbJ@tZN8ZW*%@i~~XiV19V*6Wh$tD9PTP&bR75PRlosGB0PHvSw zSSVw{r5%CwDLxXcN@f3RHNeK|50u!wds?P@<7U!c+Xp*0QRHCckJL6e~9F4Miv_i+o$`Nfi39Eg7%ZxxuZ>^vun+t*(=n^T*5|8sH<~N zf5lFYzW5PG)?|>$-W@RjrEfILQFu3*R(pFIXe+t(>{1G;;u~^WcwvujWOXMj-Hjc? zk5ys8R=^)P4pVVU#DXQQiZuniv#%FukfMfHAV(9E&Ib($ZWFyt=;HenP(DY1*?5K? zN+cx3%d!HD>xp+wevQo)aj{GTW#3i&R5FAgQ2VYW?T4=fI9dM*Cnpu#mVCFeHvN-H zMT`pD0aMDA;J;ee*OYFP*K@MMZbkD^392z_R~%0%IBD{t%x5ugH$F6f-FcEWcjx1- z&ccSp(`5#OmGw(hus&rrNJ4B1t5gRz+xizqgnbzR!zE*3;+jH|2w+#5*INd{*BNNt zZ>W7Xjeshrnk@_Wc&`=VPmQieJ^M*&8Vw`weB{6I9fGNpSdP|TY#Kf6^LYAFljC$W zYrYnLOz-oCV$emS7=7Z%lJ+IOR=mntV;1~fekVAW`}$<9EMP-Q%1<|#OP1K2m2P{$X{W59)IGH#jg(mL%F-;1852F z*UJyg6VBc8-;sj!w{qvoG})se`proU#I-Zv`I)oD&&YuudkI!InoH8z^9L+1N#MH# zp@ls7w#dxhBy;p&_@BRQq#n>4bH}yettj^86S53?VxE?6ZNyDI6PVP&>`-%IkeB>_C{~5dUu2rfOZd z+C|01=G`c5L97+9kl_R>-0;)mB;iaPrkIfaTR%ou@=k}Joom0SCYc0yx|Nugl?W1Wz7F7hwt)* z`2#Oa;mwUt%6Dm!YEhr(gt_6XqPcAd$DMM?F^cBBw;E9MRTD{ULdKHqC(t` zQr^Y|*PC<8IHkAPET z4!^Cp=d@i^`i`@2Okzm9@%sFXk%7!$pjp<5g}tpMJh(!sF6I(N#$QTfJ4gP`v*i5e zgyKT!bP54=uYkV=nBfQ_Z73q}nRNcQ6_cezN5Ts06k>tIjW@ZWaZd{!TUOxYBawGX z32*ehntes{oY>rhZ--$kwGfp}g{@c-Z5q9<`Z*257jw*pr9^|$)JhiLN009~jyH07 z8594Pp7>dsH=OjY$dXX zo)%2?a3Fn)1^e3zEKvFcPT!<@B>jvZG}p z(M@|JcuDEZz2$Es8+4J52jSlR6qInsuLO>)VrHX-|2>bFzI?3J*3x^*pdkgefu}Eu z;$%{Kj=tQr$dF6;v61!Z%sZZt#k`dtUQ|!Pn;EHJ^bpQ@)!4F=Bedm9sq*Xq*qMo= z6S|SLQ6ekc=B#iHGnpDD-@RWUO}2c;ySm4L)SWVo7Qm6w;Q0t35D);TtMp z*>n4Yw5AzazIVsaHOAdEW{JGmc$V~AxS$2dNVN03AB_vprz1KY#xq}##rhZBA}2?Z z9h+{E)96TLcQNVL9DtVU2U{3QG!j5NSqOyv0XZ5x^S8HDG#r}y{%GTv&Cqz}?=?yy zds`Bh2uGe~M`sPPsdkAjKp%322Z$DkaBh-+t=;BafA6gv& zU*|6Q>-!Ohf1zVL8~CGZ3;#a#q%XY?q)iPoScxe)4B%c1ePCi;k-+=*5v@UJ5oFiZ z3j39m*(fq|-^kSHD&}WyLs^wcdGLBDl$nw@%%Z}{IenjA$?r<+Ot*)gG=G2p%u zV`0v09f!|fI`FSUK<)uirO2qwSxQin#(JAwbHmDt8&W6W0|)ivN#pxi7iwT)R=oKK zPwe^F`XQPpN_4eA1CbvnI1DlW`h04GEJXH%u=5g<0C!5qRwVUiaHFL1jhZXVI`5l( z*}ZZL0$zWg^L{L2zsPRCs6*SAL6^W`1M^mkE}Q<0F23uph*hI;jT9q;l`BDr0N9g- zB?Xb+)h_7q!IP(64(|X5^9iz^^46dwc#7I9jOm%vz}oV5Q_ee~DA$m@op~U3xp&YU zwr!kjRDX_C7&U{!cMGbI3xbcsA9@qsIJ3Ir(PU9T0_z;*5)-2cjP{20d(r*BCpGX9JNe5pS;yW4 zT(qzJJ0+n}$gaB&nT8tVvUBiiiVU!=oTeTeX}cK+Hune7T$~%CBFnFiO(X^kgWp0- zv?w!_s?e~bIt*X=#S9LjA6`E4R8}b0bH*k0 zy^Eu70glrlpPRLYds)yf9P%~{BQ1^<{6wc-OpXCTAs@qc3g5?ue!@5*atj>E507Or@GMp!l#CjCxx>M%8Hob% zX&?#2yXnkeKFkk3HV5D{MV7$#&BWC}CH?v>%%I?X6mCP;fGG35E~YhMWEq2|ZXesa z-#VJTShu>X)%1CY@Ts)p<^z7jC0EHah{yKU@_m=0ZHeXjPS*v)tt%#&cq~Cd2J{DB zNbn;pW;2z8c9=swnLl3!DT&jj{r{hiIBB=|EAbyKl|8i?`89=bR|b6cwomX+tZ;TN zy`E+_!*K@uGXa&*vRbp4$YA}kjTGJ7M#`;j|KG*89_SjwJGx)L5N-(Q#hhNpgx=X#jR8=-*L!@Kfl@XS8~ z8IP)0aUQNQ-+w#wHEmKgU(8w>chGkIGR#Jd^dQ;Q7OEl2z>LJbj~J~^Rx(U*Yi-T} zBhtN*Ss-P6%iGn8(3`rx<2CC; zYqr?k`H(5P>P;=XeUGbl6+;_CEsIA1m|zU@Nzu?c44GTbbGNi+b&do;@1^*ULkPU1 zjR6Gk6P(op4G@$b2I7I#*<0|k(Y}9OPzVcL8mA=ER{4^H=h8X0FQHGUeYH#}z83X0 zaI1d)tHaxuC5Dd~wIRkf8o$wE{A@#b@m(n9W;v^@JCKmX@O^`%hhZcE4kAh5;;$Ce zcO@KYM(NEFF5-n&D^kwJ4={sOB$RCSm^@@}U(&Q z8xSCJeF`N&lWkz%j27_Y|V-3^TnSKguDUdXD?HyT;8JMc8ttY7nXXr%tP zu*oB*>52d*sls_M9)PZlA^cyZ0k|AI_S@_U2a2!Cz$jP=)3~KS&ZmvxuPmPUFqonPu>O4Kt>oqb(Z= zNT!2nZ#7ni3LVRi{y;|@yO7Y`rx;HbLgOtZDi!cRe^I4FPhK+))|9-IDDT^|Ofr?K zxzJ6+=dJEg(sT=VJw?X8gasD14kf7$CDbvoy@EtK|JG4d6J#J6>Eb)Ap8&KD6AZ** z-RC4R3LDkEEjQf{Lb)6k{K}Odbjn)%krcO;@$SLpUEnB$D-;6#+)D651J99TRiaX2 zG*cS#(I(i*u_WBhM<4%oTy@T#87??>y**H3WrVsK%+H)hXVTdvCL6KXC@$*|oPCarbVG8q4ed^@A- z7mQH3A?s9XMui<7G^wnL{bz!rFN}ge|DL)g#L~vA)p1j}frzxnUK*d3xQLV57PQbD zLmQlMy&q<9iZ*)(_HH;f?a5Fl&*o;7z>RY175MP23!)3WHQpnGB}@JNZg{7f;0o}a z>yLy4(kiqdzE2L9=yp^HMG|G6 zIufOjOqCNM6*3R!|2g{p{?FsN&%O7ld)uG=*=xP)UGI9=XZuQLpL>m%qcMKgH*QdE zR-SY##p*5$qtcH*+x-TUbx*L_U>E|-)EL2KnILO8*mL)u7`P5v4+IT}-xQ2CzG!a( z>d2(E!c)lvE@~JKEaY_f6jaB|an}z_$qep$@ptQ)KDK7=1P_O$=1MIGIZ5sO@<=LO zy8#_;#wDhIkLL93W$-+ z4wOkqWy~fJQd!su+(Mgo*Mu1q`HGd6S%1WQgA;8bB6>98b!h%7_uraZOqonowR|Dp zPPYCh2a}i?MaQ<#l`-$KTmD1YX;2&fe}YCt05p-rq(JTAO$1VgI;jDVs)uZZq+;Mb zHPh4IvL)3wA0lvd0Pt`j7}f)mKgz(kM6E}~7O?EupGxhk!ph)mDk$twzjYJX)A!m$ z3qIJsmMa$C>txZlTx|tyc7Rjo55M$)U+uLC-0e@ykbUBK>aMEn3&7Jn+dp`DdUu$_ zcNyTtT%=*!LOw1!Y-xc;=v*8_-NTAJO*mwM|nZ(Hc0j#iCI8eU=LmrXz zzww05Mi0F&?>uDFybFCVesHBrozPvjZvD=GrRRg5;cw<=wWsS=50Y?ab3AkzdBcxU zfNL>A5FaB1Y3y5Qr|^P_aG6YdLF5CNhWKB@)f*;l5QWBtkSvZ#LoT4}e^Z%{Rvk;CS_$I!>mahm@<7sA;9MNQ;| z?4Jj)iHjlPEwo5BqICaT=Ws?^xIa1l#=E&3IuH{f6)#7Mny;K&+3{8UukXgB>K}u( zHr7^*u3bZOd{-=0sFil8mF5?A#MN9?o#gY}-M4B9PRb&*k_(&3oH$9|!OLYx4#|8s zHapVBLjajkfr~v1K>ZY0jMyzJK>Q!MU_Lho??%jeq5mdcoE^zL>)qAZ)FB0F=-TIr zL&)u0-y+=&fJRNVzCqt0!?`z#9C)3~hf)M&9oGY%-|@TnE#fW?84R8-$9^aiA~(-WN)6_XVE79=jBOZIXq7 z1@PrZb7N493F)m_6}`ue$R*?CV`$vj`e~ zQ7&XlQ?P_ks5FcE{wX%)IDGn1+9x#kDW0!V5b+(@>(ms7QF)L>ed}-jw3M$nV2^=x zeKeXP00GaAceP@%gvB?Y!ePT~0<&wH&|wLe4%!>Xv?!(c3qH-{p64xBHb1x>DY)1{ zOWJ$ug@U9!H1#fzZ8D%yYU~Xm0irlY=y5mS9%5LE{@fl4csy`D2q=^pz$`$X;WIt` zS+%I3z`&y58&$6c{&tq|}_6MLk87axQ1j2nv3_!D4QH+Ura7pxbY0jli8Z|UFX%D!7zXPPfc@k*g`CC zEHy&*v_(G!qhB+q%|Ob+2yn8VPFGQfhJa3tiOT%X4Lks>UX%vywum?x#|>}kYj2(B zOZ`U9ZLZ0^`%TqL(l%K?O4ZMP(6_i@{qRdpgqJz`_kWZZqqS|S0gaS0`l!)>E&l8K zpGO%3dPyU0d|%xdKAhnHwrFKt7oc)8lk)d9G2|Fno9SqvYx z;Vu<7e6j^|TIKPyRNNv%fZp8pF7313TfZv1Sis`$-`-4KJU}*c$PC^x{Pw3jleT7V>AXx*oi$=faTo zOqD$Fy4DAtF%uyTAVu+^&z9aLZtU1B+mwuVSO^MJ*1Pd@<6A^i>tuvT&)cI3Z*;9k z(A>=pK=Ei2g@idl;@@tc0y>|)^STp|liqFxsDNRx2)_Bl=4%Z2)tyY;t1GQEbLxK~ zPWX9woKcTu)0S&7QR81Ht%eU*vli1#Giaqba3fXbr%_KU)XstT>|uD+yOl;#gkJjl zzyUcGoTnYa+2b!R1Z^Lvr{-cS@6$t<*V27Y9YiTEzMnsxd{|=&TLFh$qPb)X`2aEF zvt3>mRf!qFwUS{{MK(4&T^X@O%o!7i%1l)WR4j2`fQV-bEdBI}h#mB8IT$qY;p$zS z8}BvOu@hlOp9zIL+kXykGFtVOvrG3m)4_V}+q}g<1j8|PoWBaVT}1+4F2K!*>%EfM z%XcT2hFNT1_I3KYI?}{wu>u7s@Tb_Bp$Gh-B!R4!OQwU6* zGu8UR%BQV1m-N1ldPGECGux15*I5lcvNRRU0rXUixfO8E?47R|jZJO9!iW4*d{}@b zaG0>TdfwesY#1TOaN2u3px`df#q81?JRe|)eHtNH43UNrULFiOb+x!&2` z1t)L>@h;{>qe7L~fK-V`a5a~lnN5d; z%)N*{frigs#PMU~lh70e=;eWtOmXNXSk&(2v23uZcy$aFSq?H>>v(?x)U`hiizp%jpTp19txu z`q6q%@(s{fkXmre$>B6fh4VF&g)1VCfj$<0N-a3KA-(eMwReYdJ#ojkGfOHW-9 zvSrbZdFY(`W1FAX#zMS-r@*D)12qTm|6T_hdA^(NK+0we@XqtYw!-APsbW1F`(on! z-CGxr%!i3e>%+ec;9svUumB|Xt&CckKt+h>uc@$}iZ?W`R%LykCxm2489fOKKhWiP zmIb(?e7NK8w3Fl$2D)3qkdY|%ajkS5;D`-iK7Dw}7+%V*?u<{z$F}XkDuzJO_3izhx+MOlu<`g=4N06Ps9C30 zt_VS{F6}hVR@y#p#Gem(C=6L^qjsN?(Ba(3q{|7^Ji?=|sE!IwNn!9RHaKtrB#y5b z*G5yx*S*de{HbAIGQUQ6-X6hi*m$vWqKn+xH#5XdVcj+zAa`yy@u-_ zv84{c)^iQUSJ6!VYy}3-wi5-I#ybY@Hy>@rq@CrjgL?0Rpybv}qiQ^O{VzLwEVIO& zZ&I+8c!Va;Pq_2!z-@GX;410LN{vyW6q!z6-nj*l2AetFpmRcwW644Mt3Tp znXCYuQA{Rcz^yJqDLi3o#+SnA5nmXZHSh0Me>c81@b8wk{W28soGqE4xKCRd_?D`O zK_ALbi^(Cs`oLQN;1m?@(Y;0ST3Qf)!y)_roU*kK*D5<4nrjSPv{^p*wgUyz{+VDYq`0pN><{@bt+Fk3uB|Lx zR;_d4LLu0RBD?G3Zvv0j zc&X+BCI7BxEAs6MHWF~Z!};WNol!5af66X&R*U1d?jtMVTe*!(b^S3xuPzzQi$SMQ zjQ)i)=|2-d2{yP2e1gAf15SbEZhhac5apWq3r+{lZ-2CgP8E5-@2KpAaI*}RkPLtr z%csP|5fKN~8h53eOKr60t`p$)u$gjah**^u?^=5I*8RweN()WY-M2LMmT%RQrE{EC z7tmp_V$S7!_Bk}yE)9i2We@zvfTiRUZs&?nYN`6b^@+QW{Uh6I zB*MS=@fTl&9l|1X!9v5PfDX*}trq@f<*_bT;ZL6DLrSa0oh|f1D{$LBlCihc!u#rG z5wUlfPzF4%7Cxy`6$WRDMqHfL20A9TlyU+$J+w5mp zn;9pcE8-or*To^yx^JD`pMrWr*@fh%>6c&edWDT_x)Yjfs=y`czfGXlh(T|F1+ltQ z#Z98mjaX_7s7YDRc^SjMV8hwF6abQN*^!^4nVfup*r&#kJS(hlSwLADa@@R#PdWPH ztU<*lF1TYU+1%evDyQ+{IQfwH#NDy6RmKdmI1*<`1$`0y3K`&(+6%pXJ&^ev3m^2auKA3r zs75yx7GkfQaSMP?|H?%lpMS{~4=)_K07OJT-IZ)B2Q~KHK#!Ruc^HHN$CUqeCQ?mf zspbb#l6FJm2~;g^AUvq=Z6-^LdA^P$Fy;hpe4o^_5PNO1o&oA-!Qlw7&rnYgWk<^8 zF-GG3Sd-`C&;c-D1~%k|I$W;Ui@Wp5Ve6vcaKv9$Lm*|Y8^#UZ18f}_>$KYAO|)nM zF@_oWTufGAxXKFgKZRMcBJp&fAzS*CGvUF{FFP;QV(A%Y;UJ&fe9er8^2G$ zUe4h53}o$s6h%EZ{ZsWgw*W(9&dso#)VwxguTFp%Pz1ju-Qmxl@0-8F z4~3r1nF))df~^xN%|H9Dav(XJ2%r^&ih({Of%@n|_Tz(Mko9C7)oW&{rSYxym)`Xc zALVZ;13M|xtJu|P4aDj1wF@_&eYOQq;24=5=KB=Ees|qn@bo1iuy#$asCl5rUje>o zoAtARA)h+5tqZ7pY6y=^zY-`J{;Ss%_59JS@5rT>wwbsPIWG=4eI$Vkd=yl(kKkN~GDt$h9%g7w6<~+w*g+p@T=21YsyQpXw%e&cA+@gPUi(&7k2raa*a1wWP7PhGvn@1LQ+f~}va2J{aH&B|P<{ZJx>N}Hf6i_Osx z6fFcPAAk>LVg~_FbR6zxO-rQO2z*XKhuawwD1W^>q}1=DhvX$C5q^~+b{4DBctvlx zg9BYhhu4caYM%*U00&WMi=HdaID;k3?In9b=_&4mqdy#!=r6MiPql4`$=H(D4stljkbL?iTQQapFZ?T&QGE3gEj~wffL$9kOF8drK-b$x zQg`QKPlw-$b&@Op^kul4NbeLuwW%Z07f#853YN-AX$Kg{cTjp=6HFVE{= z?WpzE`4ELoOz;y%PIseSTClr(wi}E$xhQ}@m;>-q{hG#)h)wITkJI@LHMEQ*^TPQz zW7b%6D2^bSsR_FqWZyuaS?-+$J7JqCY@O>;NLrc0E)7ULzbrPddgDo7BaKZPk^)j+ zE@TJ;mjO8A3s0OxrIS%| z(qSHifA$EE>NR^x*z32EqSd!tG@-0wT6iLOo7q1kk?eM`6ulUPV=8u)`%R?uK3}T2 zuCe*GxlrN%a8>9t{V8@x)O-S)c)p96VSHD(EgC#;Q3sj+z4WTH&uC({G&0bF8>EnY zgh>_lP2_Na2M1{$V0-ME0OG)g9MNz^Rc1jJdeqYZA6x&nnIZE~wJE=SYT(Qrw6%%S zhT(gKg-@rYP>G%z*3D!r4}t*Qj3UI%1DA=Qp7~r&ylRpVkmDWJo|b^>Y%3q(-3;+y zKq>99BX`d=_4bgJLD`gUT+PpHn_na!e|IdQsP2X7YWlcb*vT@Dmub|}JXVAQA4@n! z+XTA@s3ss}kkL%}BDbqqxWVPms@QMcV{>OpBDN+nP8Kh`)$3;-dlFxq6<{8q4qqq$ zT-P~+>{H+%f*YLYYeHM@y%EawZcn6U8@t+OQI(|`Qo+)l(BwV_Hz7tvbCc2BDPwre zDW;KjC5qZaL{+GkkbTNh+xmfLACzU*6&&TIAUE@xNUAXgPAx8te40SzTG)RIws5I% z(jmE~x52GS*Fh{#pygBI|#bdM9j}o-&>Sj)21RYDQscIEAPFG?#Eq3ybu@%_eFoMpee)CcP_vV zt5;$U_oT2Oeen~`2U}_CQPf32oTMQfCjqgrL$__vTnQ$GzZmWMt%FKiIV7iPY^tf| zy{`|_yXe=)JT#{;N2-VyhS~SP{XjCW-9au)p^o?q48MPh8+_=(ZMPnz6Y=krYPyGp z#y61%79SE}1{ z%Up=KAmX}+J~o1_?w~v)%P3_}=(O5FVer5Wai1NE3bVZH5ou`MxS63thq&NhV03F1 z?YoB?Wc%>PkN4lRVuNQtD7`E7_l|D{&+F>+?VkM#_TeJ(E!xZ}Lsz8y+OEiJwq~w7 zkc8LkVCyaBw+1Y<`vyf8sS`ZR;ziTXo(>va;}6S^aGi?&tr=TKe?FsNzjxs?(Q{zY zSRx2et||9s=qsfS2IKL0m9<@|qN|sS0M7)TX#&7}sCqQ};#;-xMv7|>b1LD2nv(N8 zhBUvAG{1x7V}^m_e%ZnqW>&L$QEB+ig#(PY*$^KPI*WhGip!1{SC-=tjq+iaays*aP8A8foem#Pf-#17;vDV~($83uukN`0O@Aqfi1x&zJe)8)8>D;$61U`n z`vDIN7F{tT{sQN%_hM~04u3l@&`kc`oh>5^MgpJD#plm9{op|k40s&D*tb!bs2_!Vufy#8U~y8N3a7wtCscJTF!QkI}{i(;E^o z@q~pt$+&kMYP9LJcgyq3e^%b+-KpsIx&R$oS7@$nxqOIoDFbf9I*q> z-R@O(B*GPKUDfHHqNgmZ>)m)*qnp1#*E?r!XtFhSe?h=|@U|Cn@@vZ1MhN_%)aP;LrNCX0cuSowKQuU(PumdK}A)Sps2~XAEIi(4ieK!5$k$D0DXz z3RK*H*akLKxw@P<&0|hJM@aK$S{Zx>5PZK+pk8H05YQ+SKvqwB6o7k^i>-Ph?1r&xUQ-3fR{WSNnkgXR1E+wBJv?0T|E!Z zJ^Wrtk227pzupP+g-`BgPXPDWf%R5F^cWuhJdez@-g?0HQrC|8}N!EE&_p;FN_ z>xgGhuAG1VrMiT!;rHy_1ylFvw?uFZ(ohRd)APHJ$6ZMd=;3ZCP!XyIKS0;o7@kb+ z*sT2XloDZ8s12@fV(y(RsVjf7YU8MhfMXMYtk0u=6`<%DK_rI^v>+gE;z(wc2A%I-;Fe<;WjepI=9pQ_ zUNCw6v6vi(1BO#;GEAT&eD~tgj7G#`@{4oy12VX03p0}q2MIe6{J%T9;KYCz1-2~R z+BbqP>Y&4~TH~m^3Dl}otmKPwoK0V?teo+h>$%$d;ketMf}cDVTBF_WNF+4GP}7Y7 zsfE+zzzzDJnQSCCWZ8b{pLBlv|Np=(QW_)G=+A~tm6T5rhvKQMIJYQ^ctTfr8o^Wq z_)>KKz{(SDrjuHdWs#VC15VS$nmfsrJhjKuVP}_&_GlKk_fv7iOf5TtHfXfmmMFIC zEgz$=AKZBOgB~ukXZe~(WRE}=xQbCA=5T?s_5nX~TTfbH;udN9HF%mHfZSMQrJw~Z zU0_G&KXg|74~`O{zWo}m-unKM@0*i~OS+#N0vZ9IufSl3Pd^r3I^UbY4tEx$L{i&V za8!NZiEk59(;jx@vs~^xi~pg=^)_tww#z_p$((5uL!C3`1HH;D!-c$yKjNRHB!JK1 z7)~h~-vXNSKUu+Fx_sa}1(}Ubq>OL6kZ%F3W8~G*j;Z^d-H7>Kq{@H1k+#$J(g)eK zqtgxL_rE8P zr%?e@-&g%-?tTRH?~bV|Dqn1hWG**?*@arkr83C6M}e7<@53hAyDF)#HLC5ct(c?a zz`CEJqNcaOLuS{ZVOIvuZWxK_c{*;jvq(8O?7Ww3tMI$14*Tne!jH<$|K6aj!?(dW z2SBj~gI94>0|3#Lgmhrof(NMAh#9F>e_k!-uSoRi+4^#sje~m_J{Uq^L+00WxZvLQ z)1U3JxPm`NwLO_BDz9e7t+1pT><{2F{ zQz82T>{Xlh|DXJ_A2*`Qg~+y&gCDk$)i2i@y{|`4ZfLIm$MvNykEuiebqi}&qJy=UM%>6C<-Tg+Z!C|>KI8Kt8O;;`-dtFL+QEGq3@8je=Pe7z8w7)p zc5Y|HyIuUFw)v;xR>dcc=m8trzgXb1(5pp%?$7oRM1=9qd-ES0Clw_ooidszJm7(5884@W`~Yinm15d;piM`O4?g|L$x*Fn z=&k=5415qMrOq_dIE4`x2;vZc763agcd)$8IAyB_Ndzfp&#K4)o1HB>Q4;85k?o&U z(58)I^neCKJ)91bd{&ZNiaIbXg?u1%k2C=c^0HvMh>gg9dfliml`w9w0bVI_q|!1y zuzCLac2f;(B2^xG)9ra%*Y7(&DQUyNp8btidE75gHQ8#$XBkg@Vr5;8b-Ru? z0<7?h80w|cvdpIHt;jyd4Srp%#hwp#kB``>Qn5vb76acpu}Y3+EH_1_)`r?+C6w_P zrTLZSxryMpz85$*{fzI+E-LNx<;{lQjS=-*N2yC;i_7XOe{ZvK8jEJ;F@X^KA({H8 znEV_#tB_qgXqlI}TZb*W)MSVm6XnXpSC8Phd^J$SSRLu0Nw<>oVyVV3d<)o~gD3z(NZ8o0q{Z)7fvp4}qcd2>gnK`!!pjv*2c%W0 z%=)1}`(FvCHdq$u=zNbamPK?nm^OQ@g})51$3FSC<|*;6UO5(g$}j*)o<}*#uh||9 z|I;bg-p_H^O!8U7GiHO}J+Z)}9(?K9-x=4%6x@HE#iua@_yLY!NdXJ!qAX6pp0H3V z`|#_$qf;1u-5fUbP&+IF>t5YdCWwXKkgH)6d6xM{m`EvsqmhJGRGi|9A^U zCSyPJJx>6yQXOK{{wpvQCk_lo;^OXE@~jM2I3Yw5$jP+MoqU_C?FqfvGK}&gUoAUfsm*Tq z?adNv!3=|-ii+L4p@$kk^drjnUEtVOX`OK@KyJGS4zPL3eZtG0MfKVqJ^kIQx1cb3c$*W zQiTZ4#%?#(Mp_Wzhkr<*Kh=v^am_XelY}IV6o}U@K!!iqTGTxem$CJr!XSIB@aPLAfw>P zj9_~XOVFRrA4=Kl&N(EVp>#pDsD*+oW{9|Xz7M*8b$Oy^Mz|Hkp)2s{dfm;~E!nlr z>Gh9i<_F7@1+xHCwOVP5K9kRGp)m5OG~|zl#q1G-^2Je|^UpUImI9R_uhjiek7RsN@KA+ZN?%ZKTC4 zwSn9qnK?(SaGl{hvgma;X!|U1gy6Xn0@iJ;78LL7h1w`(Vb7$!z4HIvmKmdsyx(u` z{T;shgYj7%g!dQtT_*@xh043&4X2v6r=wm18gJ=Qe_2a{KaD&&kqjmfX5B zwi+?GPL3F(ET4VV@zs(5@5dGWq`5oIPdHW=?f^z{R=Za6o9JfY4O*tU2Ikl1-{c;0 zwumD`Sfz-7GZoT+P*h8%-UdBn?fiUFw{nXD0SDmML{WDGE1`Ff zDh`E04f(8u15n`%$w4_~R%=%R#bn?p7krlfqZk zQ~(jP5~!jH1g%}Ig3v0v&R10B7%ZlLn@b4d%20~gl{aE{%OYX-ixvdGqCGN2NfX z?u$QEl^<1`*?dW0QAkzaVmc4_RD%!20u%}hLc;bMfI~04+-!uyDn(gDQ}0##qA8hY z{MsqAo6VRc!d^>493fao0vzJFv8**Kkcl9U(tfX2Q-$Hq17$??*s;;|Qdyi$k0ZTQ za16h>j=o*p8Vh8aUE6QH_d^#J5SPF_ot&=n#2*bUvES-GT zZna4x&vHu6044PETpq&@Uj;t?=5K1YS71X7TFLrOy{g!$<8X)|1L6T*BjxvmyA_ly zh#Ut||Ht7xtW6}UqR$u=W&A|yrM)qRC)7-~h9tClZhki9y85AV_uuf+&k;OwqHB}J z+cB*R^DG1xbis9wH^{1A--(yxBSWkZE3TG@vhEussrSr$LU~wRjzCJK8pV`&cONut zb%yY+JUrhQ;C5IX(9l|qm^WgbF3J;<$hTZVSLBE|u`l`Z6I%b;UuTPIE;Cbx3l&xp z%(jEr2(UT<9Pv$g5-Ee^l(9QH&S>u|Ew_#{j;t=Tp>!#^#I;GE$wX^#dc4$6pl@&s1N2rmriWe`%_i!<=hLSVP(f+?;eMHpcl61xgJjH7me3Y;Z9 ztoObjA>ug~*FhN2gl2f)=WItHDM19B@j^H@iXkR_EVP|fU9g#0n`0i3Ai_!2&RK_&?#U3^Zq)QrMMUBFgE zqmRF$lupWZ!f*9gQAi`$1!+YTawhdMyHwvAc{0AIJm}Gh7R(-ySA#hQ{PaQaA6AL( z)r4;F2FVVGj@rXeNI%6ETw%XBf~`OB4P`1!1lK!`z?KPtLihnE0__`brLCHvUAL^P zfpvtHb%$igo3~=tNq)SHb6q?wf{?mqSN|Skp4l_r1OyYT@U^WWA+d4#*|DBff$=a? zSAb^SI%w-|O=QiJDsUViPg^M3Xjt=M_<|+%6B_0@Yk_+x;@fouh|+zTe%8*8qpwt) z@vXoQick!DaT$)p$&>JxEFl-T`vlzY1M42dYQj!F>3j&n28;s0+~x9pUBwCoKmbrv zRX$gVxNE$P>%38A-h#Dh!GanZSUK1{^$l1efq*xU{_K4v6;eLT6zZTP&fibNN^147 z+B2TCQi1{GrGVT-653;aqKQ(&WyXg9okq!NcU)0qZ!3k>bRd=EePl1SmU;}AeXd0L zc-#$kL`2d=LJK+;E<3?YA1KJdLxsy}d-!1J<2O@~1;EdHXP+74hjyc3du3rClM6N~ z*Bm>J?ePfR9D>=vCA7Fseh9D)!4)hv14Wn+MyXT!*xme?9(x{>02Np_&|Bi~rPkGp z;O_j&=6~re2!%hHs*vk<GVESqQ_P zaq>A=$Z>-8&DS%Il02&7m%(zOs08X(0<}JYI(jy_05Dk`qzWlOW*KHg3E@^Ys7_Gkq&hX#AtcaFRjp#{--!s_&P0pdSG=$%yX7L4jlmPy;G8Q_A}K z4#3mJNcQOaKGs5Ra1}Fg*Ks(4V9?d!Y160JRVS|ziaLvz37R>kH+MG<>_j%q*j;93 zh#nFk+-$$0bC)wi2pXu1ICwed_mL(__ShVq?$1?UxVEfmyD+P#;i2!QkC%i`3{ekr zF*<;n3~8B=)|( zpQEAwc52T``u+UBMf~I$;^l~R=T??_+9g{)nQ_Gq(Wi#+`OuM>=C zARHbG2mo@rL^{PJ^32uo0xeCN)vs3s`?WFbsNz((^H-C=b6J2gD3@xB#DJgUvK)X? z>wR&vT{xPONcAq5{k3O6eJnAF3YORIhxD9nSo1e)Iq88N$G*EZ926-nAB-oI?3d&R z5^`TuWaLvyiy{)?MG#~Yp?a5-RW*F;_eC}+}AhWB`&QYyuxgM=Xe19|ho&OoB z28EGkkb!@;k#A4by(@H4?b59_U(kOp+rPx2c|zw)$FITk5cs((<9>KKfna6oa;{)D_9sH{Cl~AB12@pR6)LuCf)(m{C&+iISiznE3savmx3;!-$w*)AZJ(tug z;c?{GozeR1=5SvKm4QQ`=MJ(0$%nmzEXNDDvcp5T?9u~eB7k`HOOXyI%Ki1Y6eWA| zPy1mUp+*l#nZjSr>pwWp@3Csoy<7Ir8363BQx9vxC)&xGVCyJ_H}kpG!0lWHdfSQ3 zzM)+UC@@?RQz2qyeMU2b$tl_Z1E28pFMN$k3F_nv@AC)PwSY4DW5zJH*OxX`O zz}o9TR4B?QDj9n`6P@OzK(%ksD+-$G+xhYv}Yu zS|%c?C9Qu`KigKVsHY`x%PXRMx1o??13h&+jNdG8V3alvp5qB=2Us#P+q@l&RXWpC zV!`d9I)QRPWjne@Hl(GB+9Y|`Y-y35b=QK${;N2xj~4?W=K-T2AL$?;!Kg(ss7Fb} zW&I+-x091W4{>$SrXMO3#fkI9jkHzy7V^TJyvk9a`%9p%0^TOa$h`8jQw1>j;K2BEUmKY^acXW{JVySb$W(vq4v$C4sk=T&4$!rj}wg}`}HXKv^ zVynK#55(}^IkkY6Yd;*u_(lYhy8*=q0(2DM?*PgdLOi^z_w&1~v)EokKd@Yz>RVN6 zr7)Wc0}c%oU&5Jr0w88_IRUC-S`4Zr3d<(#G(BUGq2R=FU2&VqB^Q_kJtYF%Y778q zXFd8wo1U14+pL_u*8OObPOLY(sgQVcY&jvl6=m?B(=YZD5`o=i`G{f4@#{eF&PITb znVe`MXUy!mPh^mWmGAP`C(^2dxc(088JeF`9_!qi)YQXrXP#^@xV{HoBFa~&A?4Yv+S8mO! z&gke`sP1;PKS>=v+oNyI>RjH=O8~?T_70(WM7#b(syhyt@6J#)8soB?#z zNJ=Ddff#2U|Bp=__3nr!dyY_8JC6;V+G*Y?@a}o-wEh$)vVGUo_lF$nqcYVLlUw1S zseA~mnUus>!lu8ggL$#3B^e@9ceEdwPG7K7@-Np(cJgBo|4$ii%ACmiV!)NTm|Q@~ zt1e9I^oW(^kszhN1AB)IU)EW?DdEi+OrfIMH4o&F0F&BSLU6YsK>(Bn;GZ>ggFoh; zgp#NRr<4Ie8;`%i4wrBqfUHPsd!5Ety0b~|TlHsAv^y)?q#08Gi`GYn{ z%g)I|C)q&RNCue>ycrv&(EytyX`GY){NBEB3_KG}s zt`u;1wbNk8gfdND!)8}ESO)KV!|y4IjDLN9+4gKS!M>zKd7D?%pZXD${u-Pes7PfN zg9L;#uafZZPX$Tr%X7lV&Oqbt{-><{w+pD2cv6&)fMLbgaeA}ytu4)f2G?n2dWF0j z$>N$S@3CEizZ=T-Pd#e0Okom5_=+qM4xCu8&2Ayowk zeC~KoWdYAi3_n576N`rQz}`{*69;=EJEYoIg~ zJ^<^cx1n7Vd3H(w`A-ci4TkPCsNoAaw3|nA-$*-U2XMw^26O{ezo>(j?r)nFU$N2n z+6!#8_r*JDlb4a!Xk)>+jVb_LSfhf zc>il!0t^%L;oJ;_?ni-8r)1PC35+(k zKW4!38J{N+Gt|chgS1sogP99FJinNMXdH|>8YqBhY&`j%y@Q13^bkap&2~d+tZYzl z&P>-0W<)vqivfHK*#E02z-y9F$J@V6KR_ImGlcn+zM((mc#)6av<~Wd;B4wCqTbG% zb$PsiVtKQ9({jKerv6mUy@85T?_&R;R*%d=c z{CArB(>MrR+Xa-U`5iPsf=~b3<$Uw>2f~7OY)*gqK0Er?> z;cN8qf>t;GjQ`{xseb!$Z>VOe|GZMf!kD5F{%$dai;(1Z;PQZt-8!(FOh1HEx}hc= zSTF8)?2uLy`4A%@?&?2Tu>sCTo~m{`cI$3x1);}v<=T_|h05Rp0x=95<}Bm82U&!b z3J|vURZB4Bj#osV6=%dy+qv&vfs4~W%5d){R*)wRKP{*bdpmH!g1F%JX7c!!fXZjl z>%l{0n7y%*8JMd!0*ydDKlFB~`A9Kr75XL7TvKO4IX?c4AhL%UVH1OM|Vcsq*1e~r#;c18SZevL?z_kM}-;GH{dnx{ehO_Y`qr+^KW~- z<0er^G*ieA#E6L2_fKpazB-%GJv-Y>M@Hza%Wynls?O!+Xy5<{i)=_c|2`TnZoF(` z^|s&zEjZwrn6)T_>YR9@w=fWKx#bO|Az)DCdFs6JeM3NjFoU=XU<(5%<9#f_>-?8r zFvstfn-4dVpXJ$sObuw22m9TeD!!Ri$z+_yDb3O@1YZG`+o>}NROM@*uHYh3p2kjL zgY#u1A0y7jxIKpbG1-rK73MzUEe zIWUe;kCY@5&SZ5RgvGvHE@`-3xd(jvH4&BTg@Iq1vR)Q+fUA1r3A%_5bNjoBDf3^P zxpzSCU5T0MtPUfG3qt-g&`2}cbu1=63JBr$m_KkCAs;eNqBbNEhEC{+L?ysd7j4@8b<`2V*@Y~a^*7xe(&a|PnV(F+0-t%{U))X2c?#&u!>s;N*CFVQ zwLB!X$%CAoTy^^9shU#y9asvV!`bX&LfC|$g|AUh*`bAsoycJQD&O&S(!m<@i%0#l zqACJ~uba1l0*r@f;qEU}6P`DJ<+<$Vd6w|zyZ)Re9&a(nUwZje@c>L}hv)zb_3*!A zJH_bt4!*cwqC5k5nEeIdD~tl0IBFkMY7xHvkPMm?fAn%)ZspN?y$Bz!o;6@XC7m(% ztop&z{RE{K3%i z3iU{!-)w)axYt+?a0M%kSFgJLBz|>3xZwAN4d2sPKpWw~Nu2{mXUNWdmav7^Zm9CA zv9)mK>Hi}%{mOFy_jVn$ZGErTm@F*(se>xjr{yH+^%(4oWg zP2Je-N;Rbx`D{)|(U8F>*{TBGL=z+9+#5p%-@JUb7P#H8DEOV9ajRX%il5weP)R-m zmAaq8-*60-uM!tNutU+(NW2Q%tpJx9!)1Ke8!VkFn)rh)a%RxaJ`zgU)r9hoaa1O? zODuPH|4_RDES!m0!ppAWWp{O;mp@o@gxWcy+7j7s&yzKBY5XoLfHqUi(4_r&`J=hB z{9YmG?=Vd?gpo)P_EE}9Z8Gw^d0L&Wr-KM$0BMQOOL z$00{65BT|UdIwiwCF>pV(umbRum^JOzow5DfH|19qwuss_AUSs6j#-t(1Gg4`pH-S zNTodf#^SREmo#N#TsRnWZFWyun#o`rN<6CKA%xn|qLqMzy7CqFd3y_XHE0KJY7czt z3M?iFC9^`@5>TiFnk(;|(GOn)BdcaA%|pOL_2O9u2Y2#ScHk29S6_a~Ym~{JE8VS~ z_9D*%+_8->6F@_cQ`3Z}`%l%zQr9mV5AfA^fmK;={XyM!lTqhgmYO^A=(M|m($|>d z1#C=42|(nYY$wzcoiN|3K)6|!HLGPonNe|!=m0hB;r{l~APpYmX32Iz0eB$UM;^Le{n-wb^9BpDmr2ZXA8Shx&=Ww{MMGyyuCmHRmdap__!O)HfU zaZ&&`rGcaFM=EuAko)^PiL5}6*-QrQi>ewS$n7m~g|B`DFX?ZO4+CL2_%VQWBhd{t z=8R^2P4`{4+N*T=pS?0`wf7$M1)ZG!{lSybYVaXImEFUUvTwL-;}_$A7=0PmFzSa-~Od zsnWT3>22z*4E>Ez7nL-m>Bhqsh?*yA7n;NbnTI&n5+j={4mGN832 zX#61ORV7oY*(!2RztG#q);A-5wx29+=+5r0rVqb10Q8}Y3B+jgYy%p+C8NcI2YUK0 zX88Hhv$=<$j`H&O@Sasr8FM4Elgv-1i{uX}LbqkWVDY_9GUtRRpTVin9fl>$Up)Bq zr|?nK*lQ?$fLr)t2obL0II$a&TTbr8fPy8OsuV+I=0?W%4aQStg_FPhd^Gp_J1*E!mVb9? zPS-9_tN`s3jX~A$L*25_vY9+ zN0Akw;^>4#CD}@%?7g>A$RS5|Xpj*aWY3N=OG3!r+1dQA$p%sTmlXnLS zx7l&fA&w$K_P^f^17dDxrpMQZW`6mrQ!IR2f8*y5qODu^TadzxVPjML`wB!X_I+W_ z7ep$SM(OOcYC`KSAK9zgPG^38Qu>>^=BBsyM>b(B~?!wq*hK)y8 zFNp!yhrL%=$bz{u0Ry{420=Q&f>9ZaptL2RhvbNfV*Qu?!*&JnRMsG=cte^$aqT*t zgeT6U+KXAFsS&2Tt!@U_-lKK~Y>H>Jo4&l|K|%Y%#(l!7WB zkEV{A>TVuwCSDF64A6-J4E7=rm3i!mtiEb1s26Q{YPH)q|Jhz~(5e&46q6vlH5nD3 zUfR~!DIpe|b|#(O|HO_y8=LC0T*u5bW!($OH3GG}tN8U9T+)oN8o(Ff_`7M#&CzI@ zL{2(UI?OIZUu#s}gLOV??0cO%?wFC4a>6(Dz=`OZ(e1UnM9=uC8pq8waUW%=$`vVc zs?Jj7BonU`Q=Ox>c6-S1Qf{RI(gb~)Cui?5s6E65e6ys7G{NZPXZ40rcxKLXxb)(t zN!lp<$t#emV4)(9?27%Eb(s0>1EGl^-oTj8|UBE`doFn z$5eh}cs&6O&Yb${%=M<`0nOJp`-sFJSTt%BAE~OEJ+ts-q9`7rJp0^RbNO0?wJ_M* zuxAJV)h~5D5z%@ZedfTf+alE?1kDE=9-Co7AF%EOE^{+`@gj;q`cJ7=!4Ugaf8^r= ze2%!8@fZ@clLjm@9(Vs$ndPF#?|7?@{C!KKzAwia;=>yjBT6)Hg~L!xj1H~&-n$7l z{=7%{y9w}^$CKKtep*{u{W-Irva+-6(V0hr&m$m8g!c!2O9gT+uSG!xdIS;cwcIR6 zl;`4BLBFcm+SI7oz^af6NFpE?-%?2 z!iHsT`R!##f%;E;R7Gs6IR-q938C^q7^9!VZ&(=|_DBQ9@*@di$*U2B(Nz{RtvJ?W zD?eQYSc`Zc`V4BWLWPSB%Pe7DqHklz`0Jd5PDE&I!}Z$e!<4#c*RDf4XY+%D^dg6& z@!&r>L&kyoe7moR5(0~lTHT-6GX~LZ`n!nWHH}(C$9te%(F6!v?E&(GEAUg>nysoD zC7rq^Mg$QBw4PCLX(sNwLO_dEV)UWf!NI&|*R$?-LVvA{&dECvx;Vm&_DX`FnqwCQ zvYH%y9hL_X1Y>pqINqMx(+-&8z?USbxyX!ua)jKr_0j-Q1+ZjPgy5RaVeW`j9umX% zibR^tdC^jkNDsOU>>YpWTN#uWuP3`>cH5N@m+l^3UUx~4KZRk}%94!ib}Zw=RuD4) zTwv5VS$CTeTVY{TvsI={(6noSDL1V4`=#tHD$8v^b>S6*_rwbeH;Hz=7p|oqosc48%b(`jb*8kobMzq_Vpb$|*U%wQ#5|NoqvY ze&``XrEOQ1z>4!ALeDG}v7$z9)J8~;I(^jc?R(;FjA^}^Z0rq{CbfV7LqLeIz~QYX zt>7-p!*~30l|j6A@&T`|lP1gmuV@np7f3&)$NLZp5ZC{XO_@Mb5eS(vkoEp}#)=TQ z@rn80?^PJ4Eq=KUQ<@cT4{%`}yMUD<<3#ZML*stO;BJW^D|t}^v(a3!`$*5)6x*_A zwrdZwL-9tci1oV`akz9nTzW%SYP!VIq?C293b*J-i(UulHkmC4|M50z-fZ`Zo_|7` zY+Dv%>vmC+R#GZ5TCd&ehN-ez7%qSpwR8RpWo`i@_B}BUsah|E{mwp!n!8^_aWsGL z_1%)FfuZU8Z?X!dW4Ba;&<9&RT&Ir4*O6A6nzI8{9J_)iLB&)yW5S@GJw>^##FB3-J+P;%!x%-?lfV&FoSR-_-iMvML#rAQw4^=)iUw>~MW+;qi{p8!vpirgh&NrVJCHfK zgC?h+mHTbA{D4M01&aO-8#agrYfFtS4@3G|Fus&62-ycBE}xAS%ilOzz)dBDC{#4o zUV3&9S6M^BzLZ9~zGc#s$&p?yRz@))3k>-_bW)ba3j9QnZTyrh3j zd(bbYZhw<(wm*&t)T55x9=EeGXAzeT2Z4s1%j--2PlZFBWLo|}oH4Ps`bfi zY<2JD#zkUgJ_p~|HlzR(U18d?-H?9qUl5$)^9YyD6_e>}pG1alM(%7E+%7#*mRp2` zfx5rZ9Wm&jR&W~<@EP))AWI_lzf_G*&2~$EuB~>CMD5S*K$s5{u-Qc9l^8q|a0~D{ zIJ+-N;1<<$sN&2kAAZCg)_!e;ZlT3gfAkY+jd)7wa5fe8Y$Jq5;%wJ6qtH?{-^Z=w z9-o8V66Y~Lx2>z8O^g)!@(SX;%IuV_%TDYU7RX{AfU+*VyqtS_`At+Lhx)r&6p8_n2mTqOvWYoI8DPpo+oo?Qa_KQOQhq|9>tmL1Pg_xf9HOKL{7i{u1XYwnvHri$+jO*_Ht6;gR_?X^ewb{uwp*9I z{NB>)k7qN_lE=NM!;di`=61KZdxWlV3};QD>Yi;69<0>a4|_7q?8qIR_0@e@P}A3( zr|_4*W{vpw-rm~lgh`wSC4i`0Ot39=wc-<^+yb7w8YxNJUMlOWd<+8#pEY)s{tu3D z{I5yp!UzkR!(~ICMQq9zr`G3{+iFVqTRewp=G6mJ+P%L%*8Q>qPM@cvFQ<@p2aR+c zCl;XF!WQ#SAxM$_?nL+c$r-W4D<8LWzA4!zB9;5w;>fwoCPJ^UO%YCYvgx)wBGw0l zo4vmR+SF`~T1Cy|B&`#FUcN(wt1@j>u1KSkEwpZJ#>CW=m04T0?ned$3*RMKjxn+^x@^K>sv~4&ggIJ3~x)nJvzN< zFZ@z!p>EqbyJ?To@`!x5n_G)V*z=uBn{bW+rPiybN13H1LV*WCG3J}233sJNc3(y6 zo)H3qSX;jG-?dXp^d*P49R~{%{V;<<157t>HK9yfKx9(B;@H`ilPw^S`siI0@Hyn7 z-wD#`NfcL63z#ibfvx-xO&V5Z2Y(neT)v0 z`SSAB++#eEs%_cM4Weg!TA07ojR7E(qt|*;pH~vuiQ6bYg;;0bEjooLY~jL^U>6^w zNwGV;*fsrPUhL0l)t~PKQ(@;@qu_|q?w*XN00i0mzsrg z1jJLRG`DI5_R)mr@>nSJ|MZE#b;f$RdBzarRImi%WXdn0H-&nwO15r)S4~T|;uyS6 zV4^ZmzDS}=UX<1&{{+7cvPR5%YEl(3w}o|cVL|$A>(5>pa^F+D-t?_V z0USaWslTsxAS<_Z*|xpDDWV*PKNT`PTgc`anaydMSvBiF^8cF>uzgk5ILnlx<|nT; znhNAK{TNt0AWw|Z17^8N^kR|zou(>$#fPekq%berU_fQ_!@Bp1=bHQM*A-ptmjB9Z zva{8y>fhqNuXa(R`)m~E;-?>ppuXwiDsmEEM4NBShP=MGHDe>boE-c|=z7tfza3`aOxUgK!b{|(GeR$@Ui=5&JXJ_wHnKPS(lLB z9fg`#-Eqan88Q*X6tFdcpZ9mu+1ul9(JK5A4~PT`G5YB&A#c{hAqkcBv>+nID0cT0nGl86KjZGvgj8{T2{@`pjF?Z z-vP@P4L3GFVJXSLA*RTO#c7~0^Kfqb{oy8mTsgkglr1Wk>t?C@rcl@hgu?gOH-&i=2ZyvA3g$Bi9w=4lIyIfanhZFwi? z(P~G;y+vuo1iN;A5^1p86$ymh4_bM94x_l6ZS{`fWyh8<;w-Ybn76g`6yob2RIsGl zq9iQ&tNzSM!DbliNNiIk=(L{kVA>Bq=cIpBkDH6ZJ5Zt9jUzzqr~M`E`G*Z0u4yvx z=_PNAun}ymGNMgC1)17KAwg#hhFAyW)!Sv@@=iyhVzY^WU>oep zEk`&YC$bTkXGx97=1CEYo1%->ZQ+lL*BX>U@sjJx2>Tz=**>OXNkyedQ%&4&NTx}~ z51TCdBt+VD*fsU&&6abK;rhnW^q#_#{?Q0= zZ=qj|DS?bB2R}s^FaJL*;<0EERvDUgo?H69nZOD zx(Z2XNzWco)2eMrl1>?txBH442Ec$FbUGqT6A@p(`AQ6 zw!*KIY)KIpD9~5tk zyI|&F?;!zgXU6Ccd=Qkxs8`;RHv(-FFW~E?LHsuo{K%-@k`Ab!w-a-7@ zOdzDakC>`rH-{GjlR|K}U_neYE`7>cwKA^wJci=Ujc}aI8Kfy0=^Ob}1ABSawo0^03Bq_~b)!H60oeO_db%nNN7!yndkSOoTKuzISR(s=?|`C* z)qH~Rpy>&eDFQhsj`6cLX5;%XA59Ip-0LLB4%i&4@DS%zM0PYU6@`~-oP>m9wr|51 z2(gOMh1rL9*=WsrSkEAQ2Xh<`{AS;IkkURFpf~$l9eQMZP-Y`UyLp=#EuwnszjGp#e*rHDnjh$81sNIjViP1*fb7mw;Xh&ysR@Mkh`!qIBm zMG0|5q2Hl?r22QyEY7%0$7Yxs!J*IHA!tCz@SjjzH7zDllY25kWMZS)I(%nR+xNBu zYrhsGfy|D8%s@iXL-=moi!J2Gu9l=2qqy$Z0Mmu((Jy8~qGgLC@J|=Ge^sNdVA!23 z$@V&IAw1uJ#~wqWjDWwtD}LQo=8KfZHWT*tWe=Gc11Pzn56j!Zt%@utqrFIW31l}& z5TFng)@)v{%}R;*Dh46#U?iK!6uC{Z|~%14lj^vnpq|D-P&7lXce22JBn zhcQZX4CcbNkYF?Aqu^)C1-+EEj)6$)X99@w2~8j+Jbv2EJvtGGH%T_^v7k2LDaiJ^ z#P;pC1)Vz;)_?9T{@x7?PVE}CZT}DP$LMy_B(a6;ncmB^DOW%6wFQ4TtoagY|Ftmd z+`!c?t)t6S4gXk>M8Qin#G$X7>>hh!Nk2k}wT@Tg+gH~ciu-38*J%@eMoR{MQs0Z@ z*mcr$f-eKL9v2+2${T&q8ybJC0<#35Jm*qd=k1QJ3cO>Kh)zG?eDolg)KOzrjc2kM z8|i&Se!!?1&h(U5okF;e3NvU3hHLsTSp*#MpP7pumS6P*_78t-i9Dqh!rUu-%F32V zr6Zo;@q6SsktjLBA@?<zpbk1bXhKtxr%~#tw8sLZu`#4t9#r3 z1bAG(%(h)o(O#Y#7O1{Lh@$&joO0|n{vHWT1K!%`T(1xSayF+El2FPOo{_jSkCfpF z8TJEMzLvG{--^?(Enb^7giRtJ&3Lre`i%rZ84(P0O+%ze8kFgG{*dl_8rwJb7jGoI z@ROUA-Dvz#$&W*RC$%kL!m`DHvJZfRrR&1BiB%N74bksEJ;$Y=B%W!|^wjN!!T@ zv+Q6?w|Dk?JXxAMb2HY0FX9;oyuX{W^C6;_)yRrI^886P2preGvRj(RV88)aSTX#}3=+h`Q-w*<0Mo~ZVYMw0BNW)HL6q})nlk}^N;#&vqz!mpf&EZ{nG zGmU&cRgm#Zog7cb2Rx6S2;dbOuwSE9)Ol6u=N(~p=1THnSVEri0V!3LMtrlN=ut%d z9?RjGsnN{yi}^}t#aSCSQ&b;Z&xWRagKMm)K~u4V1cJR_+0Via8thpvggw;$GZ0Z& zMMQbVx(rO-`XDZfAv)fD+JRE`rMGc|6{$=%WkO{65%_7nm6Ep+kHFKq3C&&Q^9%)L zZ+o_HqgUx@z-!OoirMv^2bClRR%UY{=Tg(f*~rqx;R-hPjJhYz(T8W-FJ$E|2b0p2 z)R0fmTSRB_bk>J-h-WY9O9#0mOA0*djlRu^pt0)hsj)f5&w=8)URW4`9wP841g1+L znc8!$|1K5EmOQ3V{HH7Er{`WZWez>9%Ur9Wt@+!@z`h$%<2-)$vNQp$2+!lg zdg#w*mkTrudK=V>*U5h6)F8SHE@F4VR3=_P>wDSCUR+*(#Fkm0RXsv_nh z0jGkUbp#X`bTVa+K&q|`jM*{e(uDHeE04}s&c$g%8eh>F61eu}{eh*MCm;cZoVu}{ zh4_Z6DAlDcO6%3Xg5Nk)E3cL0!&rco-Zvba4R35w|6&W*mD)-Cdb8bY822x6A)-^d z13q)k3`atc0V`%Yc1N{MG_9Wo)E6%~uy!)}U`-@kCO}Fr9Ce{V|Gc4* zM|PfWHiqgMD_a03-wE3bTU&SQGT8=myoPIz8b9hD)ha(Zs9|2tUpi12aetbm&CEh6 zIwkVpqD2tRzW@&+^@F0G2)jFkiF+vlQ!EAWKQ}=zb=>V|5*^k`?LR-|N;TJJqT;@_ z=SKcty@;}_0t3REmdU`1oz9l6tc2;uHuMnW1pA%mC2dJ}u&Bzg1X96`P; zO;5S=PEa)*>FmgO=K+;g-*dqseBbBa3}STqHxlO7&mwPsphWx-Mo53PwgcUZbfk`& z7(`-%7`ol=$RS{ki_P!#pC;r(_kT`}cB}iheavQ;lj9hn*_+iSjkis_F0PDtRZi?s zQPq2HDH6A6YvrbLV3)^9(Usj`-st5K_~|i{fL3y?)#Ids)V|~m1bO|mQ#U~@NN3xb zk{4ojuU*r+dpW<}j!o4O08-du&VceQJux3|6?a{;|H6G<;F}4?v^6?tH^Fd23O7?# zLg4PNFTLds(jI12Jw+U18!I z2%KoMyVrlIH5|Y3pw`xE?N^#@$BxbUgIeAYx|wiz20ly> z-w0wvNqV{EAGd@m>=9mWo2PejJ~c3qL!7Rf942!rbX8#rSye_04{Z4E#&hGl zZK5xw=^K$4n_bMw$v*Tw&yEQkuM|=l$UKRnH24>AtqCJ`;)=0%D6QtJi9kY1!-bH} z2_UyAqeE3zJ<3op)yDk}!@*u_*b#G02tGS>5tBK4jhK}CIt)JrSjl?PE`(3EE6c{s zcXU-U!?Y70`J!2t1HznB4+B9Z!MpRv%x8B!ZDprKR_Wnu>m{uX>wqnPhU%?wf#Lu~ znu5#`+F{e7LJF{j*uB7E{0pU9n7tol^RDSYWJsW@{$u%p1&t&QX}AL2z`)KK<^kT^g;(7vtobxgF zSzSmMCdQwK)#t$Q+SW|grXW(+TOc^%{#Tn{U2){A4PNiC3%l+C;{88uE3l5h4MGd1 z=>@&uq+VOtwi$_GEtoQw!?{4~>t2I&cuCp%a5K?>LT?Nu$L6Snh=%AqK}@e}AOhh~ z+&QyIi*5D8)7m6f@%8P!$R$W3#wz#srIi?CWijI|{nlk|WO4Wr_{~U;az68D_BB3p zI*j@>B!JZ*s}hlO;SbBHu$o`YbaAwrt448XXEQc3Y63|3qz`6)Wz_Xa5y)@>Hi@9amS#-#-szo}+v52cMZ{*C`V&@Gy>Ex$S-kRunCCqUAxMIB{rM5%}2 z^kSvWCrM`I@{m#4sAQXRbg0cD?%I!V&N&S9d_LB{x?ENh-0olK;FxbG2O7q*b(5|d z8XffkNn&|&$l%3fOQpKRI-Vx5!afDZirOdLM>@e4e3oGuZt;z>QTew8%N9UE0KA{Ey>o2B`I%}kX^?&y=F4n(?-1inMlQsKGD24 zr2kP`bs`$=7^UE5r9oKXvu(lPP==Ra6e{cRZnuW&pSi+-No~#FV@>Fo1e+) zda!>s>Fh^SzlmU-#>sQd<0~R^O<%Aj4L{tSHFuJ?tf!X@vR9|%tcHh18#@_->Ya6t z&Q2%tQP=Fk+QAhCTAL7K#up)px$?ZHXjQDAUWq`TNKSI8kGyapOIAgcyXPQ?X}F)7 z=7_&Ai)pXG8tvIV^b_K9jA|!1Y=sh{k?7dVyFbCV03t^ z8jH)zxD`)kHE<{>Gr$)?K171#K{+f=&fBYkBDN;sjOF5_%J(KDI&E1^NO5#bweneg zH>eDW0T*z)<5c_o>L9+48sSOgaq7po-`?iORrc;tj$;IYx~t%<&ae6xp{<&D6#fJ2 zOJe0`ex(l9a>+e&;(`_Ktb7bw;il;dlFPGFQrOm2!HHQPMnF(<7nogtirVyw2rvkH zdECbRj>P>oql;y5!6@<CNysO8ENH6L$^l8*1f+X+8XoTGl`c@&RtrFJ_@0vPT;7E z3gTd7C!=QJIH6*kR7zEf%f{SJ?xoV|V3LtkS*c_j?k~4r>6;w76zHgv&pbB1{{PGEt>i_5Bs3gvEhC+I zlF9u7%GvloxIOC_ZZFEXRq|cGSpAnzgY(j)p9IyDy}RQ5MBj34H5JLS{pi3h=jC<* z!!Gmh;t1@urzs(k4Ed2++->)Pg%_pRM=>~!{4+J9`wvpX>oH) zONXE?Z#-3gU;8|!%t*QQcSQ6}^UIjtxZ;YlDEbG3J?~3d`K1)%C^az@YRIxMz0Clf ze!Tl*KJ%SbL9qz5iep*P2`wnC_g)X(A}{}7(omgYoccEG0)%$(V(HonvcHnRHvtTX z>|mNhi^J?y*`fE{YxZI3Vy%rh?X4Pt`K%z~QMUunFL0@TckLP+Qa8y3lOD(gJPa{j z6+Fj8oixLiUaIT!kU~$~M2|;G68s&S70OY!^JcSkdk!q3nMV7&3pK{gitFh0Pt9Zm z$81sO23qpHjn9iPzci;JP6p-)Cc!txV!4`$Q|G>+LvnbJ;@+6$>zOZTWnC-j(FM)8O5c{iA?ZL&w|hF3K+#mP%< zI@kY~WDVD__{z_+@DfviMuY1ntP8XQr_-#X;p%{U?v5eog;_$DNV z8d{KRqF<w;v)>v?-*Z^TS(ak-QU}qq(G!;?0N@`t{n8#q*#MJqo0Ds zi!Og*;e;rPUJ$b9_!7?=1byk<`myJ-GO-lX;&Y5j8jG1=T1oMc^~0C=4&^)d-52?9 z`|9*fpIXN=mLjA6GjumL=g;y!*hS&yF&eIvyALYs&kcCEb2Yvb`7hYv|6(5KQK?~@ ziAlFP!~oGU%2U40it+om(&6njnnbt^gWhh0Ke5VPq3-eMwcF|v4}b*LSMYX|ol&iX z&ZJqt*k+xN1>37%OpBc$M+`hiKW|#Etgf_hGv4h?0kC@h)ADOVzM0R7X+#svwo#UR<%qNqyzfo@99Udf@gcj^*&da$0GrU#o%*3UFHn zq4j)=uT*8k!uSJ;RFu*=V^k1~Af{F+-yT=Yy3^WEh+NFT-MZ5zn*Y_UU4Qn24abJV z5Y2F!xAxrAuP)=mzg5112jujS#u0~F5IM2$U;Yhyp4U{grMkK3}*Cb&B zAiAM9hW6}Z`mojJe_M04nPLb^vQvUk2j4K{x8TW?;X7fIh+a+N-RXm2$++AAX}7GF~3C z)(6A!d#e|z#^^$Eld4s_zYuEQImoiD(2OXqh_WfRKlh75{>-b*0g<;&(!3o9b`Ucf2{c-uHZn@)!qIw{>FT2}2`{ag=RL^a`gWNg~q~bdSi% z+Sj@s6c@y~T&Dc1Wp*8>PJgc39qk6RMdRDnH$Te>ZP5$2F0njiovJFd6_JD!^XMJ9 z*EUJnz1~dOJu~U=vWS&k)r1_MNIRWky#gF)65k;<+7_!llB>ou< zhJ8%!b2ib;5P1wbJ=g#rt&Vk7k8@wkWLbxmfH`TnlJ5li@O0LU`<`SBnpV?olJ&gh zycW;VYug$T9m1^0#fO7*cjHj}T=#7m%G)17kFEBPg)t7##f$w;ql}<=^ru01eqnk6 zQXh8Cm-1dT@oPq?Rpb^5yo7}rGW1$kY$30Nq~ur6`?4D5f-~0|zIE_o*S$>|8o_}{ zYSQfzVJlzi=mlZWlawK$=trS=vO=u>&&z~f!~>q#&pBr6{Ij2Ti9GHPkg|AXh2NK( z=`07&YJ0DDaHP?*Q7n39u{>g5N1)xE04-G}wD z)tFt!nbV|l6MI%fiNbYtRa=%d7iGqGFKCHg;A43wIOmc9!BRf3!U{bXJa7JKt?woe zp$9~!Zkph4;4m+t|F7IznnGGFpJE22cu)wPQy%TV^y$Qb_h`NKD}mTI`hEHR*?*dg zm5`|qAw&W!S`3JcqLfLys6f|{SU&@ATq6i9CT9*$&jP)rt9CLwqs3Gmt)TlDhi7O+0nlJ`L+9I=x5q3i5tr z5mXoapx?iI(~w&;@4_PzWq!Wx-;e$-SSKwPE=?TOUwT;=r+J|6iff#>NDy?@ir~eD!)iGYj@9p#Qz-SVs?l*HxMT z(1E*3XjzVl!*h@|Gbjl3K(U_Diw~8Agm2$h^^-Aums+M()qnYRFqr+CRt>u0jv%@( zPExvWcy0jS$6!)n0S9dQmqoSG&uUkV0h4J$&vNgpmB($aw!9RT18{@VSR<|FgD(c) z?A-lf0}|{GPTkz=RDaTRQ{$(q&GAeheS-8RMqLnzr^}%VIw&s9&PVUJXiB_g(88B7`aGAeuPbAg`f04yx2yLd zSGs9ZA6DwZ$e`B@)BaWJ%i>bmZR6L7^+*%pfK?kHjFANQkbgy9jvFtE9g6aavm$F}jaV zprdhi1G}^v7Y!fy?>X{@?YgG6?@tM&kxTp^NC*C*Q9|}SK?R~xs8>~Ikyy59^M*e$ z3HBw8R2n+fPDJv>pg-(%Dr>w|XA8M@Q_eTW%Z+X&9n%$d>EM%oWp={C_J z{QDQInY2lN)T%i7PfABw18Yf(Jw2L93`(6Je7Xx_4(bp7PU^XPrkA@zUBO(BmEO_h z8{#{Ad}*`irT~xEN5i%sxM~7a(qP zIa#Vw$ipfDiV_Wq za=FPk0et>M{&)*O?%xHPLQZAb4d9$Uq zO^mHfQj%mk;yu=#njX4&vq8?^B7!X+Sh>A^h#ua1)MFk_PYi>_Q=x%%rKz)bgAQJw z0~3g(gY%P6xVCde&_Xebb_G;W@vD2_!=TE#o=coLu`%FEH=ngf6`d30^iqEGz?~_%8#+JDy*#H&06I35N>6w+6c8Iiw}uRPOS>N~54^A31fxzqX2KA?A6mH4hQcgSr@ zQp375#nIKK^veVVB%Eise6!t^8jdwc8JI#k+);j-4Ax~-Q9aYD^%3@r?qzOndtEJHEdmC16t?7Qe?YpbPR|sn4m5mHz+Tcb_tE5I# z6K3u3r+QPvE>=c!)q6MM@D?oFVU}^|U25u07mmh10kz+MXu}Ej8u=tiVGrRyMY(Bm z`(^=VcO`07d!&(w)$q2P17my2^$-K6Mpy&>6ks`bZ{vorVNiCk{SZMw7BnA&{JTIj z#eW8jzI;!tK58I8xy-Vic(!Jm%EQc;areDI_@h(ae|}J=?l?G}tUTY|cqS-nh2pde27z~K)G_!qub|GerlDNGYcQ*6O`cQKCeq9S$_GZUov{=eyaL@ zm{Ax6M?I*?`<%9XBY`k9j=5hDKjz>Eze4OX!{W;h@8Z{l&13*a!AZL-?&f^pgQ;S4 zReQjwd6F5>uyA+V_=gZx3;R?Crd1>GTAV83(*xv47nLx#d+MVhIsj2yCq7b-K|e3| z&bMJSLAY9U6=DP84BQWl^Jlh6J{`JEd+F+%9KXo?6L(5Ovs)%-T?h)C<%g$S76(WT!`x7bxM;ls_Poh;Qm0eata_@|B zgUn!$r;gZAgndt~UeHgl&f2%#T1R~N54;f|8_QmdSwM)`B!+@t{p9_N^kp07{;Ym) z)k$jl`AL_rezYDn%Non?kB^3F=fd-mLoWWB5EkcjB?grPrg0|mZke^oo6iHvby?P( zh{VLWB_vhI-ie0mooKuHGpbf&Up=Mv$e{Z7F6z%|?URnkH~u9Zj-d}@bRYXYF>T)T zrbR#Ew5LsERj0>Bivj{1o%fW|ppL#Ppi(asE@+R?oX2-Q8u&H!Zvg~)UklrsBaLY@ z6MhPtQvqnwgtabjq#W*iy$zc}q#kQzrk>1#%~mEMpsR>Pi!X?Q6ZrpIYc+9RG&@!6ZtB<&h)HJ~%xZ@G2E{EJldgtY-}HkQ$aN5(&Al z^9it?{HMilU>MSLUE`&y8{w4scjw6I$Y2gz;Y6%@awiP8u4wAbq4eLTrdwGhwl7sL>Z?}K z%l=7Q!NF;_|G{TRTe~?yiZp_cVSJ)3yp+#;&L@FUgC7>Hx!o6oU)}SS_{mUc)hhlx zKJ!ADk=E(^ZsNcuupADTwg4Kq&u{a1ZRR3BErypae%M7m{^iU6>{cLObXZWpWa1^^ z)UEj~!CUum3XV-1w2^veG2$dJZx&_zI;U3@y|kjVE!Ip$ zW>jDce+Aet%@I}bG1Q-Ux6^3@7W_*@cZz2Kb%khLAM4!!Xr4OGjB=T^z+c7y#=EJ|a7ITCq5J z(lv>aiUB~e(B9PT$!74ePbYh{ily7V12tRuPYI*8LR+D>M^DdifWt#V4<%sZhpMCX zL}a!CQ99{@g;*QkMj3;=t8{vLoa_=LtWV)J9&2VZG!iwGdOcZMA<|(=<;}@=n207* zn(tk)Si|vdpSCgD3M$iG_WzxbRQxe5;c=mq}$n_VJd zvDyS5nI7V%6dut5`V54oQs){s}M;OsmG`b$I{MF9v}# zqIO3=6h7>IE6>@{`w=r&WwkL%0G}_Wef#c$R736~qi`pax32S{uyJFZIv(>m4J=p1O z%07|tD|&gbrL8=^y@04ervAL#nLD9LVyJt3FSQqa7-8!1GiUp8Ukmf-vJa$HcmTp4 z5e*88yxR_TkWkUiKpKsET=@W^!$7mJRRsyY+sJ`P2#Q{dIa?U7Xfp`q$;Uf4g^! zZLfj)`icKe5LLM7h2I7wi5C%P19=_<*9hyG$g_(=h#`v!bXd-TmgN4U4pCe)ahePy zu+!od0lNytQ^^*~BP7qR)a^Na4%5D>VR1Fp`S2b6$RWKZ(ACE#5*tuJVg8dm(*WF< z3e@u$?v~p~(@gA#o!rwU5cWETb5gt71W|q+gih)X+}C@90kG`LgAr93tvE_upMIJ6 zb$-UJ>*O#K!MecvwWn-_Y>Wm2qKi^CQtBu8Z^}Ky$+Y@<5*MYg670!&VR68F1hh4Z zW_^T0n(qjihuhswyv4hy-#!1ky1cNXsF%QzNN?WT>T_z;P6oy6j*FyC@6{6`B&`TF zepfXr>s~ll4^u`pX{|r6_{<$xGQDoIWS)gjO-hrHzSYbxi!E6NAYpfLV*z1XQDrna z5C7XD{Tgys0{Z*f_oS*1`boUl*$=iSSB2^90N)J@#dMmsB|e$3$j5C-2XC zkDGJ)FpF2j=t}<-P0_&w0T=fa(IwN{tm1eMeMRuaegAyj*H7QEPzW)}BBrP?tr52s z$9fMBWW9*7HBBR`b2U#ML4{qqk>{AE0HIG?TEA*+AR)^mek`zmYul`JKe=h;u#1K< zwg)xGnwa`puXf9p{P&KZV*0j=WJPTSQ~OE&(4alW$j_NQt_Me_C=>OQC5xTPl@0BBDt!ejO+N{$Ggb!;%zHOsDD+$NB}|Lx~P) zHjH|yi`ZiITyzbb+xt&YYe~9;elis53qmHN)j*IV6xtX(_wDtJMzN974)1Rnx4y=5 zA)t9CXjvy!GOCE*Bs9^efLl)<;qW)}B&PQ!9-JnPIRyfB|IIz|D8a`K7im1XBv$K| zaOtfSGIX~_=JhCF7No7j@R<)(q4x-YaV>TH`&`GdM4}(|BoKJp{qb1~9@F!xh~zHy4Q zR+J|vqKEx8w#P=6FDq4V*q%&T{93gY6gIw))%1sD`CNRHv)0kXay`-ha;DA*vv1mr z1av(YGiS?Us9n@x$64MNth0k#=L^ipcWQt#*46wepLEW2W2@+54}^J_SSLY%cub4_lfXiL@B#Y}&C z{ooH51?71Y(E{Wz8h{MwMD6_Cdhas(fJP?Rnr6F%H>yWmPHQ=ks%6;wJY&K|V_lUZ zJqW54gdI3d1)`c|JX*cFB}J*q2?t+C%{V38Ws9HwUqBcW_&R^`Gpts=Q1gZx%(RXP z#pg&rYigJ|Te|gu!wc3xOcEGQkdE$@zTfGrE+WRg7b5C&yt~d<7r36W!;h06vVBl{ zO5QjOR*1SQcjf}c2|ZfX5f%1dhxd)x1s~rUyGizbW9zcLPXW9yc3p?Kjm9vTjPJmoS-RQpcf5DRfN8CK&fk>P; z>cXPz2$2s}dbnFbwmJ2AC-gQ+J8ph&SR3~6B3g(0EqI#dvP;nZ$I6wIV5j<=R|4^s zd5T5OfebDE>XFt4Lfi~B^nr2J)ZzNobTzBjwenfg;(E%IOOcbrUt>uM~hCdEBz1eZLym+4)wI7|AluZ`t4P2@I6H^!G5d z38V2Zz4hM+e!1{pVB2glHh?cz(}<5}?Gs*6*T`v$ua3ii?7-A@L8f|TsTYa4Q&qFs z*}tBB@Se`lV@`BqAl~ZtdO9oVQz=04F=!J=EAbk7Ow`FJp>gn@U*jru(%MY%ek$f6 zCGZslfrH&lv)!yqvb=Ni`3Be@8VhV&g+(vTQ2u73qXBvL(hZ_(ytL;gMu=oyZYDxI zJ+Bb3&gaZx-AfZ0tCn!4ixg|W3KT=lBo;o@Y4@>;v7qi0L#C7hj3#H`|&WOh4LTb;$(drpm~p21d> z1;o^4ACjMhi~o+g>syBj(%vlpeeS}c-iHT?ULNK_WhA?br*&FITmBhRwTYJKOA(wT zcC?0wst59sIuS0IK~1hg6;szLWfBlCM*shqy6!-z|Nme1$|jV3&PqlkBcn2oGm@1( z&dSP+LK)dvN1bHEsmzwWvUNsARx-*Sl^sg;d!EntkKaF0AASDleec)n`Fu<+@0QnU zqJdA(IdZq7-{T*UPycD6n?7)-{=3de_eAz!e|BBs!kJx1xkuv)`=4G8Y<1rHQokvx z9QccSIbd1tVYt>z)Nx!6rLALZ#TG@sy)41W46)6Y@lIdFE}mu)q~Uy<)CUP|%CCMuWT>*<&0G^2Q)at2S^!c-DXIr| zBEV|B2KwZO2YL8uT5{S(DeH=wq?e*GgKXV!soQ|?aDk{^4n;1hOOirtq7JpYPbH(< zmFT=vE4`=IIgWV1M0NWUH6p~8mj*9Tn+8+tXc^Muo_VxhGfI@dOg+sy{LdQLj0Je` z!>fw@k}yJBT9bMb6%NCtB~f*H89)YR!*7Kk@-)!@Vmway>T!b#xw?A<+3GqZ)GFve z9~ODdRM61q5 z#r^%BSHE_rZtgul*l$RDfT}J|yLGoT{O8#D7D26->dB3gugs5xtVXT)xnfwv++OH# zLE?P_+2`qB%rT{5r=N@Q{!z{fbp`{m zr9AZ&(8VlS>rsisGXcX?g64t^fGK2nwit*kh;7=Yv-36DRX&~e%V*;Hf7d`{tIBeF z9T2F)ixU|qCX`LHF&Tu|tPR#Uy&#=^q;f3EkA;VbMxW~dMa8IIbY4g>)nOU`3v+Ag zkto6gVDU3v-85i3!=6EKiXzNq7`_T_3chBE!*jr;%n*Ik5SfUAIN|KlEQoG?YC1+K zzuv;YM~dC7uG6G|zD%7wd|y~8s}8>vJO2gUuZ_G79uF3$cpEoL-guzS+{aQ5Ph+Fj~&n_%9~+~8Bu+;`Y+uP?Dk z>&%c!w&b-*I@*N)jj@Tg`E47)^IK|1oVE+ma~#?N!U?f}@q+8gKY&@#;NJE2c5}1r zUMdk$8brmH6g2@sjTl^P*-KVZ%_wba*%ae~aLe=0{uRFaGBBFF7nFaOSHpi@HNpP@ zz-gg$LUgE8$c)!Kfs(OzuF#_5LvrKp7?7gaeP-yD4g^YXhn{TMo+BYoWe0_$W(-Ca z9d_?uc~#(rGMqF)aMf!-v;Kb}B)1{6^d4_@9)YBN=Rcr9b^ZZRge;Ag+%Uxn{< zaA(YQVwT)}3elh9{d9sqxDt)5k4UhMtCnAnl|2eEeIn^r`bgQ)R`U8g@nhT76m~y5 zj}4cePfS$Xr zOMa&1;6h(d(fGM&NnJ;cJXyOpM6~T-HS5CZUB`;IL{0NoX^LpYWefy(>uVVfryC*!yv8+l+mY4bf zF4LydWt@r978KIa`D%U6Db|3uVbAf{^uN@qd8gE^I;MSD`cbO(%vhc=anFCNJ=Spr zq2VVn;{HM4L7WF%D~<11wI`(9>b(ydm3Q_N2DVmtg4XU78?W+Z2AOOPx#-$=f1;hu z#HbHN6G~s`(4di48SUC8kg+j!-sYZ)rKI{=0tl=}l9!C{t0XM@jqIPPx@^>eQI;qt zls>B91MOqc4Ie!^Z1PLDSN)s?cE0|1NF9cH&d=B{>Dv5kiQWz~zoU4}6ZY)clu8@l z>4(R9E!JHm%3<1gJ4so1LgmSC;KGjJoH5;`vP)8C zWEj8&r0FDc5%*2LmV*fW2R=KJ%iX>0D?<M0u!*9u$3^In) zuN+$b<9^~~=2K(oGV?2}c1C-D--wI}a0xh?2*N?4AOIFU4X)4j-^*rmw5ih4wSl7i zjhh;K*_Jma*%GnD&{p>H)LaB+|0=QHW?B{%0cAWx2jF$9Sy^?O#vrkUWTEci|s@X$$wh%`dL{MO{|d=`!8Qp z0U`S5p}g+4h8vID2MOu?drXt6Cw~H0mXl>O{sAr*yNeFKeMQGAc(&WNBEtfk5M_^^ zRDWU!Si68El~CD`J;f_Dvcm1NF8*yop1O^VAh0UdPNx5 zgnY^`W}I(=s2wA97cK|pr8&pv4vF1}zE8mykYx%NJ8j;aCWTw%14xTWSyu4S}m!X!m(K};8o$%ANHI0-FXgP6x<7UCKp4qk|zbmz= zaJJo0hN)RNBsmhjj$nDgq??YFNsi1?riK$d+yW|rd>ir!024B~P7+}}&)+D!LX8s> zb8~Mx0V}{p3_xQ&g%p+4Ag(P}@N3`8Vd_A=R1~d(1{Q-C(pynk8jlm}DRkl^Vx=&4 zv&BY6{^Kq0MTRK8azMzz#pyQ^mnG{X3DY~%%hLwuhVJmPfxuL6JHLACiiut*&(<|q)!l9_%0wVC5>BD+UKD=qsk@3qjA3OZCI7fn+4ZnkLke} z5oTU9#o#B;xh53q(1<@7zuG9}Xs>|IhNzJ16muoI_Wm!floBtSc)JVsZ}N^&oI@M6 zMAOJObX4xOuQ)U!`nm+|WdLL`Ohs|WWXl~%1r@cw;qbn-039fM`=04SRMz=|y^gqB zl1m|S!}2ewUKfpy>1nS6k9uOM$Yu1ywxD)IS=JN9e#!&dKZfT0P&Xk!6*dv&5Y9qr%}9x&GP+u5)0M4+>xk zW7KHH({y7jtc47bQ>$F`-Lb^Xde49~wcwpUP6OV&`q3CPggWbWO0gqNj!p`JNFyi+ zf%N3d`8pGS(?Mqw?pRE00~+pnk0}e_D5t5A}NL`Rw$FTAD`PUQBin z`{JYmok8d%#ri*GU0T9kSaIa#^_OJ-Mpy=Z-wXONo^zQU;fbRIMDq3zC|kQM!v$Ju z2=*fm8epmF?o>(zSZYpTxqd-F*7;bv_ZOEEazH6Rjn5XtY6>hq@ni90O8A?f21^>N z^h-E&Ue~bwytot|THW|6VU&2WbkeH0s?NojL=gnT%mbVIM|IDRuf}{j^~dn-C$EJ$u7` zX!%Tqlw0nSFiQl+>7&@`z+bP&KOGILFG%m32@BJrEmRj$q)|`Ln~`EmM>2}R-y8mM zMOZY5~G~(ARVKGkL4`zZ&XIXr-QL4 zQ0}DJk}HvhSqu$5jo(607Lu6|prKq(ha7%5+fc)*Nug^OmAu%~A=MC-G%g;+A4^a_ z7~a*Ix@%lRh*W{joj@1Qa^sA7ob2LS=i!v6wozSjU)$Gbyph?`|D6kl4)$Q2K4hbm ztgIX2ly5{*9qGEg(!MY2s9t>vDZK$$!4MFLP&1Te?KHP%+B6ZrgT7yRvZoEAvA9yk zHKh1>9jX;y>GdWJj5Oc0;ee?9c7@#k`Zzl$6u=!%9j^PYGeM6PqT6MV(nx6XsB0)$ z8;)GtY}4+v1kGY-|ya^JXDyz-TYGQfk(^aEfZ8C@buvXuTvicL?oMsT+SWW zDK^w>`!6)ZAfbfV)8$CaNMJ(Fx%oUuBs`FRuDgB3?XT+xQR)1fR|9U+D=|k|fD>Y# z9Yi!qFaaFgCR`449bb#lP;%SmR_(Qd`ij0=7K~Uu6%nR2q33UR)10JoMs-c087TQq z!dE5>$?Fbfk4|5aDF3*10P15R)})U{Zyhu!_8TA_BhUG~Sn0BLAyzfE7SOXgZe*5~ z@0Wz5!mh;LC-6i8KM;reCN4!7)e}HI2*6edvRTTnNZ11_RGcS~;x>hk0l;BY?<{B= zdmi3z{(L$jufofBaghcmUY3PXs^eNi8kjb}V%sdu!be5QD2?eIMS}IJm8>maB)+D0 zkgJ_YFwX-z?6n%?{y5%~(HlW^``PwKTopcRjujs(#~#l3F1__WD>vnt*7l`$2xG zoaMhCUYXCB{iC)0sulm&51F&0WU!vF^S2gWx(IC@jJBAgC^xi3{5Ta~lZ!wj$xy2* zucp>#Ih&zLF_&p*J90sY$J)f`_SwvoVTU2&9S-sgZ9o#@?)AdBcN~%)c853Ps+pRMh&R_5Iz5Ok3C#s+eoGM=6=p73+IxD=4 z0C4P~{kU|n(Mh9r^&&JLPeT+(70nB)%ed00%q)<$DluX2Fk&58a-KP6l#T>UL~AVS zdD#;y$Cycv?5btuOwgz+Gq{SOu(0O+RHYjtB?Nw4A~H1Bf@#PDKLcI*3N(7y98^&t zvsFCx-aIiC7G1@k%~{1D3_Cym&|$(_Tof5Wy_Ru%C_K!0%;|y{`FZ1e%PDD6Vld2) zw41y(hQr!&Ftl;wMXqEVrkOm1VSexGZ9)-K+@Vw5gDkA%C|_($BU zJDQWaibU+Vphl}QrTzV0ZScBdD_>k{F7M(wa_mi$AvHV`CqRfWv_<2aF{Y+Ma3ES5 zA+MJHXz%O{}wCW&Tj%jq8uIi$q`;i7?>fY0h zaP3In;$wyP30@U!()13ipPo&7a!rP?@Q8KIeE~c%^J@Et_nbCp`Ir~zUn_>DO5>@M$x=a+X`Wm=EO^(u#`HdX>Xxp(d5iqaCvQ-ogA(KChd0EV7@%aM z&#PS2SbcRUCgBuu`Y!;`|Mkf3pr=v?C6@T6_f9_udQ+%~3f_L1VHoliCpQ>Rq7yv( ztW3X-M8}Dl>!e6woVRIsTK+x$Phvts882k>k6WJC`V)deM4tUJvjDxNT-sc!=vZo0 zw068=6JFX?ldHfDv^A5t(m>4Yg^KY(D5vT>5%@_NVqQfA5R_z?heVSpln|o5$((w& zd5@qW5kuiZ96TFwyz#*WbO5}u$e#%( z?GLt3uCVbfFqf>|3c6SJ$VzNf_rpvC&T2j8v7Aafo*|@%344M^#KP)>{7j}b1&e!+ zofVsB)8*|shoIhT?ea6C`Po(=TfGF*)~5w~zkndF%C=%k^UyylCSMuOOrS=PM6~ z?OZs@VG1)Ham-Tw`VdFd)`I#*wJ8i;#X$UIfE!Z5TZ`Pm=-aYRS-F;&or0x2r%nsR zh;&<(i1+MY%=(7&FH|J@b0db@G3=(o5{BTzX!z|&ZmjH=Vo=p86_<*r4TFT|{T@o|$cu{c-JcW69)aAadqiB6b?=g5!iiLA9oU zP|an2sUyI5&L#s?m>82G82uI$%=fm0FEs2rs<1&7!uI%**HN}ucS`#>aCa-EVjw{# z@KRr@-9C_7Hcj5{f--3fD;J*3z9Y+xQsTA{T(Q@VyDD+rTY!{`oMUI$mvlc^bdBjo zTQq#JUc7GvA$8t13TXJ_B%piL0b^~tmbAT)b}5?h^jy%R8MPf(Tv|i&J~JKL zVh(yD#hKT3{h*Ez7hqH6C1X&Ub;gTMEtq9yP_9O-J0w z^*EQNMNr~3OQeslE(e&>klEs+` zg0R)qVr=?*$Qh)z;{d{^JyihtP+z^M4WaRPS&^hrF*A@noi$)q($Fq4B7zF`WNvO{ z5`>s>1+Jw%Qb*oSRCwV%tL^2zN)4X7o!NfK7+2|{hB0T^F<}9Lb$M+EkG;y8G2$`W zPrJ<-;bV~j>kTZthfJ2WT zxc_>XeE-*zu){v~e45Lx@5DlI0R?s~m%2p-k^$J}Lcs2u2FK;c+XT-rBG4Z2UF8Mq znh?B#Lc2VHP4hbnPEsf!USK@Z3UWhn*h(H4@=LH`&(BC(S_L2P0#VJX^e0CTp3?EE zlQHRGPXZSIyC}m*C=-xSZxP9KIptf#yFjNryn0X1YC4Z26LxM{SZJ#bE=JjQLUhJ z8vzek(ow|J)y5IXU!r+k8s@~kGaiX8dxgq`I(jHrkQV8{8&P(XjTt;+O_#l1t@kB^ z&`4|2y_1&~aWj!xSLedS8@Z3Ue|S`lc5=^c&bK7Qnr(*~O3mGZ4F>G@daxtsf;w*oH) zKCW&O*Jj49-1sk)`hWQjCK(8H`J-FD4A zo}c7)1buLQ-}`jx(6BxlM&4!viSh?StMhu(T3)Q$cS#yta8mw$!6>!5hd{ zDuTLk240|VUU^wp0=APY+CSB<1U}DC0#41(|GaG%$M&tMNtIN8ZUwfOw6KtXpGQwP zmEy?>EEy`k8Ih1k*!=jEA>z#pf@`PYUDl``eV=4&R)mE$h@)nrmZ)tx6KNi{;dI0{ z@7Q&pkwjcfBZqncd^Aqr%0fKkhQ z=*QI?lrZUrt^I7vzO(h?CJ8pqR2b$C^%Z)g6118fTX-gKdbz(*Or#KXN;7AmcI<~v zDl%_%ewbh60qSNKnNUX(6VGurHmNg;%XX)qrbW2=lj#cH?sYnQ_`5h{*X2-q@a~6p zET!bvnW8(YeIo03jjzyX7mrzR(+)CbvI9`{N5FoFoA|XX5~KOqk{p7=gaT5C4Jc_> ziIEH|#TX1a(E3(yZ)Z3RD-wUu2; z;gGI=Qsu;?Wb4b|?z7wa@Oj5=!nH8h0zq~9E%q@Dr9-2H5(Y7g&@b-!yJt%P z+uc6XmJ}>S9U%YLB-inpYEp(61 zj_M6*D#sFHbaghIMej3B%W!atbY;CKoh$iI)V8VqH$)ZWu9j;^%S((FY!RzJ8I`rMnEjqU5OF*7&$x(I6rY7wiBJG8kp@;3itZDfWQ8Zv_Iwgbn&)2x=(aZ)K9&T> z&Q0`L->JPi%a^Doe^O zfKmoM`$Rbd*O2>POlQ>drSXvO$BdvB3l(YF3E~9^`U8rizV40bm~uP9MEHTJ*<5T2 zK#lBcNa5E3Sey4OW#t{a$3<+}$!z@?FL{E)!?ZMO(wN$>+QfVf$zU9-7X-NU)sj=a zS<}SCV}!=>!gwgBfj03~wNEXumP|I#{s@sa3CBmF%!;w>P=<+Z!5rpQI2s*yqA0

K^V*MgA@BFX^IGlb(pM%bH6N=8@z-IAtba1W4(d~Gr;4=pT0qk8eCW=&pc zAF^ae^-P<4^q`TD(pafpgnIx)&RiMIP18Y264I2{-zv{_kiG#^)p^9V#58d((?8MS_G z75u76$qV_Vv(bf2v_2Ed1CLz&@7^_6p_~Xe`Qw3-{qte=-PJvt&MWwP_}hR2uAM%S zYM)})y}M}?$OGrhtx0S7>SW>J+80Sre;=g*^nWRGnovhDXva$%erlFei;H~h zm?nlbjy{Qdaz$SKINf8-m~!^lrepT^{oitwm4}#_-3S2h49HMClNg-OFfQqxr)c@e z8l|SoD8`7D`_7Mp2Mk;h+pAa$>pe3y!EE{9#Ok`U3Z{^mPe;ryEG#RBFe@5MYjFel z;}R3Mq6tCjP&s(bI2%ovfNE`Pu#~DFEbf^YZuauKy+P_NN~KDpm{_hzfS|-l)-c-h z{beSW&xsMt1^Pcd8JjuO24>aOqdCb1i>?R>m79)h532WFM9{as5S5u*sU!2oy(aNq~Tl}ei`L6BYBn445?f>qG>JP??ibDo}W*4MvD(dvJ`^I2|aYtQd z>}JCLU1W?hg?d&%-3^bwl{afv_M1Xv%cG8_RYqA@IP#o&AGL}iHJHx@SI2|R5jkXTGWC<8yk^@8M9`lGIENRe~}4&I;T9`|L{uN zv>;1L`o`V`uPa9R{6kELfI0m{LTNPULAwfFj`ir8wn~X!w3I?3D#7Ch*Bih!P^c#c z>ig^pF@hg+U6~T~pP>fP{?GfF`@iH_m=eH$b2FjjzhMW{fJ-#ZNSBIB3Hdu6DdpgK zF_#vuzu4e{D8f&1{_$I%dn~M~Kc@1XJPc`^M}bzKANq*xt9jWy!TWX`_vuGuafS+2 z)qeA3t%x(5ms&_*GRn4c0sPL|>Uu(nxg^2Tsw0)}TJR&@?-~wXO_AH4NvLAb62#X6 z1G~F@`}vAAk|8EO>~PZHXvpc@U#%dG6JS7dBd?c;lNOrGE95+ydthySz{HbfbVF1reqcXaaDI+#)4gu$&oF z+fK0j^0e*3>ojEGR+QxFbcZw?BM_^_ig6Ffb=)Gp(TIq~MWm&X7;8oslI2Soa-|;Q zlt7*py*=P`Hdp4!U+fy^^A^N|64pF*|w{L{pYR<+Hxsc)?4DhNB$E*0+w4eU5+?vac@1 zT%ORMW!3PUWEJ+swpfB=EP;jp#pf8QB=9Z?q5~hALK6{v3ARjriAtNx=N^hBeNc*^ z?pC4?gC1=zrvmV5v|pI{?{o%J&`88bs+iYxF(lmDxBu(R z+iSBF$rXr*JqZ(PETOf-AEC|p!B5xT!BaG`0Lrtg!JZw1xi6O*F+M9Bb~ zO!l-^ZlvMdAt%G%296K5`V`#>SLM0Eonc9z!w%(qDyzyBk|UF@-eQk-8SueBhe zDP&H#Qs~f~@E2?Q*u1XfqbT10an@eZKR}yj3UOa55p1zM;*!x!P0X;2+<(nhQDUMX z{>LDD?*1XUO5hbLCGR()&7j{iCI7_4A8_iDl#A~Dfa!Ul_@C0aDsnmCtbD}CEn8()&nN{y^qmWuR=(xJ&8 zgv6@bHA1I3@B8|{lTR9;FI{!f&6l1QKE-E!8H6*W&a!hFO6Bc1Y}Zpo%@s<1d&ldE zKJxsWZcFfxS1(FTt)G$Ha=z-L}$X?w#tz{B)9?2R_Hy zY+$%r#$9}!ApW7q&CB!qte6S1BV%jfCUcz}?tSDfuY9_KfZKWpznG|`Rf-PY|5z&e(6+c`*7*4>7?IUyv7 z^w(1lw_^?B5zZy<-|HBAnaj#)UCqWC*$x{}1tYr{s!R1tkeyOIVgiyBr>d$taL(b{ zp@K>cIuSkPogaU`{}~ovH+vaHk!-uIzs35`s;1;*pwm{?_j|%^k;V|6$5!!CdY3xp zhvf_%+Q}3Z42^zR#GT5Z5utN+WxB*Otve`gtsN06iXYQEnDmQqF$-cN3IBReBncv-2mwM@X4Dh8A$M7i^2 zYM7uN%B9Z@P^v3z4~YjUEw3|GZx8lYw5=gdKrA%o0^;rKj>@kgnYe$b?Mr^;u}psN zsZXtbx=pyan93J69yCqqZg60~;G-(byY*U7oqc_$?NCAm*Emfn=iX4-k-$8*Fqo>2&S<_HK61Bw7J9nA!8DyCbm1R6ZvH-xV@{4TlK1S+ zT*^Q)_Vawv8UDD1|B53cIS zr&f5qiGVeQi669VDoO(F427|%ez=G&R$^1~E(jnH+)T_7zy6%Y`cGrFQHQ%%gp}+? zA;UZm9b8gE58>RLA_b5N;lzAuQ@JXDC5(!X>jXNC1+X(qYtgTHy{-X66gq5JA!bBD zBF7cGE66UjAP8u5m*q%XXC`Nw;tA;5)(j< zYZ^gJ^RcM7Qx93AAe98|v+>(v_wV}uo@J=P+;5hjJU6D-aG+2d8%=aRjR2Zu+`KT> z=M4W)C{eKHcKD?$aGmR(>{ePv&?riV80j95ih%^zM20j1aoTe5PwBNYN#k?F@BPBQ zT0Pj?csMX4>=mOTxw5UQh%8WNc8r81PND{h1af7F*&sxq!OMrcGVC?(H#~8WAtezM zx||Z#DW*~3x=lPiHYaxb>6J5z8GnO`Hq-pCS$yy3pD+0n&mk8j}xoi>MQysC1g5!0ls5BVbo~?=iwgKEY}AG*VXI_`!^t6;1qE* z5pGi__8jwyn4pmq-SYnwPZgK*zC}LC1W!c9Dw*}U)M~vQX(l*!8SOQXYr*b=7?0BE zNJ)>fbBmhOJH@?2jQ5G7Q(l{1R7yYYqQbVG@gub3f(GV!Bto-s9iX8(E+XMce41n! zozIqR5V>^G4Kpk&rIH^tT8Q<9n~F5HQXG9DJ>a7K8H$kFhjcI$X&Btk z3;C@~i$zt|)tP5Q8YZzUVP#fR8T)hSnbO>8dzsJ?Vl`a4Xft@CCP1Aql+hO?5Gf;r zY*XhsP5GKwe-&p%R-_zzD% zZY{=ez!Rj=epUmt3g&t!-R;WU=FgYzz9i1TKgh85Dd^%3IWC9tzp%vz6PF@yTP@Dl73}oeAsgac8|@LC%a)yHgEc9 z!xuo1piOXS#VDYK(WB65`y!|n!U3)(;V=>qotobf!1p1O5}iZ;2W-e8`oNd6nh<{Y zr;&VUtmhZ#TgBsY*A{>1U#73w1-Aj&&fIJmv6rybZ}HdL0hdF%?_bhbl2DE;Jc*Fm zNgHE=6rNYV3#Y8owZS?NfEhs3TI|a#yx^trI)WW1jNvhH@Ej%4xoeJEQLJOzf|o-4 zuV}+S1Pj#L^U-!cM&WI|-U5qK0G22%eTe0EyOo``hM?jB|58}NjZv|YWhXXZZEYHN4cjCT)X>1lBYM_OD?(W|lICm_O00QcGIMbt78Qkg=@W%g>2mHk z6@l-;;Z>FLWuqS1sv>_Z9dTzn*w`=@+6q#1lC-&K6sl73ljAqm^IuM&EwVPK(E2?t zSsU=b%#}ZIr7p`l6D=Gvtok9DD%0=*u`01b!3HNiL#dJQU>h0vEjES5=gRxu2W;>i zxzeRqu~GmtG4LZ-Vj0^ppI7*lYw`&pBIf^rP5&7e;i?q}M*uz%1=Uj2fHVnKdA2i< zTL^wVjs0YmT|s=pJ$e4d*>sdnHzus7$QM55%C|;pd2gd!b--SZ93G1LjJqdHuhVTU zeihE!SN59ssfp1`BuT$ipcI^3oj_LC zMce(YqA{;1Az9rsIl^}FB#)B?k#O+d>4VK|76NVI9xS&!3X!$d4dHE+T6NKp;k@KoKCw3x8?+uUrg7(w zRJpOB=;PNXDNrhb3!A6Uk6v#NWn@b0zStq~qgp$~KcJ194Lb<6=FFQox$k9S;N9q* zEykzvxF@%?S@zFL9s6Nw|-+ua;l4 zms6|E^Il~Co&DPcca_%(CA<>`zwOyz)u^uV+HHAqLgj_!oRdU(3meZyEW6o6$B2iI z2AWHv+`uqFoSWLAqGoaFEeKP}Jt}H`7Qzq2kSz^Ip+;wQwmqq_s|{B2x02a@-9bh> z&k2nsNB$-Tq7BA`i9J1q@f}t_^>!rV=+q=dZrj{+oSMH(u2DL&D_uL1@gb{JOg@>4 z;8<3!GQA4QE)}0E+9O&oIt*xF}xth#n-Xj2gd9uWj5jG!=W zO-&aQ!^@8-F)*}e0GdQ$`N4rE7~Z6>xHD@N&i0 zsUy2oTpjJxB)Oi+T%6(n@khw$YBy0q;LiUC^DLHo;#bt<>W$M(mta*4F|0t?xd)V4 z0sG}e6=!M&{)dYHVp}TfR4SB5%F3nHI8Q>Kr`QOpT7Fhkc{n0SmR{r`2-{`U(SeUw z@lZ=|_$@EdI@_N_M+tfDcz5HiF*Sqp`~jTl=Z$Y5KM!G)=`xTaqK{Ec^rT`O4*5oE zS14H}oTZYkx{X|;^6ZNaifnR%9 zCG!rDn0Wl4tb9-ndhvdlnh^`ElL!H~#45pOoGu;r+CD83@|{=>>x{B{DGW7R@Fd;C zRrN?3SPt1mz@gISk}#kwmG9~lm<#O^)Y6yudIwj2U zy(XL0p)SQ+R~4~A;%rrhW1};Ikd?zdhR?`FFNJw zi_tbBnq7U-TdTxeUbm2`c?7-A+YvVJ9D4^dv*}d}I)h)jeTz(ntMYCx-W^G#H&Qe+TM(|R^z|B%-Ugp< zO;!pBUQS>Xq_OfC45t)q)24mzs$N=4(mU+boN2Hjc;#Phn)d3v<){8gghUGn6I`k6a7y`(5MUpPQ!N}L(&fyiq>dj+razqnv!-?uq` zkV_mh#ot(4lfE*k*%6C|pJ-z_)qgJ4woI z@PxUwfqwLlb+)luCc3`l1S|y-hMJ}NH`JlrOub43Z3`n>g_A};Wj$?AyX|bsvXIUf0lFvibB3B_1$@XCs~><1*~B*0_D$d zmj%r6o9dRhrwmV0t~LtHXmOLkOqc|^jTI{R#zLEz>P7^`sj<}Fbu7e~3UZ`%sm_~H zWjbrAi>ACSpEd7F)@F}2W#*{?^g<*kiF&N}#P^ViFRu2m#c`nE_|jdEFc%0x!ZGMb zlq-4tvY(M)_*UoT?PDF<+HYa?2t<Ybu?(vPXN3xbydgyKlzmYD=daI?oclFxlo7I;AXH7v;p5I@f5%%5gW5?-+#d}p2 z;31fD`!e@Z+R_@jLz!gYQu#$yK8j*-Cc(3U0cv6XdaFS16k zX%&sgQF%U(Fs$<|zmIx;MEnmCPfKuaI|lg9{xG5Hl*V0<`m|Dv^|}W!4J0~CaKDWj zM1wyIO9=Pvu{_m|H$iy#>ZT%F&U7O(C-z&BA05)Z6Z08aMS`UK#&UuTGeypbqS}le z-P{o3of`YPa%WZWkZSO5IU)_3F%$N*_H5!Wkb~#DYGN&UFzIAGhr!kb7Doe~zoTV5Bg-lqA$=3oy$ z{ol^kHUAA8<`yR0_q(^2PqUXvt2%*w4BWiIBkol-L!C+`Nf3e^dc33~pmA_^)ls5+ zK_@}{`>f)iD-45M>}FvIsx}Nyz#1sT0TDf4KJVvp!^g=*ml>Aa=*u#Ht*we01Dv-l z8t}i?o+PjlOYY9q`X&W7Ys5?6bAobL?A&u1^c!pIP0g=bQ5^MkAMBK_U$jb;KS^|4 zCGC71B{Yl#FuMor#+sVF`?R`h%Veh&^`fjiVZ6MY-&NW0c+UgVF|4}z?R;jQfvP$q zh#+T$lkEuj(gU1&_@2yJ2q8~A1%6CHW8vpN+%HQNXB`%x~>c*|YC_P`%Aq-9<^NbcLAYYHE>ty1RlXST0r4-R{G-_$au}k=}Zn$37 zj%Pjv%yF~RwcGZ?{aB35&+est8`o(i(jOCh)3N@I!|p|m(;QR#cB!4I%2n+_Zy#Kq zy3i-4?T~+om^uf5=CGQKk%>%>qZ$J3+|+2&iljTo`hQFJF1Gn5t3nh82Z(7q8G5MuaPC+ZhnacuPwj~l@y|cMAPj`cM$?;epX2n7 z5FK!zG+w?KuBLF}m)>I}>}ZOaUERMK*!Yl}&+@kemyrco532N^JqSXYepG+SgHstJ zJ;xsO)HRMldWCeIMwJY@(Q3tx$$ck=Q&IUe&O^%Re`cLB#MM(FkD{zn@0Y({GM)Sl(UH*1BZ4CCm-C!S@G4ooyc=#mv z{;0tXDL2qFgSis*zvbDfja@ytS$?eKi*8H-Y;^cMr@x1sAhRyoW zS2s%-7QJ@Vrz~E&`s@NRtotba^4C_G_gT8zW{Q??I66&O+g=>|;N56^fk~qLeg%^d zo9UL_fbr^N!Ghh#JjpD{Yc9NO7}v9U66O2iA6_aTf=lJnejNl?>~JY@Kd%1XqFeN? zsdoJ66Tx0B=qwv~*5V9$=t~vMsg?Bt$W7!j3Wpg%O~krz)!Q!%O97^>(Y1r+{6&3Y zW~7oTXJbgurK0X(%X}uVCrOlBOLv~$Og*V{D&&jXRszgCkR^4Z^@#+(`Kyam34!}^xvU{+<8wY2W3?vDfD`~)`rrlGU5AP;eHh1|ouZ}qn+Cu`+-T5u)$(T*}qhB`R^ z|0C+V zH^2Aw`Tl;tKYBdWqsQmb=ic`k&+&S_C_%HjQIc7yT=WQqdaO4xafe&SUfmJJBDFz( z?Xe%KSU2l4bo9E|xwoVV3BL=!XRD}%UuC$eYg_AR&2#UOW~Y8T=XH!mls1q5HZLPc z&;oTQSA$>vRx+l8ojus!VLb;%E~7rU{-Zws_Pn|%TyjnAtqtYY7vd?3@2^7%iQqWE9Ro_9lRGUt1MFNm9rR)coa4UX zHXc6{oFec=l<20et-F-;wh!H*7g7w%9BvWUsxk&iv9*yiQ|V29ib{BV^S_z;92@aQSx|AY zL_+2tH3RO*i$zA}%9?l;GL;6(>yU`cXxkej2e0aZ6OGsOfD4B-K_3SY3Xc)r1In+9 zXW|@(tnK)sr;npxH^96|+fAJ>xuL+-wON&w2YCgyE6_8EoBl53;>7!U(UFYwl%J3|1vvP&?5oL zg?NP;LsX~WDcb4*LPSQDXd;kI`3{;V$a!%*7GhmK{*@FE6UXu=-Papqw)c|8{dQJ9 zg3H6iyCcM&31)^6Zv~#8`;4QDUPQX>l+U{SAdq*uV@I~CmK!Q^_~^uhquB3j7a#}r z+i#UBrnKO}&&GcF7Ul|iwrew>x8emHYW-^AN+}Jy2&01*gT68QDqCB_wr`Y>NbP`m z>J?GLMB5YzFo)*UQ**5gqO$p8$arIOsm$rtFQ8RLDe{#~JfE7~tPB$|h;C&q8m{X4 zf&cURpPamnQC9N%SW{1BS5n$2D^OD(dj)Lexh&ZzD+88Bxgjn7gq3S>_$F(Wl&+i? zb+MsYaJ|}gzGxUZn$1~!hXGLK4lhLgkzoe|&x|n9FY#(vv^Zb~!2kUy5UNta6uk0i z#Z&1@B{MFXR95~6rkFo8?>6|ew4EAUu;8X0)71o|G<77tSR9X}Pq;27mdv+Ok8c$J zMaVJG2xFksmO;6mlVP@#SDm^++xDlEkMwl*_F%K|*MQ=Ni(@XY@Rzs& z+g2RwcM$x0fH*Kh4FM`tag(GCQAfT4eF|8DXuU*<@^u8qQD8y8`A#!jz42`zC#SGfJnCF)vv-kOuHkD~JE7gQRcoVqI*Gpk00$j^}B9-rM zmMS`qt%tt`EpBbEvwMm2128+y2okbEWvv0i!!U~`W80whr7>^#gQ%70z#FJ!5n!w| zHJ5?LFc8>n92IXOc-@-pGxft2yW5aymZ4bcC8E^9n&8~vA`X7b#F9|cY)aNd^)qi+j9*}IE>#_4 zZsUc1=7$1?%Igo2JKB zxKT935zFOHrYH&aT&}^Yo%n<&;b73&7jUD4=O7OdiQ0I5&Tpb%WP}oIdqQb2m-SrB z{5Jd7Q%ZsUtIwI~#j+2*zUkKDRV?cdQ=YVBxMgmZIJ=?w$nmD#aqKog@g!8bc+n8(}wrb9z*Uy560EgiI|yHwNJeI5Y| zhG;5M7Fd{fbjtnFjEiji+a?NIEx9f@0MPB_cnvSlEzKnvZ)QvJ6J!tXzdhtKhNmM} zKWU1QS4M+%ccg>RnmXw|trlQ$+Em7JNhXFx+sWTl<4cZ@;CYGl2YTbv{=ex1yFi zj4TCmRnt=Au;dSh=UB;eg&-&^asMe&TGTHe;#{^OY0RWyEF0TT9mP1<8K43)TxdPq z%31%E)gB&cd(gJt@Y}WKmA%gaIro*u);_Vyoa@%*Y#OH+j@p?2{o%cdt59Wf$>Bf~Y)ym5Q5y16 zsGz{qn6X+H@>@F<-g8FoIV)jUk8 z9x0v>cz?&k5ViHrt_J!!p_$ zlgok#zV5MQ5Xm-3@mQC;aU)uCzUQ$(vn|o}IG-X+p3os`!pQaNYG;n@mI)~5XBx2lFHlctsD{H)SUj11hnfs+8(zuyVJXE%MUVA-7`$5;T1nG%V(lqeH+a$KteBk9kI~+Qm zT!_J!5RM9`y^t0GHo=#mfOCpapqaN3Z;{1E6c@@m*$iIDe+64gi$h`2Qc%yVbbn`# z`--2lDps;2X2HLo5lzM4v0BRey!;W;84CxYTRuB5BxdCL1Jmj+)GtX?~i@6GfKzBq&6294zA|S#La2OQR3k6xa(q0 zs)HSQSO2}Th4xsS@JrZ5+a3f=gyXn~OVq)p6scr0{H5k>-D0^O$Yvi6d84ciROwRP zZIREeE~?Urtthxet!%Q?nu!;!CCTr&rp30TW`EujuUxNQo2g&*(0J-@;UlPui_M9` zP0h&GD5;O$6o5Y+Ff$+j(1CQS>70|dY!Qd@Sx33hHLPt*`c5$zBY7{WZi+o|$Jhy? zZ!-_yQY=pW=pF3)#!~ByX2r$Q;b;+BqUqu(>Mzu>PcL-wtKfV0z^q@yj4Djza|U zyzU5kAf&yS`6=&WJ|cB)KM{8NQ(o3AB;uKg(5EAUS)$@ksTUFx=`=z>kS zsIAmr?nQ$KaDDY|oR`w=-9;D|o!L zp}Mz2UcYR-+;zSc)wtK#rctGJ%E^^wN4X?PrS}e`U1KuMuC6WUWguL)lX}npQ4xm0 zU29ZaE$Hl+G<2x0)kXMdJ+5K9!durV7kaDoM^RUt;hq+-k=2Xi#o~&uqj?HO`rt-% z8?B~ow`F9E19=$UoD?vKMK0&HLYm!M$?gAW$H0)T<*hi~>cQF(qn+N~eb+xC1T)ft zZLe?JGLsTfVnx;Uw{(h1N--|t2|wcUdy<5A#G#8OW){isKwCW!vG`czX`OR*eGYC# zs3}2_e!dUZ&K|d^!c<3lUj?i}U~ONX8>>m$$|D712bgFwc&Dx3xW#8NlPmlbHsV+K zEwriF^cF--F7wCFWCgBU5lkt}TsCks!m&c7+0ypMRj&d5#X+kxjttqkgS_1h9=+VS>p9s(*i!r-aQ1F z$CAdE(|~~%E63LhO7gIzO?|z+0It4%ci=ckp>qFGS<5i;YIS|adTwv>juC+kl!D#y zT@0F)10BKS^_5tqWumcY9`B5T1D}yWhmel!6LB#Uccl2#D%i%~eJ(!=&kq7sAQXJC zWFSs8>$1E=nRxgSr-VRepA%J=P?wu8PVP|HxvM;77!SQr`$73F4jy@@@Jn|24Po9d zweHG>qS5XAwC)1D93k`f4v#`~91~{yxDpGtK8MUt56Sb*lhCpLYK2uzx z5icnSWU#)P5>1I;-?PfoSr@+TiG0T1WdiT9T5~96{x)tQR0Q|9%UJ}BWSgtgO`6mt zi;|cY*Dac*j^Zt0?X_Uf*m^TMGxMefUh2tiYy+=PQ3G#9<&t^Q$S+rXE|aOhs4B&7 zR+Ny9ht?Yu%MG;4hg%(omG71rfV%#mj{$6i$N<&cS<@fXe%1UvsmK@AQ~1>K?N^2gzfF@e!4OK!-gPZ25Y^jw9ySg_d3 z>+^EX(v%UQLtt_knOHL{sXAI>vb@qic&wmM`CFO#rox1l(A&MVlF}w;+{y?psPe>~ zW3_SeUc>tCpqJO-rYMssrcx|&F08zs@bZS9ptdY{#UkY*(MaGKeKd&Gy%mJy4LaEq zNG}p31s5|_OjyyX_6;uiS;aq(=kzYjr|*@@P*_xuR5Nz0y{H>Qz}q!KBQ9PeXZofe zsriYP?vqV|=-Agtl9q=v$LZfFb$qCbBxbEn7!T^Yqqdl9%i?zq>&i;rY_sdRPu))B zrV}88?cL6P9AvMi#(jatL<#`qr1P*a7zc_9s`UbL??v8Q7z-->OWfScyZ^67UxliJ zw#-%;G|eEW@pL%U6%;di%X0M5f|Czb=GU!N;lrV`sw5@n%|(&YUL}ht2DR z^l6>a7_Sx~t8|Ov+D%}4RYm#R5!54e8J;y(hyYx^v|Xzxl%9XXzUy7a^dEM93S+*K zlG0l4JoV|8Ft=xhNQ0+HJMFaPIpGPJx*HZ2ABQD%*lImJR9_F8iGQ!)0kJ*PhvyCV zcRUa?BCY?|nTdkVEMpn8Jo0+t+_;Ar`5il1c5Q|d zT?P_6dMrBsdATT5RT(t2bU=JJx&(iP&&i5@7A$@}VX%tpl}g<#Zno_OLHPA*$H#_e zrPmW8D!&+i6s1vq@$%9&ayBXWW^0?fK2O`dH!r&i@!^w?Y`&&ic4ZBJhiH`iW|8G()~!Pux40f5-_f0cQ>G!TmBp6DY*Jr`X16 zr{&K6Ej41#)p)SMDB-O^5fpO1g-I*YZ|n3?PH=@%dkD8uU?f+OV8WppL!h#T?mRVB zh~ssr!)7=hrz<52WlESe9rK9st=BOoXfx`pt`;zPUNZ!Pc2_CCGF-Gf66~C-m&lj0 zD{?TS?iGviW#IUoZ+GuTbXR~=uTv*vExl1byxMXlmrYs^^Cw@yCtzu*M4fqqbNAEr zPqtJEs`F`a9$2(=$cU?`3@a9Wo}V;@@*hU!owrSp zHF?gB6&yZV_KtnA5_X7L%ylF<#Jb`^jN{Bc2kpLJJI?p{bl^VwF+Ld&5O^cY*~cZ_ zDRaQF?AREFg{acGJT8vWf40#F$5vGc^l&-{_0kf4-eu1)Y`6j`1wn0a=i?(3ZIPH0 z07wZuclJLOy4>CWeA`>&nu2VUn9Mt+M*wvh+E6IuGH?7onV)U=pq>?`lz@?t#IbDj zbYj4AF#As1@hfGt_g@?GWUv*`_BUIa`WGe}v99C}zxQ{VtWzBG{rI-pd?|NT+TUrh zyQ%WXmBUWAYbb(`h`Ctfmh)m@SHBwI&_2PV&YVp5kOmj+CWi5xpdCxP^tHUEL4#Pq zor&-LDYSfja>YNCg()GTBI{NWJn|Ai(;MquFYc1i(ur%6rIYDLUaECJ2jMPyc;hKT z#a`ZIOQ#WI9{;iCMwwqf+dco-5`wZnrkybKc8mSPG|NmtI8W2GVey`=ikip7UgMjc z)Jv~yY2y9j3?o=$K^-;$JEr5g@RwhKSY`pQ{#270FeN?U!dZC&_<+mPv5>)_-VR$D z)KuWxy3v#44ou5wX32Kqc{^)ujWU3vzsIQ+u$i)$<3?BRkfSsSMF_8@YZ6niqE5N4 zY3-q3#<#tG#!k&(nb*_%mm7HB+BqJm4EZ{|uQT(oc~jriDc4Q+{lle(#l}v#dclJn zEPSTmg%KjL-My5r#{_@RL2{G1R=?!cN9YJ4lmx}9Tz+0VHe*DauXOrXvU0G6v%pP2 zl3b^1?v0yDSD&?}Ykik_wp~*%sJBeo!Sg;vj_-g}!oL30enGzz(p;n;D|I-Ao~4VcFXZ~>RU3}-1anYqVc~>FpV3Y4dP6s{1>#7j4-VB;}$l= z$E8y-`5+6r@c2QMgN~M)@9*6TLYe($IB&-;hR_+K@C84L2 zExz>VqY6)0IL|N5cAM{_3m2JE-!`+JF&NK9j<+mK_sZWJ^mI~~9$d-hZt@OJC&>Q| zj++vR_39zLK8x+DRL0!MJvO~g^%Z4vME2WY2*^HLW6^ITM(*K z>&Czh*`Dc^gEIEhANb6U+RfgUUVBmhVSFIa!4exchKl-Rdx<<(>Ft!`sbveVzZgqI zqWoTPnr`!|

bEIr(#WKMaUDi(GX?v~S^Rz#YV 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%0AaIx7XOiRog5OzKyk=) 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

3V}Q>%5o_*! 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 M!TWbt?TSA45AOSH3IG5A diff --git a/docs/iris/src/_static/jquery.cycle.all.latest.js b/docs/iris/src/_static/jquery.cycle.all.latest.js deleted file mode 100644 index 75d7ab98f8..0000000000 --- a/docs/iris/src/_static/jquery.cycle.all.latest.js +++ /dev/null @@ -1,1331 +0,0 @@ -/*! - * jQuery Cycle Plugin (with Transition Definitions) - * Examples and documentation at: http://jquery.malsup.com/cycle/ - * Copyright (c) 2007-2010 M. Alsup - * Version: 2.88 (08-JUN-2010) - * Dual licensed under the MIT and GPL licenses. - * http://jquery.malsup.com/license.html - * Requires: jQuery v1.2.6 or later - */ -;(function($) { - -var ver = '2.88'; - -// if $.support is not defined (pre jQuery 1.3) add what I need -if ($.support == undefined) { - $.support = { - opacity: !($.browser.msie) - }; -} - -function debug(s) { - if ($.fn.cycle.debug) - log(s); -} -function log() { - if (window.console && window.console.log) - window.console.log('[cycle] ' + Array.prototype.join.call(arguments,' ')); -}; - -// the options arg can be... -// a number - indicates an immediate transition should occur to the given slide index -// a string - 'pause', 'resume', 'toggle', 'next', 'prev', 'stop', 'destroy' or the name of a transition effect (ie, 'fade', 'zoom', etc) -// an object - properties to control the slideshow -// -// the arg2 arg can be... -// the name of an fx (only used in conjunction with a numeric value for 'options') -// the value true (only used in first arg == 'resume') and indicates -// that the resume should occur immediately (not wait for next timeout) - -$.fn.cycle = function(options, arg2) { - var o = { s: this.selector, c: this.context }; - - // in 1.3+ we can fix mistakes with the ready state - if (this.length === 0 && options != 'stop') { - if (!$.isReady && o.s) { - log('DOM not ready, queuing slideshow'); - $(function() { - $(o.s,o.c).cycle(options,arg2); - }); - return this; - } - // is your DOM ready? http://docs.jquery.com/Tutorials:Introducing_$(document).ready() - log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)')); - return this; - } - - // iterate the matched nodeset - return this.each(function() { - var opts = handleArguments(this, options, arg2); - if (opts === false) - return; - - opts.updateActivePagerLink = opts.updateActivePagerLink || $.fn.cycle.updateActivePagerLink; - - // stop existing slideshow for this container (if there is one) - if (this.cycleTimeout) - clearTimeout(this.cycleTimeout); - this.cycleTimeout = this.cyclePause = 0; - - var $cont = $(this); - var $slides = opts.slideExpr ? $(opts.slideExpr, this) : $cont.children(); - var els = $slides.get(); - if (els.length < 2) { - log('terminating; too few slides: ' + els.length); - return; - } - - var opts2 = buildOptions($cont, $slides, els, opts, o); - if (opts2 === false) - return; - - var startTime = opts2.continuous ? 10 : getTimeout(els[opts2.currSlide], els[opts2.nextSlide], opts2, !opts2.rev); - - // if it's an auto slideshow, kick it off - if (startTime) { - startTime += (opts2.delay || 0); - if (startTime < 10) - startTime = 10; - debug('first timeout: ' + startTime); - this.cycleTimeout = setTimeout(function(){go(els,opts2,0,(!opts2.rev && !opts.backwards))}, startTime); - } - }); -}; - -// process the args that were passed to the plugin fn -function handleArguments(cont, options, arg2) { - if (cont.cycleStop == undefined) - cont.cycleStop = 0; - if (options === undefined || options === null) - options = {}; - if (options.constructor == String) { - switch(options) { - case 'destroy': - case 'stop': - var opts = $(cont).data('cycle.opts'); - if (!opts) - return false; - cont.cycleStop++; // callbacks look for change - if (cont.cycleTimeout) - clearTimeout(cont.cycleTimeout); - cont.cycleTimeout = 0; - $(cont).removeData('cycle.opts'); - if (options == 'destroy') - destroy(opts); - return false; - case 'toggle': - cont.cyclePause = (cont.cyclePause === 1) ? 0 : 1; - checkInstantResume(cont.cyclePause, arg2, cont); - return false; - case 'pause': - cont.cyclePause = 1; - return false; - case 'resume': - cont.cyclePause = 0; - checkInstantResume(false, arg2, cont); - return false; - case 'prev': - case 'next': - var opts = $(cont).data('cycle.opts'); - if (!opts) { - log('options not found, "prev/next" ignored'); - return false; - } - $.fn.cycle[options](opts); - return false; - default: - options = { fx: options }; - }; - return options; - } - else if (options.constructor == Number) { - // go to the requested slide - var num = options; - options = $(cont).data('cycle.opts'); - if (!options) { - log('options not found, can not advance slide'); - return false; - } - if (num < 0 || num >= options.elements.length) { - log('invalid slide index: ' + num); - return false; - } - options.nextSlide = num; - if (cont.cycleTimeout) { - clearTimeout(cont.cycleTimeout); - cont.cycleTimeout = 0; - } - if (typeof arg2 == 'string') - options.oneTimeFx = arg2; - go(options.elements, options, 1, num >= options.currSlide); - return false; - } - return options; - - function checkInstantResume(isPaused, arg2, cont) { - if (!isPaused && arg2 === true) { // resume now! - var options = $(cont).data('cycle.opts'); - if (!options) { - log('options not found, can not resume'); - return false; - } - if (cont.cycleTimeout) { - clearTimeout(cont.cycleTimeout); - cont.cycleTimeout = 0; - } - go(options.elements, options, 1, (!opts.rev && !opts.backwards)); - } - } -}; - -function removeFilter(el, opts) { - if (!$.support.opacity && opts.cleartype && el.style.filter) { - try { el.style.removeAttribute('filter'); } - catch(smother) {} // handle old opera versions - } -}; - -// unbind event handlers -function destroy(opts) { - if (opts.next) - $(opts.next).unbind(opts.prevNextEvent); - if (opts.prev) - $(opts.prev).unbind(opts.prevNextEvent); - - if (opts.pager || opts.pagerAnchorBuilder) - $.each(opts.pagerAnchors || [], function() { - this.unbind().remove(); - }); - opts.pagerAnchors = null; - if (opts.destroy) // callback - opts.destroy(opts); -}; - -// one-time initialization -function buildOptions($cont, $slides, els, options, o) { - // support metadata plugin (v1.0 and v2.0) - var opts = $.extend({}, $.fn.cycle.defaults, options || {}, $.metadata ? $cont.metadata() : $.meta ? $cont.data() : {}); - if (opts.autostop) - opts.countdown = opts.autostopCount || els.length; - - var cont = $cont[0]; - $cont.data('cycle.opts', opts); - opts.$cont = $cont; - opts.stopCount = cont.cycleStop; - opts.elements = els; - opts.before = opts.before ? [opts.before] : []; - opts.after = opts.after ? [opts.after] : []; - opts.after.unshift(function(){ opts.busy=0; }); - - // push some after callbacks - if (!$.support.opacity && opts.cleartype) - opts.after.push(function() { removeFilter(this, opts); }); - if (opts.continuous) - opts.after.push(function() { go(els,opts,0,(!opts.rev && !opts.backwards)); }); - - saveOriginalOpts(opts); - - // clearType corrections - if (!$.support.opacity && opts.cleartype && !opts.cleartypeNoBg) - clearTypeFix($slides); - - // container requires non-static position so that slides can be position within - if ($cont.css('position') == 'static') - $cont.css('position', 'relative'); - if (opts.width) - $cont.width(opts.width); - if (opts.height && opts.height != 'auto') - $cont.height(opts.height); - - if (opts.startingSlide) - opts.startingSlide = parseInt(opts.startingSlide); - else if (opts.backwards) - opts.startingSlide = els.length - 1; - - // if random, mix up the slide array - if (opts.random) { - opts.randomMap = []; - for (var i = 0; i < els.length; i++) - opts.randomMap.push(i); - opts.randomMap.sort(function(a,b) {return Math.random() - 0.5;}); - opts.randomIndex = 1; - opts.startingSlide = opts.randomMap[1]; - } - else if (opts.startingSlide >= els.length) - opts.startingSlide = 0; // catch bogus input - opts.currSlide = opts.startingSlide || 0; - var first = opts.startingSlide; - - // set position and zIndex on all the slides - $slides.css({position: 'absolute', top:0, left:0}).hide().each(function(i) { - var z; - if (opts.backwards) - z = first ? i <= first ? els.length + (i-first) : first-i : els.length-i; - else - z = first ? i >= first ? els.length - (i-first) : first-i : els.length-i; - $(this).css('z-index', z) - }); - - // make sure first slide is visible - $(els[first]).css('opacity',1).show(); // opacity bit needed to handle restart use case - removeFilter(els[first], opts); - - // stretch slides - if (opts.fit && opts.width) - $slides.width(opts.width); - if (opts.fit && opts.height && opts.height != 'auto') - $slides.height(opts.height); - - // stretch container - var reshape = opts.containerResize && !$cont.innerHeight(); - if (reshape) { // do this only if container has no size http://tinyurl.com/da2oa9 - var maxw = 0, maxh = 0; - for(var j=0; j < els.length; j++) { - var $e = $(els[j]), e = $e[0], w = $e.outerWidth(), h = $e.outerHeight(); - if (!w) w = e.offsetWidth || e.width || $e.attr('width') - if (!h) h = e.offsetHeight || e.height || $e.attr('height'); - maxw = w > maxw ? w : maxw; - maxh = h > maxh ? h : maxh; - } - if (maxw > 0 && maxh > 0) - $cont.css({width:maxw+'px',height:maxh+'px'}); - } - - if (opts.pause) - $cont.hover(function(){this.cyclePause++;},function(){this.cyclePause--;}); - - if (supportMultiTransitions(opts) === false) - return false; - - // apparently a lot of people use image slideshows without height/width attributes on the images. - // Cycle 2.50+ requires the sizing info for every slide; this block tries to deal with that. - var requeue = false; - options.requeueAttempts = options.requeueAttempts || 0; - $slides.each(function() { - // try to get height/width of each slide - var $el = $(this); - this.cycleH = (opts.fit && opts.height) ? opts.height : ($el.height() || this.offsetHeight || this.height || $el.attr('height') || 0); - this.cycleW = (opts.fit && opts.width) ? opts.width : ($el.width() || this.offsetWidth || this.width || $el.attr('width') || 0); - - if ( $el.is('img') ) { - // sigh.. sniffing, hacking, shrugging... this crappy hack tries to account for what browsers do when - // an image is being downloaded and the markup did not include sizing info (height/width attributes); - // there seems to be some "default" sizes used in this situation - var loadingIE = ($.browser.msie && this.cycleW == 28 && this.cycleH == 30 && !this.complete); - var loadingFF = ($.browser.mozilla && this.cycleW == 34 && this.cycleH == 19 && !this.complete); - var loadingOp = ($.browser.opera && ((this.cycleW == 42 && this.cycleH == 19) || (this.cycleW == 37 && this.cycleH == 17)) && !this.complete); - var loadingOther = (this.cycleH == 0 && this.cycleW == 0 && !this.complete); - // don't requeue for images that are still loading but have a valid size - if (loadingIE || loadingFF || loadingOp || loadingOther) { - if (o.s && opts.requeueOnImageNotLoaded && ++options.requeueAttempts < 100) { // track retry count so we don't loop forever - log(options.requeueAttempts,' - img slide not loaded, requeuing slideshow: ', this.src, this.cycleW, this.cycleH); - setTimeout(function() {$(o.s,o.c).cycle(options)}, opts.requeueTimeout); - requeue = true; - return false; // break each loop - } - else { - log('could not determine size of image: '+this.src, this.cycleW, this.cycleH); - } - } - } - return true; - }); - - if (requeue) - return false; - - opts.cssBefore = opts.cssBefore || {}; - opts.animIn = opts.animIn || {}; - opts.animOut = opts.animOut || {}; - - $slides.not(':eq('+first+')').css(opts.cssBefore); - if (opts.cssFirst) - $($slides[first]).css(opts.cssFirst); - - if (opts.timeout) { - opts.timeout = parseInt(opts.timeout); - // ensure that timeout and speed settings are sane - if (opts.speed.constructor == String) - opts.speed = $.fx.speeds[opts.speed] || parseInt(opts.speed); - if (!opts.sync) - opts.speed = opts.speed / 2; - - var buffer = opts.fx == 'shuffle' ? 500 : 250; - while((opts.timeout - opts.speed) < buffer) // sanitize timeout - opts.timeout += opts.speed; - } - if (opts.easing) - opts.easeIn = opts.easeOut = opts.easing; - if (!opts.speedIn) - opts.speedIn = opts.speed; - if (!opts.speedOut) - opts.speedOut = opts.speed; - - opts.slideCount = els.length; - opts.currSlide = opts.lastSlide = first; - if (opts.random) { - if (++opts.randomIndex == els.length) - opts.randomIndex = 0; - opts.nextSlide = opts.randomMap[opts.randomIndex]; - } - else if (opts.backwards) - opts.nextSlide = opts.startingSlide == 0 ? (els.length-1) : opts.startingSlide-1; - else - opts.nextSlide = opts.startingSlide >= (els.length-1) ? 0 : opts.startingSlide+1; - - // run transition init fn - if (!opts.multiFx) { - var init = $.fn.cycle.transitions[opts.fx]; - if ($.isFunction(init)) - init($cont, $slides, opts); - else if (opts.fx != 'custom' && !opts.multiFx) { - log('unknown transition: ' + opts.fx,'; slideshow terminating'); - return false; - } - } - - // fire artificial events - var e0 = $slides[first]; - if (opts.before.length) - opts.before[0].apply(e0, [e0, e0, opts, true]); - if (opts.after.length > 1) - opts.after[1].apply(e0, [e0, e0, opts, true]); - - if (opts.next) - $(opts.next).bind(opts.prevNextEvent,function(){return advance(opts,opts.rev?-1:1)}); - if (opts.prev) - $(opts.prev).bind(opts.prevNextEvent,function(){return advance(opts,opts.rev?1:-1)}); - if (opts.pager || opts.pagerAnchorBuilder) - buildPager(els,opts); - - exposeAddSlide(opts, els); - - return opts; -}; - -// save off original opts so we can restore after clearing state -function saveOriginalOpts(opts) { - opts.original = { before: [], after: [] }; - opts.original.cssBefore = $.extend({}, opts.cssBefore); - opts.original.cssAfter = $.extend({}, opts.cssAfter); - opts.original.animIn = $.extend({}, opts.animIn); - opts.original.animOut = $.extend({}, opts.animOut); - $.each(opts.before, function() { opts.original.before.push(this); }); - $.each(opts.after, function() { opts.original.after.push(this); }); -}; - -function supportMultiTransitions(opts) { - var i, tx, txs = $.fn.cycle.transitions; - // look for multiple effects - if (opts.fx.indexOf(',') > 0) { - opts.multiFx = true; - opts.fxs = opts.fx.replace(/\s*/g,'').split(','); - // discard any bogus effect names - for (i=0; i < opts.fxs.length; i++) { - var fx = opts.fxs[i]; - tx = txs[fx]; - if (!tx || !txs.hasOwnProperty(fx) || !$.isFunction(tx)) { - log('discarding unknown transition: ',fx); - opts.fxs.splice(i,1); - i--; - } - } - // if we have an empty list then we threw everything away! - if (!opts.fxs.length) { - log('No valid transitions named; slideshow terminating.'); - return false; - } - } - else if (opts.fx == 'all') { // auto-gen the list of transitions - opts.multiFx = true; - opts.fxs = []; - for (p in txs) { - tx = txs[p]; - if (txs.hasOwnProperty(p) && $.isFunction(tx)) - opts.fxs.push(p); - } - } - if (opts.multiFx && opts.randomizeEffects) { - // munge the fxs array to make effect selection random - var r1 = Math.floor(Math.random() * 20) + 30; - for (i = 0; i < r1; i++) { - var r2 = Math.floor(Math.random() * opts.fxs.length); - opts.fxs.push(opts.fxs.splice(r2,1)[0]); - } - debug('randomized fx sequence: ',opts.fxs); - } - return true; -}; - -// provide a mechanism for adding slides after the slideshow has started -function exposeAddSlide(opts, els) { - opts.addSlide = function(newSlide, prepend) { - var $s = $(newSlide), s = $s[0]; - if (!opts.autostopCount) - opts.countdown++; - els[prepend?'unshift':'push'](s); - if (opts.els) - opts.els[prepend?'unshift':'push'](s); // shuffle needs this - opts.slideCount = els.length; - - $s.css('position','absolute'); - $s[prepend?'prependTo':'appendTo'](opts.$cont); - - if (prepend) { - opts.currSlide++; - opts.nextSlide++; - } - - if (!$.support.opacity && opts.cleartype && !opts.cleartypeNoBg) - clearTypeFix($s); - - if (opts.fit && opts.width) - $s.width(opts.width); - if (opts.fit && opts.height && opts.height != 'auto') - $slides.height(opts.height); - s.cycleH = (opts.fit && opts.height) ? opts.height : $s.height(); - s.cycleW = (opts.fit && opts.width) ? opts.width : $s.width(); - - $s.css(opts.cssBefore); - - if (opts.pager || opts.pagerAnchorBuilder) - $.fn.cycle.createPagerAnchor(els.length-1, s, $(opts.pager), els, opts); - - if ($.isFunction(opts.onAddSlide)) - opts.onAddSlide($s); - else - $s.hide(); // default behavior - }; -} - -// reset internal state; we do this on every pass in order to support multiple effects -$.fn.cycle.resetState = function(opts, fx) { - fx = fx || opts.fx; - opts.before = []; opts.after = []; - opts.cssBefore = $.extend({}, opts.original.cssBefore); - opts.cssAfter = $.extend({}, opts.original.cssAfter); - opts.animIn = $.extend({}, opts.original.animIn); - opts.animOut = $.extend({}, opts.original.animOut); - opts.fxFn = null; - $.each(opts.original.before, function() { opts.before.push(this); }); - $.each(opts.original.after, function() { opts.after.push(this); }); - - // re-init - var init = $.fn.cycle.transitions[fx]; - if ($.isFunction(init)) - init(opts.$cont, $(opts.elements), opts); -}; - -// this is the main engine fn, it handles the timeouts, callbacks and slide index mgmt -function go(els, opts, manual, fwd) { - // opts.busy is true if we're in the middle of an animation - if (manual && opts.busy && opts.manualTrump) { - // let manual transitions requests trump active ones - debug('manualTrump in go(), stopping active transition'); - $(els).stop(true,true); - opts.busy = false; - } - // don't begin another timeout-based transition if there is one active - if (opts.busy) { - debug('transition active, ignoring new tx request'); - return; - } - - var p = opts.$cont[0], curr = els[opts.currSlide], next = els[opts.nextSlide]; - - // stop cycling if we have an outstanding stop request - if (p.cycleStop != opts.stopCount || p.cycleTimeout === 0 && !manual) - return; - - // check to see if we should stop cycling based on autostop options - if (!manual && !p.cyclePause && !opts.bounce && - ((opts.autostop && (--opts.countdown <= 0)) || - (opts.nowrap && !opts.random && opts.nextSlide < opts.currSlide))) { - if (opts.end) - opts.end(opts); - return; - } - - // if slideshow is paused, only transition on a manual trigger - var changed = false; - if ((manual || !p.cyclePause) && (opts.nextSlide != opts.currSlide)) { - changed = true; - var fx = opts.fx; - // keep trying to get the slide size if we don't have it yet - curr.cycleH = curr.cycleH || $(curr).height(); - curr.cycleW = curr.cycleW || $(curr).width(); - next.cycleH = next.cycleH || $(next).height(); - next.cycleW = next.cycleW || $(next).width(); - - // support multiple transition types - if (opts.multiFx) { - if (opts.lastFx == undefined || ++opts.lastFx >= opts.fxs.length) - opts.lastFx = 0; - fx = opts.fxs[opts.lastFx]; - opts.currFx = fx; - } - - // one-time fx overrides apply to: $('div').cycle(3,'zoom'); - if (opts.oneTimeFx) { - fx = opts.oneTimeFx; - opts.oneTimeFx = null; - } - - $.fn.cycle.resetState(opts, fx); - - // run the before callbacks - if (opts.before.length) - $.each(opts.before, function(i,o) { - if (p.cycleStop != opts.stopCount) return; - o.apply(next, [curr, next, opts, fwd]); - }); - - // stage the after callacks - var after = function() { - $.each(opts.after, function(i,o) { - if (p.cycleStop != opts.stopCount) return; - o.apply(next, [curr, next, opts, fwd]); - }); - }; - - debug('tx firing; currSlide: ' + opts.currSlide + '; nextSlide: ' + opts.nextSlide); - - // get ready to perform the transition - opts.busy = 1; - if (opts.fxFn) // fx function provided? - opts.fxFn(curr, next, opts, after, fwd, manual && opts.fastOnEvent); - else if ($.isFunction($.fn.cycle[opts.fx])) // fx plugin ? - $.fn.cycle[opts.fx](curr, next, opts, after, fwd, manual && opts.fastOnEvent); - else - $.fn.cycle.custom(curr, next, opts, after, fwd, manual && opts.fastOnEvent); - } - - if (changed || opts.nextSlide == opts.currSlide) { - // calculate the next slide - opts.lastSlide = opts.currSlide; - if (opts.random) { - opts.currSlide = opts.nextSlide; - if (++opts.randomIndex == els.length) - opts.randomIndex = 0; - opts.nextSlide = opts.randomMap[opts.randomIndex]; - if (opts.nextSlide == opts.currSlide) - opts.nextSlide = (opts.currSlide == opts.slideCount - 1) ? 0 : opts.currSlide + 1; - } - else if (opts.backwards) { - var roll = (opts.nextSlide - 1) < 0; - if (roll && opts.bounce) { - opts.backwards = !opts.backwards; - opts.nextSlide = 1; - opts.currSlide = 0; - } - else { - opts.nextSlide = roll ? (els.length-1) : opts.nextSlide-1; - opts.currSlide = roll ? 0 : opts.nextSlide+1; - } - } - else { // sequence - var roll = (opts.nextSlide + 1) == els.length; - if (roll && opts.bounce) { - opts.backwards = !opts.backwards; - opts.nextSlide = els.length-2; - opts.currSlide = els.length-1; - } - else { - opts.nextSlide = roll ? 0 : opts.nextSlide+1; - opts.currSlide = roll ? els.length-1 : opts.nextSlide-1; - } - } - } - if (changed && opts.pager) - opts.updateActivePagerLink(opts.pager, opts.currSlide, opts.activePagerClass); - - // stage the next transition - var ms = 0; - if (opts.timeout && !opts.continuous) - ms = getTimeout(els[opts.currSlide], els[opts.nextSlide], opts, fwd); - else if (opts.continuous && p.cyclePause) // continuous shows work off an after callback, not this timer logic - ms = 10; - if (ms > 0) - p.cycleTimeout = setTimeout(function(){ go(els, opts, 0, (!opts.rev && !opts.backwards)) }, ms); -}; - -// invoked after transition -$.fn.cycle.updateActivePagerLink = function(pager, currSlide, clsName) { - $(pager).each(function() { - $(this).children().removeClass(clsName).eq(currSlide).addClass(clsName); - }); -}; - -// calculate timeout value for current transition -function getTimeout(curr, next, opts, fwd) { - if (opts.timeoutFn) { - // call user provided calc fn - var t = opts.timeoutFn.call(curr,curr,next,opts,fwd); - while ((t - opts.speed) < 250) // sanitize timeout - t += opts.speed; - debug('calculated timeout: ' + t + '; speed: ' + opts.speed); - if (t !== false) - return t; - } - return opts.timeout; -}; - -// expose next/prev function, caller must pass in state -$.fn.cycle.next = function(opts) { advance(opts, opts.rev?-1:1); }; -$.fn.cycle.prev = function(opts) { advance(opts, opts.rev?1:-1);}; - -// advance slide forward or back -function advance(opts, val) { - var els = opts.elements; - var p = opts.$cont[0], timeout = p.cycleTimeout; - if (timeout) { - clearTimeout(timeout); - p.cycleTimeout = 0; - } - if (opts.random && val < 0) { - // move back to the previously display slide - opts.randomIndex--; - if (--opts.randomIndex == -2) - opts.randomIndex = els.length-2; - else if (opts.randomIndex == -1) - opts.randomIndex = els.length-1; - opts.nextSlide = opts.randomMap[opts.randomIndex]; - } - else if (opts.random) { - opts.nextSlide = opts.randomMap[opts.randomIndex]; - } - else { - opts.nextSlide = opts.currSlide + val; - if (opts.nextSlide < 0) { - if (opts.nowrap) return false; - opts.nextSlide = els.length - 1; - } - else if (opts.nextSlide >= els.length) { - if (opts.nowrap) return false; - opts.nextSlide = 0; - } - } - - var cb = opts.onPrevNextEvent || opts.prevNextClick; // prevNextClick is deprecated - if ($.isFunction(cb)) - cb(val > 0, opts.nextSlide, els[opts.nextSlide]); - go(els, opts, 1, val>=0); - return false; -}; - -function buildPager(els, opts) { - var $p = $(opts.pager); - $.each(els, function(i,o) { - $.fn.cycle.createPagerAnchor(i,o,$p,els,opts); - }); - opts.updateActivePagerLink(opts.pager, opts.startingSlide, opts.activePagerClass); -}; - -$.fn.cycle.createPagerAnchor = function(i, el, $p, els, opts) { - var a; - if ($.isFunction(opts.pagerAnchorBuilder)) { - a = opts.pagerAnchorBuilder(i,el); - debug('pagerAnchorBuilder('+i+', el) returned: ' + a); - } - else - a = ''+(i+1)+''; - - if (!a) - return; - var $a = $(a); - // don't reparent if anchor is in the dom - if ($a.parents('body').length === 0) { - var arr = []; - if ($p.length > 1) { - $p.each(function() { - var $clone = $a.clone(true); - $(this).append($clone); - arr.push($clone[0]); - }); - $a = $(arr); - } - else { - $a.appendTo($p); - } - } - - opts.pagerAnchors = opts.pagerAnchors || []; - opts.pagerAnchors.push($a); - $a.bind(opts.pagerEvent, function(e) { - e.preventDefault(); - opts.nextSlide = i; - var p = opts.$cont[0], timeout = p.cycleTimeout; - if (timeout) { - clearTimeout(timeout); - p.cycleTimeout = 0; - } - var cb = opts.onPagerEvent || opts.pagerClick; // pagerClick is deprecated - if ($.isFunction(cb)) - cb(opts.nextSlide, els[opts.nextSlide]); - go(els,opts,1,opts.currSlide < i); // trigger the trans -// return false; // <== allow bubble - }); - - if ( ! /^click/.test(opts.pagerEvent) && !opts.allowPagerClickBubble) - $a.bind('click.cycle', function(){return false;}); // suppress click - - if (opts.pauseOnPagerHover) - $a.hover(function() { opts.$cont[0].cyclePause++; }, function() { opts.$cont[0].cyclePause--; } ); -}; - -// helper fn to calculate the number of slides between the current and the next -$.fn.cycle.hopsFromLast = function(opts, fwd) { - var hops, l = opts.lastSlide, c = opts.currSlide; - if (fwd) - hops = c > l ? c - l : opts.slideCount - l; - else - hops = c < l ? l - c : l + opts.slideCount - c; - return hops; -}; - -// fix clearType problems in ie6 by setting an explicit bg color -// (otherwise text slides look horrible during a fade transition) -function clearTypeFix($slides) { - debug('applying clearType background-color hack'); - function hex(s) { - s = parseInt(s).toString(16); - return s.length < 2 ? '0'+s : s; - }; - function getBg(e) { - for ( ; e && e.nodeName.toLowerCase() != 'html'; e = e.parentNode) { - var v = $.css(e,'background-color'); - if (v.indexOf('rgb') >= 0 ) { - var rgb = v.match(/\d+/g); - return '#'+ hex(rgb[0]) + hex(rgb[1]) + hex(rgb[2]); - } - if (v && v != 'transparent') - return v; - } - return '#ffffff'; - }; - $slides.each(function() { $(this).css('background-color', getBg(this)); }); -}; - -// reset common props before the next transition -$.fn.cycle.commonReset = function(curr,next,opts,w,h,rev) { - $(opts.elements).not(curr).hide(); - opts.cssBefore.opacity = 1; - opts.cssBefore.display = 'block'; - if (w !== false && next.cycleW > 0) - opts.cssBefore.width = next.cycleW; - if (h !== false && next.cycleH > 0) - opts.cssBefore.height = next.cycleH; - opts.cssAfter = opts.cssAfter || {}; - opts.cssAfter.display = 'none'; - $(curr).css('zIndex',opts.slideCount + (rev === true ? 1 : 0)); - $(next).css('zIndex',opts.slideCount + (rev === true ? 0 : 1)); -}; - -// the actual fn for effecting a transition -$.fn.cycle.custom = function(curr, next, opts, cb, fwd, speedOverride) { - var $l = $(curr), $n = $(next); - var speedIn = opts.speedIn, speedOut = opts.speedOut, easeIn = opts.easeIn, easeOut = opts.easeOut; - $n.css(opts.cssBefore); - if (speedOverride) { - if (typeof speedOverride == 'number') - speedIn = speedOut = speedOverride; - else - speedIn = speedOut = 1; - easeIn = easeOut = null; - } - var fn = function() {$n.animate(opts.animIn, speedIn, easeIn, cb)}; - $l.animate(opts.animOut, speedOut, easeOut, function() { - if (opts.cssAfter) $l.css(opts.cssAfter); - if (!opts.sync) fn(); - }); - if (opts.sync) fn(); -}; - -// transition definitions - only fade is defined here, transition pack defines the rest -$.fn.cycle.transitions = { - fade: function($cont, $slides, opts) { - $slides.not(':eq('+opts.currSlide+')').css('opacity',0); - opts.before.push(function(curr,next,opts) { - $.fn.cycle.commonReset(curr,next,opts); - opts.cssBefore.opacity = 0; - }); - opts.animIn = { opacity: 1 }; - opts.animOut = { opacity: 0 }; - opts.cssBefore = { top: 0, left: 0 }; - } -}; - -$.fn.cycle.ver = function() { return ver; }; - -// override these globally if you like (they are all optional) -$.fn.cycle.defaults = { - fx: 'fade', // name of transition effect (or comma separated names, ex: 'fade,scrollUp,shuffle') - timeout: 4000, // milliseconds between slide transitions (0 to disable auto advance) - timeoutFn: null, // callback for determining per-slide timeout value: function(currSlideElement, nextSlideElement, options, forwardFlag) - continuous: 0, // true to start next transition immediately after current one completes - speed: 1000, // speed of the transition (any valid fx speed value) - speedIn: null, // speed of the 'in' transition - speedOut: null, // speed of the 'out' transition - next: null, // selector for element to use as event trigger for next slide - prev: null, // selector for element to use as event trigger for previous slide -// prevNextClick: null, // @deprecated; please use onPrevNextEvent instead - onPrevNextEvent: null, // callback fn for prev/next events: function(isNext, zeroBasedSlideIndex, slideElement) - prevNextEvent:'click.cycle',// event which drives the manual transition to the previous or next slide - pager: null, // selector for element to use as pager container - //pagerClick null, // @deprecated; please use onPagerEvent instead - onPagerEvent: null, // callback fn for pager events: function(zeroBasedSlideIndex, slideElement) - pagerEvent: 'click.cycle', // name of event which drives the pager navigation - allowPagerClickBubble: false, // allows or prevents click event on pager anchors from bubbling - pagerAnchorBuilder: null, // callback fn for building anchor links: function(index, DOMelement) - before: null, // transition callback (scope set to element to be shown): function(currSlideElement, nextSlideElement, options, forwardFlag) - after: null, // transition callback (scope set to element that was shown): function(currSlideElement, nextSlideElement, options, forwardFlag) - end: null, // callback invoked when the slideshow terminates (use with autostop or nowrap options): function(options) - easing: null, // easing method for both in and out transitions - easeIn: null, // easing for "in" transition - easeOut: null, // easing for "out" transition - shuffle: null, // coords for shuffle animation, ex: { top:15, left: 200 } - animIn: null, // properties that define how the slide animates in - animOut: null, // properties that define how the slide animates out - cssBefore: null, // properties that define the initial state of the slide before transitioning in - cssAfter: null, // properties that defined the state of the slide after transitioning out - fxFn: null, // function used to control the transition: function(currSlideElement, nextSlideElement, options, afterCalback, forwardFlag) - height: 'auto', // container height - startingSlide: 0, // zero-based index of the first slide to be displayed - sync: 1, // true if in/out transitions should occur simultaneously - random: 0, // true for random, false for sequence (not applicable to shuffle fx) - fit: 0, // force slides to fit container - containerResize: 1, // resize container to fit largest slide - pause: 0, // true to enable "pause on hover" - pauseOnPagerHover: 0, // true to pause when hovering over pager link - autostop: 0, // true to end slideshow after X transitions (where X == slide count) - autostopCount: 0, // number of transitions (optionally used with autostop to define X) - delay: 0, // additional delay (in ms) for first transition (hint: can be negative) - slideExpr: null, // expression for selecting slides (if something other than all children is required) - cleartype: !$.support.opacity, // true if clearType corrections should be applied (for IE) - cleartypeNoBg: false, // set to true to disable extra cleartype fixing (leave false to force background color setting on slides) - nowrap: 0, // true to prevent slideshow from wrapping - fastOnEvent: 0, // force fast transitions when triggered manually (via pager or prev/next); value == time in ms - randomizeEffects: 1, // valid when multiple effects are used; true to make the effect sequence random - rev: 0, // causes animations to transition in reverse - manualTrump: true, // causes manual transition to stop an active transition instead of being ignored - requeueOnImageNotLoaded: true, // requeue the slideshow if any image slides are not yet loaded - requeueTimeout: 250, // ms delay for requeue - activePagerClass: 'activeSlide', // class name used for the active pager link - updateActivePagerLink: null, // callback fn invoked to update the active pager link (adds/removes activePagerClass style) - backwards: false // true to start slideshow at last slide and move backwards through the stack -}; - -})(jQuery); - - -/*! - * jQuery Cycle Plugin Transition Definitions - * This script is a plugin for the jQuery Cycle Plugin - * Examples and documentation at: http://malsup.com/jquery/cycle/ - * Copyright (c) 2007-2010 M. Alsup - * Version: 2.72 - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - */ -(function($) { - -// -// These functions define one-time slide initialization for the named -// transitions. To save file size feel free to remove any of these that you -// don't need. -// -$.fn.cycle.transitions.none = function($cont, $slides, opts) { - opts.fxFn = function(curr,next,opts,after){ - $(next).show(); - $(curr).hide(); - after(); - }; -} - -// scrollUp/Down/Left/Right -$.fn.cycle.transitions.scrollUp = function($cont, $slides, opts) { - $cont.css('overflow','hidden'); - opts.before.push($.fn.cycle.commonReset); - var h = $cont.height(); - opts.cssBefore ={ top: h, left: 0 }; - opts.cssFirst = { top: 0 }; - opts.animIn = { top: 0 }; - opts.animOut = { top: -h }; -}; -$.fn.cycle.transitions.scrollDown = function($cont, $slides, opts) { - $cont.css('overflow','hidden'); - opts.before.push($.fn.cycle.commonReset); - var h = $cont.height(); - opts.cssFirst = { top: 0 }; - opts.cssBefore= { top: -h, left: 0 }; - opts.animIn = { top: 0 }; - opts.animOut = { top: h }; -}; -$.fn.cycle.transitions.scrollLeft = function($cont, $slides, opts) { - $cont.css('overflow','hidden'); - opts.before.push($.fn.cycle.commonReset); - var w = $cont.width(); - opts.cssFirst = { left: 0 }; - opts.cssBefore= { left: w, top: 0 }; - opts.animIn = { left: 0 }; - opts.animOut = { left: 0-w }; -}; -$.fn.cycle.transitions.scrollRight = function($cont, $slides, opts) { - $cont.css('overflow','hidden'); - opts.before.push($.fn.cycle.commonReset); - var w = $cont.width(); - opts.cssFirst = { left: 0 }; - opts.cssBefore= { left: -w, top: 0 }; - opts.animIn = { left: 0 }; - opts.animOut = { left: w }; -}; -$.fn.cycle.transitions.scrollHorz = function($cont, $slides, opts) { - $cont.css('overflow','hidden').width(); - opts.before.push(function(curr, next, opts, fwd) { - $.fn.cycle.commonReset(curr,next,opts); - opts.cssBefore.left = fwd ? (next.cycleW-1) : (1-next.cycleW); - opts.animOut.left = fwd ? -curr.cycleW : curr.cycleW; - }); - opts.cssFirst = { left: 0 }; - opts.cssBefore= { top: 0 }; - opts.animIn = { left: 0 }; - opts.animOut = { top: 0 }; -}; -$.fn.cycle.transitions.scrollVert = function($cont, $slides, opts) { - $cont.css('overflow','hidden'); - opts.before.push(function(curr, next, opts, fwd) { - $.fn.cycle.commonReset(curr,next,opts); - opts.cssBefore.top = fwd ? (1-next.cycleH) : (next.cycleH-1); - opts.animOut.top = fwd ? curr.cycleH : -curr.cycleH; - }); - opts.cssFirst = { top: 0 }; - opts.cssBefore= { left: 0 }; - opts.animIn = { top: 0 }; - opts.animOut = { left: 0 }; -}; - -// slideX/slideY -$.fn.cycle.transitions.slideX = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $(opts.elements).not(curr).hide(); - $.fn.cycle.commonReset(curr,next,opts,false,true); - opts.animIn.width = next.cycleW; - }); - opts.cssBefore = { left: 0, top: 0, width: 0 }; - opts.animIn = { width: 'show' }; - opts.animOut = { width: 0 }; -}; -$.fn.cycle.transitions.slideY = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $(opts.elements).not(curr).hide(); - $.fn.cycle.commonReset(curr,next,opts,true,false); - opts.animIn.height = next.cycleH; - }); - opts.cssBefore = { left: 0, top: 0, height: 0 }; - opts.animIn = { height: 'show' }; - opts.animOut = { height: 0 }; -}; - -// shuffle -$.fn.cycle.transitions.shuffle = function($cont, $slides, opts) { - var i, w = $cont.css('overflow', 'visible').width(); - $slides.css({left: 0, top: 0}); - opts.before.push(function(curr,next,opts) { - $.fn.cycle.commonReset(curr,next,opts,true,true,true); - }); - // only adjust speed once! - if (!opts.speedAdjusted) { - opts.speed = opts.speed / 2; // shuffle has 2 transitions - opts.speedAdjusted = true; - } - opts.random = 0; - opts.shuffle = opts.shuffle || {left:-w, top:15}; - opts.els = []; - for (i=0; i < $slides.length; i++) - opts.els.push($slides[i]); - - for (i=0; i < opts.currSlide; i++) - opts.els.push(opts.els.shift()); - - // custom transition fn (hat tip to Benjamin Sterling for this bit of sweetness!) - opts.fxFn = function(curr, next, opts, cb, fwd) { - var $el = fwd ? $(curr) : $(next); - $(next).css(opts.cssBefore); - var count = opts.slideCount; - $el.animate(opts.shuffle, opts.speedIn, opts.easeIn, function() { - var hops = $.fn.cycle.hopsFromLast(opts, fwd); - for (var k=0; k < hops; k++) - fwd ? opts.els.push(opts.els.shift()) : opts.els.unshift(opts.els.pop()); - if (fwd) { - for (var i=0, len=opts.els.length; i < len; i++) - $(opts.els[i]).css('z-index', len-i+count); - } - else { - var z = $(curr).css('z-index'); - $el.css('z-index', parseInt(z)+1+count); - } - $el.animate({left:0, top:0}, opts.speedOut, opts.easeOut, function() { - $(fwd ? this : curr).hide(); - if (cb) cb(); - }); - }); - }; - opts.cssBefore = { display: 'block', opacity: 1, top: 0, left: 0 }; -}; - -// turnUp/Down/Left/Right -$.fn.cycle.transitions.turnUp = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,true,false); - opts.cssBefore.top = next.cycleH; - opts.animIn.height = next.cycleH; - }); - opts.cssFirst = { top: 0 }; - opts.cssBefore = { left: 0, height: 0 }; - opts.animIn = { top: 0 }; - opts.animOut = { height: 0 }; -}; -$.fn.cycle.transitions.turnDown = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,true,false); - opts.animIn.height = next.cycleH; - opts.animOut.top = curr.cycleH; - }); - opts.cssFirst = { top: 0 }; - opts.cssBefore = { left: 0, top: 0, height: 0 }; - opts.animOut = { height: 0 }; -}; -$.fn.cycle.transitions.turnLeft = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,false,true); - opts.cssBefore.left = next.cycleW; - opts.animIn.width = next.cycleW; - }); - opts.cssBefore = { top: 0, width: 0 }; - opts.animIn = { left: 0 }; - opts.animOut = { width: 0 }; -}; -$.fn.cycle.transitions.turnRight = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,false,true); - opts.animIn.width = next.cycleW; - opts.animOut.left = curr.cycleW; - }); - opts.cssBefore = { top: 0, left: 0, width: 0 }; - opts.animIn = { left: 0 }; - opts.animOut = { width: 0 }; -}; - -// zoom -$.fn.cycle.transitions.zoom = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,false,false,true); - opts.cssBefore.top = next.cycleH/2; - opts.cssBefore.left = next.cycleW/2; - opts.animIn = { top: 0, left: 0, width: next.cycleW, height: next.cycleH }; - opts.animOut = { width: 0, height: 0, top: curr.cycleH/2, left: curr.cycleW/2 }; - }); - opts.cssFirst = { top:0, left: 0 }; - opts.cssBefore = { width: 0, height: 0 }; -}; - -// fadeZoom -$.fn.cycle.transitions.fadeZoom = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,false,false); - opts.cssBefore.left = next.cycleW/2; - opts.cssBefore.top = next.cycleH/2; - opts.animIn = { top: 0, left: 0, width: next.cycleW, height: next.cycleH }; - }); - opts.cssBefore = { width: 0, height: 0 }; - opts.animOut = { opacity: 0 }; -}; - -// blindX -$.fn.cycle.transitions.blindX = function($cont, $slides, opts) { - var w = $cont.css('overflow','hidden').width(); - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts); - opts.animIn.width = next.cycleW; - opts.animOut.left = curr.cycleW; - }); - opts.cssBefore = { left: w, top: 0 }; - opts.animIn = { left: 0 }; - opts.animOut = { left: w }; -}; -// blindY -$.fn.cycle.transitions.blindY = function($cont, $slides, opts) { - var h = $cont.css('overflow','hidden').height(); - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts); - opts.animIn.height = next.cycleH; - opts.animOut.top = curr.cycleH; - }); - opts.cssBefore = { top: h, left: 0 }; - opts.animIn = { top: 0 }; - opts.animOut = { top: h }; -}; -// blindZ -$.fn.cycle.transitions.blindZ = function($cont, $slides, opts) { - var h = $cont.css('overflow','hidden').height(); - var w = $cont.width(); - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts); - opts.animIn.height = next.cycleH; - opts.animOut.top = curr.cycleH; - }); - opts.cssBefore = { top: h, left: w }; - opts.animIn = { top: 0, left: 0 }; - opts.animOut = { top: h, left: w }; -}; - -// growX - grow horizontally from centered 0 width -$.fn.cycle.transitions.growX = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,false,true); - opts.cssBefore.left = this.cycleW/2; - opts.animIn = { left: 0, width: this.cycleW }; - opts.animOut = { left: 0 }; - }); - opts.cssBefore = { width: 0, top: 0 }; -}; -// growY - grow vertically from centered 0 height -$.fn.cycle.transitions.growY = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,true,false); - opts.cssBefore.top = this.cycleH/2; - opts.animIn = { top: 0, height: this.cycleH }; - opts.animOut = { top: 0 }; - }); - opts.cssBefore = { height: 0, left: 0 }; -}; - -// curtainX - squeeze in both edges horizontally -$.fn.cycle.transitions.curtainX = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,false,true,true); - opts.cssBefore.left = next.cycleW/2; - opts.animIn = { left: 0, width: this.cycleW }; - opts.animOut = { left: curr.cycleW/2, width: 0 }; - }); - opts.cssBefore = { top: 0, width: 0 }; -}; -// curtainY - squeeze in both edges vertically -$.fn.cycle.transitions.curtainY = function($cont, $slides, opts) { - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,true,false,true); - opts.cssBefore.top = next.cycleH/2; - opts.animIn = { top: 0, height: next.cycleH }; - opts.animOut = { top: curr.cycleH/2, height: 0 }; - }); - opts.cssBefore = { left: 0, height: 0 }; -}; - -// cover - curr slide covered by next slide -$.fn.cycle.transitions.cover = function($cont, $slides, opts) { - var d = opts.direction || 'left'; - var w = $cont.css('overflow','hidden').width(); - var h = $cont.height(); - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts); - if (d == 'right') - opts.cssBefore.left = -w; - else if (d == 'up') - opts.cssBefore.top = h; - else if (d == 'down') - opts.cssBefore.top = -h; - else - opts.cssBefore.left = w; - }); - opts.animIn = { left: 0, top: 0}; - opts.animOut = { opacity: 1 }; - opts.cssBefore = { top: 0, left: 0 }; -}; - -// uncover - curr slide moves off next slide -$.fn.cycle.transitions.uncover = function($cont, $slides, opts) { - var d = opts.direction || 'left'; - var w = $cont.css('overflow','hidden').width(); - var h = $cont.height(); - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,true,true,true); - if (d == 'right') - opts.animOut.left = w; - else if (d == 'up') - opts.animOut.top = -h; - else if (d == 'down') - opts.animOut.top = h; - else - opts.animOut.left = -w; - }); - opts.animIn = { left: 0, top: 0 }; - opts.animOut = { opacity: 1 }; - opts.cssBefore = { top: 0, left: 0 }; -}; - -// toss - move top slide and fade away -$.fn.cycle.transitions.toss = function($cont, $slides, opts) { - var w = $cont.css('overflow','visible').width(); - var h = $cont.height(); - opts.before.push(function(curr, next, opts) { - $.fn.cycle.commonReset(curr,next,opts,true,true,true); - // provide default toss settings if animOut not provided - if (!opts.animOut.left && !opts.animOut.top) - opts.animOut = { left: w*2, top: -h/2, opacity: 0 }; - else - opts.animOut.opacity = 0; - }); - opts.cssBefore = { left: 0, top: 0 }; - opts.animIn = { left: 0 }; -}; - -// wipe - clip animation -$.fn.cycle.transitions.wipe = function($cont, $slides, opts) { - var w = $cont.css('overflow','hidden').width(); - var h = $cont.height(); - opts.cssBefore = opts.cssBefore || {}; - var clip; - if (opts.clip) { - if (/l2r/.test(opts.clip)) - clip = 'rect(0px 0px '+h+'px 0px)'; - else if (/r2l/.test(opts.clip)) - clip = 'rect(0px '+w+'px '+h+'px '+w+'px)'; - else if (/t2b/.test(opts.clip)) - clip = 'rect(0px '+w+'px 0px 0px)'; - else if (/b2t/.test(opts.clip)) - clip = 'rect('+h+'px '+w+'px '+h+'px 0px)'; - else if (/zoom/.test(opts.clip)) { - var top = parseInt(h/2); - var left = parseInt(w/2); - clip = 'rect('+top+'px '+left+'px '+top+'px '+left+'px)'; - } - } - - opts.cssBefore.clip = opts.cssBefore.clip || clip || 'rect(0px 0px 0px 0px)'; - - var d = opts.cssBefore.clip.match(/(\d+)/g); - var t = parseInt(d[0]), r = parseInt(d[1]), b = parseInt(d[2]), l = parseInt(d[3]); - - opts.before.push(function(curr, next, opts) { - if (curr == next) return; - var $curr = $(curr), $next = $(next); - $.fn.cycle.commonReset(curr,next,opts,true,true,false); - opts.cssAfter.display = 'block'; - - var step = 1, count = parseInt((opts.speedIn / 13)) - 1; - (function f() { - var tt = t ? t - parseInt(step * (t/count)) : 0; - var ll = l ? l - parseInt(step * (l/count)) : 0; - var bb = b < h ? b + parseInt(step * ((h-b)/count || 1)) : h; - var rr = r < w ? r + parseInt(step * ((w-r)/count || 1)) : w; - $next.css({ clip: 'rect('+tt+'px '+rr+'px '+bb+'px '+ll+'px)' }); - (step++ <= count) ? setTimeout(f, 13) : $curr.css('display', 'none'); - })(); - }); - opts.cssBefore = { display: 'block', opacity: 1, top: 0, left: 0 }; - opts.animIn = { left: 0 }; - opts.animOut = { left: 0 }; -}; - -})(jQuery); diff --git a/docs/iris/src/_static/logo_banner.png b/docs/iris/src/_static/logo_banner.png deleted file mode 100644 index 14fb3497e9ff6f9e6f5811be041ad48d770572c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31305 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 diff --git a/docs/iris/src/_static/style.css b/docs/iris/src/_static/style.css deleted file mode 100644 index 69fa84394e..0000000000 --- a/docs/iris/src/_static/style.css +++ /dev/null @@ -1,99 +0,0 @@ -body { - font-family: 'Noto Sans', sans-serif; -} - -.sidebar { z-index: 10; } - -.highlight { background: none; } - -p.hr_p { - overflow: hidden; - text-align: center; -} -p.hr_p a { - font-size: small; - color: #1C86EE; -} -p.hr_p:before, -p.hr_p:after { - background-color: #abc; - border: 1px solid #abc; - content: ""; - display: inline-block; - height: 1px; - position: relative; - vertical-align: middle; - width: 50%; -} -p.hr_p:before { - right: 0.5em; - margin-left: -50%; -} -p.hr_p:after { - left: 0.5em; - margin-right: -50%; -} - -.header-content { - background-color: white; - text-align: left; - padding: 0px; - height: 149px; -} - -.header-content img { - height: 100px; - vertical-align: middle; - float: left; - margin: 20px 2em 0.8em 4%; - padding: 0px; -} - -.header-content .strapline { - display: inline-block; - width: calc(100% - 110px - 2em - 4%); -} - -.strapline p { - font-size: medium; - font-family: 'Alike', serif; - font-weight: bold; - color: #444444; - max-width: 52ch; - margin-top: 0.25em; -} - -.header-content h1 { - font-size: 3.5rem; - font-family: 'Alike', serif; - margin-top: 40px; - padding: 0px; - color: #323232; - padding-bottom: 0.2em; -} - -.header-content h1 span.version { - font-size: 1.5rem; -} - -.github-forkme { - position: absolute; - top: 0; - right: 80px; - border: 0; -} - -/* Take into account the resizing effect of the page (which has a minimum */ -/* width of 740px + 80px margins). */ -@media screen and (max-width: calc(740px + 80px + 80px)) { - .github-forkme { - right: calc(100% - 740px - 80px); - } -} - -@media screen and (max-width: calc(740px + 80px)) { - .github-forkme { - left: calc(740px + 80px - 149px); - right: 0px; - } -} diff --git a/docs/iris/src/_static/theme_override.css b/docs/iris/src/_static/theme_override.css new file mode 100644 index 0000000000..5edc286630 --- /dev/null +++ b/docs/iris/src/_static/theme_override.css @@ -0,0 +1,28 @@ +/* import the standard theme css */ +@import url("css/theme.css"); + +/* now we can add custom any css */ + +/* set the width of the logo */ +.wy-side-nav-search>a img.logo, +.wy-side-nav-search .wy-dropdown>a img.logo { + width: 12rem +} + +/* color of the logo background in the top left corner */ +.wy-side-nav-search { + background-color: lightgray; +} + +/* color of the font for the version in the top left corner */ +.wy-side-nav-search>div.version { + color: black; + font-weight: bold; +} + +/* Ensures tables do now have width scroll bars */ +table.docutils td { + white-space: unset; + word-wrap: break-word; +} + diff --git a/docs/iris/src/_templates/index.html b/docs/iris/src/_templates/index.html deleted file mode 100644 index c18f0268fa..0000000000 --- a/docs/iris/src/_templates/index.html +++ /dev/null @@ -1,146 +0,0 @@ -{% extends "layout.html" %} -{% set title = 'Iris documentation homepage' %} -{% block extrahead %} -{{ super() }} - - - - - - - - -{% endblock %} - - - -{% block body %} - - - - -

- Iris implements a data model based on the CF conventions - giving you a powerful, format-agnostic interface for working with your data. - It excels when working with multi-dimensional Earth Science data, where tabular - representations become unwieldy and inefficient. -

-

- CF Standard names, - units, and coordinate metadata - are built into Iris, giving you a rich and expressive interface for maintaining - an accurate representation of your data. Its treatment of data and - associated metadata as first-class objects includes: -

-
    -
  • a visualisation interface based on matplotlib and - cartopy,
  • -
  • unit conversion,
  • -
  • subsetting and extraction,
  • -
  • merge and concatenate,
  • -
  • aggregations and reductions (including min, max, mean and weighted averages),
  • -
  • interpolation and regridding (including nearest-neighbor, linear and area-weighted), and
  • -
  • operator overloads (+, -, *, /, etc.).
  • -
-

- A number of file formats are recognised by Iris, including CF-compliant NetCDF, GRIB, - and PP, and it has a plugin architecture to allow other formats to be added seamlessly. -

-

- Building upon NumPy and - dask, - Iris scales from efficient single-machine workflows right through to multi-core - clusters and HPC. - Interoperability with packages from the wider scientific Python ecosystem comes from Iris' - use of standard NumPy/dask arrays as its underlying data storage. -

- -
-
-
-
- -
- -
- - -
- -{% endblock %} diff --git a/docs/iris/src/_templates/layout.html b/docs/iris/src/_templates/layout.html index f854455f71..9b4983697e 100644 --- a/docs/iris/src/_templates/layout.html +++ b/docs/iris/src/_templates/layout.html @@ -1,71 +1,25 @@ {% extends "!layout.html" %} -{%- block extrahead %} -{{ super() }} - - - - - - - - - - -{% endblock %} - - -{% block rootrellink %} -
  • home
  • -
  • examples
  • -
  • gallery
  • -
  • contents
  • -{% endblock %} - - -{% block relbar1 %} - - - Fork Iris on GitHub - - - -
    - - Iris logo - -
    -

    - Iris v3.0 -

    -

    - A powerful, format-agnostic, community-driven Python library for analysing and - visualising Earth science data. -

    -
    -
    - -{{ super() }} +{% block menu %} + {{ super() }} + + {# menu_links and menu_links_name are set in conf.py (html_context) #} + + {% if menu_links %} +

    + + {% if menu_links_name %} + {{ menu_links_name }} + {% else %} + External links + {% endif %} + +

    +
      + {% for text, link in menu_links %} +
    • {{ text }}
    • + {% endfor %} +
    + {% endif %} {% endblock %} - - -{% block footer %} - - - - - -{% endblock %} diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 0e4c53bccc..2308c065ba 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -17,78 +17,58 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import datetime +# ---------------------------------------------------------------------------- + +import ntpath import os import sys -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.abspath("sphinxext")) -# add some sample files from the developers guide.. -sys.path.append(os.path.abspath(os.path.join("developers_guide"))) +# function to write useful output to stdout, prefixing the source. +def autolog(message): + print("[{}] {}".format(ntpath.basename(__file__), message)) -# -- General configuration ----------------------------------------------------- +# -- Are we running on the readthedocs server, if so do some setup ----------- -# Temporary value for use by LaTeX and 'man' output. -# Deleted at the end of the module. -_authors = "Iris developers" +on_rtd = os.environ.get("READTHEDOCS") == "True" -# If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' +if on_rtd: + autolog("Build running on READTHEDOCS server") -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.coverage", - "sphinx.ext.doctest", - "sphinx.ext.extlinks", - "sphinx.ext.graphviz", - "sphinx.ext.imgmath", - "sphinx.ext.intersphinx", - "matplotlib.sphinxext.mathmpl", - "matplotlib.sphinxext.plot_directive", - # better class documentation - "custom_class_autodoc", - # Data instance __repr__ filter. - "custom_data_autodoc", - "gen_example_directory", - "generate_package_rst", - "gen_gallery", - # Add labels to figures automatically - "auto_label_figures", -] +# -- Path setup -------------------------------------------------------------- -# list of packages to document -autopackage_name = ["iris"] +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] +import datetime +import warnings -# The suffix of source filenames. -source_suffix = ".rst" +# custom sphinx extensions +sys.path.append(os.path.abspath("sphinxext")) -# The encoding of source files. -# source_encoding = 'utf-8-sig' +# add some sample files from the developers guide.. +sys.path.append(os.path.abspath(os.path.join("developers_guide"))) + +# why isnt the iris path added to it is discoverable too? We dont need to, +# the sphinext to generate the api rst knows where the source is. If it +# is added then the travis build will likely fail. -# The master toctree document. -master_doc = "contents" +# -- Project information ----------------------------------------------------- -# General information about the project. project = "Iris" + # define the copyright information for latex builds. Note, for html builds, # the copyright exists directly inside "_templates/layout.html" upper_copy_year = datetime.datetime.now().year -copyright = "Copyright Iris contributors" +copyright = "Iris contributors" +_authors = "Iris developers" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# + import iris # The short X.Y version. @@ -100,44 +80,60 @@ # The full version, including alpha/beta/rc tags. release = iris.__version__ -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None +autolog("Iris Version = {}".format(version)) +autolog("Iris Release = {}".format(release)) -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' +# -- General configuration --------------------------------------------------- -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ["sphinxext", "build"] +# Create a variable that can be insterted in the rst "|copyright_years|". +# You can add more vairables here if needed +rst_epilog = """ +.. |copyright_years| replace:: {year_range} +""".format( + year_range="2010 - {}".format(upper_copy_year) +) -# The reST default role (used for this markup: `text`) to use for all documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.todo", + "sphinx.ext.duration", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx_copybutton", + "sphinx_gallery.gen_gallery", + "matplotlib.sphinxext.mathmpl", + "matplotlib.sphinxext.plot_directive", + # better api documentation (custom) + "custom_class_autodoc", + "custom_data_autodoc", + "generate_package_rst", +] -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True +# sphinx_copybutton config +copybutton_prompt_text = ">>> " -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False +# sphinx.ext.todo configuration +todo_include_todos = True -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# Define the default highlight language. This also allows the >>> removal -# javascript (copybutton.js) to function. -highlight_language = "default" - -# A list of ignored prefixes for module index sorting. +# api generation configuration +autodoc_member_order = "groupwise" +autodoc_default_flags = ["show-inheritance"] +autosummary_generate = True +autosummary_imported_members = True +autopackage_name = ["iris"] +autoclass_content = "init" modindex_common_prefix = ["iris"] +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + intersphinx_mapping = { "cartopy": ("http://scitools.org.uk/cartopy/docs/latest/", None), "matplotlib": ("http://matplotlib.org/", None), @@ -146,6 +142,14 @@ "scipy": ("http://docs.scipy.org/doc/scipy/reference/", None), } +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# plot directive options (extension: matplotlib.sphinxext.plot_directive --- +plot_formats = [ + ("png", 100), +] + # -- Extlinks extension ------------------------------------------------------- extlinks = { @@ -153,103 +157,91 @@ "pull": ("https://github.com/SciTools/iris/pull/%s", "PR #"), } -# -- Doctest ------------------------------------------------------------------ +# -- Doctest ("make doctest")-------------------------------------------------- doctest_global_setup = "import iris" -# -- Autodoc ------------------------------------------------------------------ - -autodoc_member_order = "groupwise" -autodoc_default_flags = ["show-inheritance"] - -# include the __init__ method when documenting classes -# document the init/new method at the top level of the class documentation rather than displaying the class docstring -autoclass_content = "init" - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = "default" -html_theme = "sphinxdoc" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -html_context = {"copyright_years": "2010 - {}".format(upper_copy_year)} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [] +# -- Options for HTML output -------------------------------------------------- -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_logo = "_static/iris-logo-title.png" +html_favicon = "_static/favicon.ico" +html_theme = "sphinx_rtd_theme" + +html_theme_options = { + "display_version": True, + "style_external_links": True, + "logo_only": "True", +} -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None +html_context = { + "copyright_years": "2010 - {}".format(upper_copy_year), + # menu_links and menu_links_name are used in _templates/layout.html + # to include some nice icons. See http://fontawesome.io for a list of + # icons (used in the sphinx_rtd_theme) + "menu_links_name": "Support", + "menu_links": [ + ( + ' Source Code', + "https://github.com/SciTools/iris", + ), + ( + ' Users Google Group', + "https://groups.google.com/forum/#!forum/scitools-iris", + ), + ( + ' Developers Google Group', + "https://groups.google.com/forum/#!forum/scitools-iris-dev", + ), + ( + ' StackOverflow For "How do I?"', + "https://stackoverflow.com/questions/tagged/python-iris", + ), + ( + ' Legacy documentation', + "https://scitools.org.uk/iris/docs/v2.4.0/index.html", + ), + ], +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_style = "theme_override.css" -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -html_additional_pages = {"index": "index.html", "gallery": "gallery.html"} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -html_show_sphinx = False - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' +# url link checker. Some links work but report as broken, lets ignore them. +linkcheck_ignore = [ + "https://github.com/SciTools/iris/commit/69597eb3d8501ff16ee3d56aef1f7b8f1c2bb316#diff-1680206bdc5cfaa83e14428f5ba0f848", + "http://www.wmo.int/pages/prog/www/DPFS/documents/485_Vol_I_en_colour.pdf", + "http://code.google.com/p/msysgit/downloads/list", +] -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = '' +# list of sources to exclude from the build. +exclude_patterns = [] -# Output file base name for HTML help builder. -htmlhelp_basename = "Irisdoc" +# -- sphinx-gallery config ---------------------------------------------------- -html_use_modindex = False +sphinx_gallery_conf = { + # path to your example scripts + "examples_dirs": ["../gallery_code"], + # path to where to save gallery generated output + "gallery_dirs": ["generated/gallery"], + # filename pattern for the files in the gallery + "filename_pattern": "/plot_", + # filename patternt to ignore in the gallery + "ignore_pattern": r"__init__\.py", +} +# Remove matplotlib agg warnings from generated doc when using plt.show +warnings.filterwarnings( + "ignore", + category=UserWarning, + message="Matplotlib is currently using agg, which is a" + " non-GUI backend, so cannot show the figure.", +) # -- Options for LaTeX output -------------------------------------------------- @@ -261,15 +253,15 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ( - "contents", - "Iris.tex", - "Iris Documentation", - " \\and ".join(_authors), - "manual", - ), -] +# latex_documents = [ +# ( +# "contents", +# "Iris.tex", +# "Iris Documentation", +# " \\and ".join(_authors), +# "manual", +# ), +# ] # The name of an image file (relative to this directory) to place at the top of # the title page. @@ -293,24 +285,11 @@ # If false, no module index is generated. # latex_domain_indices = True -latex_elements = {} -latex_elements["docclass"] = "MO_report" +# latex_elements = {} +# latex_elements["docclass"] = "MO_report" # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "iris", "Iris Documentation", _authors, 1)] - -########################## -# plot directive options # -########################## - -plot_formats = [ - ("png", 100), - # ('hires.png', 200), ('pdf', 250) -] - - -# Delete the temporary value. -del _authors +# man_pages = [("index", "iris", "Iris Documentation", _authors, 1)] diff --git a/docs/iris/src/contents.rst b/docs/iris/src/contents.rst deleted file mode 100644 index ecaf025a7a..0000000000 --- a/docs/iris/src/contents.rst +++ /dev/null @@ -1,32 +0,0 @@ -===================================== -Iris documentation table of contents -===================================== -.. toctree:: - :maxdepth: 1 - - installing.rst - -.. toctree:: - :maxdepth: 3 - - userguide/index.rst - -.. toctree:: - :maxdepth: 1 - :hidden: - - iris/iris.rst - -.. toctree:: - :maxdepth: 2 - - whatsnew/index.rst - -.. toctree:: - :maxdepth: 1 - - examples/index.rst - developers_guide/index.rst - whitepapers/index.rst - copyright.rst - diff --git a/docs/iris/src/copyright.rst b/docs/iris/src/copyright.rst index 71e860336d..08a40e5a1e 100644 --- a/docs/iris/src/copyright.rst +++ b/docs/iris/src/copyright.rst @@ -1,4 +1,4 @@ -========================================== + Iris copyright, licensing and contributors ========================================== @@ -28,7 +28,7 @@ are licensed under the UK's Open Government Licence: .. admonition:: Documentation, example and data license - (C) British Crown Copyright 2019. + (C) British Crown Copyright |copyright_years| You may use and re-use the information featured on this website (not including logos) free of charge in any format or medium, under the terms of the diff --git a/docs/iris/src/developers_guide/code_format.rst b/docs/iris/src/developers_guide/code_format.rst index 8033babceb..c889146269 100644 --- a/docs/iris/src/developers_guide/code_format.rst +++ b/docs/iris/src/developers_guide/code_format.rst @@ -1,6 +1,6 @@ .. _iris_code_format: -Code Formatting +Code formatting *************** To enforce a consistent code format throughout Iris, we recommend using `pre-commit `_ to run diff --git a/docs/iris/src/developers_guide/contributing_documentation.rst b/docs/iris/src/developers_guide/contributing_documentation.rst new file mode 100644 index 0000000000..f8e01ed927 --- /dev/null +++ b/docs/iris/src/developers_guide/contributing_documentation.rst @@ -0,0 +1,145 @@ + +.. toctree:: + :maxdepth: 2 + +.. _contributing.documentation: + +Contributing to the documentation +================================== + +Documentation is important and we encourage any improvements that can be made. +If you believe the documentation is not clear please contribute a change to +improve the documentation for all users. + +Any change to the Iris project whether it is a bugfix, new feature or +documentation update must use the :ref:`development-workflow`. + +.. contents:: Contents: + :local: + + +Requirements +------------ + +The documentation uses specific packages that need to be present. Please see +:ref:`installing_iris` for instructions. + + +Building +-------- + +The build is run from the documentation directory ``iris/docs/iris/src``. In +this directory run:: + + make html + +The build output for the html is found in the ``_build/html`` sub directory. +When updating the documentation ensure the html build has *no errors* or +*warnings* otherwise it may fail the automated `travis-ci`_ build. + +Once the build is complete, if it is rerun it will only rebuild the impacted +build artefacts so should take less time. + +There is also an option to perform a build but skip the +:ref:`contributing.documentation.gallery` creation completely. This can be +achieved via:: + + make html-noplot + +If you wish to run a clean build you can run:: + + make clean + make html + +This is useful for a final test before commiting your changes. + +.. note:: In addition to the automated `travis-ci`_ build of the documentation, + the https://readthedocs.org/ service is also used. The configuration + of this held in a file in the root of the + `github Iris project `_ named + ``.readthedocs.yml``. + +.. _travis-ci: https://travis-ci.org/github/SciTools/iris + +.. _contributing.documentation.testing: + +Testing +------- + +There are three ways to test various aspects of the documentation. + +Each :ref:`contributing.documentation.gallery` entry has a corresponding test. +The below command must be run in the ``iris/docs/iris`` directory:: + + make gallerytest + +Many documentation pages includes python code itself that can be run to ensure it +is still valid. The below command can be run in the ``iris/docs/iris`` or +``iris/docs/iris/src`` directory:: + + make doctest + +Finally, all the hyperlinks in the documentation can be checked automatically. +If there is a link that is known to work it can be excluded from the checks by +adding it to the ``linkcheck_ignore`` array that is defined in the +`conf.py `_. +The hyperlink check can be run via:: + + make linkcheck + +If this fails check the output for the text **broken** and then correct +or ignore the url. + +.. note:: All of the above tests are automatically run as part of the + `travis-ci`_ automated build. + + +.. _contributing.documentation.api: + +Generating API documentation +---------------------------- + +In order to auto generate the API documentation based upon the docstrings a custom +set of python scripts are used, these are located in the directory +``iris/docs/iris/src/sphinxext``. Once the ``make html`` command has been run, +the output of these scripts can be found in +``iris/docs/iris/src/_build/generated/api``. + +If there is a particularly troublesome module that breaks the ``make html`` you +can exclude the module from the API documentation. Add the entry to the +``exclude_modules`` tuple list in the +``iris/docs/iris/src/sphinxext/generate_package_rst.py`` file. + + +.. _contributing.documentation.gallery: + +Gallery +------- + +The Iris :ref:`sphx_glr_generated_gallery` uses a sphinx extension named +`sphinx-gallery `_ +that auto generates reStructuredText (rst) files based upon a gallery source +directory that abides directory and filename convention. + +The code for the gallery entries are in ``iris/docs/iris/gallery_code``. +Each sub directory in this directory is a sub section of the gallery. The +respective ``README.rst`` in each folder is included in the gallery output. + +For each gallery entry there must be a corresponding test script located in +``iris/docs/iris/gallery_tests``. + +To add an entry to the gallery simple place your python code into the appropriate +sub directory and name it with a prefix of ``plot_``. If your gallery entry does not +fit into any existing sub directories then create a new directoy and place it in +there. + +The reStructuredText (rst) output of the gallery is located in +``iris/docs/iris/src/_build/generated/gallery``. + +For more information on the directory structure and options please see the +`sphinx-gallery getting started `_ +documentation. + + + + diff --git a/docs/iris/src/developers_guide/documenting/docstrings.rst b/docs/iris/src/developers_guide/documenting/docstrings.rst index 4499f3fe34..afc56014ea 100644 --- a/docs/iris/src/developers_guide/documenting/docstrings.rst +++ b/docs/iris/src/developers_guide/documenting/docstrings.rst @@ -1,11 +1,12 @@ -================ +=========== Docstrings -================ +=========== Guiding principle: Every public object in the Iris package should have an appropriate docstring. This document has been influenced by the following PEP's, + * Attribute Docstrings `PEP-224 `_ * Docstring Conventions `PEP-257 `_ @@ -60,7 +61,7 @@ The class constructor should be documented in the docstring for its ``__init__`` If a class subclasses another class and its behavior is mostly inherited from that class, its docstring should mention this and summarise the differences. Use the verb "override" to indicate that a subclass method replaces a superclass method and does not call the superclass method; use the verb "extend" to indicate that a subclass method calls the superclass method (in addition to its own behavior). -Attribute and Property docstrings +Attribute and property docstrings --------------------------------- Here is a simple example of a class containing an attribute docstring and a property docstring: diff --git a/docs/iris/src/developers_guide/documenting/rest_guide.rst b/docs/iris/src/developers_guide/documenting/rest_guide.rst index 8ce97a3c4a..e42e27c18e 100644 --- a/docs/iris/src/developers_guide/documenting/rest_guide.rst +++ b/docs/iris/src/developers_guide/documenting/rest_guide.rst @@ -3,26 +3,36 @@ reST quickstart =============== -reST (http://en.wikipedia.org/wiki/ReStructuredText) is a lightweight markup language intended to be highly readable in source format. This guide will cover some of the more frequently used advanced reST markup syntaxes, for the basics of reST the following links may be useful: +reST (http://en.wikipedia.org/wiki/ReStructuredText) is a lightweight markup +language intended to be highly readable in source format. This guide will +cover some of the more frequently used advanced reST markup syntaxes, for the +basics of reST the following links may be useful: - * http://sphinx.pocoo.org/rest.html - * http://docs.geoserver.org/trunk/en/docguide/sphinx.html + * https://www.sphinx-doc.org/en/master/usage/restructuredtext/ * http://packages.python.org/an_example_pypi_project/sphinx.html Reference documentation for reST can be found at http://docutils.sourceforge.net/rst.html. Creating links -------------- -Basic links can be created with ```Text of the link `_`` which will look like `Text of the link `_ +Basic links can be created with ```Text of the link `_`` +which will look like `Text of the link `_ -Documents in the same project can be cross referenced with the syntax ``:doc:`document_name``` for example, to reference the "docstrings" page ``:doc:`docstrings``` creates the following link :doc:`docstrings` +Documents in the same project can be cross referenced with the syntax +``:doc:`document_name``` for example, to reference the "docstrings" page +``:doc:`docstrings``` creates the following link :doc:`docstrings` -References can be created between sections by first making a "label" where you would like the link to point to ``.. _name_of_reference::`` the appropriate link can now be created with ``:ref:`name_of_reference``` (note the trailing underscore on the label) +References can be created between sections by first making a "label" where +you would like the link to point to ``.. _name_of_reference::`` the +appropriate link can now be created with ``:ref:`name_of_reference``` +(note the trailing underscore on the label) -Cross referencing other reference documentation can be achieved with the syntax ``:py:class:`zipfile.ZipFile``` which will result in links such as :py:class:`zipfile.ZipFile` and :py:class:`numpy.ndarray`. +Cross referencing other reference documentation can be achieved with the +syntax ``:py:class:`zipfile.ZipFile``` which will result in links such as +:py:class:`zipfile.ZipFile` and :py:class:`numpy.ndarray`. diff --git a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst index 203a422457..ae4361b2eb 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst @@ -18,7 +18,7 @@ When a new release is prepared, the what's new contributions are combined into a draft what's new document for the release. -Writing a Contribution +Writing a contribution ====================== As introduced above, a contribution is the description of a change to Iris @@ -36,7 +36,7 @@ It is not in itself the final documentation content, so it does not have to be perfect or complete in every respect. -Adding Contribution Files +Adding contribution files ========================= Each release must have a directory called ``contributions_``, @@ -91,7 +91,7 @@ For example: * GRIB2_pdt11 -Complete Examples +Complete examples ----------------- Some sample what's new contribution filenames: @@ -106,7 +106,7 @@ Some sample what's new contribution filenames: latest contributions directory conform to this naming scheme. -Compiling a Draft +Compiling a draft ================= Compiling a draft from the supplied contributions should be done when preparing diff --git a/docs/iris/src/developers_guide/gitwash/development_workflow.rst b/docs/iris/src/developers_guide/gitwash/development_workflow.rst index 4da6b700ba..312e114188 100644 --- a/docs/iris/src/developers_guide/gitwash/development_workflow.rst +++ b/docs/iris/src/developers_guide/gitwash/development_workflow.rst @@ -18,7 +18,7 @@ In what follows we'll refer to the upstream iris ``master`` branch, as * When you are starting a new set of changes, fetch any changes from trunk, and start a new *feature branch* from that. * Make a new branch for each separable set of changes |emdash| "one task, one - branch" (`ipython git workflow`_). + branch". * Name your branch for the purpose of the changes - e.g. ``bugfix-for-issue-14`` or ``refactor-database-code``. * If you can possibly avoid it, avoid merging trunk or any other branches into @@ -31,7 +31,7 @@ This way of working helps to keep work well organized, with readable history. This in turn makes it easier for project maintainers (that might be you) to see what you've done, and why you did it. -See `linux git workflow`_ and `ipython git workflow`_ for some explanation. +See `linux git workflow`_ for some explanation. Consider deleting your master branch ==================================== diff --git a/docs/iris/src/developers_guide/gitwash/forking_hell.rst b/docs/iris/src/developers_guide/gitwash/forking_hell.rst index 2b38c02736..4b591d7b0e 100644 --- a/docs/iris/src/developers_guide/gitwash/forking_hell.rst +++ b/docs/iris/src/developers_guide/gitwash/forking_hell.rst @@ -1,7 +1,7 @@ .. _forking: ====================================================== -Making your own copy (fork) of iris +Making your own copy (fork) of Iris ====================================================== You need to do this only once. The instructions here are very similar @@ -17,7 +17,7 @@ If you don't have a github account, go to the github page, and make one. You then need to configure your account to allow write access |emdash| see the ``Generating SSH keys`` help on `github help`_. -Create your own forked copy of `iris`_ +Create your own forked copy of `Iris`_ ====================================================== #. Log into your github account. diff --git a/docs/iris/src/developers_guide/gitwash/git_development.rst b/docs/iris/src/developers_guide/gitwash/git_development.rst index c5b910d863..1b4398e132 100644 --- a/docs/iris/src/developers_guide/gitwash/git_development.rst +++ b/docs/iris/src/developers_guide/gitwash/git_development.rst @@ -4,8 +4,6 @@ Git for development ===================== -Contents: - .. toctree:: :maxdepth: 2 diff --git a/docs/iris/src/developers_guide/gitwash/git_install.rst b/docs/iris/src/developers_guide/gitwash/git_install.rst index 3be5149b90..d63f188b2e 100644 --- a/docs/iris/src/developers_guide/gitwash/git_install.rst +++ b/docs/iris/src/developers_guide/gitwash/git_install.rst @@ -7,12 +7,12 @@ Overview ======== -================ ============= +================ =============================== Debian / Ubuntu ``sudo apt-get install git`` Fedora ``sudo yum install git`` Windows Download and install msysGit_ OS X Use the git-osx-installer_ -================ ============= +================ =============================== In detail ========= @@ -21,6 +21,4 @@ See the git page for the most recent information. Have a look at the github install help pages available from `github help`_ -There are good instructions here: http://book.git-scm.com/2_installing_git.html - .. include:: links.inc diff --git a/docs/iris/src/developers_guide/gitwash/git_links.inc b/docs/iris/src/developers_guide/gitwash/git_links.inc index 8e628ae19e..28cae6a025 100644 --- a/docs/iris/src/developers_guide/gitwash/git_links.inc +++ b/docs/iris/src/developers_guide/gitwash/git_links.inc @@ -15,7 +15,6 @@ .. _msysgit: http://code.google.com/p/msysgit/downloads/list .. _git-osx-installer: http://code.google.com/p/git-osx-installer/downloads/list .. _subversion: http://subversion.tigris.org/ -.. _git cheat sheet: http://github.com/guides/git-cheat-sheet .. _pro git book: http://progit.org/ .. _git svn crash course: http://git-scm.com/course/svn.html .. _learn.github: http://learn.github.com/ diff --git a/docs/iris/src/developers_guide/gitwash/git_resources.rst b/docs/iris/src/developers_guide/gitwash/git_resources.rst index d18b0ef48b..6f05422771 100644 --- a/docs/iris/src/developers_guide/gitwash/git_resources.rst +++ b/docs/iris/src/developers_guide/gitwash/git_resources.rst @@ -1,7 +1,7 @@ .. _git-resources: ============= -git resources +Git resources ============= Tutorials and summaries @@ -9,8 +9,6 @@ Tutorials and summaries * `github help`_ has an excellent series of how-to guides. * `learn.github`_ has an excellent series of tutorials -* The `pro git book`_ is a good in-depth book on git. -* A `git cheat sheet`_ is a page giving summaries of common commands. * The `git user manual`_ * The `git tutorial`_ * The `git community book`_ @@ -22,7 +20,6 @@ Tutorials and summaries * Fernando Perez' git page |emdash| `Fernando's git page`_ |emdash| many links and tips * A good but technical page on `git concepts`_ -* `git svn crash course`_: git for those of us used to subversion_ Advanced git workflow ===================== @@ -30,7 +27,6 @@ Advanced git workflow There are many ways of working with git; here are some posts on the rules of thumb that other projects have come up with: -* Linus Torvalds on `git management`_ * Linus Torvalds on `linux git workflow`_ . Summary; use the git tools to make the history of your edits as clean as possible; merge from upstream edits as little as possible in branches where you are doing diff --git a/docs/iris/src/developers_guide/gitwash/index.rst b/docs/iris/src/developers_guide/gitwash/index.rst index 35eee1944a..a69515548a 100644 --- a/docs/iris/src/developers_guide/gitwash/index.rst +++ b/docs/iris/src/developers_guide/gitwash/index.rst @@ -3,8 +3,6 @@ Working with *iris* source code ================================================ -Contents: - .. toctree:: :maxdepth: 2 diff --git a/docs/iris/src/developers_guide/graphics_tests.rst b/docs/iris/src/developers_guide/graphics_tests.rst index 2782f319ec..e84a59d48d 100644 --- a/docs/iris/src/developers_guide/graphics_tests.rst +++ b/docs/iris/src/developers_guide/graphics_tests.rst @@ -1,26 +1,29 @@ +:orphan: + .. _developer_graphics_tests: Graphics tests ************** The only practical way of testing plotting functionality is to check actual -output plots. -For this, a basic 'graphics test' assertion operation is provided in the method -:meth:`iris.tests.IrisTest.check_graphic` : This tests plotted output for a -match against a stored reference. -A "graphics test" is any test which employs this. - -At present, such tests include the testing for modules `iris.tests.test_plot` -and `iris.tests.test_quickplot`, all output plots from the gallery examples -(contained in `docs/iris/example_tests`), and a few other 'legacy' style tests -(as described in :ref:`developer_tests`). +output plots. For this, a basic 'graphics test' assertion operation is +provided in the method :meth:`iris.tests.IrisTest.check_graphic` : This +tests plotted output for a match against a stored reference. A +"graphics test" is any test which employs this. + +At present, such tests include the testing for modules ``iris.tests.test_plot`` +and ``iris.tests.test_quickplot``, all output plots from the gallery +contained in ``docs/iris/gallery_tests``, and a few other 'legacy' style tests +as described in :ref:`developer_tests` +. It is conceivable that new 'graphics tests' of this sort can still be added. However, as graphics tests are inherently "integration" style rather than true unit tests, results can differ with the installed versions of dependent libraries (see below), so this is not recommended except where no alternative is practical. -Testing actual plot results introduces some significant difficulties : +Testing actual plot results introduces some significant difficulties: + * Graphics tests are inherently 'integration' style tests, so results will often vary with the versions of key dependencies, i.e. the exact versions of third-party modules which are installed : Obviously, results will depend on @@ -36,7 +39,7 @@ Testing actual plot results introduces some significant difficulties : given multiple independent sources of variation. -Graphics Testing Strategy +Graphics testing strategy ========================= In the Iris Travis matrix, and over time, graphics tests must run with @@ -63,8 +66,8 @@ This consists of : existing accepted reference images, for each failing test. -How to Add New 'Acceptable' Result Images to Existing Tests -======================================== +How to add new 'Acceptable' result images to existing tests +=========================================================== When you find that a graphics test in the Iris testing suite has failed, following changes in Iris or the run dependencies, this is the process diff --git a/docs/iris/src/developers_guide/index.rst b/docs/iris/src/developers_guide/index.rst deleted file mode 100644 index a98a9f0f3a..0000000000 --- a/docs/iris/src/developers_guide/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. _userguide-index: - -.. This is the source doc for the user guide - -##################### - Iris developer guide -##################### - - -.. toctree:: - :maxdepth: 3 - - documenting/index.rst - gitwash/index.rst - code_format.rst - pulls.rst - tests.rst - deprecations.rst - release.rst diff --git a/docs/iris/src/developers_guide/pulls.rst b/docs/iris/src/developers_guide/pulls.rst index 6546a15642..62535d27c6 100644 --- a/docs/iris/src/developers_guide/pulls.rst +++ b/docs/iris/src/developers_guide/pulls.rst @@ -1,6 +1,6 @@ .. _pr_check: -Pull Request Check List +Pull request check List *********************** A pull request to a SciTools project master should be ready to merge into the @@ -15,7 +15,7 @@ The check list summarises criteria which will be checked before a pull request is merged. Before submitting a pull request please consider this list. -The Iris Check List +The Iris check list ==================== * Have you provided a helpful description of the Pull Request? @@ -75,7 +75,7 @@ The Iris Check List * Do the documentation and code-example tests pass? - * Run with ``make doctest`` and ``make extest``, from within the subdirectory + * Run with ``make doctest`` and ``make gallerytest``, from within the subdirectory ``./docs/iris``. * note that code examples must *not* raise deprecations. This is now checked and will result in an error. @@ -85,8 +85,8 @@ The Iris Check List * ``./.travis.yml`` is used to manage the continuous integration testing. * the files ``./conda-requirements.yml`` and - ``./minimal-conda-requirements.yml`` are used to define the software - environments used, using the conda_ package manager. + ``./minimal-conda-requirements.yml`` are used to define the software + environments used, using the conda_ package manager. * Have you provided updates to supporting projects for test or example data? @@ -108,7 +108,7 @@ The Iris Check List .. _PEP8: http://www.python.org/dev/peps/pep-0008/ .. _python-pep8: https://pypi.python.org/pypi/pep8 -.. _conda: http://conda.readthedocs.io/en/latest/ +.. _conda: https://docs.conda.io/en/latest/ .. _iris-test-data: https://github.com/SciTools/iris-test-data .. _iris-sample-data: https://github.com/SciTools/iris-sample-data .. _test-images-scitools: https://github.com/SciTools/test-images-scitools diff --git a/docs/iris/src/developers_guide/release.rst b/docs/iris/src/developers_guide/release.rst index 437478a6a0..5d1a683d44 100644 --- a/docs/iris/src/developers_guide/release.rst +++ b/docs/iris/src/developers_guide/release.rst @@ -5,7 +5,7 @@ Releases A release of Iris is a tag on the SciTools/Iris Github repository. -Release Branch +Release branch ============== Once the features intended for the release are on master, a release branch should be created, in the SciTools/Iris repository. This will have the name: @@ -18,7 +18,7 @@ for example: This branch shall be used to finalise the release details in preparation for the release candidate. -Release Candidate +Release candidate ================= Prior to a release, a release candidate tag may be created, marked as a pre-release in github, with a tag ending with :literal:`rc` followed by a number, e.g.: @@ -38,24 +38,24 @@ The documentation should include all of the what's new snippets, which must be c Upon release, the documentation shall be added to the SciTools scitools.org.uk github project's gh-pages branch as the latest documentation. -Testing the Conda Recipe +Testing the conda recipe ======================== Before a release is cut, the SciTools conda-recipes-scitools recipe for Iris shall be tested to build the release branch of Iris; this test recipe shall not be merged onto conda-recipes-scitools. -The Release +The release =========== The final steps are to change the version string in the source of :literal:`Iris.__init__.py` and include the release date in the relevant what's new page within the documentation. Once all checks are complete, the release is cut by the creation of a new tag in the SciTools Iris repository. -Conda Recipe +Conda recipe ============ Once a release is cut, the SciTools conda-recipes-scitools recipe for Iris shall be updated to build the latest release of Iris and push this artefact to anaconda.org. The build and push is all automated as part of the merge process. -Merge Back +Merge back ========== After the release is cut, the changes shall be merged back onto the scitools master. @@ -63,7 +63,7 @@ After the release is cut, the changes shall be merged back onto the scitools mas To achieve this, first cut a local branch from the release branch, :literal:`{release}.x`. Next add a commit changing the release string to match the release string on scitools/master. This branch can now be proposed as a pull request to master. This work flow ensures that the commit identifiers are consistent between the :literal:`.x` branch and :literal:`master`. -Point Releases +Point releases ============== Bug fixes may be implemented and targeted as the :literal:`.x` branch. These should lead to a new point release, another tag. diff --git a/docs/iris/src/developers_guide/tests.rst b/docs/iris/src/developers_guide/tests.rst index 417db96f32..0322abfdba 100644 --- a/docs/iris/src/developers_guide/tests.rst +++ b/docs/iris/src/developers_guide/tests.rst @@ -7,6 +7,7 @@ The Iris tests may be run with ``python setup.py test`` which has a command line utility included. There are three categories of tests within Iris: + - Unit tests - Integration tests - Legacy tests diff --git a/docs/iris/src/index.rst b/docs/iris/src/index.rst new file mode 100644 index 0000000000..035d0c07b5 --- /dev/null +++ b/docs/iris/src/index.rst @@ -0,0 +1,87 @@ +Iris Documentation +================== + +**A powerful, format-agnostic, community-driven Python library for analysing and +visualising Earth science data.** + +Iris implements a data model based on the `CF conventions `_ +giving you a powerful, format-agnostic interface for working with your data. +It excels when working with multi-dimensional Earth Science data, where tabular +representations become unwieldy and inefficient. + +`CF Standard names `_, +`units `_, and coordinate metadata +are built into Iris, giving you a rich and expressive interface for maintaining +an accurate representation of your data. Its treatment of data and +associated metadata as first-class objects includes: + +* visualisation interface based on `matplotlib `_ and + `cartopy `_, +* unit conversion, +* subsetting and extraction, +* merge and concatenate, +* aggregations and reductions (including min, max, mean and weighted averages), +* interpolation and regridding (including nearest-neighbor, linear and area-weighted), and +* operator overloads (``+``, ``-``, ``*``, ``/``, etc.). + +A number of file formats are recognised by Iris, including CF-compliant NetCDF, GRIB, +and PP, and it has a plugin architecture to allow other formats to be added seamlessly. + +Building upon `NumPy `_ and +`dask `_, +Iris scales from efficient single-machine workflows right through to multi-core +clusters and HPC. +Interoperability with packages from the wider scientific Python ecosystem comes from Iris' +use of standard NumPy/dask arrays as its underlying data storage. + + +.. toctree:: + :maxdepth: 1 + :caption: Getting started + + installing + generated/gallery/index + + +.. toctree:: + :maxdepth: 1 + :caption: User Guide + + userguide/index + userguide/iris_cubes + userguide/loading_iris_cubes + userguide/saving_iris_cubes + userguide/navigating_a_cube + userguide/subsetting_a_cube + userguide/real_and_lazy_data + userguide/plotting_a_cube + userguide/interpolation_and_regridding + userguide/merge_and_concat + userguide/cube_statistics + userguide/cube_maths + userguide/citation + userguide/code_maintenance + + +.. toctree:: + :maxdepth: 1 + :caption: Developers Guide + + developers_guide/contributing_documentation + developers_guide/documenting/index + developers_guide/gitwash/index + developers_guide/code_format + developers_guide/pulls + developers_guide/tests + developers_guide/deprecations + developers_guide/release + generated/api/iris + + +.. toctree:: + :maxdepth: 1 + :caption: Reference + + whatsnew/index + techpapers/index + copyright diff --git a/docs/iris/src/installing.rst b/docs/iris/src/installing.rst index 6b6999ab82..faa46afa50 100644 --- a/docs/iris/src/installing.rst +++ b/docs/iris/src/installing.rst @@ -1,7 +1,6 @@ .. _installing_iris: -**************** Installing Iris -**************** +*************** .. include:: ../../../INSTALL diff --git a/docs/iris/src/sphinxext/auto_label_figures.py b/docs/iris/src/sphinxext/auto_label_figures.py deleted file mode 100644 index 6fb72826fe..0000000000 --- a/docs/iris/src/sphinxext/auto_label_figures.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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 -# licensing details. - -import os -from docutils import nodes - - -def auto_label_figures(app, doctree): - """ - Add a label on every figure. - """ - - for fig in doctree.traverse(condition=nodes.figure): - for img in fig.traverse(condition=nodes.image): - fname, ext = os.path.splitext(img['uri']) - if ext == '.png': - fname = os.path.basename(fname).replace('_', '-') - fig['ids'].append(fname) - - -def setup(app): - app.connect('doctree-read', auto_label_figures) diff --git a/docs/iris/src/sphinxext/custom_class_autodoc.py b/docs/iris/src/sphinxext/custom_class_autodoc.py index a558732bd1..cbde413f2d 100644 --- a/docs/iris/src/sphinxext/custom_class_autodoc.py +++ b/docs/iris/src/sphinxext/custom_class_autodoc.py @@ -8,9 +8,12 @@ from sphinx.ext.autodoc import * from sphinx.util import force_decode from sphinx.util.docstrings import prepare_docstring - import inspect +# stop warnings cluttering the make output +import warnings +warnings.filterwarnings("ignore") + class ClassWithConstructorDocumenter(autodoc.ClassDocumenter): priority = 1000000 @@ -80,4 +83,4 @@ def format_args(self): def setup(app): - app.add_autodocumenter(ClassWithConstructorDocumenter) + app.add_autodocumenter(ClassWithConstructorDocumenter, override=True) diff --git a/docs/iris/src/sphinxext/custom_data_autodoc.py b/docs/iris/src/sphinxext/custom_data_autodoc.py index ade07dbc4e..eecd395101 100644 --- a/docs/iris/src/sphinxext/custom_data_autodoc.py +++ b/docs/iris/src/sphinxext/custom_data_autodoc.py @@ -44,5 +44,5 @@ def handler(app, what, name, obj, options, signature, return_annotation): def setup(app): - app.add_autodocumenter(IrisDataDocumenter) + app.add_autodocumenter(IrisDataDocumenter, override=True) app.connect('autodoc-process-signature', handler) diff --git a/docs/iris/src/sphinxext/gen_example_directory.py b/docs/iris/src/sphinxext/gen_example_directory.py deleted file mode 100644 index c5de195670..0000000000 --- a/docs/iris/src/sphinxext/gen_example_directory.py +++ /dev/null @@ -1,168 +0,0 @@ -# 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 -# licensing details. - - -''' -Generate the rst files for the examples -''' - -import os -import re -import shutil -import sys - - -def out_of_date(original, derived): - ''' - Returns True if derivative is out-of-date wrt original, - both of which are full file paths. - - TODO: this check isn't adequate in some cases, e.g., if we discover - a bug when building the examples, the original and derived will be - unchanged but we still want to force a rebuild. - ''' - return (not os.path.exists(derived) or - os.stat(derived).st_mtime < os.stat(original).st_mtime) - - -docstring_regex = re.compile(r'[\'\"]{3}(.*?)[\'\"]{3}', re.DOTALL) - - -noplot_regex = re.compile(r'#\s*-\*-\s*noplot\s*-\*-') - - -def generate_example_rst(app): - # Example code can be found at the same level as the documentation - # src folder. - rootdir = os.path.join(os.path.dirname(app.builder.srcdir), 'example_code') - - # Examples are built as a subfolder of the src folder. - exampledir = os.path.join(app.builder.srcdir, 'examples') - - if not os.path.exists(exampledir): - os.makedirs(exampledir) - - datad = {} - for root, subFolders, files in os.walk(rootdir): - for fname in files: - if (fname.startswith('.') or fname.startswith('#') or - fname.startswith('_') or fname.find('.svn') >= 0 or - not fname.endswith('.py')): - continue - - fullpath = os.path.join(root, fname) - with open(fullpath) as fh: - contents = fh.read() - # indent - relpath = os.path.split(root)[-1] - datad.setdefault(relpath, []).append((fullpath, fname, contents)) - - subdirs = sorted(datad.keys()) - - index = [] - index.append('''\ -Iris examples -============= - -.. toctree:: - :maxdepth: 2 - -''') - - for subdir in subdirs: - rstdir = os.path.join(exampledir, subdir) - if not os.path.exists(rstdir): - os.makedirs(rstdir) - - outputdir = os.path.join(app.builder.outdir, 'examples') - if not os.path.exists(outputdir): - os.makedirs(outputdir) - - outputdir = os.path.join(outputdir, subdir) - if not os.path.exists(outputdir): - os.makedirs(outputdir) - - index.append(' {}/index.rst\n'.format(subdir)) - subdir_root_path = os.path.join(rootdir, subdir) - subdirIndex = [] - - # Use the __init__.py file's docstring for the subdir example page (if - # __init__ exists). - if os.path.exists(os.path.join(subdir_root_path, '__init__.py')): - import imp - mod = imp.load_source( - subdir, - os.path.join(subdir_root_path, '__init__.py')) - subdirIndex.append(mod.__doc__) - else: - line = 'Examples in {}\n'.format(subdir) - subdirIndex.extend([line, '=' * len(line)]) - - # Append the code to produce the toctree. - subdirIndex.append(''' -.. toctree:: - :maxdepth: 1 - -''') - - sys.stdout.write(subdir + ', ') - sys.stdout.flush() - - data = sorted(datad[subdir]) - - for fullpath, fname, contents in data: - basename, ext = os.path.splitext(fname) - outputfile = os.path.join(outputdir, fname) - - rstfile = '{}.rst'.format(basename) - outrstfile = os.path.join(rstdir, rstfile) - - subdirIndex.append(' {}\n'.format(rstfile)) - - if not out_of_date(fullpath, outrstfile): - continue - - out = [] - out.append('.. _{}-{}:\n\n'.format(subdir, basename)) - - # Copy the example code to be in the src examples directory. This - # means we can define a simple relative path in the plot directive, - # which can also copy the file into the resulting build directory. - shutil.copy(fullpath, rstdir) - - docstring_results = docstring_regex.search(contents) - if docstring_results is not None: - out.append(docstring_results.group(1)) - else: - title = '{} example code: {}'.format(subdir, fname) - out.append(title + '\n') - out.append('=' * len(title) + '\n\n') - - if not noplot_regex.search(contents): - rel_example = os.path.relpath(outputfile, app.builder.outdir) - out.append('\n\n.. plot:: {}\n'.format(rel_example)) - out.append(' :include-source:\n\n') - else: - out.append('[`source code <{}>`_]\n\n'.format(fname)) - out.append('.. literalinclude:: {}\n\n'.format(fname)) - # Write the .py file contents (we didn't need to do this for - # plots as the plot directive does this for us.) - with open(outputfile, 'w') as fhstatic: - fhstatic.write(contents) - - with open(outrstfile, 'w') as fh: - fh.writelines(out) - - subdirIndexFile = os.path.join(rstdir, 'index.rst') - with open(subdirIndexFile, 'w') as fhsubdirIndex: - fhsubdirIndex.writelines(subdirIndex) - - with open(os.path.join(exampledir, 'index.rst'), 'w') as fhindex: - fhindex.writelines(index) - - -def setup(app): - app.connect('builder-inited', generate_example_rst) diff --git a/docs/iris/src/sphinxext/gen_gallery.py b/docs/iris/src/sphinxext/gen_gallery.py deleted file mode 100644 index b4b88ff3bd..0000000000 --- a/docs/iris/src/sphinxext/gen_gallery.py +++ /dev/null @@ -1,201 +0,0 @@ -# -# (C) Copyright 2012 MATPLOTLIB (vn 1.2.0) -# - -''' -Generate a thumbnail gallery of examples. -''' - -import os -import glob -import re -import warnings - -import matplotlib.image as image -from sphinx.util import status_iterator - -from sphinx.util import status_iterator - -template = '''\ -{{% extends "layout.html" %}} -{{% set title = "Thumbnail gallery" %}} - - -{{% block body %}} - -

    Click on any image to see full size image and source code

    -
    - - - -{} -{{% endblock %}} -''' - -multiimage = re.compile('(.*?)(_\d\d){1,2}') - - -def make_thumbnail(args): - image.thumbnail(args[0], args[1], 0.4) - - -def out_of_date(original, derived): - return (not os.path.exists(derived) or - os.stat(derived).st_mtime < os.stat(original).st_mtime) - - -def gen_gallery(app, doctree): - if app.builder.name != 'html': - return - - outdir = app.builder.outdir - rootdir = 'examples' - - # Images we want to skip for the gallery because they are an unusual - # size that doesn't layout well in a table, or because they may be - # redundant with other images or uninteresting. - skips = set([ - 'mathtext_examples', - 'matshow_02', - 'matshow_03', - 'matplotlib_icon']) - - thumbnails = {} - rows = [] - random_image = [] - toc_rows = [] - - link_template = ('' - '{alternative_text}' - '') - - header_template = ('
    ' - '

    {}' - '' - '

    ') - - toc_template = ('
  • ' - '{}' - '
  • ') - - random_image_content_template = ''' -// This file was automatically generated by gen_gallery.py & should not be -// modified directly. - -images = new Array(); - -{} - -''' - - random_image_template = "['{thumbfile}', '{full_image}', '{link}'];" - random_image_join = 'images[{}] = {}' - - dirs = ('General', 'Meteorology', 'Oceanography') - - for subdir in dirs: - rows.append(header_template.format(subdir, subdir, subdir)) - toc_rows.append(toc_template.format(subdir, subdir)) - - origdir = os.path.join(os.path.dirname(outdir), rootdir, subdir) - if not os.path.exists(origdir): - origdir = os.path.join(os.path.dirname(outdir), 'plot_directive', - rootdir, subdir) - thumbdir = os.path.join(outdir, rootdir, subdir, 'thumbnails') - if not os.path.exists(thumbdir): - os.makedirs(thumbdir) - - data = [] - - for filename in sorted(glob.glob(os.path.join(origdir, '*.png'))): - if filename.endswith('hires.png'): - continue - - path, filename = os.path.split(filename) - basename, ext = os.path.splitext(filename) - if basename in skips: - continue - - # Create thumbnails based on images in tmpdir, and place them - # within the build tree. - orig_path = str(os.path.join(origdir, filename)) - thumb_path = str(os.path.join(thumbdir, filename)) - if out_of_date(orig_path, thumb_path) or True: - thumbnails[orig_path] = thumb_path - - m = multiimage.match(basename) - if m is not None: - basename = m.group(1) - - data.append((subdir, basename, - os.path.join(rootdir, subdir, 'thumbnails', - filename))) - - for (subdir, basename, thumbfile) in data: - if thumbfile is not None: - anchor = os.path.basename(thumbfile) - anchor = os.path.splitext(anchor)[0].replace('_', '-') - link = 'examples/{}/{}.html#{}'.format( - subdir, - basename, - anchor) - rows.append(link_template.format( - href=link, - thumb_file=thumbfile, - alternative_text=basename)) - random_image.append(random_image_template.format( - link=link, - thumbfile=thumbfile, - basename=basename, - full_image='_images/' + os.path.basename(thumbfile))) - - if len(data) == 0: - warnings.warn('No thumbnails were found in {}'.format(subdir)) - - # Close out the
    opened up at the top of this loop. - rows.append('
    ') - - # Generate JS list of images for front page. - random_image_content = '\n'.join([random_image_join.format(i, line) - for i, line in enumerate(random_image)]) - random_image_content = random_image_content_template.format( - random_image_content) - random_image_script_path = os.path.join(app.builder.srcdir, - '_static', - 'random_image.js') - with open(random_image_script_path, 'w') as fh: - fh.write(random_image_content) - - content = template.format('\n'.join(toc_rows), - '\n'.join(rows)) - - # Only write out the file if the contents have actually changed. - # Otherwise, this triggers a full rebuild of the docs. - - gallery_path = os.path.join(app.builder.srcdir, - '_templates', - 'gallery.html') - if os.path.exists(gallery_path): - with open(gallery_path, 'r') as fh: - regenerate = fh.read() != content - else: - regenerate = True - if regenerate: - with open(gallery_path, 'w') as fh: - fh.write(content) - - for key in status_iterator(thumbnails, 'generating thumbnails... ', - length=len(thumbnails)): - image.thumbnail(key, thumbnails[key], 0.3) - - -def setup(app): - app.connect('env-updated', gen_gallery) diff --git a/docs/iris/src/sphinxext/generate_package_rst.py b/docs/iris/src/sphinxext/generate_package_rst.py index 0c6510c170..5ce9f6d014 100644 --- a/docs/iris/src/sphinxext/generate_package_rst.py +++ b/docs/iris/src/sphinxext/generate_package_rst.py @@ -8,11 +8,23 @@ import sys import re import inspect +import ntpath + +# list of tuples for modules to exclude. Useful if the documentation throws +# warnings, especially for experimental modules. +exclude_modules = [ + ("experimental/raster", "iris.experimental.raster") # gdal conflicts +] + + +# print to stdout, including the name of the python file +def autolog(message): + print("[{}] {}".format(ntpath.basename(__file__), message)) document_dict = { # Use autoclass for classes. - 'class': ''' + "class": """ {object_docstring} .. @@ -22,20 +34,21 @@ :undoc-members: :inherited-members: -''', - 'function': ''' +""", + "function": """ .. autofunction:: {object_name} -''', +""", # For everything else, let automodule do some magic... - None: ''' + None: """ .. autodata:: {object_name} -'''} +""", +} -horizontal_sep = ''' +horizontal_sep = """ .. raw:: html

    ↑ top ↑

    @@ -47,21 +60,22 @@ --> -''' +""" def lookup_object_type(obj): if inspect.isclass(obj): - return 'class' + return "class" elif inspect.isfunction(obj): - return 'function' + return "function" else: return None -def auto_doc_module(file_path, import_name, root_package, - package_toc=None, title=None): - doc = r'''.. _{import_name}: +def auto_doc_module( + file_path, import_name, root_package, package_toc=None, title=None +): + doc = r""".. _{import_name}: {title_underline} {title} @@ -77,54 +91,66 @@ def auto_doc_module(file_path, import_name, root_package, {module_elements} +""" -''' if package_toc: - sidebar = ''' -.. sidebar:: Modules in this package - + sidebar = """ {package_toc_tree} - '''.format(package_toc_tree=package_toc) + """.format( + package_toc_tree=package_toc + ) else: - sidebar = '' + sidebar = "" try: mod = __import__(import_name) except ImportError as e: - message = r'''.. error:: + message = r""".. error:: This module could not be imported. Some dependencies are missing:: - ''' + str(e) - return doc.format(title=title or import_name, - title_underline='=' * len(title or import_name), - import_name=import_name, root_package=root_package, - sidebar=sidebar, module_elements=message) + """ + str( + e + ) + return doc.format( + title=title or import_name, + title_underline="=" * len(title or import_name), + import_name=import_name, + root_package=root_package, + sidebar=sidebar, + module_elements=message, + ) mod = sys.modules[import_name] elems = dir(mod) - if '__all__' in elems: - document_these = [(attr_name, getattr(mod, attr_name)) - for attr_name in mod.__all__] + if "__all__" in elems: + document_these = [ + (attr_name, getattr(mod, attr_name)) for attr_name in mod.__all__ + ] else: - document_these = [(attr_name, getattr(mod, attr_name)) - for attr_name in elems - if (not attr_name.startswith('_') and - not inspect.ismodule(getattr(mod, attr_name)))] + document_these = [ + (attr_name, getattr(mod, attr_name)) + for attr_name in elems + if ( + not attr_name.startswith("_") + and not inspect.ismodule(getattr(mod, attr_name)) + ) + ] def is_from_this_module(arg): - name = arg[0] + # name = arg[0] obj = arg[1] - return (hasattr(obj, '__module__') and - obj.__module__ == mod.__name__) + return ( + hasattr(obj, "__module__") and obj.__module__ == mod.__name__ + ) - sort_order = {'class': 2, 'function': 1} + sort_order = {"class": 2, "function": 1} # Sort them according to sort_order dict. def sort_key(arg): - name = arg[0] + # name = arg[0] obj = arg[1] return sort_order.get(lookup_object_type(obj), 0) @@ -133,63 +159,81 @@ def sort_key(arg): lines = [] for element, obj in document_these: - object_name = import_name + '.' + element + object_name = import_name + "." + element obj_content = document_dict[lookup_object_type(obj)].format( object_name=object_name, - object_name_header_line='+' * len(object_name), - object_docstring=inspect.getdoc(obj)) + object_name_header_line="+" * len(object_name), + object_docstring=inspect.getdoc(obj), + ) lines.append(obj_content) lines = horizontal_sep.join(lines) - module_elements = '\n'.join(' * :py:obj:`{}`'.format(element) - for element, obj in document_these) + module_elements = "\n".join( + " * :py:obj:`{}`".format(element) for element, obj in document_these + ) lines = doc + lines - return lines.format(title=title or import_name, - title_underline='=' * len(title or import_name), - import_name=import_name, root_package=root_package, - sidebar=sidebar, module_elements=module_elements) + return lines.format( + title=title or import_name, + title_underline="=" * len(title or import_name), + import_name=import_name, + root_package=root_package, + sidebar=sidebar, + module_elements=module_elements, + ) def auto_doc_package(file_path, import_name, root_package, sub_packages): - max_depth = 1 if import_name == 'iris' else 2 - package_toc = '\n '.join(sub_packages) - package_toc = ''' + max_depth = 1 if import_name == "iris" else 2 + package_toc = "\n ".join(sub_packages) + + package_toc = """ .. toctree:: :maxdepth: {:d} :titlesonly: + :hidden: {} -'''.format(max_depth, package_toc) +""".format( + max_depth, package_toc + ) - if '.' in import_name: + if "." in import_name: title = None else: - title = import_name.capitalize() + ' reference documentation' + title = import_name.capitalize() + " API" - return auto_doc_module(file_path, import_name, root_package, - package_toc=package_toc, title=title) + return auto_doc_module( + file_path, + import_name, + root_package, + package_toc=package_toc, + title=title, + ) def auto_package_build(app): root_package = app.config.autopackage_name if root_package is None: - raise ValueError('set the autopackage_name variable in the ' - 'conf.py file') + raise ValueError( + "set the autopackage_name variable in the " "conf.py file" + ) if not isinstance(root_package, list): - raise ValueError('autopackage was expecting a list of packages to ' - 'document e.g. ["itertools"]') + raise ValueError( + "autopackage was expecting a list of packages to " + 'document e.g. ["itertools"]' + ) for package in root_package: do_package(package) def do_package(package_name): - out_dir = package_name + os.path.sep + out_dir = "generated/api" + os.path.sep # Import the root package. If this fails then an import error will be # raised. @@ -199,38 +243,45 @@ def do_package(package_name): package_folder = [] module_folders = {} + for root, subFolders, files in os.walk(rootdir): for fname in files: name, ext = os.path.splitext(fname) # Skip some non-relevant files. - if (fname.startswith('.') or fname.startswith('#') or - re.search('^_[^_]', fname) or fname.find('.svn') >= 0 or - not (ext in ['.py', '.so'])): + if ( + fname.startswith(".") + or fname.startswith("#") + or re.search("^_[^_]", fname) + or fname.find(".svn") >= 0 + or not (ext in [".py", ".so"]) + ): continue # Handle new shared library naming conventions - if ext == '.so': - name = name.split('.', 1)[0] + if ext == ".so": + name = name.split(".", 1)[0] - rel_path = root_package + \ - os.path.join(root, fname).split(rootdir)[-1] - mod_folder = root_package + \ - os.path.join(root).split(rootdir)[-1].replace('/', '.') + rel_path = ( + root_package + os.path.join(root, fname).split(rootdir)[-1] + ) + mod_folder = root_package + os.path.join(root).split(rootdir)[ + -1 + ].replace("/", ".") # Only add this package to folder list if it contains an __init__ # script. - if name == '__init__': + if name == "__init__": package_folder.append([mod_folder, rel_path]) else: - import_name = mod_folder + '.' + name + import_name = mod_folder + "." + name mf_list = module_folders.setdefault(mod_folder, []) mf_list.append((import_name, rel_path)) if not os.path.exists(out_dir): os.makedirs(out_dir) for package, package_path in package_folder: - if '._' in package or 'test' in package: + if "._" in package or "test" in package: continue paths = [] @@ -242,60 +293,83 @@ def do_package(package_name): continue if not spackage.startswith(package): continue - if spackage.count('.') > package.count('.') + 1: + if spackage.count(".") > package.count(".") + 1: continue - if 'test' in spackage: + if "test" in spackage: continue - split_path = spackage.rsplit('.', 2)[-2:] - if any(part[0] == '_' for part in split_path): + split_path = spackage.rsplit(".", 2)[-2:] + if any(part[0] == "_" for part in split_path): continue - paths.append(os.path.join(*split_path) + '.rst') + paths.append(os.path.join(*split_path) + ".rst") - paths.extend(os.path.join(os.path.basename(os.path.dirname(path)), - os.path.basename(path).split('.', 1)[0]) - for imp_name, path in module_folders.get(package, [])) + paths.extend( + os.path.join( + os.path.basename(os.path.dirname(path)), + os.path.basename(path).split(".", 1)[0], + ) + for imp_name, path in module_folders.get(package, []) + ) paths.sort() + + # check for any modules to exclude + for exclude_module in exclude_modules: + if exclude_module[0] in paths: + autolog( + "Excluding module in package: {}".format(exclude_module[0]) + ) + paths.remove(exclude_module[0]) + doc = auto_doc_package(package_path, package, root_package, paths) - package_dir = out_dir + package.replace('.', os.path.sep) + package_dir = out_dir + package.replace(".", os.path.sep) if not os.path.exists(package_dir): - os.makedirs(out_dir + package.replace('.', os.path.sep)) + os.makedirs(out_dir + package.replace(".", os.path.sep)) - out_path = package_dir + '.rst' + out_path = package_dir + ".rst" if not os.path.exists(out_path): - print('Creating non-existent document {} ...'.format(out_path)) - with open(out_path, 'w') as fh: + autolog("Creating {} ...".format(out_path)) + with open(out_path, "w") as fh: fh.write(doc) else: - with open(out_path, 'r') as fh: - existing_content = ''.join(fh.readlines()) + with open(out_path, "r") as fh: + existing_content = "".join(fh.readlines()) if doc != existing_content: - print('Creating out of date document {} ...'.format( - out_path)) - with open(out_path, 'w') as fh: + autolog("Creating {} ...".format(out_path)) + with open(out_path, "w") as fh: fh.write(doc) for import_name, module_path in module_folders.get(package, []): - doc = auto_doc_module(module_path, import_name, root_package) - out_path = out_dir + import_name.replace('.', os.path.sep) + '.rst' - if not os.path.exists(out_path): - print('Creating non-existent document {} ...'.format( - out_path)) - with open(out_path, 'w') as fh: - fh.write(doc) - else: - with open(out_path, 'r') as fh: - existing_content = ''.join(fh.readlines()) - if doc != existing_content: - print('Creating out of date document {} ...'.format( - out_path)) - with open(out_path, 'w') as fh: - fh.write(doc) + # check for any modules to exclude + for exclude_module in exclude_modules: + if import_name == exclude_module[1]: + autolog( + "Excluding module file: {}".format(exclude_module[1]) + ) + else: + doc = auto_doc_module( + module_path, import_name, root_package + ) + out_path = ( + out_dir + + import_name.replace(".", os.path.sep) + + ".rst" + ) + if not os.path.exists(out_path): + autolog("Creating {} ...".format(out_path)) + with open(out_path, "w") as fh: + fh.write(doc) + else: + with open(out_path, "r") as fh: + existing_content = "".join(fh.readlines()) + if doc != existing_content: + autolog("Creating {} ...".format(out_path)) + with open(out_path, "w") as fh: + fh.write(doc) def setup(app): - app.connect('builder-inited', auto_package_build) - app.add_config_value('autopackage_name', None, 'env') + app.connect("builder-inited", auto_package_build) + app.add_config_value("autopackage_name", None, "env") diff --git a/docs/iris/src/whitepapers/change_management.rst b/docs/iris/src/techpapers/change_management.rst similarity index 96% rename from docs/iris/src/whitepapers/change_management.rst rename to docs/iris/src/techpapers/change_management.rst index b279c91b96..2218eb4212 100644 --- a/docs/iris/src/whitepapers/change_management.rst +++ b/docs/iris/src/techpapers/change_management.rst @@ -1,3 +1,5 @@ +:orphan: + .. _change_management: Change Management in Iris from the User's perspective @@ -44,25 +46,28 @@ User Actions : How you should respond to changes and releases Checklist : * when a new **testing or candidate version** is announced - if convenient, test your working legacy code against it and report any problems. + + * if convenient, test your working legacy code against it and report any problems. * when a new **minor version is released** - * review the 'Whats New' documentation to see if it introduces any - deprecations that may affect you. - * run your working legacy code and check for any deprecation warnings, - indicating that modifications may be necessary at some point - * when convenient : + * review the 'Whats New' documentation to see if it introduces any + deprecations that may affect you. + * run your working legacy code and check for any deprecation warnings, + indicating that modifications may be necessary at some point + * when convenient : * review existing code for use of deprecated features * rewrite code to replace deprecated features * when a new major version is **announced** - ensure your code runs, without producing deprecation warnings, in the + + * ensure your code runs, without producing deprecation warnings, in the previous minor release * when a new major version is **released** - check for new deprecation warnings, as for a minor release + + * check for new deprecation warnings, as for a minor release Details @@ -81,6 +86,7 @@ Our practices are intended be compatible with the principles defined in the `SemVer project `_ . Key concepts covered here: + * :ref:`Release versions ` * :ref:`Backwards compatibility ` * :ref:`Deprecation ` @@ -95,18 +101,18 @@ Backwards compatibility usages unchanged (see :ref:`terminology ` below). Minor releases may only include backwards-compatible changes. -The following are examples of backward-compatible changes : +The following are examples of backward-compatible changes: * changes to documentation * adding to a module : new submodules, functions, classes or properties * adding to a class : new methods or properties * adding to a function or method : new **optional** arguments or keywords -The following are examples of **non-** backward-compatible changes : +The following are examples of **non-** backward-compatible changes: * removing (which includes *renaming*) any public module or submodule * removing any public component : a module, class, method, function or - data object property of a public API component + data object property of a public API component * removing any property of a public object * removing an argument or keyword from a method or function * adding a required argument to a method or function diff --git a/docs/iris/src/whitepapers/index.rst b/docs/iris/src/techpapers/index.rst similarity index 54% rename from docs/iris/src/whitepapers/index.rst rename to docs/iris/src/techpapers/index.rst index dd0876d257..773c8f7059 100644 --- a/docs/iris/src/whitepapers/index.rst +++ b/docs/iris/src/techpapers/index.rst @@ -1,8 +1,9 @@ -.. _whitepapers_index: +.. _techpapers_index: + + +Iris Technical Papers +===================== -============================ -Iris technical 'Whitepapers' -============================ Extra information on specific technical issues. .. toctree:: diff --git a/docs/iris/src/whitepapers/missing_data_handling.rst b/docs/iris/src/techpapers/missing_data_handling.rst similarity index 100% rename from docs/iris/src/whitepapers/missing_data_handling.rst rename to docs/iris/src/techpapers/missing_data_handling.rst diff --git a/docs/iris/src/whitepapers/um_files_loading.rst b/docs/iris/src/techpapers/um_files_loading.rst similarity index 100% rename from docs/iris/src/whitepapers/um_files_loading.rst rename to docs/iris/src/techpapers/um_files_loading.rst diff --git a/docs/iris/src/userguide/citation.rst b/docs/iris/src/userguide/citation.rst index 01b655574e..7ce0a8ffc0 100644 --- a/docs/iris/src/userguide/citation.rst +++ b/docs/iris/src/userguide/citation.rst @@ -23,7 +23,7 @@ For example:: ******************* -Downloaded Software +Downloaded software ******************* Suggested format:: @@ -36,7 +36,7 @@ For example:: ******************** -Checked out Software +Checked out software ******************** Suggested format:: diff --git a/docs/iris/src/userguide/code_maintenance.rst b/docs/iris/src/userguide/code_maintenance.rst index 00ba30506c..f5914da471 100644 --- a/docs/iris/src/userguide/code_maintenance.rst +++ b/docs/iris/src/userguide/code_maintenance.rst @@ -1,11 +1,11 @@ -Code Maintenance +Code maintenance ================ From a user point of view "code maintenance" means ensuring that your existing working code stays working, in the face of changes to Iris. -Stability and Change +Stability and change --------------------- In practice, as Iris develops, most users will want to periodically upgrade @@ -25,7 +25,7 @@ maintenance effort is probably still necessary : for some completely unconnected reason. -Principles of Change Management +Principles of change management ------------------------------- When you upgrade software to a new version, you often find that you need to diff --git a/docs/iris/src/userguide/cube_maths.rst b/docs/iris/src/userguide/cube_maths.rst index 8fe6eb12d5..6af4d5b3a6 100644 --- a/docs/iris/src/userguide/cube_maths.rst +++ b/docs/iris/src/userguide/cube_maths.rst @@ -208,7 +208,7 @@ The result could now be plotted using the guidance provided in the .. only:: html A very similar example to this can be found in - :doc:`/examples/Meteorology/deriving_phenomena`. + :ref:`sphx_glr_generated_gallery_meteorology_plot_deriving_phenomena.py`. .. only:: latex diff --git a/docs/iris/src/userguide/cube_statistics.rst b/docs/iris/src/userguide/cube_statistics.rst index 3ca7d9a2e0..31e165d35c 100644 --- a/docs/iris/src/userguide/cube_statistics.rst +++ b/docs/iris/src/userguide/cube_statistics.rst @@ -93,7 +93,8 @@ can be used instead of ``MEAN``, see :mod:`iris.analysis` for a full list of currently supported operators. For an example of using this functionality, the -:ref:`Hovmoller diagram ` example found +:ref:`sphx_glr_generated_gallery_meteorology_plot_hovmoller.py` +example found in the gallery takes a zonal mean of an ``XYT`` cube by using the ``collapsed`` method with ``latitude`` and ``iris.analysis.MEAN`` as arguments. @@ -147,7 +148,7 @@ These areas can now be passed to the ``collapsed`` method as weights: Several examples of area averaging exist in the gallery which may be of interest, including an example on taking a :ref:`global area-weighted mean -`. +`. .. _cube-statistics-aggregated-by: diff --git a/docs/iris/src/userguide/end_of_userguide.rst b/docs/iris/src/userguide/end_of_userguide.rst deleted file mode 100644 index c8f951a634..0000000000 --- a/docs/iris/src/userguide/end_of_userguide.rst +++ /dev/null @@ -1,15 +0,0 @@ -End of the user guide -===================== - -If this was your first time reading the user guide, we hope you found it enjoyable and informative. -It is advised that you now go back to the :doc:`start ` and try experimenting with your own data. - - - -Iris gallery ------------- -It can be very daunting to start coding a project from an empty file, that is why you will find many in-depth -examples in the Iris gallery which can be used as a goal driven reference to producing your own visualisations. - -If you produce a visualisation which you think would add value to the gallery, please get in touch with us and -we will consider including it as an example for all to benefit from. diff --git a/docs/iris/src/userguide/index.rst b/docs/iris/src/userguide/index.rst index 4fb7b62155..2a3b32fe11 100644 --- a/docs/iris/src/userguide/index.rst +++ b/docs/iris/src/userguide/index.rst @@ -1,11 +1,9 @@ .. _user_guide_index: +.. _user_guide_introduction: -=============== -Iris user guide -=============== +Introduction +============ -How to use the user guide ---------------------------- If you are reading this user guide for the first time it is strongly recommended that you read the user guide fully before experimenting with your own data files. @@ -18,24 +16,16 @@ links in order to understand the guide but they may serve as a useful reference Since later pages depend on earlier ones, try reading this user guide sequentially using the ``next`` and ``previous`` links. -User guide table of contents -------------------------------- - -.. toctree:: - :maxdepth: 2 - :numbered: - - iris_cubes.rst - loading_iris_cubes.rst - saving_iris_cubes.rst - navigating_a_cube.rst - subsetting_a_cube.rst - real_and_lazy_data.rst - plotting_a_cube.rst - interpolation_and_regridding.rst - merge_and_concat.rst - cube_statistics.rst - cube_maths.rst - citation.rst - code_maintenance.rst - end_of_userguide.rst +* :doc:`iris_cubes` +* :doc:`loading_iris_cubes` +* :doc:`saving_iris_cubes` +* :doc:`navigating_a_cube` +* :doc:`subsetting_a_cube` +* :doc:`real_and_lazy_data` +* :doc:`plotting_a_cube` +* :doc:`interpolation_and_regridding` +* :doc:`merge_and_concat` +* :doc:`cube_statistics` +* :doc:`cube_maths` +* :doc:`citation` +* :doc:`code_maintenance` diff --git a/docs/iris/src/userguide/iris_cubes.rst b/docs/iris/src/userguide/iris_cubes.rst index dc423afba1..5929c402f2 100644 --- a/docs/iris/src/userguide/iris_cubes.rst +++ b/docs/iris/src/userguide/iris_cubes.rst @@ -1,13 +1,9 @@ -.. _user_guide_introduction: - -=================== -Introduction -=================== - .. _iris_data_structures: +==================== Iris data structures --------------------- +==================== + The top level object in Iris is called a cube. A cube contains data and metadata about a phenomenon. In Iris, a cube is an interpretation of the *Climate and Forecast (CF) Metadata Conventions* whose purpose is to: @@ -33,6 +29,7 @@ by definition, its phenomenon. * Each coordinate has a name and a unit. * When a coordinate is added to a cube, the data dimensions that it represents are also provided. + * The shape of a coordinate is always the same as the shape of the associated data dimension(s) on the cube. * A dimension not explicitly listed signifies that the coordinate is independent of that dimension. * Each dimension of a coordinate must be mapped to a data dimension. The only coordinates with no mapping are diff --git a/docs/iris/src/userguide/loading_iris_cubes.rst b/docs/iris/src/userguide/loading_iris_cubes.rst index bf50acc614..bbb83194db 100644 --- a/docs/iris/src/userguide/loading_iris_cubes.rst +++ b/docs/iris/src/userguide/loading_iris_cubes.rst @@ -38,10 +38,12 @@ This shows that there were 2 cubes as a result of loading the file, they were: ``air_potential_temperature`` and ``surface_altitude``. The ``surface_altitude`` cube was 2 dimensional with: + * the two dimensions have extents of 204 and 187 respectively and are represented by the ``grid_latitude`` and ``grid_longitude`` coordinates. The ``air_potential_temperature`` cubes were 4 dimensional with: + * the same length ``grid_latitude`` and ``grid_longitude`` dimensions as ``surface_altitide`` * a ``time`` dimension of length 3 diff --git a/docs/iris/src/userguide/merge_and_concat.rst b/docs/iris/src/userguide/merge_and_concat.rst index b742b3ef5f..0d844ac403 100644 --- a/docs/iris/src/userguide/merge_and_concat.rst +++ b/docs/iris/src/userguide/merge_and_concat.rst @@ -1,7 +1,7 @@ .. _merge_and_concat: ===================== -Merge and Concatenate +Merge and concatenate ===================== We saw in the :doc:`loading_iris_cubes` chapter that Iris tries to load as few cubes as diff --git a/docs/iris/src/userguide/plotting_a_cube.rst b/docs/iris/src/userguide/plotting_a_cube.rst index d82cbbb027..f646aa4b3e 100644 --- a/docs/iris/src/userguide/plotting_a_cube.rst +++ b/docs/iris/src/userguide/plotting_a_cube.rst @@ -190,7 +190,7 @@ and providing the label keyword to identify it. Once all of the lines have been added the :func:`matplotlib.pyplot.legend` function can be called to indicate that a legend is desired: -.. plot:: ../example_code/General/lineplot_with_legend.py +.. plot:: ../gallery_code/general/plot_lineplot_with_legend.py :include-source: This example of consecutive ``qplt.plot`` calls coupled with the @@ -272,7 +272,7 @@ Brewer colour palettes *********************** Iris includes colour specifications and designs developed by -`Cynthia Brewer `_. +`Cynthia Brewer `_ These colour schemes are freely available under the following licence:: Apache-Style Software License for ColorBrewer software and ColorBrewer Color Schemes @@ -298,7 +298,7 @@ For adding citations to Iris plots, see :ref:`brewer-cite` (below). Available Brewer Schemes ======================== The following subset of Brewer palettes found at -`colorbrewer.org `_ are available within Iris. +`colorbrewer2.org `_ are available within Iris. .. plot:: userguide/plotting_examples/brewer.py diff --git a/docs/iris/src/userguide/plotting_examples/1d_quickplot_simple.py b/docs/iris/src/userguide/plotting_examples/1d_quickplot_simple.py index 30a5fc4318..f3772328ab 100644 --- a/docs/iris/src/userguide/plotting_examples/1d_quickplot_simple.py +++ b/docs/iris/src/userguide/plotting_examples/1d_quickplot_simple.py @@ -11,4 +11,5 @@ temperature_1d = temperature[5, :] qplt.plot(temperature_1d) + plt.show() diff --git a/docs/iris/src/userguide/plotting_examples/1d_simple.py b/docs/iris/src/userguide/plotting_examples/1d_simple.py index b76752ac18..ea90faf402 100644 --- a/docs/iris/src/userguide/plotting_examples/1d_simple.py +++ b/docs/iris/src/userguide/plotting_examples/1d_simple.py @@ -11,4 +11,5 @@ temperature_1d = temperature[5, :] iplt.plot(temperature_1d) + plt.show() diff --git a/docs/iris/src/userguide/plotting_examples/1d_with_legend.py b/docs/iris/src/userguide/plotting_examples/1d_with_legend.py index 1ee75e1ed9..26aeeef9a6 100644 --- a/docs/iris/src/userguide/plotting_examples/1d_with_legend.py +++ b/docs/iris/src/userguide/plotting_examples/1d_with_legend.py @@ -1,5 +1,4 @@ import matplotlib.pyplot as plt - import iris import iris.plot as iplt diff --git a/docs/iris/src/userguide/plotting_examples/brewer.py b/docs/iris/src/userguide/plotting_examples/brewer.py index e4533a28f5..f2ede9f9bc 100644 --- a/docs/iris/src/userguide/plotting_examples/brewer.py +++ b/docs/iris/src/userguide/plotting_examples/brewer.py @@ -4,19 +4,26 @@ import iris.palette -a = np.linspace(0, 1, 256).reshape(1, -1) -a = np.vstack((a, a)) - -maps = sorted(iris.palette.CMAP_BREWER) -nmaps = len(maps) - -fig = plt.figure(figsize=(7, 10)) -fig.subplots_adjust(top=0.99, bottom=0.01, left=0.2, right=0.99) -for i, m in enumerate(maps): - ax = plt.subplot(nmaps, 1, i + 1) - plt.axis("off") - plt.imshow(a, aspect="auto", cmap=plt.get_cmap(m), origin="lower") - pos = list(ax.get_position().bounds) - fig.text(pos[0] - 0.01, pos[1], m, fontsize=8, horizontalalignment="right") - -plt.show() +def main(): + a = np.linspace(0, 1, 256).reshape(1, -1) + a = np.vstack((a, a)) + + maps = sorted(iris.palette.CMAP_BREWER) + nmaps = len(maps) + + fig = plt.figure(figsize=(7, 10)) + fig.subplots_adjust(top=0.99, bottom=0.01, left=0.2, right=0.99) + for i, m in enumerate(maps): + ax = plt.subplot(nmaps, 1, i + 1) + plt.axis("off") + plt.imshow(a, aspect="auto", cmap=plt.get_cmap(m), origin="lower") + pos = list(ax.get_position().bounds) + fig.text( + pos[0] - 0.01, pos[1], m, fontsize=8, horizontalalignment="right" + ) + + plt.show() + + +if __name__ == "__main__": + main() diff --git a/docs/iris/src/userguide/plotting_examples/cube_blockplot.py b/docs/iris/src/userguide/plotting_examples/cube_blockplot.py index cd380f5e35..0961a97fdb 100644 --- a/docs/iris/src/userguide/plotting_examples/cube_blockplot.py +++ b/docs/iris/src/userguide/plotting_examples/cube_blockplot.py @@ -1,5 +1,4 @@ import matplotlib.pyplot as plt - import iris import iris.quickplot as qplt diff --git a/docs/iris/src/userguide/plotting_examples/cube_brewer_cite_contourf.py b/docs/iris/src/userguide/plotting_examples/cube_brewer_cite_contourf.py index 6dce2b39de..45ba800485 100644 --- a/docs/iris/src/userguide/plotting_examples/cube_brewer_cite_contourf.py +++ b/docs/iris/src/userguide/plotting_examples/cube_brewer_cite_contourf.py @@ -1,5 +1,4 @@ import matplotlib.pyplot as plt - import iris import iris.quickplot as qplt import iris.plot as iplt diff --git a/docs/iris/src/userguide/saving_iris_cubes.rst b/docs/iris/src/userguide/saving_iris_cubes.rst index f14f83006e..fa67b6213d 100644 --- a/docs/iris/src/userguide/saving_iris_cubes.rst +++ b/docs/iris/src/userguide/saving_iris_cubes.rst @@ -97,7 +97,7 @@ netCDF NetCDF is a flexible container for metadata and cube metadata is closely related to the CF for netCDF semantics. This means that cube metadata is well represented in netCDF files, closely resembling the in memory metadata representation. Thus there is no provision for similar save customisation functionality for netCDF saving, all customisations should be applied to the cube prior to saving to netCDF. -Bespoke Saver +Bespoke saver -------------- A bespoke saver may be written to support an alternative file format. This can be provided to the :py:func:`iris.save` function, enabling Iris to write to a different file format. diff --git a/docs/iris/src/whatsnew/1.0.rst b/docs/iris/src/whatsnew/1.0.rst index 2a415c1bfe..6340d0495d 100644 --- a/docs/iris/src/whatsnew/1.0.rst +++ b/docs/iris/src/whatsnew/1.0.rst @@ -69,7 +69,7 @@ CF-netCDF coordinate systems ============================ The coordinate systems in Iris are now defined by the CF-netCDF -`grid mappings `_. +`grid mappings `_. As of Iris 1.0 a subset of the CF-netCDF coordinate systems are supported, but this will be expanded in subsequent versions. Adding this code is a relatively simple, incremental process - it would make a @@ -79,13 +79,13 @@ contributing to the project. The coordinate systems available in Iris 1.0 and their corresponding Iris classes are: -================================================================================================== ========================================= -CF name Iris class -================================================================================================== ========================================= -`Latitude-longitude `_ :class:`~iris.coord_systems.GeogCS` -`Rotated pole `_ :class:`~iris.coord_systems.RotatedGeogCS` -`Transverse Mercator `_ :class:`~iris.coord_systems.TransverseMercator` -================================================================================================== ========================================= +================================================================================================================= ========================================= +CF name Iris class +================================================================================================================= ========================================= +`Latitude-longitude `_ :class:`~iris.coord_systems.GeogCS` +`Rotated pole `_ :class:`~iris.coord_systems.RotatedGeogCS` +`Transverse Mercator `_ :class:`~iris.coord_systems.TransverseMercator` +================================================================================================================= ========================================= For convenience, Iris also includes the :class:`~iris.coord_systems.OSGB` class which provides a simple way to create the transverse Mercator @@ -147,8 +147,7 @@ Hybrid-pressure With the introduction of the :class:`~iris.aux_factory.HybridPressureFactory` class, it is now possible to represent data expressed on a -hybrid-pressure vertical coordinate, as defined by the second variant in -`Appendix D `_. +`hybrid-pressure vertical coordinate `_. A hybrid-pressure factory is created with references to the coordinates which provide the components of the hybrid coordinate ("ap" and "b") and the surface pressure. In return, it provides a virtual "pressure" @@ -160,7 +159,7 @@ the derived "pressure" coordinate for certain data [#f1]_ from the .. [#f1] Where the level type is either 105 or 119, and where the surface pressure has an ECMWF paramId of - `152 `_. + `152 `_. NetCDF diff --git a/docs/iris/src/whatsnew/1.10.rst b/docs/iris/src/whatsnew/1.10.rst index 26f21c0252..bc2b7528b2 100644 --- a/docs/iris/src/whatsnew/1.10.rst +++ b/docs/iris/src/whatsnew/1.10.rst @@ -1,4 +1,4 @@ -What's New in Iris 1.10 +What's new in Iris 1.10 *********************** :Release: 1.10 @@ -7,7 +7,7 @@ What's New in Iris 1.10 This document explains the new/changed features of Iris in version 1.10 (:doc:`View all changes `.) -Iris 1.10 Features +Iris 1.10 features ================== .. _iris_grib_added: @@ -76,7 +76,7 @@ Iris 1.10 Features * Cubes with anonymous dimensions can now be concatenated. This can only occur along a dimension that is not anonymous. * NetCDF saving of ``valid_range``, ``valid_min`` and ``valid_max`` cube attributes is now allowed. -Bugs Fixed +Bugs fixed ========== * Altered Cell Methods to display coordinate's standard_name rather than var_name where appropriate to avoid human confusion. * Saving multiple cubes with netCDF4 protected attributes should now work as expected. @@ -101,7 +101,7 @@ Bugs Fixed * Concatenation no longer occurs when the auxiliary coordinates of the cubes do not match. This check is not applied to AuxCoords that span the dimension the concatenation is occuring along. This behaviour can be switched off by setting the ``check_aux_coords`` kwarg in :meth:`iris.cube.CubeList.concatenate` to False. * Fixed a bug in :meth:`iris.cube.Cube.subset` where an exception would be thrown while trying to subset over a non-dimensional scalar coordinate. -Incompatible Changes +Incompatible changes ==================== * The source and target for :meth:`iris.experimental.regrid.PointInCell.regridder` must now have defined coordinate systems (i.e. not ``None``). Additionally, the source data X and Y coordinates must have the same cube dimensions. @@ -170,7 +170,7 @@ Documentation Changes * It is now clear that repeated values will form a group under :meth:`iris.cube.Cube.aggregated_by` even if they aren't consecutive. Hence, the documentation for :mod:`iris.cube` has been changed to reflect this. * The documentation for :meth:`iris.analysis.calculus.curl` has been updated for clarity. * False claims about :meth:`iris.fileformats.pp.save`, :meth:`iris.fileformats.pp.as_pairs`, and :meth:`iris.fileformats.pp.as_fields` being able to take instances of :class:`iris.cube.CubeList` as inputs have been removed. -* A :doc:`new code example <../examples/Meteorology/wind_speed>`, demonstrating the use of a quiver plot to display wind speeds over Lake Victoria, has been added. +* A new code example :ref:`sphx_glr_generated_gallery_meteorology_plot_wind_speed.py`, demonstrating the use of a quiver plot to display wind speeds over Lake Victoria, has been added. * The docstring for :data:`iris.analysis.SUM` has been updated to explicitly state that weights passed to it aren't normalised internally. * A note regarding the impossibility of partially collapsing multi-dimensional coordinates has been added to the user guide. diff --git a/docs/iris/src/whatsnew/1.11.rst b/docs/iris/src/whatsnew/1.11.rst index eb93ec2f8c..560bb07032 100644 --- a/docs/iris/src/whatsnew/1.11.rst +++ b/docs/iris/src/whatsnew/1.11.rst @@ -1,4 +1,4 @@ -What's New in Iris 1.11 +What's new in Iris 1.11 *********************** :Release: 1.11 @@ -7,14 +7,14 @@ What's New in Iris 1.11 This document explains the new/changed features of Iris in version 1.11 (:doc:`View all changes `.) -Iris 1.11 Features +Iris 1.11 features ================== * If available, display the ``STASH`` code instead of ``unknown / (unknown)`` when printing cubes with no ``standard_name`` and no ``units``. * Support for saving to netCDF with data packing has been added. * The coordinate system :class:`iris.coord_systems.LambertAzimuthalEqualArea` has been added with NetCDF saving support. -Bugs Fixed +Bugs fixed ========== * Fixed a floating point tolerance bug in :func:`iris.experimental.regrid.regrid_area_weighted_rectilinear_src_and_grid` for wrapped longitudes. @@ -25,7 +25,7 @@ Bugs Fixed * When saving to NetCDF, the existing behaviour of writing string attributes as ASCII has been maintained across known versions of netCDF4-python. -Documentation Changes +Documentation changes ===================== * Fuller doc-string detail added to :func:`iris.analysis.cartography.unrotate_pole` and :func:`iris.analysis.cartography.rotate_pole`. diff --git a/docs/iris/src/whatsnew/1.12.rst b/docs/iris/src/whatsnew/1.12.rst index 59ea47d876..bd02f0937a 100644 --- a/docs/iris/src/whatsnew/1.12.rst +++ b/docs/iris/src/whatsnew/1.12.rst @@ -1,4 +1,4 @@ -What's New in Iris 1.12 +What's new in Iris 1.12 *********************** :Release: 1.12 @@ -7,7 +7,7 @@ What's New in Iris 1.12 This document explains the new/changed features of Iris in version 1.12 (:doc:`View all changes `.) -Iris 1.12 Features +Iris 1.12 features ================== .. _showcase: @@ -125,7 +125,7 @@ Deprecations of the new fast-loading mechanism provided by :meth:`iris.fileformats.um.structured_um_loading`. -Documentation Changes +Documentation changes ===================== * Corrected documentation of :class:`iris.analysis.AreaWeighted` scheme to make the usage scope clearer. diff --git a/docs/iris/src/whatsnew/1.13.rst b/docs/iris/src/whatsnew/1.13.rst index 532c160f13..7435e5bb07 100644 --- a/docs/iris/src/whatsnew/1.13.rst +++ b/docs/iris/src/whatsnew/1.13.rst @@ -1,4 +1,4 @@ -What's New in Iris 1.13 +What's new in Iris 1.13 *********************** :Release: 1.13 @@ -8,7 +8,7 @@ What's New in Iris 1.13 This document explains the new/changed features of Iris in version 1.13 (:doc:`View all changes `.) -Iris 1.13 Features +Iris 1.13 features ================== * Allow the reading of NAME trajectories stored by time instead of by particle number. @@ -16,7 +16,7 @@ Iris 1.13 Features * Data arrays may be shared between cubes, and subsets of cubes, by using the :meth:`iris.cube.share_data` flag. -Bug Fixes +Bug fixes ========= * The bounds are now set correctly on the longitude coordinate if a zonal mean diagnostic has been loaded from a PP file as per the CF Standard. diff --git a/docs/iris/src/whatsnew/1.4.rst b/docs/iris/src/whatsnew/1.4.rst index 053a6e1096..3586b05a5c 100644 --- a/docs/iris/src/whatsnew/1.4.rst +++ b/docs/iris/src/whatsnew/1.4.rst @@ -105,8 +105,8 @@ Iris-Pandas interoperablilty Conversion to and from Pandas Series_ and DataFrames_ is now available. See :mod:`iris.pandas` for more details. -.. _Series: http://pandas.pydata.org/pandas-docs/stable/api.html#series -.. _DataFrames: http://pandas.pydata.org/pandas-docs/stable/api.html#dataframe +.. _Series: https://pandas.pydata.org/pandas-docs/stable/reference/series.html +.. _DataFrames: https://pandas.pydata.org/pandas-docs/stable/reference/frame.html .. _load-opendap: diff --git a/docs/iris/src/whatsnew/1.5.rst b/docs/iris/src/whatsnew/1.5.rst index 7af1e40285..6a4f418259 100644 --- a/docs/iris/src/whatsnew/1.5.rst +++ b/docs/iris/src/whatsnew/1.5.rst @@ -101,7 +101,7 @@ Iris 1.5 features the direction of vertical axes will be reversed if the corresponding coordinate has a "positive" attribute set to "down". - see: :ref:`Oceanography-atlantic_profiles` + see: :ref:`sphx_glr_generated_gallery_oceanography_plot_atlantic_profiles.py` * New PP stashcode translations added including 'dewpoint' and 'relative_humidity'. diff --git a/docs/iris/src/whatsnew/1.7.rst b/docs/iris/src/whatsnew/1.7.rst index 2f3a52fbb9..2d4395239b 100644 --- a/docs/iris/src/whatsnew/1.7.rst +++ b/docs/iris/src/whatsnew/1.7.rst @@ -229,20 +229,19 @@ Deprecations :func:`iris.fileformats.grib.reset_load_rules` functions. * Matplotlib is no longer a core Iris dependency. -Documentation Changes +Documentation changes ===================== * New sections on :ref:`cube broadcasting ` and :doc:`regridding and interpolation ` have been added to the :doc:`user guide `. * An example demonstrating custom log-scale colouring has been added. - See :ref:`General-anomaly_log_colouring`. + See :ref:`sphx_glr_generated_gallery_general_plot_anomaly_log_colouring.py`. * An example demonstrating the creation of a custom :class:`iris.analysis.Aggregator` has been added. - See :ref:`General-custom_aggregation`. + See :ref:`sphx_glr_generated_gallery_general_plot_custom_aggregation.py`. * An example of reprojecting data from 2D auxiliary spatial coordinates - (such as that from the ORCA grid) has been added. See :ref:`General-orca_projection`. + (such as that from the ORCA grid) has been added. See :ref:`sphx_glr_generated_gallery_general_plot_orca_projection.py`. * A clarification of the behaviour of :func:`iris.analysis.calculus.differentiate`. -* A new :doc:`"whitepapers" ` section has been added to the documentation along +* A new :doc:`"Technical Papers" ` section has been added to the documentation along with the addition of a paper providing an :doc:`overview of the load process for UM-like - fileformats (e.g. PP and Fieldsfile) `. - + fileformats (e.g. PP and Fieldsfile) `. diff --git a/docs/iris/src/whatsnew/1.8.rst b/docs/iris/src/whatsnew/1.8.rst index c763411ed8..54763a194b 100644 --- a/docs/iris/src/whatsnew/1.8.rst +++ b/docs/iris/src/whatsnew/1.8.rst @@ -169,7 +169,7 @@ Deprecations "iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid" has been removed, as :class:`iris.analysis.Linear` now includes this functionality. -Documentation Changes +Documentation changes ===================== * A chapter on :doc:`merge and concatenate ` has been added to the :doc:`user guide `. diff --git a/docs/iris/src/whatsnew/1.9.rst b/docs/iris/src/whatsnew/1.9.rst index 7a4848b434..7fda661ebc 100644 --- a/docs/iris/src/whatsnew/1.9.rst +++ b/docs/iris/src/whatsnew/1.9.rst @@ -1,4 +1,4 @@ -What's New in Iris 1.9 +What's new in Iris 1.9 ********************** :Release: 1.9.2 @@ -7,7 +7,7 @@ What's New in Iris 1.9 This document explains the new/changed features of Iris in version 1.9 (:doc:`View all changes `.) -Iris 1.9 Features +Iris 1.9 features ================= * Support for running on Python 3.4 has been added to the whole code base. Some features which depend on external libraries will not be available until they also support Python 3, namely: @@ -66,7 +66,7 @@ Iris 1.9 Features * The :meth:`iris.experimental.um.Field.get_data` method can now be used to read Fieldsfile data after the original :class:`iris.experimental.um.FieldsFileVariant` has been closed. -Bugs Fixed +Bugs fixed ========== * Fixed a bug in :meth:`iris.unit.Unit.convert` (and the equivalent in `cf_units `_) @@ -109,7 +109,7 @@ Version 1.9.2 * Fixed a bug avoiding sorting classes directly when :meth:`iris.cube.Cube.coord_system` is used in Python3. * Fixed a bug regarding unsuccessful dot import. -Incompatible Changes +Incompatible changes ==================== * GRIB message/file reading and writing may not be available for Python 3 due to GRIB API limitations. @@ -121,7 +121,7 @@ Deprecations but it is *not* set. * Deprecated :class:`iris.aux_factory.LazyArray` -Documentation Changes +Documentation changes ===================== * A chapter on :doc:`saving iris cubes ` has been added to the :doc:`user guide `. diff --git a/docs/iris/src/whatsnew/2.0.rst b/docs/iris/src/whatsnew/2.0.rst index 43d60a8539..61568d3a8e 100644 --- a/docs/iris/src/whatsnew/2.0.rst +++ b/docs/iris/src/whatsnew/2.0.rst @@ -1,4 +1,4 @@ -What's New in Iris 2.0.0 +What's new in Iris 2.0.0 ************************ :Release: 2.0.0rc1 @@ -9,7 +9,7 @@ This document explains the new/changed features of Iris in version 2.0.0 (:doc:`View all changes `). -Iris 2.0.0 Features +Iris 2.0.0 features =================== .. _showcase: @@ -114,7 +114,7 @@ all existing toggles in :attr:`iris.FUTURE` now default to :data:`True`. off is now deprecated. -Bugs Fixed +Bugs fixed ========== * Indexing or slicing an :class:`~iris.coords.AuxCoord` coordinate will return a coordinate with @@ -289,7 +289,7 @@ been removed. In particular: removed from the :class:`iris.fileformats.rules.Loader` constructor. -Documentation Changes +Documentation changes ===================== * A new UserGuide chapter on :doc:`Real and Lazy Data diff --git a/docs/iris/src/whatsnew/2.1.rst b/docs/iris/src/whatsnew/2.1.rst index 00f7115431..a82d3b8470 100644 --- a/docs/iris/src/whatsnew/2.1.rst +++ b/docs/iris/src/whatsnew/2.1.rst @@ -1,4 +1,4 @@ -What's New in Iris 2.1 +What's new in Iris 2.1 ********************** :Release: 2.1 @@ -8,7 +8,7 @@ This document explains the new/changed features of Iris in version 2.1 (:doc:`older "What's New" release notes can be found here`.) -Iris 2.1 Dependency updates +Iris 2.1 dependency updates =========================== * The `cf_units `_ dependency @@ -30,7 +30,7 @@ Iris 2.1 Dependency updates Full requirements can be seen in the `requirements `_ directory of the Iris' the source. -Iris 2.1 Features +Iris 2.1 features ================= * Added ``repr_html`` functionality to the :class:`~iris.cube.Cube` to provide @@ -60,7 +60,7 @@ Iris 2.1 Features * The :class:`~iris.coord_systems.Mercator` projection has been updated to accept the ``standard_parallel`` keyword argument (:pull:`3041`). -Bugs Fixed +Bugs fixed ========== * All var names being written to NetCDF are now CF compliant. @@ -73,7 +73,7 @@ Bugs Fixed * :mod:`iris.quickplot` labels now honour the axes being drawn to when using the ``axes`` keyword (:pull:`3010`). -Incompatible Changes +Incompatible changes ==================== * The deprecated :mod:`iris.experimental.um` was removed. Please use consider using `mule `_ diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst index 1eff99ecb4..75be5460b3 100644 --- a/docs/iris/src/whatsnew/2.2.rst +++ b/docs/iris/src/whatsnew/2.2.rst @@ -1,4 +1,4 @@ -What's New in Iris 2.2 +What's new in Iris 2.2 ************************ :Release: 2.2.0 @@ -10,7 +10,7 @@ of version 2.2 (:doc:`View all changes `). -Iris 2.2 Features +Iris 2.2 features =================== .. _showcase: @@ -70,7 +70,7 @@ Iris 2.2 Features a NaN-tolerant array comparison. -Iris 2.2 Dependency updates +Iris 2.2 dependency updates ============================= * Iris is now using the latest version release of dask (currently 0.19.3) @@ -82,7 +82,7 @@ Iris 2.2 Dependency updates its changes in all SciTools libraries. -Bugs Fixed +Bugs fixed ========== * The bug has been fixed that prevented printing time coordinates with bounds @@ -109,7 +109,7 @@ Bugs fixed in v2.2.1 -Documentation Changes +Documentation changes ===================== * Iris' `INSTALL` document has been updated to include guidance for running diff --git a/docs/iris/src/whatsnew/2.3.rst b/docs/iris/src/whatsnew/2.3.rst index 872fb44cd6..6fb7088339 100644 --- a/docs/iris/src/whatsnew/2.3.rst +++ b/docs/iris/src/whatsnew/2.3.rst @@ -1,4 +1,4 @@ -What's New in Iris 2.3.0 +What's new in Iris 2.3.0 ************************ :Release: 2.3.0 @@ -7,7 +7,7 @@ What's New in Iris 2.3.0 This document explains the new/changed features of Iris in version 2.3.0 (:doc:`View all changes `.) -Iris 2.3.0 Features +Iris 2.3.0 features =================== .. _showcase: @@ -103,12 +103,12 @@ Iris 2.3.0 Features relaxed tolerance : This means that some cubes may now test 'equal' that previously did not. - Previously, Iris compared cube data arrays using: - ``abs(a - b) < 1.e-8`` + Previously, Iris compared cube data arrays using + ``abs(a - b) < 1.e-8`` We now apply the default operation of :func:`numpy.allclose` instead, - which is equivalent to: - ``abs(a - b) < (1.e-8 + 1.e-5 * b)`` + which is equivalent to + ``abs(a - b) < (1.e-8 + 1.e-5 * b)`` * Added support to render HTML for :class:`~iris.cube.CubeList` in Jupyter Notebooks and JupyterLab. @@ -135,7 +135,7 @@ Iris 2.3.0 Features `metarelate/metOcean commit 448f2ef, 2019-11-29 `_ -Iris 2.3.0 Dependency Updates +Iris 2.3.0 dependency updates ============================= * Iris now supports Proj4 up to version 5, but not yet 6 or beyond, pending `fixes to some cartopy tests `_; had been erroneously using Geostationary. -* :class:`~iris.coords.CellMethod` will now only use valid `NetCDF name tokens `_ to reference the coordinates involved in the statistical operation. -* The following var_name properties will now only allow valid `NetCDF name - tokens - `_ to - reference the said NetCDF variable name. Note that names with a leading +* :class:`~iris.coords.CellMethod` will now only use valid `NetCDF name tokens`_ to reference the coordinates involved in the statistical operation. +* The following var_name properties will now only allow valid + `NetCDF name tokens`_ + to reference the said NetCDF variable name. Note that names with a leading underscore are not permitted. - - :attr:`iris.aux_factory.AuxCoordFactory.var_name` - - :attr:`iris.coords.CellMeasure.var_name` - - :attr:`iris.coords.Coord.var_name` - - :attr:`iris.coords.AuxCoord.var_name` - - :attr:`iris.cube.Cube.var_name` + +.. _NetCDF name tokens: https://www.unidata.ucar.edu/software/netcdf/documentation/NUG/netcdf_data_set_components.html#object_name + + * :attr:`iris.aux_factory.AuxCoordFactory.var_name` + * :attr:`iris.coords.CellMeasure.var_name` + * :attr:`iris.coords.Coord.var_name` + * :attr:`iris.coords.AuxCoord.var_name` + * :attr:`iris.cube.Cube.var_name` + * Rendering a cube in Jupyter will no longer crash for a cube with attributes containing ``\n``. * NetCDF variables which reference themselves in their ``cell_measures`` @@ -199,12 +201,12 @@ Bugs Fixed the original attached AuxCoords. -Documentation Changes +Documentation changes ===================== * Adopted a `new colour logo for Iris <../_static/Iris7_1_trim_full.png>`_ -* Added a gallery example showing `how to concatenate NEMO ocean model data - <../examples/Oceanography/load_nemo.html>`_. +* Added a gallery example showing how to concatenate NEMO ocean model data, + see :ref:`sphx_glr_generated_gallery_oceanography_plot_load_nemo.py`. * Added an example in the `Loading Iris Cubes: Constraining on Time <../userguide/loading_iris_cubes .html#constraining-on-time>`_ diff --git a/docs/iris/src/whatsnew/2.4.rst b/docs/iris/src/whatsnew/2.4.rst index 2facb97a7a..776cc8aa69 100644 --- a/docs/iris/src/whatsnew/2.4.rst +++ b/docs/iris/src/whatsnew/2.4.rst @@ -1,4 +1,4 @@ -What's New in Iris 2.4.0 +What's new in Iris 2.4.0 ************************ :Release: 2.4.0 @@ -8,7 +8,7 @@ This document explains the new/changed features of Iris in version 2.4.0 (:doc:`View all changes `.) -Iris 2.4.0 Features +Iris 2.4.0 features =================== .. admonition:: Last python 2 version of Iris @@ -44,12 +44,12 @@ Iris 2.4.0 Features from the attributes dictionary of a :class:`~iris.cube.Cube`. -Iris 2.4.0 Dependency Updates +Iris 2.4.0 dependency updates ============================= * Iris is now able to use the latest version of matplotlib. -Bugs Fixed +Bugs fixed ========== * Fixed a problem which was causing file loads to fetch *all* field data whenever UM files (PP or Fieldsfiles) were loaded. diff --git a/docs/iris/src/whatsnew/aggregate_directory.py b/docs/iris/src/whatsnew/aggregate_directory.py index c7b497307f..6fe92f6764 100644 --- a/docs/iris/src/whatsnew/aggregate_directory.py +++ b/docs/iris/src/whatsnew/aggregate_directory.py @@ -42,7 +42,7 @@ SOFTWARE_NAME = "Iris" EXTENSION = ".rst" VALID_CATEGORIES = [ - {"Prefix": "newfeature", "Title": "Features"}, + {"Prefix": "newfeature", "Title": "features"}, {"Prefix": "bugfix", "Title": "Bugs Fixed"}, {"Prefix": "incompatiblechange", "Title": "Incompatible Changes"}, {"Prefix": "deprecate", "Title": "Deprecations"}, @@ -165,7 +165,7 @@ def generate_header(release, unreleased=False): else: isodatestamp = datetime.date.today().strftime("%Y-%m-%d") header_text = [] - title_template = "What's New in {} {!s}\n" + title_template = "What's new in {} {!s}\n" title_line = title_template.format(SOFTWARE_NAME, release) title_underline = ("*" * (len(title_line) - 1)) + "\n" header_text.append(title_line) diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-10_modernise_documentation_using_themes_and_readthedocs.txt b/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-10_modernise_documentation_using_themes_and_readthedocs.txt new file mode 100644 index 0000000000..5ff8c001e8 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-10_modernise_documentation_using_themes_and_readthedocs.txt @@ -0,0 +1,2 @@ +* Updated documentation to use a modern sphinx theme and be served from + https://scitools-iris.readthedocs.io/en/latest/. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt index 454fc3617f..18378691cb 100644 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt +++ b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt @@ -1,3 +1,3 @@ * The :class:`~iris.fileformats.nimrod` provides richer meta-data translation -when loading Nimrod-format data into cubes. This covers most known operational -use-cases. + when loading Nimrod-format data into cubes. This covers most known operational + use-cases. diff --git a/docs/iris/src/whatsnew/index.rst b/docs/iris/src/whatsnew/index.rst index 03834a43a7..00b925a48e 100644 --- a/docs/iris/src/whatsnew/index.rst +++ b/docs/iris/src/whatsnew/index.rst @@ -7,10 +7,10 @@ These "What's new" pages describe the important changes between major Iris versions. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 latest.rst - 3.0.rst + 3.0.0.rst 2.4.rst 2.3.rst 2.2.rst diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index 37daeec4aa..4746425bb3 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -515,6 +515,7 @@ def __init__( match. Kwargs: + * standard_name: A string or callable representing the standard name to match against. @@ -534,6 +535,7 @@ def __init__( where the standard_name is not set, then use standard_name=None. Returns: + * Boolean Example usage:: @@ -544,8 +546,8 @@ def __init__( iris.NameConstraint(standard_name='air_temperature', STASH=lambda stash: stash.item == 203) - """ + self.standard_name = standard_name self.long_name = long_name self.var_name = var_name diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 5b7dff813d..0d4d3bfdab 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -27,11 +27,11 @@ The gallery contains several interesting worked examples of how an :class:`~iris.analysis.Aggregator` may be used, including: - * :ref:`Meteorology-COP_1d_plot` - * :ref:`General-SOI_filtering` - * :ref:`Meteorology-hovmoller` - * :ref:`Meteorology-lagged_ensemble` - * :ref:`General-custom_aggregation` + * :ref:`sphx_glr_generated_gallery_meteorology_plot_COP_1d.py` + * :ref:`sphx_glr_generated_gallery_general_plot_SOI_filtering.py` + * :ref:`sphx_glr_generated_gallery_meteorology_plot_hovmoller.py` + * :ref:`sphx_glr_generated_gallery_meteorology_plot_lagged_ensemble.py` + * :ref:`sphx_glr_generated_gallery_general_plot_custom_aggregation.py` """ @@ -487,7 +487,8 @@ def __init__( A variety of ready-made aggregators are provided in this module, such as :data:`~iris.analysis.MEAN` and :data:`~iris.analysis.MAX`. Custom aggregators can also be created for special purposes, see - :ref:`General-custom_aggregation` for a worked example. + :ref:`sphx_glr_generated_gallery_general_plot_custom_aggregation.py` + for a worked example. """ #: Cube cell method string. diff --git a/lib/iris/analysis/stats.py b/lib/iris/analysis/stats.py index ba3ed2504c..bb283a0e89 100644 --- a/lib/iris/analysis/stats.py +++ b/lib/iris/analysis/stats.py @@ -64,7 +64,7 @@ def pearsonr( correlation at each time/altitude point. Reference: - http://www.statsoft.com/textbook/glosp.html#Pearson%20Correlation + https://en.wikipedia.org/wiki/Pearson_correlation_coefficient This operation is non-lazy. diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 03e942c6c9..964e56c313 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1200,6 +1200,7 @@ def add_ancillary_variable(self, ancillary_variable, data_dims=None): the cube Kwargs: + * data_dims Integer or iterable of integers giving the data dimensions spanned by the ancillary variable. @@ -1207,6 +1208,7 @@ def add_ancillary_variable(self, ancillary_variable, data_dims=None): Raises a ValueError if an ancillary variable with identical metadata already exists on the cube. """ + if self.ancillary_variables(ancillary_variable): raise ValueError("Duplicate ancillary variables not permitted") diff --git a/lib/iris/experimental/stratify.py b/lib/iris/experimental/stratify.py index 2992360247..e357f2ca9d 100644 --- a/lib/iris/experimental/stratify.py +++ b/lib/iris/experimental/stratify.py @@ -68,8 +68,8 @@ def relevel(cube, src_levels, tgt_levels, axis=None, interpolator=None): that are generally monotonic in the direction of interpolation, such as height/pressure or salinity/depth. - Parameters - ---------- + Args: + cube : :class:`~iris.cube.Cube` The phenomenon data to be re-levelled. diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 1db4e6c61e..75f328a80e 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -10,7 +10,7 @@ References: [CF] NetCDF Climate and Forecast (CF) Metadata conventions, Version 1.5, October, 2010. -[NUG] NetCDF User's Guide, http://www.unidata.ucar.edu/software/netcdf/docs/netcdf.html +[NUG] NetCDF User's Guide, https://www.unidata.ucar.edu/software/netcdf/documentation/NUG/ """ diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 4d7ddedc61..867b0c9faf 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -959,7 +959,7 @@ def write( than global attributes. * unlimited_dimensions (iterable of strings and/or - :class:`iris.coords.Coord` objects): + :class:`iris.coords.Coord` objects): List of coordinate names (or coordinate objects) corresponding to coordinate dimensions of `cube` to save with the NetCDF dimension variable length 'UNLIMITED'. By default, no @@ -992,10 +992,10 @@ def write( Used to manually specify the HDF5 chunksizes for each dimension of the variable. A detailed discussion of HDF chunking and I/O performance is available here: - http://www.hdfgroup.org/HDF5/doc/H5.user/Chunking.html. Basically, - you want the chunk size for each dimension to match as closely as - possible the size of the data block that users will read from the - file. `chunksizes` cannot be set if `contiguous=True`. + https://www.unidata.ucar.edu/software/netcdf/documentation/NUG/netcdf_perf_chunking.html. + Basically, you want the chunk size for each dimension to match + as closely as possible the size of the data block that users will + read from the file. `chunksizes` cannot be set if `contiguous=True`. * endian (string): Used to control whether the data is stored in little or big endian @@ -2506,7 +2506,7 @@ def save( than global attributes. * unlimited_dimensions (iterable of strings and/or - :class:`iris.coords.Coord` objects): + :class:`iris.coords.Coord` objects): List of coordinate names (or coordinate objects) corresponding to coordinate dimensions of `cube` to save with the NetCDF dimension variable length 'UNLIMITED'. By default, no unlimited dimensions are @@ -2538,7 +2538,7 @@ def save( * chunksizes (tuple of int): Used to manually specify the HDF5 chunksizes for each dimension of the variable. A detailed discussion of HDF chunking and I/O performance is - available here: http://www.hdfgroup.org/HDF5/doc/H5.user/Chunking.html. + available here: https://www.unidata.ucar.edu/software/netcdf/documentation/NUG/netcdf_perf_chunking.html. Basically, you want the chunk size for each dimension to match as closely as possible the size of the data block that users will read from the file. `chunksizes` cannot be set if `contiguous=True`. diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index 8602defa16..9132e16680 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -792,7 +792,7 @@ def _unique_id(self): bits[0] = os.path.splitext(file_name)[0] folder, location = os.path.split(path) bits = [location] + bits - while location not in ["iris", "example_tests"]: + while location not in ["iris", "gallery_tests"]: folder, location = os.path.split(folder) bits = [location] + bits test_id = ".".join(bits) diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json index e6a225f022..a353507d12 100644 --- a/lib/iris/tests/results/imagerepo.json +++ b/lib/iris/tests/results/imagerepo.json @@ -1,129 +1,129 @@ { - "example_tests.test_COP_1d_plot.TestCOP1DPlot.test_COP_1d_plot.0": [ + "gallery_tests.test_plot_COP_1d.TestCOP1DPlot.test_plot_COP_1d.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/baff589936602d8ec977334ae4dac9b61a6dc4d99532c86cc2913e36c4cc0f61.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aefec91c3601249cc9b3336dc4c8cdb31a64c6d997b3c0eccb5932d285e42f33.png" ], - "example_tests.test_COP_maps.TestCOPMaps.test_cop_maps.0": [ + "gallery_tests.test_plot_COP_maps.TestCOPMaps.test_plot_cop_maps.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/ea9138db95668524913e6ac168997e85957e917e876396b96a81b5ce3c496935.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea9130db95668524913c6ac178995b0d956e917ec76396b96a853dcf94696935.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea9130db95668524913e6ac168991f0d956e917ec76396b96a853dcf94796931.png" ], - "example_tests.test_SOI_filtering.TestSOIFiltering.test_soi_filtering.0": [ + "gallery_tests.test_plot_SOI_filtering.TestSOIFiltering.test_plot_soi_filtering.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fac460b9c17b78723e05a5a9954edaf062332799954e9ca5c63b9a52d24e5a95.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa8460b9c17b78723e05a5a9954edaf062333799954e9ca5c63b9a52d24e4a9d.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa167295c5e0696a3c17a58c9568da536233da19994cdab487739b4b9b444eb5.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa56f295c5e0694a3c17a58d95e8da536233da99984c5af4c6739b4a9a444eb4.png" ], - "example_tests.test_TEC.TestTEC.test_TEC.0": [ + "gallery_tests.test_plot_TEC.TestTEC.test_plot_TEC.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9a42846e9a49c7596e3cce6c907b3a83c17e1b8239b3e4f33bc4.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e1a561b69b1a9e43846e9a49c7596e2cce6c907b3a83c16e1b9231b3e4f33b8c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e5a761b69a589a4bc46f9e48c65c6631ce61d1ce3982c13739b33193c0ee3f8c.png" ], - "example_tests.test_anomaly_log_colouring.TestAnomalyLogColouring.test_anomaly_log_colouring.0": [ + "gallery_tests.test_plot_anomaly_log_colouring.TestAnomalyLogColouring.test_plot_anomaly_log_colouring.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/ec4464e185a39f93931e9b1e91696d2949dde6e63e26a47a5ad391938d9a5a0c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ecc164e78e979b19b3789b0885a564a56cc2c65e3ec69469db1bdb9a853c1e24.png" ], - "example_tests.test_atlantic_profiles.TestAtlanticProfiles.test_atlantic_profiles.0": [ + "gallery_tests.test_plot_atlantic_profiles.TestAtlanticProfiles.test_plot_atlantic_profiles.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc5d08fcd00fdb1c93fcb21c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/9f8260536bd28e1320739437b5f437b0a51d66f4cc7c09f4d00fdb1c93fcb21c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/9f8a60536bd28e1320739437b5f437b0a53d66f4cc5c08f4d00fdb1c93fcb21c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/9fc060f462a08f07203ebc77a1f36707e61f4e38d8f7d08a910197fc877cec58.png", "https://scitools.github.io/test-iris-imagehash/images/v4/97c160f462a88f07203ebc77a1e36707e61f4e38d8f3d08a910597fc877cec58.png" ], - "example_tests.test_atlantic_profiles.TestAtlanticProfiles.test_atlantic_profiles.1": [ + "gallery_tests.test_plot_atlantic_profiles.TestAtlanticProfiles.test_plot_atlantic_profiles.1": [ "https://scitools.github.io/test-iris-imagehash/images/v4/a6eaa57e6e81ddf999311ba3b3775e20845d5889c199673b4e22a4675e8ca11c.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eeea64dd6ea8cd99991f1322b3761e06845718d89995b3131f32a4765ec2a1cd.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eeea64dd6ea8cd99991d1322b3741e2684571cd89995b3131f32a4765ee2a1cc.png" ], - "example_tests.test_coriolis_plot.TestCoriolisPlot.test_coriolis_plot.0": [ + "gallery_tests.test_plot_coriolis.TestCoriolisPlot.test_plot_coriolis.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/e78665de9a699659e55e9965886979966986c5e63e98c19e3a256679e1981a24.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e68665de9a699659c1fe99a5896965966996c46e3e19c1da3a652669c51e1a26.png" ], - "example_tests.test_cross_section.TestCrossSection.test_cross_section.0": [ + "gallery_tests.test_plot_cross_section.TestCrossSection.test_plot_cross_section.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/ea95317b9562e4d1649f5a05856e4ca4da52947e4ea5f13f1b499d42f13b1b41.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea91b17b9562e4d1609f5a05856e4ca45a52957e5ea5f13b1bca9dc0b17b1ac1.png" ], - "example_tests.test_cross_section.TestCrossSection.test_cross_section.1": [ + "gallery_tests.test_plot_cross_section.TestCrossSection.test_plot_cross_section.1": [ "https://scitools.github.io/test-iris-imagehash/images/v4/ea9521fb956a394069921e93f07f4aad856cc47e4e95857a1ea5da3591ba1b81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea9521fb956a394068931e9be07e4aa5856cc47e4a91957a1ba55bb5b17a3b81.png" ], - "example_tests.test_custom_aggregation.TestCustomAggregation.test_custom_aggregation.0": [ + "gallery_tests.test_plot_custom_aggregation.TestCustomAggregation.test_plot_custom_aggregation.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fe816e81917e907eb43e873f85677ac190f0703c6a95811f1ac33ce1a57a6f18.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe816e81817e907eb43e873f85637ac198d8703c6a94811f1ac73ee1a57a6f90.png" ], - "example_tests.test_custom_file_loading.TestCustomFileLoading.test_custom_file_loading.0": [ + "gallery_tests.test_plot_custom_file_loading.TestCustomFileLoading.test_plot_custom_file_loading.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/faa0cbf1845e34be913787416edcc8bc3bc81f9b63332662a4ed30cdc1b2cd21.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fba0cbf1845e34be912787416edcc8bc3b881f9b62332762a5ad32cdc1b2cd21.png", "https://scitools.github.io/test-iris-imagehash/images/v4/faa1cb47845e34bc912797436cccc8343f11359b73523746c48c72d9d9b34da5.png" ], - "example_tests.test_deriving_phenomena.TestDerivingPhenomena.test_deriving_phenomena.0": [ + "gallery_tests.test_plot_deriving_phenomena.TestDerivingPhenomena.test_plot_deriving_phenomena.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/b9993986866952e6c9464639c4766bd9c669916e7b99c1663f99768990763e81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b99139de866952e6c946c639c47e6bd18769d16e7a9981662e813699d0763e89.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ec97681793689768943c97e8926669d186e8c33f6c99c32e6b936c83d33e2c98.png" ], - "example_tests.test_global_map.TestGlobalMap.test_global_map.0": [ + "gallery_tests.test_plot_global_map.TestGlobalMap.test_plot_global_map.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fa9979468566857ef07e3e8978566b91cb0179883c89946686a96b9d83766f81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa997b958466846ed13e87467a997a898d66d17e2cc9906684696f99d3162f81.png" ], - "example_tests.test_hovmoller.TestGlobalMap.test_hovmoller.0": [ + "gallery_tests.test_plot_hovmoller.TestGlobalMap.test_plot_hovmoller.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/bab430b4ce4bce43c5becf89c54b1a63c543c56e1e64907e3bb469b490de1ac1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eeb46cb4934b934bc07e974bc14b38949943c0fe3e94c17f6ea46cb4c07b3f00.png" ], - "example_tests.test_inset_plot.TestInsetPlot.test_inset_plot.0": [ + "gallery_tests.test_plot_inset.TestInsetPlot.test_plot_inset.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/ebff6992f50096a5b245dac4f6559496b49248dbc95dcb699529912dcf244a54.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9ff6992b50096a5b245dac4f64594b6b49248dbc95dcb699529952dcf244a56.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ebff6992b50096ad9267dac4d64094b294924cdbc95d4b699d29952dcda46e94.png" ], - "example_tests.test_lagged_ensemble.TestLaggedEnsemble.test_lagged_ensemble.0": [ + "gallery_tests.test_plot_lagged_ensemble.TestLaggedEnsemble.test_plot_lagged_ensemble.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/bbbb31e1c44e64e4b0459b5bb1716ecac464f496ce34618eb1079b39b193ce25.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bbbb31b1c44e64e4b1579b5b917133cecc61f146c414668eb1119b1bb197ce34.png" ], - "example_tests.test_lagged_ensemble.TestLaggedEnsemble.test_lagged_ensemble.1": [ + "gallery_tests.test_plot_lagged_ensemble.TestLaggedEnsemble.test_plot_lagged_ensemble.1": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abfef958fd462c993a07d87960464b81d1009687c139d3b594e9cf87c6b89687.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aafec5e9e5e03e099a07e0f86542db879438261ec3b13ce78d8dc65a92d83d89.png" ], - "example_tests.test_lineplot_with_legend.TestLineplotWithLegend.test_lineplot_with_legend.0": [ + "gallery_tests.test_plot_lineplot_with_legend.TestLineplotWithLegend.test_plot_lineplot_with_legend.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/eae942526540b869961f8da694589da69543cc9af1014afbc3fd596b84fe19a7.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eae942146540b869961f8de694589da69543cc9af1014afbc3fd596b84fe19a7.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eafd9e12a5a061e9925ec716de489e9685078ec981b229e70ddb79219cc3768d.png" ], - "example_tests.test_load_nemo.TestLoadNemo.test_load_nemo.0": [ + "gallery_tests.test_plot_load_nemo.TestLoadNemo.test_plot_load_nemo.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/a3ff34e87f0049496d17c4d9c04fc225d256971392d39f1696df0f16cec00f36.png" ], - "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.0": [ + "gallery_tests.test_plot_orca_projection.TestOrcaProjection.test_plot_orca_projection.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fb11731a94cea4ee64b35e91d1d2304e9e5ac7397b20e1fe12852487e666ce46.png", "https://scitools.github.io/test-iris-imagehash/images/v4/bb11721a87cce5e4cce79e81d19b3b5e1e1cd3783168e07835853485e65e2e1e.png" ], - "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.1": [ + "gallery_tests.test_plot_orca_projection.TestOrcaProjection.test_plot_orca_projection.1": [ "https://scitools.github.io/test-iris-imagehash/images/v4/e5a665a69a599659e5db1865c2653b869996cce63e99e19a1a912639e7181e65.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e58661969e799659c1f719a6c867359a1996c0773649c09c3e612679c07b3f66.png" ], - "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.2": [ + "gallery_tests.test_plot_orca_projection.TestOrcaProjection.test_plot_orca_projection.2": [ "https://scitools.github.io/test-iris-imagehash/images/v4/f2c464ce9e399332e1b74ce1cc79338c6586e5b33b31b37a66c9664cc06e1a64.png", "https://scitools.github.io/test-iris-imagehash/images/v4/a58660ce9e739b31c93d1cc9c8df33863383e33b3f11c03f2664366cc8ee3cc1.png" ], - "example_tests.test_orca_projection.TestOrcaProjection.test_orca_projection.3": [ + "gallery_tests.test_plot_orca_projection.TestOrcaProjection.test_plot_orca_projection.3": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fa817a83846ea46ce539c93391de32cc86cf87a33fa168721cdb3e896e374b04.png", "https://scitools.github.io/test-iris-imagehash/images/v4/be817a87845ea56cec79817a919e338436a5c1e73fa16c736c4a3e816a1e6b1c.png" ], - "example_tests.test_polar_stereo.TestPolarStereo.test_polar_stereo.0": [ + "gallery_tests.test_plot_polar_stereo.TestPolarStereo.test_plot_polar_stereo.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/e168317a92d36d89c5bb9e94c55e6f0c9a93c15a6ec584763b21716791de3a81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/b9e16079971e9e93c8ce0f84c31e3b929f92c0ff3ca1c17e39e03961c07e3f80.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e615ec7e097a9961f9cb190f838e091c2c1e73f07c11f6f386b3cc1783e11.png" ], - "example_tests.test_polynomial_fit.TestPolynomialFit.test_polynomial_fit.0": [ + "gallery_tests.test_plot_polynomial_fit.TestPolynomialFit.test_plot_polynomial_fit.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abff4a9df26435886520c97f12414695c4b69d23934bc86adc969237d68ccc6f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aaff4a9df26435886520c97f12414695c4b69d23934bc86adc969a17d69ccc6f.png", "https://scitools.github.io/test-iris-imagehash/images/v4/aeffcb34d244348be5a2c96c3a4fc6d0c4b69f2d87294ccb9f1a125684cd7c11.png" ], - "example_tests.test_projections_and_annotations.TestProjectionsAndAnnotations.test_projections_and_annotations.0": [ + "gallery_tests.test_plot_projections_and_annotations.TestProjectionsAndAnnotations.test_plot_projections_and_annotations.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fa854f19851a30e4cc76cd0bb179325ca7c665b0c938cb4b4e719e9cb727b5c0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fac54f19851a30e4cc76cd0bb179325cb78665b0c938cb4b4e719e9c9727b5c0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb7c664b0c938cb4bce739e9c37a3b5c0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa854e19851a30e4cc76cd0bb179325cb78665b1c938c94bce739e9c3727b5c0.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa854f19851a30e4cc76cd0bb0f932dca7c665b1c92ccb4b4ed19e9c3721b5c8.png" ], - "example_tests.test_projections_and_annotations.TestProjectionsAndAnnotations.test_projections_and_annotations.1": [ + "gallery_tests.test_plot_projections_and_annotations.TestProjectionsAndAnnotations.test_plot_projections_and_annotations.1": [ "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492e9b69964936dc21974b18592.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896727243318f8dad5a7dc65ba492b93699649b6dc25b64938592.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e385699d9c3896627243318fcdad5a7dc6dba492b93699649b6dc25964938592.png", @@ -131,29 +131,29 @@ "https://scitools.github.io/test-iris-imagehash/images/v4/e3856b999c3896727243318f8dad5a75865ba492e9b69964db6cc65b74918592.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e3856d999c389662734731afcdad5a7384daa592b1b69b64d26dc29974b18590.png" ], - "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.0": [ + "gallery_tests.test_plot_rotated_pole_mapping.TestRotatedPoleMapping.test_plot_rotated_pole_mapping.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fa15615e97a193adc15e1e81c4fa3eb49d30817e3e05c17e7ba59927817e1e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ee46607e97a19781c0df1f81d0bb3e241f20c16f3fc0c1fe39263d33d06f3e80.png" ], - "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.1": [ + "gallery_tests.test_plot_rotated_pole_mapping.TestRotatedPoleMapping.test_plot_rotated_pole_mapping.1": [ "https://scitools.github.io/test-iris-imagehash/images/v4/ba056717c3e099e9b90f8e81c4da589499b696763e45e56b3b893929c17b7e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea57685f95a886a1c0de9da090be3e2697e1c0ff3f00c17e6b266c17c07f3f00.png" ], - "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.2": [ + "gallery_tests.test_plot_rotated_pole_mapping.TestRotatedPoleMapping.test_plot_rotated_pole_mapping.2": [ "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a191a1b85e9e81c4da58909996b37e3a65e16f7b817939e57a1e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ba1e605ec7a193a1b85e9e81c4da58909996b3763a65e16f7b816939ed7a1e01.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e85a697e97a18681c6da9f8190bf3e263624c1ef3b48c17a2b223c47c0ff3f81.png", "https://scitools.github.io/test-iris-imagehash/images/v4/ea57685f95a886a1c0de9da090be3e2497e1c0ef3f01c17e6b366c17c07b3f01.png" ], - "example_tests.test_rotated_pole_mapping.TestRotatedPoleMapping.test_rotated_pole_mapping.3": [ + "gallery_tests.test_plot_rotated_pole_mapping.TestRotatedPoleMapping.test_plot_rotated_pole_mapping.3": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172d0847ecd2bc913939c36846c714933799cc3cc8727e67639f939996a58.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172c6857ecd38cb3392ce36c564311931d85ec64e9787719a39993c316e66.png" ], - "example_tests.test_wind_speed.TestWindSpeed.test_wind_speed.0": [ + "gallery_tests.test_plot_wind_speed.TestWindSpeed.test_plot_wind_speed.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cd3e29847b18e639e6c14f1a09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9e960e996169306c1ee9e96c29e36739e13c07d3d61c07f39a139a1c07f3f01.png" ], - "example_tests.test_wind_speed.TestWindSpeed.test_wind_speed.1": [ + "gallery_tests.test_plot_wind_speed.TestWindSpeed.test_plot_wind_speed.1": [ "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cc3e29847b38e639e6c14f1a09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9e960e996169306c1ee9e86c29e36739e13c07d3d61c07f39a139a1c17f3f01.png" ], diff --git a/lib/iris/tests/runner/_runner.py b/lib/iris/tests/runner/_runner.py index 8175e7b19f..71b6e5fcc6 100644 --- a/lib/iris/tests/runner/_runner.py +++ b/lib/iris/tests/runner/_runner.py @@ -21,7 +21,7 @@ class TestRunner: description = ( "Run tests under nose and multiprocessor for performance. " - "Default behaviour is to run all non-example tests. " + "Default behaviour is to run all non-gallery tests. " "Specifying one or more test flags will run *only* those " "tests." ) @@ -34,7 +34,7 @@ class TestRunner: ), ("stop", "x", "Stop running tests after the first error or failure."), ("system-tests", "s", "Run the limited subset of system tests."), - ("example-tests", "e", "Run the example code tests."), + ("gallery-tests", "e", "Run the gallery code tests."), ("default-tests", "d", "Run the default tests."), ( "coding-tests", @@ -53,7 +53,7 @@ class TestRunner: "no-data", "system-tests", "stop", - "example-tests", + "gallery-tests", "default-tests", "coding-tests", "create-missing", @@ -63,7 +63,7 @@ def initialize_options(self): self.no_data = False self.stop = False self.system_tests = False - self.example_tests = False + self.gallery_tests = False self.default_tests = False self.coding_tests = False self.num_processors = None @@ -87,8 +87,8 @@ def finalize_options(self): tests.append("default") if self.coding_tests: tests.append("coding") - if self.example_tests: - tests.append("example") + if self.gallery_tests: + tests.append("gallery") if not tests: tests.append("default") print("Running test suite(s): {}".format(", ".join(tests))) @@ -114,19 +114,19 @@ def run(self): tests.append("iris.tests") if self.coding_tests: tests.append("iris.tests.test_coding_standards") - if self.example_tests: + if self.gallery_tests: import iris.config default_doc_path = os.path.join(sys.path[0], "docs", "iris") doc_path = iris.config.get_option( "Resources", "doc_dir", default=default_doc_path ) - example_path = os.path.join(doc_path, "example_tests") - if os.path.exists(example_path): - tests.append(example_path) + gallery_path = os.path.join(doc_path, "gallery_tests") + if os.path.exists(gallery_path): + tests.append(gallery_path) else: print( - "WARNING: Example path %s does not exist." % (example_path) + "WARNING: Gallery path %s does not exist." % (gallery_path) ) if not tests: tests.append("iris.tests") diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py index cfb54203b3..00ce7b7d44 100644 --- a/lib/iris/tests/test_coding_standards.py +++ b/lib/iris/tests/test_coding_standards.py @@ -104,13 +104,12 @@ def test_license_headers(self): "setup.py", "build/*", "dist/*", - "docs/iris/example_code/*/*.py", + "docs/iris/gallery_code/*/*.py", "docs/iris/src/developers_guide/documenting/*.py", - "docs/iris/src/sphinxext/gen_gallery.py", "docs/iris/src/userguide/plotting_examples/*.py", "docs/iris/src/userguide/regridding_plots/*.py", "docs/iris/src/developers_guide/gitwash_dumper.py", - "docs/iris/build/*", + "docs/iris/src/_build/*", "lib/iris/analysis/_scipy_interpolate.py", "lib/iris/fileformats/_pyke_rules/*", ) diff --git a/requirements/docs.txt b/requirements/docs.txt index 6966869c70..2d2c03f688 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1 +1,4 @@ sphinx +sphinx_rtd_theme +sphinx-copybutton +sphinx-gallery diff --git a/setup.py b/setup.py index e5dd0e7bb9..b078e3de1f 100644 --- a/setup.py +++ b/setup.py @@ -181,7 +181,6 @@ def build_std_names(cmd, directory): xml_path = os.path.join("etc", "cf-standard-name-table.xml") module_path = os.path.join(directory, "iris", "std_names.py") args = (sys.executable, script_path, xml_path, module_path) - cmd.spawn(args) diff --git a/tools/generate_std_names.py b/tools/generate_std_names.py index 3aad3bb09c..95dcce8171 100644 --- a/tools/generate_std_names.py +++ b/tools/generate_std_names.py @@ -35,14 +35,17 @@ This file is automatically generated. Do not edit this file by hand. -The file will be generated during a standard build/installation: +The file will be generated during a standard build/installation:: + python setup.py build python setup.py install -Also, the file can be re-generated in the source distribution via: +Also, the file can be re-generated in the source distribution via:: + python setup.py std_names -Or for more control (e.g. to use an alternative XML file) via: +Or for more control (e.g. to use an alternative XML file) via:: + python tools/generate_std_names.py XML_FILE MODULE_FILE """ From 90c4a879ed5666461f0655610e9502f774f9b416 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 15 Jul 2020 11:16:52 +0100 Subject: [PATCH 13/32] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 126869b267..ca43779283 100644 --- a/README.md +++ b/README.md @@ -120,4 +120,4 @@ of its [GNU LGPLv3 license](COPYING.LESSER). # Contributing Information on how to contribute can be found in the [Iris developer guide](https://scitools.org.uk/iris/docs/latest/developers_guide/index.html). -(C) British Crown Copyright 2010 - 2019, Met Office +(C) British Crown Copyright 2010 - 2020, Met Office From 3a52cbd81760bc1fb58bd62cd7dd33c977218507 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 15 Jul 2020 11:27:58 +0100 Subject: [PATCH 14/32] fix travis-ci push-built-docs (#3753) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 262e8d7791..1eb9e1285a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -178,7 +178,7 @@ script: - if [[ "${ORG}" == 'SciTools' && "${TRAVIS_EVENT_TYPE}" == 'push' && "${PUSH_BUILT_DOCS}" == 'true' ]]; then cd ${INSTALL_DIR}; pip install doctr; - doctr deploy --deploy-repo SciTools-docs/iris --built-docs docs/iris/build/html + doctr deploy --deploy-repo SciTools-docs/iris --built-docs docs/iris/src/_build/html --key-path .github/deploy_key.scitools-docs.enc --no-require-master ${TRAVIS_BRANCH:-${TRAVIS_TAG}}; From 9bc92f78896f216fe4afd5d59c9584cf51c73681 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Wed, 15 Jul 2020 13:04:41 +0100 Subject: [PATCH 15/32] added readthedocs badge (#3754) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ca43779283..469db4619a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ Travis-CI + + Documentation Status conda-forge downloads @@ -29,9 +32,6 @@ Commits since last release - -Latest docs zenodo From 70307ae8fb8c2a4648876989d5b58de7ff0e73f3 Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Thu, 23 Jul 2020 09:56:52 +0100 Subject: [PATCH 16/32] Numpy rounding fix (#3758) ensure rounding is numpy like (maintains type) --- lib/iris/fileformats/pp_load_rules.py | 2 +- requirements/core.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/fileformats/pp_load_rules.py b/lib/iris/fileformats/pp_load_rules.py index c0a4081970..b6aacb382f 100644 --- a/lib/iris/fileformats/pp_load_rules.py +++ b/lib/iris/fileformats/pp_load_rules.py @@ -627,7 +627,7 @@ def _convert_time_coords( def date2hours(t): epoch_hours = _epoch_date_hours(epoch_hours_unit, t) if t.minute == 0 and t.second == 0: - epoch_hours = round(epoch_hours) + epoch_hours = np.around(epoch_hours) return epoch_hours def date2year(t_in): diff --git a/requirements/core.txt b/requirements/core.txt index 3f2f458595..9d0d6526dd 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -8,7 +8,7 @@ cartopy>=0.12 cf-units>=2 cftime==1.1.3 dask[array]>=2 #conda: dask>=2 -matplotlib +matplotlib<3.3 netcdf4 numpy>=1.14 scipy From 7c86bc0168684345dc475457b1a77dadc77ce9bb Mon Sep 17 00:00:00 2001 From: Bill Little Date: Thu, 23 Jul 2020 10:18:22 +0100 Subject: [PATCH 17/32] flake8 outstanding files (#3755) --- .flake8 | 4 ++-- docs/iris/gallery_tests/gallerytest_util.py | 2 +- lib/iris/_concatenate.py | 1 - lib/iris/analysis/_area_weighted.py | 3 --- lib/iris/analysis/_regrid.py | 3 +-- lib/iris/fileformats/name_loaders.py | 6 +++--- lib/iris/fileformats/nimrod_load_rules.py | 4 ++-- lib/iris/tests/unit/coords/test_CellMethod.py | 2 +- .../fc_rules_cf_fc/test_build_auxiliary_coordinate.py | 1 - .../fc_rules_cf_fc/test_build_mercator_coordinate_system.py | 2 -- .../test_build_stereographic_coordinate_system.py | 2 -- .../test_has_supported_mercator_parameters.py | 3 +-- .../test_has_supported_stereographic_parameters.py | 3 --- 13 files changed, 11 insertions(+), 25 deletions(-) diff --git a/.flake8 b/.flake8 index 257b9b3d62..38cd1d82f7 100644 --- a/.flake8 +++ b/.flake8 @@ -26,8 +26,8 @@ exclude = .eggs, build, compiled_krb, - sphinxext, - tools, + docs/iris/src/sphinxext/*, + tools/*, # # ignore auto-generated files # diff --git a/docs/iris/gallery_tests/gallerytest_util.py b/docs/iris/gallery_tests/gallerytest_util.py index 38678fdb18..3ec18d0169 100644 --- a/docs/iris/gallery_tests/gallerytest_util.py +++ b/docs/iris/gallery_tests/gallerytest_util.py @@ -35,7 +35,7 @@ @contextlib.contextmanager def add_gallery_to_path(): """ - Creates a context manager which can be used to add the iris gallery + Creates a context manager which can be used to add the iris gallery to the PYTHONPATH. The gallery entries are only importable throughout the lifetime of this context manager. diff --git a/lib/iris/_concatenate.py b/lib/iris/_concatenate.py index 646613d114..32dc87d65b 100644 --- a/lib/iris/_concatenate.py +++ b/lib/iris/_concatenate.py @@ -9,7 +9,6 @@ """ from collections import defaultdict, namedtuple -from copy import deepcopy import dask.array as da import numpy as np diff --git a/lib/iris/analysis/_area_weighted.py b/lib/iris/analysis/_area_weighted.py index 06f44dc951..7ff5430ca6 100644 --- a/lib/iris/analysis/_area_weighted.py +++ b/lib/iris/analysis/_area_weighted.py @@ -4,10 +4,7 @@ # See COPYING and COPYING.LESSER in the root of the repository for full # licensing details. -import numpy as np - from iris.analysis._interpolation import get_xy_dim_coords, snapshot_grid -import iris import iris.experimental.regrid as eregrid diff --git a/lib/iris/analysis/_regrid.py b/lib/iris/analysis/_regrid.py index 0670c073ae..71584f04c0 100644 --- a/lib/iris/analysis/_regrid.py +++ b/lib/iris/analysis/_regrid.py @@ -426,8 +426,7 @@ def _get_horizontal_coord(cube, axis): if len(coords) != 1: raise ValueError( "Cube {!r} must contain a single 1D {} " - "coordinate.".format(cube.name()), - axis, + "coordinate.".format(cube.name(), axis) ) return coords[0] diff --git a/lib/iris/fileformats/name_loaders.py b/lib/iris/fileformats/name_loaders.py index 0d9d149664..0464eb37ed 100644 --- a/lib/iris/fileformats/name_loaders.py +++ b/lib/iris/fileformats/name_loaders.py @@ -882,7 +882,7 @@ def load_NAMEIII_timeseries(filename): for i, data_list in enumerate(data_lists): data_list.append(float(vals[i + 1])) - data_arrays = [np.array(l) for l in data_lists] + data_arrays = [np.array(dl) for dl in data_lists] time_array = np.array(time_list) tdim = NAMECoord(name="time", dimension=0, values=time_array) @@ -955,7 +955,7 @@ def load_NAMEII_timeseries(filename): for i, data_list in enumerate(data_lists): data_list.append(float(vals[i + 2])) - data_arrays = [np.array(l) for l in data_lists] + data_arrays = [np.array(dl) for dl in data_lists] time_array = np.array(time_list) tdim = NAMECoord(name="time", dimension=0, values=time_array) @@ -1111,7 +1111,7 @@ def load_NAMEIII_version2(filename): for i, data_list in enumerate(data_lists): data_list.append(float(vals[i + datacol1])) - data_arrays = [np.array(l) for l in data_lists] + data_arrays = [np.array(dl) for dl in data_lists] # Convert Z and T arrays into arrays of indices zind = [] diff --git a/lib/iris/fileformats/nimrod_load_rules.py b/lib/iris/fileformats/nimrod_load_rules.py index 7a0fd20fb9..4cf8755bb9 100644 --- a/lib/iris/fileformats/nimrod_load_rules.py +++ b/lib/iris/fileformats/nimrod_load_rules.py @@ -413,8 +413,8 @@ def coord_system(field, handle_metadata_errors): ) if any([is_missing(field, v) for v in crs_args]): warnings.warn( - f"Coordinate Reference System is not completely defined. " - f"Plotting and reprojection may be impaired." + "Coordinate Reference System is not completely defined. " + "Plotting and reprojection may be impaired." ) coord_sys = iris.coord_systems.TransverseMercator( *crs_args, iris.coord_systems.GeogCS(**ellipsoid), diff --git a/lib/iris/tests/unit/coords/test_CellMethod.py b/lib/iris/tests/unit/coords/test_CellMethod.py index 88906dd905..3014823f9f 100644 --- a/lib/iris/tests/unit/coords/test_CellMethod.py +++ b/lib/iris/tests/unit/coords/test_CellMethod.py @@ -84,7 +84,7 @@ def test_mixture_default(self): token = "air temperature" # includes space coord = AuxCoord(1, long_name=token) result = CellMethod(self.method, coords=[coord, token]) - expected = "{}: unknown, unknown".format(self.method, token, token) + expected = "{}: unknown, unknown".format(self.method) self.assertEqual(str(result), expected) diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_auxiliary_coordinate.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_auxiliary_coordinate.py index 70d72fb133..8734d883cd 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_auxiliary_coordinate.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_auxiliary_coordinate.py @@ -68,7 +68,6 @@ def setUp(self): # Patch the deferred loading that prevents attempted file access. # This assumes that self.cf_bounds_var is defined in the test case. def patched__getitem__(proxy_self, keys): - variable = None for var in (self.cf_coord_var, self.cf_bounds_var): if proxy_self.variable_name == var.cf_name: return var[keys] diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_mercator_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_mercator_coordinate_system.py index 2f02c71c9c..665beb8747 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_mercator_coordinate_system.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_mercator_coordinate_system.py @@ -15,8 +15,6 @@ from unittest import mock -import numpy as np - import iris from iris.coord_systems import Mercator from iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc import \ diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_stereographic_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_stereographic_coordinate_system.py index 8912614f96..e95f286a8d 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_stereographic_coordinate_system.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_stereographic_coordinate_system.py @@ -15,8 +15,6 @@ from unittest import mock -import numpy as np - import iris from iris.coord_systems import Stereographic from iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc import \ diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_has_supported_mercator_parameters.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_has_supported_mercator_parameters.py index 1c167ec45d..4be7b04249 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_has_supported_mercator_parameters.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_has_supported_mercator_parameters.py @@ -17,8 +17,6 @@ from unittest import mock -import numpy as np - from iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc import \ has_supported_mercator_parameters @@ -135,5 +133,6 @@ def test_invalid_false_northing(self): self.assertEqual(len(warns), 1) self.assertRegex(str(warns[0]), 'False northing') + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_has_supported_stereographic_parameters.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_has_supported_stereographic_parameters.py index d02695f298..f528e22029 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_has_supported_stereographic_parameters.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_has_supported_stereographic_parameters.py @@ -17,9 +17,6 @@ from unittest import mock -import numpy as np - -from iris.coord_systems import Stereographic from iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc import \ has_supported_stereographic_parameters From 11bbbeb4574122f9e16a0df89a9de47ef738da0c Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Thu, 23 Jul 2020 12:27:37 +0100 Subject: [PATCH 18/32] unpin cftime (#3757) --- requirements/core.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/core.txt b/requirements/core.txt index 9d0d6526dd..dbc0333d7c 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -6,7 +6,7 @@ cartopy>=0.12 #conda: proj4<6 cf-units>=2 -cftime==1.1.3 +cftime dask[array]>=2 #conda: dask>=2 matplotlib<3.3 netcdf4 From 6c1a211f5035df97b421340adb46ae8f359d63b7 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Fri, 24 Jul 2020 06:35:45 +0100 Subject: [PATCH 19/32] moved to oceanography section of gallery (#3761) --- .../{general => oceanography}/plot_orca_projection.py | 0 docs/iris/src/whatsnew/1.7.rst | 2 +- .../docchange_2020-Jul-23_moved_gallery_entry.txt | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) rename docs/iris/gallery_code/{general => oceanography}/plot_orca_projection.py (100%) create mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-23_moved_gallery_entry.txt diff --git a/docs/iris/gallery_code/general/plot_orca_projection.py b/docs/iris/gallery_code/oceanography/plot_orca_projection.py similarity index 100% rename from docs/iris/gallery_code/general/plot_orca_projection.py rename to docs/iris/gallery_code/oceanography/plot_orca_projection.py diff --git a/docs/iris/src/whatsnew/1.7.rst b/docs/iris/src/whatsnew/1.7.rst index 2d4395239b..757d407684 100644 --- a/docs/iris/src/whatsnew/1.7.rst +++ b/docs/iris/src/whatsnew/1.7.rst @@ -240,7 +240,7 @@ Documentation changes :class:`iris.analysis.Aggregator` has been added. See :ref:`sphx_glr_generated_gallery_general_plot_custom_aggregation.py`. * An example of reprojecting data from 2D auxiliary spatial coordinates - (such as that from the ORCA grid) has been added. See :ref:`sphx_glr_generated_gallery_general_plot_orca_projection.py`. + (such as that from the ORCA grid) has been added. See :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py`. * A clarification of the behaviour of :func:`iris.analysis.calculus.differentiate`. * A new :doc:`"Technical Papers" ` section has been added to the documentation along with the addition of a paper providing an :doc:`overview of the load process for UM-like diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-23_moved_gallery_entry.txt b/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-23_moved_gallery_entry.txt new file mode 100644 index 0000000000..d73021dcc9 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-23_moved_gallery_entry.txt @@ -0,0 +1,2 @@ +* Moved the :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` + from the general part of the gallery to oceanography. From 2fe0bb1f2acef2e243a76d94474b540b742a9e2f Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Mon, 27 Jul 2020 08:35:42 +0100 Subject: [PATCH 20/32] pin matplotlib<3.3 (#3763) --- ci/requirements/readthedocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/requirements/readthedocs.yml b/ci/requirements/readthedocs.yml index 611c69307d..5a7e3975f7 100644 --- a/ci/requirements/readthedocs.yml +++ b/ci/requirements/readthedocs.yml @@ -17,9 +17,9 @@ dependencies: - cartopy>=0.12 - proj4<6 - cf-units>=2 - - cftime==1.1.3 + - cftime - dask>=2 - - matplotlib + - matplotlib<3.3 - netcdf4 - numpy>=1.14 - scipy From e1489ff21df5a282a5eac2400a3278c3e6cb3ed3 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Mon, 3 Aug 2020 10:36:55 +0100 Subject: [PATCH 21/32] enabled pdf creation (#3765) * enabled pdf creation * removed pdf target as it is replaced by Read The Docs service feature --- .readthedocs.yml | 1 + docs/iris/Makefile | 13 ---------- docs/iris/src/conf.py | 55 ++----------------------------------------- 3 files changed, 3 insertions(+), 66 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 1306c3fc2c..55c24eaddc 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -17,3 +17,4 @@ python: formats: - htmlzip + - pdf diff --git a/docs/iris/Makefile b/docs/iris/Makefile index a220502028..411f3fd553 100644 --- a/docs/iris/Makefile +++ b/docs/iris/Makefile @@ -5,19 +5,6 @@ html: echo "make html in $$i..."; \ (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) html); done -pdf: - @for i in $(SUBDIRS); do\ - echo "make latex in $$i.."; \ - (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) latex); done - echo "\def\sphinxdocclass{MO_report}" > build/latex/docs.tex - echo "\documentclass[letterpaper,10pt,english]{MO_report}" >> build/latex/docs.tex - tail -n +4 build/latex/Iris.tex >> build/latex/docs.tex - sed 's/\\tableofcontents/\\tableofcontents\n\\pagenumbering\{arabic\}/' build/latex/docs.tex > build/latex/docs2.tex - sed 's/subsection{/section{/' build/latex/docs2.tex > build/latex/documentation.tex - (cd build/latex; pdflatex -interaction=scrollmode documentation.tex) - # call latex again to get page numbers right... - (cd build/latex; pdflatex -interaction=scrollmode documentation.tex); - all: @for i in $(SUBDIRS); do \ echo "make all in $$i..."; \ diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 2308c065ba..9b0b094c33 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -62,8 +62,8 @@ def autolog(message): # define the copyright information for latex builds. Note, for html builds, # the copyright exists directly inside "_templates/layout.html" upper_copy_year = datetime.datetime.now().year -copyright = "Iris contributors" -_authors = "Iris developers" +copyright = "Iris Contributors" +author = "Iris Developers" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -242,54 +242,3 @@ def autolog(message): message="Matplotlib is currently using agg, which is a" " non-GUI backend, so cannot show the figure.", ) - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -# latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -# latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -# latex_documents = [ -# ( -# "contents", -# "Iris.tex", -# "Iris Documentation", -# " \\and ".join(_authors), -# "manual", -# ), -# ] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Additional stuff for the LaTeX preamble. -# latex_preamble = '' - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True -# latex_elements = {} -# latex_elements["docclass"] = "MO_report" - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -# man_pages = [("index", "iris", "Iris Documentation", _authors, 1)] From b22fbfd1d0f83d0705b8913aead890fe7fc79050 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Mon, 3 Aug 2020 11:30:52 +0100 Subject: [PATCH 22/32] Gallery code consistency (#3766) Tidy up gallery code --- docs/iris/gallery_code/general/__init__.py | 4 ---- docs/iris/gallery_code/general/plot_SOI_filtering.py | 4 +++- .../gallery_code/general/plot_anomaly_log_colouring.py | 6 ++++-- docs/iris/gallery_code/general/plot_coriolis.py | 6 ++++-- .../gallery_code/general/plot_custom_aggregation.py | 1 + .../gallery_code/general/plot_custom_file_loading.py | 4 ++-- docs/iris/gallery_code/general/plot_global_map.py | 1 + docs/iris/gallery_code/general/plot_inset.py | 3 ++- .../gallery_code/general/plot_lineplot_with_legend.py | 1 + .../general/plot_projections_and_annotations.py | 6 ++++-- .../gallery_code/general/plot_rotated_pole_mapping.py | 3 ++- docs/iris/gallery_code/meteorology/__init__.py | 4 ---- docs/iris/gallery_code/meteorology/plot_COP_1d.py | 10 +++++----- docs/iris/gallery_code/meteorology/plot_COP_maps.py | 2 ++ docs/iris/gallery_code/meteorology/plot_TEC.py | 1 + .../meteorology/plot_deriving_phenomena.py | 1 + docs/iris/gallery_code/meteorology/plot_hovmoller.py | 3 ++- .../gallery_code/meteorology/plot_lagged_ensemble.py | 1 + docs/iris/gallery_code/meteorology/plot_wind_speed.py | 5 ++--- docs/iris/gallery_code/oceanography/__init__.py | 4 ---- .../oceanography/plot_atlantic_profiles.py | 5 ++++- docs/iris/gallery_code/oceanography/plot_load_nemo.py | 5 ++++- .../gallery_code/oceanography/plot_orca_projection.py | 2 +- 23 files changed, 47 insertions(+), 35 deletions(-) delete mode 100644 docs/iris/gallery_code/general/__init__.py delete mode 100644 docs/iris/gallery_code/meteorology/__init__.py delete mode 100644 docs/iris/gallery_code/oceanography/__init__.py diff --git a/docs/iris/gallery_code/general/__init__.py b/docs/iris/gallery_code/general/__init__.py deleted file mode 100644 index f67741cf37..0000000000 --- a/docs/iris/gallery_code/general/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -General visualisation examples -============================== -""" diff --git a/docs/iris/gallery_code/general/plot_SOI_filtering.py b/docs/iris/gallery_code/general/plot_SOI_filtering.py index caf4810c65..116e819af7 100644 --- a/docs/iris/gallery_code/general/plot_SOI_filtering.py +++ b/docs/iris/gallery_code/general/plot_SOI_filtering.py @@ -20,8 +20,10 @@ Monthly Weather Review, Vol 112, pp 326-332 """ -import numpy as np + import matplotlib.pyplot as plt +import numpy as np + import iris import iris.plot as iplt diff --git a/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py b/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py index 28f7ce323b..b0cee818de 100644 --- a/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py +++ b/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py @@ -27,12 +27,14 @@ See also: http://en.wikipedia.org/wiki/False_color#Pseudocolor. """ + import cartopy.crs as ccrs +import matplotlib.pyplot as plt +import matplotlib.colors as mcols + import iris import iris.coord_categorisation import iris.plot as iplt -import matplotlib.pyplot as plt -import matplotlib.colors as mcols def main(): diff --git a/docs/iris/gallery_code/general/plot_coriolis.py b/docs/iris/gallery_code/general/plot_coriolis.py index 7999e5385f..cc67d1267c 100644 --- a/docs/iris/gallery_code/general/plot_coriolis.py +++ b/docs/iris/gallery_code/general/plot_coriolis.py @@ -9,11 +9,13 @@ """ import cartopy.crs as ccrs +import matplotlib.pyplot as plt +import numpy as np + + import iris from iris.coord_systems import GeogCS import iris.plot as iplt -import matplotlib.pyplot as plt -import numpy as np def main(): diff --git a/docs/iris/gallery_code/general/plot_custom_aggregation.py b/docs/iris/gallery_code/general/plot_custom_aggregation.py index 2e73aa277a..9c847be779 100644 --- a/docs/iris/gallery_code/general/plot_custom_aggregation.py +++ b/docs/iris/gallery_code/general/plot_custom_aggregation.py @@ -13,6 +13,7 @@ certain temperature over a spell of 5 years or more. """ + import matplotlib.pyplot as plt import numpy as np diff --git a/docs/iris/gallery_code/general/plot_custom_file_loading.py b/docs/iris/gallery_code/general/plot_custom_file_loading.py index 406995d94b..0d130374a7 100644 --- a/docs/iris/gallery_code/general/plot_custom_file_loading.py +++ b/docs/iris/gallery_code/general/plot_custom_file_loading.py @@ -54,13 +54,13 @@ The cube returned from the load function is then used to produce a plot. """ + import datetime +from cf_units import Unit, CALENDAR_GREGORIAN import matplotlib.pyplot as plt import numpy as np -from cf_units import Unit, CALENDAR_GREGORIAN - import iris import iris.coords as icoords import iris.coord_systems as icoord_systems diff --git a/docs/iris/gallery_code/general/plot_global_map.py b/docs/iris/gallery_code/general/plot_global_map.py index 72e8f28743..41fd226921 100644 --- a/docs/iris/gallery_code/general/plot_global_map.py +++ b/docs/iris/gallery_code/general/plot_global_map.py @@ -6,6 +6,7 @@ title and the labels for the axes are automatically derived from the metadata. """ + import cartopy.crs as ccrs import matplotlib.pyplot as plt diff --git a/docs/iris/gallery_code/general/plot_inset.py b/docs/iris/gallery_code/general/plot_inset.py index 4735706ef7..46f5dc5d0f 100644 --- a/docs/iris/gallery_code/general/plot_inset.py +++ b/docs/iris/gallery_code/general/plot_inset.py @@ -8,10 +8,11 @@ """ +import cartopy.crs as ccrs import matplotlib.pyplot as plt import numpy as np + import iris -import cartopy.crs as ccrs import iris.quickplot as qplt import iris.plot as iplt diff --git a/docs/iris/gallery_code/general/plot_lineplot_with_legend.py b/docs/iris/gallery_code/general/plot_lineplot_with_legend.py index aed636489e..5641b9c4d0 100644 --- a/docs/iris/gallery_code/general/plot_lineplot_with_legend.py +++ b/docs/iris/gallery_code/general/plot_lineplot_with_legend.py @@ -3,6 +3,7 @@ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ """ + import matplotlib.pyplot as plt import iris diff --git a/docs/iris/gallery_code/general/plot_projections_and_annotations.py b/docs/iris/gallery_code/general/plot_projections_and_annotations.py index 4f85c43835..e59bb236d7 100644 --- a/docs/iris/gallery_code/general/plot_projections_and_annotations.py +++ b/docs/iris/gallery_code/general/plot_projections_and_annotations.py @@ -13,11 +13,13 @@ We plot these over a specified region, in two different map projections. """ + import cartopy.crs as ccrs +import matplotlib.pyplot as plt +import numpy as np + import iris import iris.plot as iplt -import numpy as np -import matplotlib.pyplot as plt # Define a Cartopy 'ordinary' lat-lon coordinate reference system. diff --git a/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py b/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py index b6a18cac92..063fe93674 100644 --- a/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py +++ b/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py @@ -11,13 +11,14 @@ * Non native projection and a Natural Earth shaded relief image underlay """ + import cartopy.crs as ccrs import matplotlib.pyplot as plt import iris +import iris.analysis.cartography import iris.plot as iplt import iris.quickplot as qplt -import iris.analysis.cartography def main(): diff --git a/docs/iris/gallery_code/meteorology/__init__.py b/docs/iris/gallery_code/meteorology/__init__.py deleted file mode 100644 index 39c05d08c6..0000000000 --- a/docs/iris/gallery_code/meteorology/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Meteorology visualisation examples -================================== -""" diff --git a/docs/iris/gallery_code/meteorology/plot_COP_1d.py b/docs/iris/gallery_code/meteorology/plot_COP_1d.py index 9b95192381..2f93627b77 100644 --- a/docs/iris/gallery_code/meteorology/plot_COP_1d.py +++ b/docs/iris/gallery_code/meteorology/plot_COP_1d.py @@ -28,14 +28,15 @@ can be found in :ref:`cube-statistics`. """ -import numpy as np + import matplotlib.pyplot as plt +import numpy as np + import iris +import iris.analysis.cartography import iris.plot as iplt import iris.quickplot as qplt -import iris.analysis.cartography - def main(): # Load data into three Cubes, one for each set of NetCDF files. @@ -93,6 +94,7 @@ def main(): time=lambda cell: 1860 <= cell.point.year <= 1999 ) observed = a1b_mean.extract(constraint) + # Assert that this data set is the same as the e1 scenario: # they share data up to the 1999 cut off. assert np.all(np.isclose(observed.data, e1_mean.extract(constraint).data)) @@ -105,9 +107,7 @@ def main(): plt.title("North American mean air temperature", fontsize=18) plt.xlabel("Time / year") - plt.grid() - iplt.show() diff --git a/docs/iris/gallery_code/meteorology/plot_COP_maps.py b/docs/iris/gallery_code/meteorology/plot_COP_maps.py index 840c371c14..a8e6055a77 100644 --- a/docs/iris/gallery_code/meteorology/plot_COP_maps.py +++ b/docs/iris/gallery_code/meteorology/plot_COP_maps.py @@ -21,7 +21,9 @@ doi:10.1029/2009EO210001. """ + import os.path + import matplotlib.pyplot as plt import numpy as np diff --git a/docs/iris/gallery_code/meteorology/plot_TEC.py b/docs/iris/gallery_code/meteorology/plot_TEC.py index 8320af90e9..df2e29ef19 100644 --- a/docs/iris/gallery_code/meteorology/plot_TEC.py +++ b/docs/iris/gallery_code/meteorology/plot_TEC.py @@ -34,6 +34,7 @@ def main(): plt.ylabel("latitude / degrees") plt.gca().stock_img() plt.gca().coastlines() + iplt.show() diff --git a/docs/iris/gallery_code/meteorology/plot_deriving_phenomena.py b/docs/iris/gallery_code/meteorology/plot_deriving_phenomena.py index 7b3f50a8a5..0bb1fa53a4 100644 --- a/docs/iris/gallery_code/meteorology/plot_deriving_phenomena.py +++ b/docs/iris/gallery_code/meteorology/plot_deriving_phenomena.py @@ -9,6 +9,7 @@ plot. """ + import matplotlib.pyplot as plt import matplotlib.ticker diff --git a/docs/iris/gallery_code/meteorology/plot_hovmoller.py b/docs/iris/gallery_code/meteorology/plot_hovmoller.py index d8954d775f..9f18b8021e 100644 --- a/docs/iris/gallery_code/meteorology/plot_hovmoller.py +++ b/docs/iris/gallery_code/meteorology/plot_hovmoller.py @@ -8,8 +8,9 @@ temperature. """ -import matplotlib.pyplot as plt + import matplotlib.dates as mdates +import matplotlib.pyplot as plt import iris import iris.plot as iplt diff --git a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py index 298d178a1e..5f2ab724b3 100644 --- a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py +++ b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py @@ -17,6 +17,7 @@ model, from each ensemble member. """ + import matplotlib.pyplot as plt import numpy as np diff --git a/docs/iris/gallery_code/meteorology/plot_wind_speed.py b/docs/iris/gallery_code/meteorology/plot_wind_speed.py index 2d8081158c..6844d3874c 100644 --- a/docs/iris/gallery_code/meteorology/plot_wind_speed.py +++ b/docs/iris/gallery_code/meteorology/plot_wind_speed.py @@ -11,6 +11,8 @@ """ +import cartopy.crs as ccrs +import cartopy.feature as cfeat import matplotlib.pyplot as plt import numpy as np @@ -18,9 +20,6 @@ import iris.coord_categorisation import iris.quickplot as qplt -import cartopy.feature as cfeat -import cartopy.crs as ccrs - def main(): # Load the u and v components of wind from a pp file diff --git a/docs/iris/gallery_code/oceanography/__init__.py b/docs/iris/gallery_code/oceanography/__init__.py deleted file mode 100644 index afac828a05..0000000000 --- a/docs/iris/gallery_code/oceanography/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Oceanography visualisation examples -=================================== -""" diff --git a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py index 8a541c11fa..a7e82c34f5 100644 --- a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py +++ b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py @@ -15,10 +15,12 @@ depth values intuitively increase downward on the y-axis. """ + +import matplotlib.pyplot as plt + import iris import iris.iterate import iris.plot as iplt -import matplotlib.pyplot as plt def main(): @@ -56,6 +58,7 @@ def main(): ax1.set_ylabel("Depth / m") for ticklabel in ax1.get_xticklabels(): ticklabel.set_color(temperature_color) + # To plot salinity in the same axes we use twiny(). We'll use a different # color to identify salinity. salinity_color = (0.6, 0.1, 0.15) diff --git a/docs/iris/gallery_code/oceanography/plot_load_nemo.py b/docs/iris/gallery_code/oceanography/plot_load_nemo.py index 645617f600..5f2b72c956 100644 --- a/docs/iris/gallery_code/oceanography/plot_load_nemo.py +++ b/docs/iris/gallery_code/oceanography/plot_load_nemo.py @@ -7,12 +7,14 @@ different time dimensions in these files can prevent Iris from concatenating them without the intervention shown here. """ + from __future__ import unicode_literals +import matplotlib.pyplot as plt + import iris import iris.plot as iplt import iris.quickplot as qplt -import matplotlib.pyplot as plt from iris.util import promote_aux_coord_to_dim_coord @@ -57,6 +59,7 @@ def main(): cube.long_name.capitalize(), lat_string, lon_string ) ) + iplt.show() diff --git a/docs/iris/gallery_code/oceanography/plot_orca_projection.py b/docs/iris/gallery_code/oceanography/plot_orca_projection.py index bf2498c229..627be8591b 100644 --- a/docs/iris/gallery_code/oceanography/plot_orca_projection.py +++ b/docs/iris/gallery_code/oceanography/plot_orca_projection.py @@ -12,9 +12,9 @@ """ +import cartopy.crs as ccrs import matplotlib.pyplot as plt -import cartopy.crs as ccrs import iris import iris.analysis.cartography import iris.plot as iplt From 3bb0403513c5fb5fd2ec1601de1b412311603f31 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Tue, 4 Aug 2020 17:19:08 +0100 Subject: [PATCH 23/32] whatsnew overhaul (#3769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * whatsnew overhaul * corrected link * various corrections * typo fix * Update docs/iris/src/developers_guide/documenting/whats_new_contributions.rst Co-authored-by: Patrick Peglar * Âwording improvements Co-authored-by: Patrick Peglar --- .travis.yml | 19 - docs/iris/Makefile | 5 + docs/iris/src/conf.py | 7 + .../contributing_documentation.rst | 2 + .../documenting/whats_new_contributions.rst | 134 +++---- docs/iris/src/developers_guide/release.rst | 152 ++++++-- docs/iris/src/index.rst | 2 + docs/iris/src/whatsnew/1.0.rst | 88 +++-- docs/iris/src/whatsnew/1.1.rst | 75 ++-- docs/iris/src/whatsnew/1.10.rst | 313 ++++++++++++---- docs/iris/src/whatsnew/1.11.rst | 56 +-- docs/iris/src/whatsnew/1.12.rst | 26 +- docs/iris/src/whatsnew/1.13.rst | 83 +++-- docs/iris/src/whatsnew/1.2.rst | 45 ++- docs/iris/src/whatsnew/1.3.rst | 98 ++--- docs/iris/src/whatsnew/1.4.rst | 156 +++++--- docs/iris/src/whatsnew/1.5.rst | 72 +++- docs/iris/src/whatsnew/1.6.rst | 326 ++++++++++------- docs/iris/src/whatsnew/1.7.rst | 272 +++++++++----- docs/iris/src/whatsnew/1.8.rst | 213 +++++++---- docs/iris/src/whatsnew/1.9.rst | 213 +++++++---- docs/iris/src/whatsnew/2.0.rst | 26 +- docs/iris/src/whatsnew/2.1.rst | 79 ++-- docs/iris/src/whatsnew/2.2.rst | 48 +-- docs/iris/src/whatsnew/2.3.rst | 95 +++-- docs/iris/src/whatsnew/2.4.rst | 75 ++-- docs/iris/src/whatsnew/aggregate_directory.py | 337 ------------------ ...x_2019-Dec-02_cell_measure_concatenate.txt | 2 - ...ov-14_cell_measure_positional_argument.txt | 4 - ...fix_2019-Nov-19_cell_measure_copy_loss.txt | 2 - .../bugfix_2020-Feb-13_cube_iter_remove.txt | 3 - ...remove_aux_factories_with_remove_coord.txt | 2 - ...9-Oct-11_remove_LBProc_flag_attributes.txt | 2 - ...-Oct-14_remove_deprecated_future_flags.txt | 3 - ...ange_2019-Dec-04_black_code_formatting.txt | 6 - ...mentation_using_themes_and_readthedocs.txt | 2 - ...change_2020-Jul-23_moved_gallery_entry.txt | 2 - ...remove_experimental_concatenate_module.txt | 3 - ...ov-13_move_experimental_equalise_cubes.txt | 3 - ...ge_2019-Nov-26_remove_coord_comparison.txt | 1 - ...nge_2020-May-22_cubelist_extract_cubes.txt | 10 - ...feature_2019-Dec-20_cache_area_weights.txt | 5 - ...re_2019-Nov-27_cell_measure_statistics.txt | 5 - ...wfeature_2019-Oct-14_cf_ancillary_data.txt | 1 - .../newfeature_2019-Oct-15_nameconstraint.txt | 1 - .../newfeature_2019-Oct-15_names_property.txt | 1 - ...ature_2019-Oct-15_relaxed_name_loading.txt | 1 - .../newfeature_2019-Oct-17_unpin_mpl.txt | 2 - ...eature_2020-Jan-06_relax_geostationary.txt | 6 - ..._2020-Jan-31_nimrod_format_enhancement.txt | 3 - docs/iris/src/whatsnew/index.rst | 2 +- docs/iris/src/whatsnew/latest.rst | 145 ++++++++ docs/iris/src/whatsnew/latest.rst.template | 46 +++ 53 files changed, 1920 insertions(+), 1360 deletions(-) delete mode 100644 docs/iris/src/whatsnew/aggregate_directory.py delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Dec-02_cell_measure_concatenate.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Nov-14_cell_measure_positional_argument.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Nov-19_cell_measure_copy_loss.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Feb-13_cube_iter_remove.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Jun-15_remove_aux_factories_with_remove_coord.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/deprecate_2019-Oct-11_remove_LBProc_flag_attributes.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/deprecate_2019-Oct-14_remove_deprecated_future_flags.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/docchange_2019-Dec-04_black_code_formatting.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-10_modernise_documentation_using_themes_and_readthedocs.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-23_moved_gallery_entry.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-12_remove_experimental_concatenate_module.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-13_move_experimental_equalise_cubes.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-26_remove_coord_comparison.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2020-May-22_cubelist_extract_cubes.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Dec-20_cache_area_weights.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Nov-27_cell_measure_statistics.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-14_cf_ancillary_data.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_nameconstraint.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_names_property.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_relaxed_name_loading.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-17_unpin_mpl.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-06_relax_geostationary.txt delete mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt create mode 100644 docs/iris/src/whatsnew/latest.rst create mode 100644 docs/iris/src/whatsnew/latest.rst.template diff --git a/.travis.yml b/.travis.yml index 1eb9e1285a..604dbbb353 100644 --- a/.travis.yml +++ b/.travis.yml @@ -122,25 +122,6 @@ script: python -m iris.tests.runner --gallery-tests; fi - # A call to check "whatsnew" contributions are valid, because the Iris test - # for it needs a *developer* install to be able to find the docs. - - > - if [[ "${TEST_TARGET}" == 'doctest' ]]; then - cd ${INSTALL_DIR}/docs/iris/src/whatsnew; - python aggregate_directory.py --checkonly; - fi - - # When pushing built docs, attempt to make a preliminary whatsnew by calling - # 'aggregate_directory.py', before the build. - - > - if [[ "${PUSH_BUILT_DOCS}" == 'true' ]]; then - cd ${INSTALL_DIR}/docs/iris/src/whatsnew; - WHATSNEW=$(ls -d contributions_* 2>/dev/null); - if [[ -n "${WHATSNEW}" ]]; then - python aggregate_directory.py --unreleased; - fi; - fi - # Build the docs. - > if [[ "${TEST_TARGET}" == 'doctest' ]]; then diff --git a/docs/iris/Makefile b/docs/iris/Makefile index 411f3fd553..4ab54b291f 100644 --- a/docs/iris/Makefile +++ b/docs/iris/Makefile @@ -5,6 +5,11 @@ html: echo "make html in $$i..."; \ (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) html); done +html-noplot: + @for i in $(SUBDIRS); do \ + echo "make html-noplot in $$i..."; \ + (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) html-noplot); done + all: @for i in $(SUBDIRS); do \ echo "make all in $$i..."; \ diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 9b0b094c33..b28f5fbb7d 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -36,6 +36,13 @@ def autolog(message): if on_rtd: autolog("Build running on READTHEDOCS server") + # list all the READTHEDOCS environment variables that may be of use + # at some point + autolog("Listing all environment variables on the READTHEDOCS server...") + + for item, value in os.environ.items(): + autolog("[READTHEDOCS] {} = {}".format(item, value)) + # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, diff --git a/docs/iris/src/developers_guide/contributing_documentation.rst b/docs/iris/src/developers_guide/contributing_documentation.rst index f8e01ed927..b7bc99b647 100644 --- a/docs/iris/src/developers_guide/contributing_documentation.rst +++ b/docs/iris/src/developers_guide/contributing_documentation.rst @@ -25,6 +25,8 @@ The documentation uses specific packages that need to be present. Please see :ref:`installing_iris` for instructions. +.. _contributing.documentation.building: + Building -------- diff --git a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst index ae4361b2eb..811aeb59cc 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst @@ -4,18 +4,14 @@ Contributing a "What's New" entry ================================= -Iris has an aggregator for building a draft what's new document for each -release. The draft what's new document is built from contributions by code authors. -This means contributions to the what's new document are written by the -developer most familiar with the change made. +Iris uses a file named ``latest.rst`` to keep a draft of upcoming changes +that will form the next release. Contributions to the :ref:`iris_whatsnew` +document are written by the developer most familiar with the change made. +The contribution should be included as part of the Iris Pull Request that +introduces the change. -A contribution provides an entry in the what's new document, which describes a -change that improved Iris in some way. This change may be a new feature in Iris -or the fix for a bug introduced in a previous release. The contribution should -be included as part of the Iris Pull Request that introduces the change. - -When a new release is prepared, the what's new contributions are combined into -a draft what's new document for the release. +The ``latest.rst`` and the past release notes are kept in +``docs/iris/src/whatsnew/``. Writing a contribution @@ -26,98 +22,54 @@ which improved Iris in some way. As such, a single Iris Pull Request may contain multiple changes that are worth highlighting as contributions to the what's new document. -Each contribution will ideally be written as a single concise bullet point. -The content of the bullet point should highlight the change that has been made -to Iris, targeting an Iris user as the audience. - -A contribution is a feature summary by the code author, which avoids the -release developer having to personally review the change in detail : -It is not in itself the final documentation content, -so it does not have to be perfect or complete in every respect. - - -Adding contribution files -========================= +Each contribution will ideally be written as a single concise bullet point +in a reStructuredText format with a trailing blank line. For example:: -Each release must have a directory called ``contributions_``, -which should be created following the release of the current version of Iris. Each -release directory must be placed in ``docs/iris/src/whatsnew/``. -Contributions to the what's new must be written in markdown and placed into this -directory in text files. The filename for each item should be structured as follows: + * Fixed :issue:`9999`. Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + -``__.txt`` - -Category --------- -The category must be one of the following: - -*newfeature* - Features that are new or changed to add functionality. -*bugfix* - A bugfix. -*incompatiblechange* - A change that causes an incompatibility with prior versions of Iris. -*deprecate* - Deprecations of functionality. -*docchange* - Changes to documentation. +Note that this example also cites the related issue, optionally you may also +include the pull request using the notation ``:pull:`9999```. Where possible +do not exceed **column 80** and ensure that any subsequent lines +of the same bullet point is aligned with the first. -Date ----- - -The date must be a hyphen-separated date in the format of: - - * a four digit year, - * a three character month name, and - * a two digit day. - -For example: - - * 2012-Jan-30 - * 2014-May-03 - * 2015-Feb-19 - -Summary -------- +The content of the bullet point should highlight the change that has been made +to Iris, targeting an Iris user as the audience. -The summary can be any remaining filename characters, and simply provides a -short identifying description of the change. +For inspiration that may include adding links to code please examine past +what's :ref:`iris_whatsnew` entries. -For example: +.. note:: The reStructuredText syntax will be checked as part of building + the documentation. Any warnings should be corrected. + `travis-ci`_ will automatically build the documention when + creating a pull request, however you can also manually + :ref:`build ` the documentation. - * whats-new-aggregator - * using_mo_pack - * correction-to-bilinear-regrid - * GRIB2_pdt11 +.. _travis-ci: https://travis-ci.org/github/SciTools/iris -Complete examples ------------------ +Contribution categories +======================= -Some sample what's new contribution filenames: +The structure of the what's new release note should be easy to read by +users. To achieve this several categories may be used. - * bugfix_2015-Aug-18_partial_pp_constraints.txt - * deprecate_2015-Nov-01_unit-module.txt - * incompatiblechange_2015-Oct-12_GRIB_optional_Python3_unavailable.txt - * newfeature_2015-Jul-03_pearsonr_rewrite.txt +*Features* + Features that are new or changed to add functionality. -.. note:: - A test in the standard test suite ensures that all the contents of the - latest contributions directory conform to this naming scheme. +*Bug Fixes* + A bug fix. +*Incompatible Changes* + A change that causes an incompatibility with prior versions of Iris. -Compiling a draft -================= +*Internal* + Changes to any internal or development related topics, such as testing, + environment dependencies etc -Compiling a draft from the supplied contributions should be done when preparing -a release. Running ``docs/iris/src/whatsnew/aggregate_directory.py`` with the -release number as the argument will create a draft what's new with the name -``.rst`` file for the specified release, by aggregating the individual -contributions from the relevant folder. -Omitting the release number will build the latest version for which a -contributions folder is present. -This command fails if a file with the relevant name already exists. +*Deprecations* + Deprecations of functionality. -The resulting draft document is only a starting point, which the release -developer will then edit to produce the final 'What's new in Iris x.x' -documentation. +*Documentation* + Changes to documentation. diff --git a/docs/iris/src/developers_guide/release.rst b/docs/iris/src/developers_guide/release.rst index 5d1a683d44..c44a248352 100644 --- a/docs/iris/src/developers_guide/release.rst +++ b/docs/iris/src/developers_guide/release.rst @@ -1,75 +1,165 @@ .. _iris_development_releases: Releases -******** +======== + +A release of Iris is a `tag on the SciTools/Iris`_ +Github repository. + +The summary below is of the main areas that constitute the release. The final +section details the :ref:`iris_development_releases_steps` to take. -A release of Iris is a tag on the SciTools/Iris Github repository. Release branch -============== +-------------- -Once the features intended for the release are on master, a release branch should be created, in the SciTools/Iris repository. This will have the name: +Once the features intended for the release are on master, a release branch +should be created, in the SciTools/Iris repository. This will have the name: - :literal:`{major release number}.{minor release number}.x` + :literal:`v{major release number}.{minor release number}.x` for example: :literal:`v1.9.x` -This branch shall be used to finalise the release details in preparation for the release candidate. +This branch shall be used to finalise the release details in preparation for +the release candidate. + Release candidate -================= +----------------- -Prior to a release, a release candidate tag may be created, marked as a pre-release in github, with a tag ending with :literal:`rc` followed by a number, e.g.: +Prior to a release, a release candidate tag may be created, marked as a +pre-release in github, with a tag ending with :literal:`rc` followed by a +number, e.g.: :literal:`v1.9.0rc1` -If created, the pre-release shall be available for at least one week prior to the release being cut. User groups should be notified of the existence of the pre-release and encouraged to test the functionality. +If created, the pre-release shall be available for a minimum of two weeks +prior to the release being cut. However a 4 week period should be the goal +to allow user groups to be notified of the existence of the pre-release and +encouraged to test the functionality. -A pre-release is expected for a minor release, but not normally provided for a point release. +A pre-release is expected for a minor release, but will not for a +point release. -If new features are required for a release after a release candidate has been cut, a new pre-release shall be issued first. +If new features are required for a release after a release candidate has been +cut, a new pre-release shall be issued first. -Documentation -============= -The documentation should include all of the what's new snippets, which must be compiled into a what's new. This content should be reviewed and adapted as required and the snippets removed from the branch to produce a coherent what's new page. +Documentation +------------- -Upon release, the documentation shall be added to the SciTools scitools.org.uk github project's gh-pages branch as the latest documentation. +The documentation should include all of the what's new entries for the release. +This content should be reviewed and adapted as required. -Testing the conda recipe -======================== +Steps to achieve this can be found in the :ref:`iris_development_releases_steps`. -Before a release is cut, the SciTools conda-recipes-scitools recipe for Iris shall be tested to build the release branch of Iris; this test recipe shall not be merged onto conda-recipes-scitools. The release -=========== +----------- + +The final steps are to change the version string in the source of +:literal:`Iris.__init__.py` and include the release date in the relevant what's +new page within the documentation. -The final steps are to change the version string in the source of :literal:`Iris.__init__.py` and include the release date in the relevant what's new page within the documentation. +Once all checks are complete, the release is cut by the creation of a new tag +in the SciTools Iris repository. -Once all checks are complete, the release is cut by the creation of a new tag in the SciTools Iris repository. Conda recipe -============ +------------ -Once a release is cut, the SciTools conda-recipes-scitools recipe for Iris shall be updated to build the latest release of Iris and push this artefact to anaconda.org. The build and push is all automated as part of the merge process. +Once a release is cut, the `Iris feedstock`_ for the conda recipe must be +updated to build the latest release of Iris and push this artefact to +`conda forge`_. + +.. _Iris feedstock: https://github.com/conda-forge/iris-feedstock/tree/master/recipe +.. _conda forge: https://anaconda.org/conda-forge/iris Merge back -========== +---------- + +After the release is cut, the changes shall be merged back onto the +Scitools/iris master branch. -After the release is cut, the changes shall be merged back onto the scitools master. +To achieve this, first cut a local branch from the release branch, +:literal:`{release}.x`. Next add a commit changing the release string to match +the release string on scitools/master. This branch can now be proposed as a +pull request to master. This work flow ensures that the commit identifiers are +consistent between the :literal:`.x` branch and :literal:`master`. -To achieve this, first cut a local branch from the release branch, :literal:`{release}.x`. Next add a commit changing the release string to match the release string on scitools/master. -This branch can now be proposed as a pull request to master. This work flow ensures that the commit identifiers are consistent between the :literal:`.x` branch and :literal:`master`. Point releases -============== +-------------- -Bug fixes may be implemented and targeted as the :literal:`.x` branch. These should lead to a new point release, another tag. -For example, a fix for a problem with 1.9.0 will be merged into 1.9.x, and then released by tagging 1.9.1. +Bug fixes may be implemented and targeted as the :literal:`.x` branch. These +should lead to a new point release, another tag. For example, a fix for a +problem with 1.9.0 will be merged into 1.9.x, and then released by tagging +1.9.1. New features shall not be included in a point release, these are for bug fixes. -A point release does not require a release candidate, but the rest of the release process is to be followed, including the merge back of changes into :literal:`master`. +A point release does not require a release candidate, but the rest of the +release process is to be followed, including the merge back of changes into +:literal:`master`. + + +.. _iris_development_releases_steps: + +Maintainer steps +---------------- + +These steps assume a release for ``v1.9`` is to be created + +Release steps +~~~~~~~~~~~~~ + +#. Create the branch ``1.9.x`` on the main repo, not in a forked repo, for the + release candidate or release. The only exception is for a point/bugfix + release as it should already exist +#. Update the what's new for the release: + + * Copy ``docs/iris/src/whatsnew/latest.rst`` to a file named + ``v1.9.rst`` + * Delete the ``docs/iris/src/whatsnew/latest.rst`` file so it will not + cause an issue in the build + * In ``v1.9.rst`` update the page title (first line of the file) to show + the date and version in the format of ``v1.9 (DD MMM YYYY)``. For + example ``v1.9 (03 Aug 2020)`` + * Review the file for correctness + * Add ``v1.9.rst`` to git and commit all changes, including removal of + ``latest.rst`` + +#. Update the what's new index ``docs/iris/src/whatsnew/index.rst`` + + * Temporarily remove reference to ``latest.rst`` + * Add a reference to ``v1.9.rst`` to the top of the list + +#. Update the ``Iris.__init__.py`` version string, to ``1.9.0`` +#. Check your changes by building the documentation and viewing the changes +#. Once all the above steps are complete, the release is cut, using + the :guilabel:`Draft a new release` button on the + `Iris release page `_ + + +Post release steps +~~~~~~~~~~~~~~~~~~ + +#. Check the documentation has built on `Read The Docs`_. The build is + triggered by any commit to master. Additionally check that the versions + available in the pop out menu in the bottom left corner include the new + release version. If it is not present you will need to configure the + versions avaiable in the **admin** dashboard in Read The Docs +#. Copy ``docs/iris/src/whatsnew/latest.rst.template`` to + ``docs/iris/src/whatsnew/latest.rst``. This will reset + the file with the ``unreleased`` heading and placeholders for the what's + new headings +#. Add back in the reference to ``latest.rst`` to the what's new index + ``docs/iris/src/whatsnew/index.rst`` +#. Update ``Iris.__init__.py`` version string to show as ``1.10.dev0`` +#. Merge back to master + +.. _Read The Docs: https://readthedocs.org/projects/scitools-iris/builds/ +.. _tag on the SciTools/Iris: https://github.com/SciTools/iris/releases diff --git a/docs/iris/src/index.rst b/docs/iris/src/index.rst index 035d0c07b5..759f2f0d7e 100644 --- a/docs/iris/src/index.rst +++ b/docs/iris/src/index.rst @@ -1,6 +1,8 @@ Iris Documentation ================== +.. todolist:: + **A powerful, format-agnostic, community-driven Python library for analysing and visualising Earth science data.** diff --git a/docs/iris/src/whatsnew/1.0.rst b/docs/iris/src/whatsnew/1.0.rst index 6340d0495d..79afd8cf1a 100644 --- a/docs/iris/src/whatsnew/1.0.rst +++ b/docs/iris/src/whatsnew/1.0.rst @@ -1,12 +1,15 @@ -What's new in Iris 1.0 -********************** +v1.0 (17 Oct 2012) +****************** -:Release: 1.0.0 -:Date: 15 Oct, 2012 - -This document explains the new/changed features of Iris in version 1.0. +This document explains the changes made to Iris for this release (:doc:`View all changes `.) + +.. contents:: Skip to section: + :local: + :depth: 3 + + With the release of Iris 1.0, we have broadly completed the transition to the CF data model, and established a stable foundation for future work. Following this release we plan to deliver significant performance @@ -28,45 +31,41 @@ to formalise their data model reach maturity, they will be included in Iris where significant backwards-compatibility can be maintained. -Iris 1.0 features -================= +Features +======== A summary of the main features added with version 1.0: * Hybrid-pressure vertical coordinates, and the ability to load from GRIB. + * Initial support for CF-style coordinate systems. + * Use of Cartopy for mapping in matplotlib. + * Load data from NIMROD files. + * Availability of Cynthia Brewer colour palettes. + * Add a citation to a plot. + * Ensures netCDF files are properly closed. + * The ability to bypass merging when loading data. + * Save netCDF files with an unlimited dimension. + * A more explicit set of load functions, which also allow the automatic cube merging to be bypassed as a last resort. + * The ability to project a cube with a lat-lon or rotated lat-lon coordinate system into a range of map projections e.g. Polar Stereographic. - -Incompatible changes --------------------- -* The "source" and "history" metadata are now represented as Cube - attributes, where previously they used coordinates. -* :meth:`iris.cube.Cube.coord_dims()` now returns a tuple instead of a list. -* The ``iris.plot.gcm`` and ``iris.plot.map_setup`` functions are now removed. - See :ref:`whats-new-cartopy` for further details. - -Deprecations ------------- -* The methods :meth:`iris.coords.Coord.cos()` and - :meth:`iris.coords.Coord.sin()` have been deprecated. -* The :func:`iris.load_strict()` function has been deprecated. Code - should now use the :func:`iris.load_cube()` and - :func:`iris.load_cubes()` functions instead. +* Cube summaries are now more readable when the scalar coordinates + contain bounds. CF-netCDF coordinate systems -============================ +---------------------------- The coordinate systems in Iris are now defined by the CF-netCDF `grid mappings `_. @@ -96,7 +95,7 @@ coordinate system used by the British .. _whats-new-cartopy: Using Cartopy for mapping in matplotlib -======================================= +--------------------------------------- The underlying map drawing package has now been updated to use `Cartopy `_. Cartopy provides a @@ -143,7 +142,7 @@ For more examples of what can be done with Cartopy, see the Iris gallery and Hybrid-pressure -=============== +--------------- With the introduction of the :class:`~iris.aux_factory.HybridPressureFactory` class, it is now possible to represent data expressed on a @@ -163,7 +162,7 @@ the derived "pressure" coordinate for certain data [#f1]_ from the NetCDF -====== +------ When saving a Cube to a netCDF file, Iris will now define the outermost dimension as an unlimited/record dimension. In combination with the @@ -189,7 +188,7 @@ processes. Brewer colour palettes -====================== +---------------------- Iris includes a selection of carefully designed colour palettes produced by Cynthia Brewer. The :mod:`iris.palette` module registers the Brewer @@ -215,7 +214,7 @@ in the citation guidance provided by Cynthia Brewer. Metadata attributes -=================== +------------------- Iris now stores "source" and "history" metadata in Cube attributes. For example:: @@ -249,7 +248,7 @@ Where previously it would have appeared as:: New loading functions -===================== +--------------------- The main functions for loading cubes are now: - :func:`iris.load()` @@ -272,7 +271,7 @@ functions instead. Cube projection -=============== +--------------- Iris now has the ability to project a cube into a number of map projections. This functionality is provided by :func:`iris.analysis.cartography.project()`. @@ -309,7 +308,24 @@ preserved. This function currently assumes global data and will if necessary extrapolate beyond the geographical extent of the source cube. -Other changes -============= -* Cube summaries are now more readable when the scalar coordinates - contain bounds. +Incompatible changes +==================== + +* The "source" and "history" metadata are now represented as Cube + attributes, where previously they used coordinates. + +* :meth:`iris.cube.Cube.coord_dims()` now returns a tuple instead of a list. + +* The ``iris.plot.gcm`` and ``iris.plot.map_setup`` functions are now removed. + See :ref:`whats-new-cartopy` for further details. + + +Deprecations +============ + +* The methods :meth:`iris.coords.Coord.cos()` and + :meth:`iris.coords.Coord.sin()` have been deprecated. + +* The :func:`iris.load_strict()` function has been deprecated. Code + should now use the :func:`iris.load_cube()` and + :func:`iris.load_cubes()` functions instead. diff --git a/docs/iris/src/whatsnew/1.1.rst b/docs/iris/src/whatsnew/1.1.rst index 274ec65ff6..ea85dbc42c 100644 --- a/docs/iris/src/whatsnew/1.1.rst +++ b/docs/iris/src/whatsnew/1.1.rst @@ -1,71 +1,64 @@ -What's new in Iris 1.1 -********************** +v1.1 (03 Jan 2013) +****************** -:Release: 1.1.0 -:Date: 7 Dec, 2012 - -This document explains the new/changed features of Iris in version 1.1. +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -With the release of Iris 1.1, we are introducing support for Mac OS X. -Version 1.1 also sees the first batch of performance enhancements, with -some notable improvements to netCDF/PP import. + +.. contents:: Skip to section: + :local: + :depth: 3 -Iris 1.1 features -================= +Features +======== -A summary of the main features added with version 1.1: +With the release of Iris 1.1, we are introducing support for Mac OS X. +Version 1.1 also sees the first batch of performance enhancements, with +some notable improvements to netCDF/PP import. * Support for Mac OS X. + * GRIB1 import now supports time units of "3 hours". + * Fieldsfile import now supports unpacked and "CRAY" 32-bit packed data in 64-bit Fieldsfiles. + * PP file import now supports "CRAY" 32-bit packed data. + * Various performance improvements, particularly for netCDF import, PP import, and constraints. + * GRIB2 export now supports level types of altitude and height (codes 102 and 103). + * iris.analysis.cartography.area_weights now supports non-standard dimension orders. + * PP file import now adds the "forecast_reference_time" for fields where LBTIM is 11, 12, 13, 31, or 32. + * PP file import now supports LBTIM values of 1, 2, and 3. + * Fieldsfile import now has some support for ancillary files. + * Coordinate categorisation functions added for day-of-year and user-defined seasons. + * GRIB2 import now has partial support for probability data defined with product template 4.9. -Bugs fixed ----------- -* PP export no longer attempts to set/overwrite the STASH code based on - the standard_name. -* Cell comparisons now work consistently, which fixes a bug where - bounded_cell > point_cell compares the point to the bounds but, - point_cell < bounded_cell compares the points. -* Fieldsfile import now correctly recognises pre v3.1 and post v5.2 - versions, which fixes a bug where the two were interchanged. -* iris.analysis.trajectory.interpolate now handles hybrid-height. - -Incompatible changes --------------------- -* N/A - -Deprecations ------------- -* N/A - Coordinate categorisation -========================= +------------------------- An :func:`~iris.coord_categorisation.add_day_of_year` categorisation function has been added to the existing suite in :mod:`iris.coord_categorisation`. + Custom seasons --------------- +~~~~~~~~~~~~~~ The conventional seasonal categorisation functions have been complemented by two groups of functions which handle user-defined, @@ -97,3 +90,19 @@ The other custom season function is: This function adds a coordinate containing True/False values determined by membership of a single custom season. + + +Bugs fixed +========== + +* PP export no longer attempts to set/overwrite the STASH code based on + the standard_name. + +* Cell comparisons now work consistently, which fixes a bug where + bounded_cell > point_cell compares the point to the bounds but, + point_cell < bounded_cell compares the points. + +* Fieldsfile import now correctly recognises pre v3.1 and post v5.2 + versions, which fixes a bug where the two were interchanged. + +* iris.analysis.trajectory.interpolate now handles hybrid-height. diff --git a/docs/iris/src/whatsnew/1.10.rst b/docs/iris/src/whatsnew/1.10.rst index bc2b7528b2..6413323203 100644 --- a/docs/iris/src/whatsnew/1.10.rst +++ b/docs/iris/src/whatsnew/1.10.rst @@ -1,14 +1,18 @@ -What's new in Iris 1.10 -*********************** +v1.10 (05 Sep 2016) +********************* -:Release: 1.10 -:Date: 5th September 2016 - -This document explains the new/changed features of Iris in version 1.10 +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.10 features -================== + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== + .. _iris_grib_added: * Support has now been added for the @@ -19,11 +23,11 @@ Iris 1.10 features iris module :mod:`iris.fileformats.grib`. * The capabilities of ``iris_grib`` are essentially the same as the existing - :mod:`iris.fileformats.grib` when used with ``iris.FUTURE.strict_grib_load=True``, - with only small detail differences. + :mod:`iris.fileformats.grib` when used with + ``iris.FUTURE.strict_grib_load=True``, with only small detail differences. - * The old :mod:`iris.fileformats.grib` module is now deprecated and may shortly be - removed. + * The old :mod:`iris.fileformats.grib` module is now deprecated and may + shortly be removed. * If you are already using the recommended :data:`iris.FUTURE` setting ``iris.FUTURE.strict_grib_load=True`` this should not cause problems, as @@ -44,79 +48,204 @@ Iris 1.10 features any problems you uncover, such as files that will no longer load with the new implementation. -* :meth:`iris.experimental.regrid.PointInCell.regridder` now works across coordinate systems, including non latlon systems. Additionally, the requirement that the source data X and Y coordinates be 2D has been removed. NB: some aspects of this change are backwards incompatible. -* Plotting non-Gregorian calendars is now supported. This adds `nc_time_axis `_ as a dependency. -* Promoting a scalar coordinate to a dimension coordinate with :func:`iris.util.new_axis` no longer loads deferred data. -* The parsing functionality for Cell Methods from netCDF files is available as part of the :mod:`iris.fileformats.netcdf` module as :func:`iris.fileformats.netcdf.parse_cell_methods`. -* Support for the NameIII Version 2 file format has been added. -* Loading netcdf data in Mercator and Stereographic projections now accepts optional extra projection parameter attributes (``false_easting``, ``false_northing`` and ``scale_factor_at_projection_origin``), if they match the default values. +* :meth:`iris.experimental.regrid.PointInCell.regridder` now works across + coordinate systems, including non latlon systems. Additionally, the + requirement that the source data X and Y coordinates be 2D has been removed. + NB: some aspects of this change are backwards incompatible. - * NetCDF files which define a Mercator projection where the ``false_easting``, ``false_northing`` and ``scale_factor_at_projection_origin`` match the defaults will have the projection loaded correctly. Otherwise, a warning will be issued for each parameter that does not match the default and the projection will not be loaded. - * NetCDF files which define a Steroegraphic projection where the ``scale_factor_at_projection_origin`` is equal to 1.0 will have the projection loaded correctly. Otherwise, a warning will be issued and the projection will not be loaded. +* Plotting non-Gregorian calendars is now supported. This adds + `nc_time_axis `_ as a dependency. -* The :mod:`iris.plot` routines :func:`~iris.plot.contour`, :func:`~iris.plot.contourf`, :func:`~iris.plot.outline`, :func:`~iris.plot.pcolor`, :func:`~iris.plot.pcolormesh` and :func:`~iris.plot.points` now support plotting cubes with anonymous dimensions by specifying the *numeric index* of the anonymous dimension within the ``coords`` keyword argument. +* Promoting a scalar coordinate to a dimension coordinate with + :func:`iris.util.new_axis` no longer loads deferred data. + +* The parsing functionality for Cell Methods from netCDF files is available + as part of the :mod:`iris.fileformats.netcdf` module as + :func:`iris.fileformats.netcdf.parse_cell_methods`. + +* Support for the NameIII Version 2 file format has been added. + +* Loading netcdf data in Mercator and Stereographic projections now accepts + optional extra projection parameter attributes (``false_easting``, + ``false_northing`` and ``scale_factor_at_projection_origin``), if they match + the default values. + + * NetCDF files which define a Mercator projection where the + ``false_easting``, ``false_northing`` and + ``scale_factor_at_projection_origin`` match the defaults will have the + projection loaded correctly. Otherwise, a warning will be issued for each + parameter that does not match the default and the projection will not be + loaded. + + * NetCDF files which define a Steroegraphic projection where the + ``scale_factor_at_projection_origin`` is equal to 1.0 will have the + projection loaded correctly. Otherwise, a warning will be issued and the + projection will not be loaded. + +* The :mod:`iris.plot` routines :func:`~iris.plot.contour`, + :func:`~iris.plot.contourf`, :func:`~iris.plot.outline`, + :func:`~iris.plot.pcolor`, :func:`~iris.plot.pcolormesh` and + :func:`~iris.plot.points` now support plotting cubes with anonymous + dimensions by specifying the *numeric index* of the anonymous dimension + within the ``coords`` keyword argument. Note that the axis of the anonymous dimension will be plotted in index space. -* NetCDF loading and saving now supports Cubes that use the LambertConformal coordinate system. -* The experimental structured Fieldsfile loader :func:`~iris.experimental.fieldsfile.load` has been extended to also load structured PP files. +* NetCDF loading and saving now supports Cubes that use the LambertConformal + coordinate system. - Structured loading is a streamlined operation, offering the benefit of a significantly faster loading alternative to the more generic :func:`iris.load` mechanism. +* The experimental structured Fieldsfile loader + :func:`~iris.experimental.fieldsfile.load` has been extended to also load + structured PP files. - Note that structured loading is not an optimised wholesale replacement of :func:`iris.load`. Structured loading is restricted to input containing contiguously ordered fields for each phenomenon that repeat regularly over the same vertical levels and times. For further details, see :func:`~iris.experimental.fieldsfile.load` + Structured loading is a streamlined operation, offering the benefit of a + significantly faster loading alternative to the more generic + :func:`iris.load` mechanism. + + Note that structured loading is not an optimised wholesale replacement of + :func:`iris.load`. Structured loading is restricted to input containing + contiguously ordered fields for each phenomenon that repeat regularly over + the same vertical levels and times. For further details, see + :func:`~iris.experimental.fieldsfile.load` * :mod:`iris.experimental.regrid_conservative` is now compatible with ESMPy v7. -* Saving zonal (i.e. longitudinal) means to PP files now sets the '64s' bit in LBPROC. + +* Saving zonal (i.e. longitudinal) means to PP files now sets the '64s' bit in + LBPROC. + * Loading of 'little-endian' PP files is now supported. -* All appropriate :mod:`iris.plot` functions now handle an ``axes`` keyword, allowing use of the object oriented matplotlib interface rather than pyplot. -* The ability to pass file format object lists into the rules based load pipeline, as used for GRIB, Fields Files and PP has been added. The :func:`iris.fileformats.pp.load_pairs_from_fields` and :func:`iris.fileformats.grib.load_pairs_from_fields` are provided to produce cubes from such lists. These lists may have been filtered or altered using the appropriate :mod:`iris.fileformats` modules. -* Cubes can now have an 'hour' coordinate added with :meth:`iris.coord_categorisation.add_hour`. -* Time coordinates from PP fields with an lbcode of the form 3xx23 are now correctly encoded with a 360-day calendar. -* The loading from and saving to netCDF of CF cell_measure variables is supported, along with their representation within a Cube as :attr:`~iris.cube.Cube.cell_measures`. -* Cubes with anonymous dimensions can now be concatenated. This can only occur along a dimension that is not anonymous. -* NetCDF saving of ``valid_range``, ``valid_min`` and ``valid_max`` cube attributes is now allowed. + +* All appropriate :mod:`iris.plot` functions now handle an ``axes`` keyword, + allowing use of the object oriented matplotlib interface rather than pyplot. + +* The ability to pass file format object lists into the rules based load + pipeline, as used for GRIB, Fields Files and PP has been added. The + :func:`iris.fileformats.pp.load_pairs_from_fields` and + :func:`iris.fileformats.grib.load_pairs_from_fields` are provided to produce + cubes from such lists. These lists may have been filtered or altered using + the appropriate :mod:`iris.fileformats` modules. + +* Cubes can now have an 'hour' coordinate added with + :meth:`iris.coord_categorisation.add_hour`. + +* Time coordinates from PP fields with an lbcode of the form 3xx23 are now + correctly encoded with a 360-day calendar. + +* The loading from and saving to netCDF of CF cell_measure variables is + supported, along with their representation within a Cube as + :attr:`~iris.cube.Cube.cell_measures`. + +* Cubes with anonymous dimensions can now be concatenated. This can only occur + along a dimension that is not anonymous. + +* NetCDF saving of ``valid_range``, ``valid_min`` and ``valid_max`` cube + attributes is now allowed. + Bugs fixed ========== -* Altered Cell Methods to display coordinate's standard_name rather than var_name where appropriate to avoid human confusion. -* Saving multiple cubes with netCDF4 protected attributes should now work as expected. -* Concatenating cubes with singleton dimensions (dimensions of size one) now works properly. -* Fixed the ``grid_mapping_name`` and ``secant_latitudes`` handling for the LambertConformal coordinate system. -* Fixed bug in :func:`iris.analysis.cartography.project` where the output projection coordinates didn't have units. -* Attempting to use :meth:`iris.sample_data_path` to access a file that isn't actually Iris sample data now raises a more descriptive error. A note about the appropriate use of `sample_data_path` has also been added to the documentation. -* Fixed a bug where regridding or interpolation with the :class:`~iris.analysis.Nearest` scheme returned floating-point results even when the source data was integer typed. It now always returns the same type as the source data. -* Fixed a bug where regridding circular data would ignore any source masking. This affected any regridding using the :class:`~iris.analysis.Linear` and :class:`~iris.analysis.Nearest` schemes, and also :func:`iris.analysis.interpolate.linear`. -* The ``coord_name`` parameter to :func:`~iris.fileformats.rules.scalar_cell_method` is now checked correctly. -* LBPROC is set correctly when a cube containing the minimum of a variable is saved to a PP file. The IA component of LBTIM is set correctly when saving maximum or minimum values. -* The performance of :meth:`iris.cube.Cube.extract` when a list of values is given to an instance of :class:`iris.Constraint` has been improved considerably. -* Fixed a bug with :meth:`iris.cube.Cube.data` where an :class:`numpy.ndarray` was not being returned for scalar cubes with lazy data. -* When saving in netcdf format, the units of 'latitude' and 'longitude' coordinates specified in 'degrees' are saved as 'degrees_north' and 'degrees_east' respectively, as defined in the CF conventions for netCDF files: sections 4.1 and 4.2. -* Fixed a bug with a class of pp files with lbyr == 0, where the date would cause errors when converting to a datetime object (e.g. when printing a cube). - - When processing a pp field with lbtim = 2x, lbyr == lbyrd == 0 and lbmon == lbmond, 'month' and 'month_number' coordinates are created instead of 'time'. - -* Fixed a bug in :meth:`~iris.analysis.calculus.curl` where the sign of the r-component for spherical coordinates was opposite to what was expected. + +* Altered Cell Methods to display coordinate's standard_name rather than + var_name where appropriate to avoid human confusion. + +* Saving multiple cubes with netCDF4 protected attributes should now work as + expected. + +* Concatenating cubes with singleton dimensions (dimensions of size one) now + works properly. + +* Fixed the ``grid_mapping_name`` and ``secant_latitudes`` handling for the + LambertConformal coordinate system. + +* Fixed bug in :func:`iris.analysis.cartography.project` where the output + projection coordinates didn't have units. + +* Attempting to use :meth:`iris.sample_data_path` to access a file that isn't + actually Iris sample data now raises a more descriptive error. A note about + the appropriate use of `sample_data_path` has also been added to the + documentation. + +* Fixed a bug where regridding or interpolation with the + :class:`~iris.analysis.Nearest` scheme returned floating-point results even + when the source data was integer typed. It now always returns the same type + as the source data. + +* Fixed a bug where regridding circular data would ignore any source masking. + This affected any regridding using the :class:`~iris.analysis.Linear` and + :class:`~iris.analysis.Nearest` schemes, and also + :func:`iris.analysis.interpolate.linear`. + +* The ``coord_name`` parameter to + :func:`~iris.fileformats.rules.scalar_cell_method` is now checked correctly. + +* LBPROC is set correctly when a cube containing the minimum of a variable is + saved to a PP file. The IA component of LBTIM is set correctly when saving + maximum or minimum values. + +* The performance of :meth:`iris.cube.Cube.extract` when a list of values is + given to an instance of :class:`iris.Constraint` has been improved + considerably. + +* Fixed a bug with :meth:`iris.cube.Cube.data` where an :class:`numpy.ndarray` + was not being returned for scalar cubes with lazy data. + +* When saving in netcdf format, the units of 'latitude' and 'longitude' + coordinates specified in 'degrees' are saved as 'degrees_north' and + 'degrees_east' respectively, as defined in the CF conventions for netCDF + files: sections 4.1 and 4.2. + +* Fixed a bug with a class of pp files with lbyr == 0, where the date would + cause errors when converting to a datetime object (e.g. when printing a cube). + + When processing a pp field with lbtim = 2x, lbyr == lbyrd == 0 and + lbmon == lbmond, 'month' and 'month_number' coordinates are created instead + of 'time'. + +* Fixed a bug in :meth:`~iris.analysis.calculus.curl` where the sign of the + r-component for spherical coordinates was opposite to what was expected. + * A bug that prevented cube printing in some cases has been fixed. -* Fixed a bug where a deepcopy of a :class:`~iris.coords.DimCoord` would have writable ``points`` and ``bounds`` arrays. These arrays can now no longer be modified in-place. -* Concatenation no longer occurs when the auxiliary coordinates of the cubes do not match. This check is not applied to AuxCoords that span the dimension the concatenation is occuring along. This behaviour can be switched off by setting the ``check_aux_coords`` kwarg in :meth:`iris.cube.CubeList.concatenate` to False. -* Fixed a bug in :meth:`iris.cube.Cube.subset` where an exception would be thrown while trying to subset over a non-dimensional scalar coordinate. + +* Fixed a bug where a deepcopy of a :class:`~iris.coords.DimCoord` would have + writable ``points`` and ``bounds`` arrays. These arrays can now no longer be + modified in-place. + +* Concatenation no longer occurs when the auxiliary coordinates of the cubes do + not match. This check is not applied to AuxCoords that span the dimension the + concatenation is occuring along. This behaviour can be switched off by + setting the ``check_aux_coords`` kwarg in + :meth:`iris.cube.CubeList.concatenate` to False. + +* Fixed a bug in :meth:`iris.cube.Cube.subset` where an exception would be + thrown while trying to subset over a non-dimensional scalar coordinate. + Incompatible changes ==================== -* The source and target for :meth:`iris.experimental.regrid.PointInCell.regridder` must now have defined coordinate systems (i.e. not ``None``). Additionally, the source data X and Y coordinates must have the same cube dimensions. + +* The source and target for + :meth:`iris.experimental.regrid.PointInCell.regridder` must now have defined + coordinate systems (i.e. not ``None``). Additionally, the source data X and Y + coordinates must have the same cube dimensions. + Deprecations ============ + * Deprecated the :class:`iris.Future` option ``iris.FUTURE.strict_grib_load``. This only affected the module :mod:`iris.fileformats.grib`, which is itself now deprecated. Please see :ref:`iris_grib package `, above. + * Deprecated the module :mod:`iris.fileformats.grib`. The new package `iris_grib `_ replaces this fuctionality, which will shortly be removed. Please see :ref:`iris_grib package `, above. -* The use of :data:`iris.config.SAMPLE_DATA_DIR` has been deprecated and replaced by the now importable `iris_sample_data `_ package. + +* The use of :data:`iris.config.SAMPLE_DATA_DIR` has been deprecated and + replaced by the now importable + `iris_sample_data `_ package. * Deprecated the module :mod:`iris.analysis.interpolate`. This contains the following public items, all of which are now deprecated and @@ -132,21 +261,38 @@ Deprecations Please use the replacement facilities individually noted in the module documentation for :mod:`iris.analysis.interpolate` + * The method :meth:`iris.cube.Cube.regridded` has been deprecated. Please use :meth:`iris.cube.Cube.regrid` instead (see :meth:`~iris.cube.Cube.regridded` for details). -* Deprecated :data:`iris.fileformats.grib.hindcast_workaround` and :class:`iris.fileformats.grib.GribWrapper`. The class :class:`iris.fileformats.grib.message.GribMessage` provides alternative means of working with GRIB message instances. + +* Deprecated :data:`iris.fileformats.grib.hindcast_workaround` and + :class:`iris.fileformats.grib.GribWrapper`. The class + :class:`iris.fileformats.grib.message.GribMessage` provides alternative means + of working with GRIB message instances. + * Deprecated the module :mod:`iris.fileformats.ff`. Please use the replacement facilities in module :mod:`iris.fileformats.um` : - * :func:`iris.fileformats.um.um_to_pp` replaces :class:`iris.fileformats.ff.FF2PP`. - * :func:`iris.fileformats.um.load_cubes` replaces :func:`iris.fileformats.ff.load_cubes`. - * :func:`iris.fileformats.um.load_cubes_32bit_ieee` replaces :func:`iris.fileformats.ff.load_cubes_32bit_ieee`. + * :func:`iris.fileformats.um.um_to_pp` replaces + :class:`iris.fileformats.ff.FF2PP`. + * :func:`iris.fileformats.um.load_cubes` replaces + :func:`iris.fileformats.ff.load_cubes`. + * :func:`iris.fileformats.um.load_cubes_32bit_ieee` replaces + :func:`iris.fileformats.ff.load_cubes_32bit_ieee`. + + All other public components are generally deprecated and will be removed in a + future release. + +* The :func:`iris.fileformats.pp.as_pairs` and + :func:`iris.fileformats.grib.as_pairs` are deprecated. These are replaced + with :func:`iris.fileformats.pp.save_pairs_from_cube` and + :func:`iris.fileformats.grib.save_pairs_from_cube`. - All other public components are generally deprecated and will be removed in a future release. +* ``iris.fileformats.pp_packing`` has been deprecated. Please install the + separate `mo_pack `_ package instead. + This provides the same functionality. -* The :func:`iris.fileformats.pp.as_pairs` and :func:`iris.fileformats.grib.as_pairs` are deprecated. These are replaced with :func:`iris.fileformats.pp.save_pairs_from_cube` and :func:`iris.fileformats.grib.save_pairs_from_cube`. -* ``iris.fileformats.pp_packing`` has been deprecated. Please install the separate `mo_pack `_ package instead. This provides the same functionality. * Deprecated logging functions (currently used only for rules logging): :data:`iris.config.iris.config.RULE_LOG_DIR`, :data:`iris.config.iris.config.RULE_LOG_IGNORE` and @@ -163,14 +309,37 @@ Deprecations :class:`iris.fileformats.rules.RulesContainer` and :func:`iris.fileformats.rules.calculate_forecast_period`. -* Deprecated the custom pp save rules mechanism implemented by the functions :func:`iris.fileformats.pp.add_save_rules` and :func:`iris.fileformats.pp.reset_save_rules`. The functions :func:`iris.fileformats.pp.as_fields`, :func:`iris.fileformats.pp.as_pairs` and :func:`iris.fileformats.pp.save_fields` provide alternative means of achieving the same ends. +* Deprecated the custom pp save rules mechanism implemented by the functions + :func:`iris.fileformats.pp.add_save_rules` and + :func:`iris.fileformats.pp.reset_save_rules`. The functions + :func:`iris.fileformats.pp.as_fields`, :func:`iris.fileformats.pp.as_pairs` + and :func:`iris.fileformats.pp.save_fields` provide alternative means of + achieving the same ends. + + +Documentation +============= + +* It is now clear that repeated values will form a group under + :meth:`iris.cube.Cube.aggregated_by` even if they aren't consecutive. Hence, + the documentation for :mod:`iris.cube` has been changed to reflect this. + +* The documentation for :meth:`iris.analysis.calculus.curl` has been updated + for clarity. + +* False claims about :meth:`iris.fileformats.pp.save`, + :meth:`iris.fileformats.pp.as_pairs`, and + :meth:`iris.fileformats.pp.as_fields` being able to take instances of + :class:`iris.cube.CubeList` as inputs have been removed. + +* A new code example + :ref:`sphx_glr_generated_gallery_meteorology_plot_wind_speed.py`, + demonstrating the use of a quiver plot to display wind speeds over Lake + Victoria, has been added. + +* The docstring for :data:`iris.analysis.SUM` has been updated to explicitly + state that weights passed to it aren't normalised internally. -Documentation Changes -===================== -* It is now clear that repeated values will form a group under :meth:`iris.cube.Cube.aggregated_by` even if they aren't consecutive. Hence, the documentation for :mod:`iris.cube` has been changed to reflect this. -* The documentation for :meth:`iris.analysis.calculus.curl` has been updated for clarity. -* False claims about :meth:`iris.fileformats.pp.save`, :meth:`iris.fileformats.pp.as_pairs`, and :meth:`iris.fileformats.pp.as_fields` being able to take instances of :class:`iris.cube.CubeList` as inputs have been removed. -* A new code example :ref:`sphx_glr_generated_gallery_meteorology_plot_wind_speed.py`, demonstrating the use of a quiver plot to display wind speeds over Lake Victoria, has been added. -* The docstring for :data:`iris.analysis.SUM` has been updated to explicitly state that weights passed to it aren't normalised internally. -* A note regarding the impossibility of partially collapsing multi-dimensional coordinates has been added to the user guide. +* A note regarding the impossibility of partially collapsing multi-dimensional + coordinates has been added to the user guide. diff --git a/docs/iris/src/whatsnew/1.11.rst b/docs/iris/src/whatsnew/1.11.rst index 560bb07032..d04355b800 100644 --- a/docs/iris/src/whatsnew/1.11.rst +++ b/docs/iris/src/whatsnew/1.11.rst @@ -1,31 +1,45 @@ -What's new in Iris 1.11 -*********************** +v1.11 (29 Oct 2016) +********************* -:Release: 1.11 -:Date: 2016-11-28 - -This document explains the new/changed features of Iris in version 1.11 +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.11 features -================== -* If available, display the ``STASH`` code instead of ``unknown / (unknown)`` when printing cubes - with no ``standard_name`` and no ``units``. + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== + +* If available, display the ``STASH`` code instead of ``unknown / (unknown)`` + when printing cubes with no ``standard_name`` and no ``units``. + * Support for saving to netCDF with data packing has been added. -* The coordinate system :class:`iris.coord_systems.LambertAzimuthalEqualArea` has been added with NetCDF saving support. + +* The coordinate system :class:`iris.coord_systems.LambertAzimuthalEqualArea` + has been added with NetCDF saving support. Bugs fixed ========== -* Fixed a floating point tolerance bug in :func:`iris.experimental.regrid.regrid_area_weighted_rectilinear_src_and_grid` + +* Fixed a floating point tolerance bug in + :func:`iris.experimental.regrid.regrid_area_weighted_rectilinear_src_and_grid` for wrapped longitudes. -* Allow :func:`iris.util.new_axis` to promote the nominated scalar coordinate of a cube - with a scalar masked constant data payload. -* Fixed a bug where :func:`iris.util._is_circular` would erroneously return false - when coordinate values are decreasing. -* When saving to NetCDF, the existing behaviour of writing string attributes as ASCII has been - maintained across known versions of netCDF4-python. - -Documentation changes -===================== + +* Allow :func:`iris.util.new_axis` to promote the nominated scalar coordinate + of a cube with a scalar masked constant data payload. + +* Fixed a bug where :func:`iris.util._is_circular` would erroneously return + false when coordinate values are decreasing. + +* When saving to NetCDF, the existing behaviour of writing string attributes + as ASCII has been maintained across known versions of netCDF4-python. + + +Documentation +============= + * Fuller doc-string detail added to :func:`iris.analysis.cartography.unrotate_pole` and :func:`iris.analysis.cartography.rotate_pole`. diff --git a/docs/iris/src/whatsnew/1.12.rst b/docs/iris/src/whatsnew/1.12.rst index bd02f0937a..1d7fc8f978 100644 --- a/docs/iris/src/whatsnew/1.12.rst +++ b/docs/iris/src/whatsnew/1.12.rst @@ -1,14 +1,18 @@ -What's new in Iris 1.12 -*********************** +v1.12 (31 Jan 2017) +********************* -:Release: 1.12 -:Date: 2017-01-30 - -This document explains the new/changed features of Iris in version 1.12 +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.12 features -================== + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== + .. _showcase: .. admonition:: Showcase Feature: New regridding schemes @@ -121,11 +125,13 @@ Iris 1.12 features Deprecations ============ + * The module :mod:`iris.experimental.fieldsfile` has been deprecated, in favour of the new fast-loading mechanism provided by :meth:`iris.fileformats.um.structured_um_loading`. -Documentation changes -===================== +Documentation +============= + * Corrected documentation of :class:`iris.analysis.AreaWeighted` scheme to make the usage scope clearer. diff --git a/docs/iris/src/whatsnew/1.13.rst b/docs/iris/src/whatsnew/1.13.rst index 7435e5bb07..30b3731d96 100644 --- a/docs/iris/src/whatsnew/1.13.rst +++ b/docs/iris/src/whatsnew/1.13.rst @@ -1,37 +1,78 @@ -What's new in Iris 1.13 -*********************** +v1.13 (17 May 2017) +************************* -:Release: 1.13 -:Date: 2017-05-17 +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) -This document explains the new/changed features of Iris in version 1.13 -(:doc:`View all changes `.) +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== -Iris 1.13 features -================== +* Allow the reading of NAME trajectories stored by time instead of by particle + number. -* Allow the reading of NAME trajectories stored by time instead of by particle number. * An experimental link to python-stratify via :mod:`iris.experimental.stratify`. -* Data arrays may be shared between cubes, and subsets of cubes, by using the :meth:`iris.cube.share_data` flag. + +* Data arrays may be shared between cubes, and subsets of cubes, by using the + :meth:`iris.cube.share_data` flag. Bug fixes ========= -* The bounds are now set correctly on the longitude coordinate if a zonal mean diagnostic has been loaded from a PP file as per the CF Standard. -* NetCDF loading will now determine whether there is a string-valued scalar label, i.e. a character variable that only has one dimension (the length of the string), and interpret this correctly. -* A line plot of geographic coordinates (e.g. drawing a trajectory) wraps around the edge of the map cleanly, rather than plotting a segment straight across the map. -* When saving to PP, lazy data is preserved when generating PP fields from cubes so that a list of cubes can be saved to PP without excessive memory requirements. -* An error is now correctly raised if a user tries to perform an arithmetic operation on two cubes with mismatching coordinates. Previously these cases were caught by the add and subtract operators, and now it is also caught by the multiply and divide operators. -* Limited area Rotated Pole datasets where the data range is ``0 <= lambda < 360``, for example as produced in New Zealand, are plotted over a sensible map extent by default. -* Removed the potential for a RuntimeWarning: overflow encountered in ``int_scalars`` which was missed during collapsed calculations. This could trip up unwary users of limited data types, such as int32 for very large numbers (e.g. seconds since 1970). -* The CF conventions state that certain ``formula_terms`` terms may be omitted and assumed to be zero (http://cfconventions.org/cf-conventions/v1.6.0/cf-conventions.html#dimensionless-v-coord) so Iris now allows factories to be constructed with missing terms. -* In the User Guide's contour plot example, clabel inline is set to be False so that it renders correctly, avoiding spurious horizontal lines across plots, although this does make labels a little harder to see. -* The computation of area weights has been changed to a more numerically stable form. The previous form converted latitude to colatitude and used difference of cosines in the cell area computation. This formulation uses latitude and difference of sines. The conversion from latitude to colatitude at lower precision causes errors when computing the cell areas. +* The bounds are now set correctly on the longitude coordinate if a zonal mean + diagnostic has been loaded from a PP file as per the CF Standard. + +* NetCDF loading will now determine whether there is a string-valued scalar + label, i.e. a character variable that only has one dimension (the length of + the string), and interpret this correctly. + +* A line plot of geographic coordinates (e.g. drawing a trajectory) wraps + around the edge of the map cleanly, rather than plotting a segment straight + across the map. + +* When saving to PP, lazy data is preserved when generating PP fields from + cubes so that a list of cubes can be saved to PP without excessive memory + requirements. + +* An error is now correctly raised if a user tries to perform an arithmetic + operation on two cubes with mismatching coordinates. Previously these cases + were caught by the add and subtract operators, and now it is also caught by + the multiply and divide operators. + +* Limited area Rotated Pole datasets where the data range is + ``0 <= lambda < 360``, for example as produced in New Zealand, are plotted + over a sensible map extent by default. + +* Removed the potential for a RuntimeWarning: overflow encountered in + ``int_scalars`` which was missed during collapsed calculations. This could + trip up unwary users of limited data types, such as int32 for very large + numbers (e.g. seconds since 1970). + +* The CF conventions state that certain ``formula_terms`` terms may be omitted + and assumed to be zero + (http://cfconventions.org/cf-conventions/v1.6.0/cf-conventions.html#dimensionless-v-coord) + so Iris now allows factories to be constructed with missing terms. + +* In the User Guide's contour plot example, clabel inline is set to be False + so that it renders correctly, avoiding spurious horizontal lines across + plots, although this does make labels a little harder to see. + +* The computation of area weights has been changed to a more numerically + stable form. The previous form converted latitude to colatitude and used + difference of cosines in the cell area computation. This formulation uses + latitude and difference of sines. The conversion from latitude to colatitude + at lower precision causes errors when computing the cell areas. + Testing ======= -* Iris has adopted conda-forge to provide environments for continuous integration testing. +* Iris has adopted conda-forge to provide environments for continuous + integration testing. diff --git a/docs/iris/src/whatsnew/1.2.rst b/docs/iris/src/whatsnew/1.2.rst index 720ae73376..982a68add6 100644 --- a/docs/iris/src/whatsnew/1.2.rst +++ b/docs/iris/src/whatsnew/1.2.rst @@ -1,16 +1,17 @@ -What's new in Iris 1.2 -********************** +v1.2 (28 Feb 2013) +****************** -:Release: 1.2.0 -:Date: 7th March 2013 - -This document explains the new/changed features of Iris in version 1.2. +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.2 features -================= -A summary of the main features added with version 1.2: +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== * :meth:`iris.cube.Cube.convert_units()` and :meth:`iris.coords.Coord.convert_units()` have been added. This is @@ -18,6 +19,7 @@ A summary of the main features added with version 1.2: another. For example, to convert a cube in kelvin to celsius, one can now call cube.convert_units('celsius'). The operation is in-place and if the units are not convertible an exception will be raised. + * :attr:`iris.cube.Cube.var_name`, :attr:`iris.coords.Coord.var_name` and :attr:`iris.aux_factory.AuxCoordFactory.var_name` attributes have been added. This attribute represents the CF variable name of the object. It is populated @@ -25,42 +27,57 @@ A summary of the main features added with version 1.2: var_name keyword argument has also been added to the :meth:`iris.cube.Cube.coord()`, :meth:`iris.cube.Cube.coords()` and :meth:`iris.cube.Cube.aux_factory()` methods. + * :meth:`iris.coords.Coord.is_compatible()` has been added. This method is used to determine whether two coordinates are sufficiently alike to allow operations such as :meth:`iris.coords.Coord.intersect()` and :func:`iris.analysis.interpolate.regrid()` to take place. A corresponding method for cubes, :meth:`iris.cube.Cube.is_compatible()`, has also been added. + * Printing a :class:`~iris.cube.Cube` is now more user friendly with regards to dates and time. All *time* and *forecast_reference_time* scalar coordinates now display human readable date/time information. + * The units of a :class:`~iris.cube.Cube` are now shown when it is printed. + * The area weights calculated by :func:`iris.analysis.cartography.area_weights` may now be normalised relative to the total grid area. -* Weights may now be passed to :meth:`iris.cube.Cube.rolling_window` aggregations, - thus allowing arbitrary digital filters to be applied to a :class:`~iris.cube.Cube`. + +* Weights may now be passed to :meth:`iris.cube.Cube.rolling_window` + aggregations, thus allowing arbitrary digital filters to be applied to a + :class:`~iris.cube.Cube`. + Bugs fixed ----------- +========== + * The GRIB hindcast interpretation of negative forecast times can be enabled via the :data:`iris.fileformats.grib.hindcast_workaround` flag. + * The NIMROD file loader has been extended to cope with orography vertical coordinates. + Incompatible changes --------------------- +==================== + * The deprecated :attr:`iris.cube.Cube.unit` and :attr:`iris.coords.Coord.unit` attributes have been removed. + Deprecations ------------- +============ + * The :meth:`iris.coords.Coord.unit_converted()` method has been deprecated. Users should make a copy of the coordinate using :meth:`iris.coords.Coord.copy()` and then call the :meth:`iris.coords.Coord.convert_units()` method of the new coordinate. + * With the addition of the var_name attribute the signatures of DimCoord and AuxCoord have changed. This should have no impact if you are providing parameters as keyword arguments, but it may cause issues if you are relying on the position/order of the arguments. + * Iteration over a :class:`~iris.cube.Cube` has been deprecated. Instead, users should use :meth:`iris.cube.Cube.slices`. diff --git a/docs/iris/src/whatsnew/1.3.rst b/docs/iris/src/whatsnew/1.3.rst index dbea08ad03..9e898a2b23 100644 --- a/docs/iris/src/whatsnew/1.3.rst +++ b/docs/iris/src/whatsnew/1.3.rst @@ -1,65 +1,42 @@ -What's new in Iris 1.3 -********************** +v1.3 (27 Mar 2013) +****************** -:Release: 1.3.0 -:Date: 27 March 2013 - -This document explains the new/changed features of Iris in version 1.3. +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.3 features -================= -A summary of the main features added with version 1.3: +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== * Experimental support for :ref:`loading ABF/ABL files`. + * Support in :func:`iris.analysis.interpolate.linear` for longitude ranges other than [-180, 180]. + * Support for :ref:`customised CF profiles` on export to netCDF. + * The documentation now includes guidance on :ref:`how to cite Iris`. + * The ability to calculate the exponential of a Cube, via :func:`iris.analysis.maths.exp()`. + * Experimental support for :ref:`concatenating Cubes` along existing dimensions via :func:`iris.experimental.concatenate.concatenate()`. -Bugs fixed ----------- -* Printing a Cube now supports Unicode attribute values. -* PP export now sets LBMIN correctly. -* Converting between reference times now works correctly for - units with non-Gregorian calendars. -* Slicing a :class:`~iris.cube.CubeList` now returns a - :class:`~iris.cube.CubeList` instead of a normal list. - -Incompatible changes --------------------- -* N/A - -Deprecations ------------- -* The boolean methods/properties on the :class:`~iris.unit.Unit` class - have been updated to `is_...()` methods, in line with the project's - naming conventions. - - ====================================== =========================================== - Deprecated property/method New method - ====================================== =========================================== - :meth:`~iris.unit.Unit.convertible()` :meth:`~iris.unit.Unit.is_convertible()` - :attr:`~iris.unit.Unit.dimensionless` :meth:`~iris.unit.Unit.is_dimensionless()` - :attr:`~iris.unit.Unit.no_unit` :meth:`~iris.unit.Unit.is_no_unit()` - :attr:`~iris.unit.Unit.time_reference` :meth:`~iris.unit.Unit.is_time_reference()` - :attr:`~iris.unit.Unit.unknown` :meth:`~iris.unit.Unit.is_unknown()` - ====================================== =========================================== - .. _whats-new-abf: Loading ABF/ABL files -===================== +--------------------- Support for the ABF and ABL file formats (as `defined `_ by the @@ -80,7 +57,7 @@ For example:: .. _whats-new-cf-profile: Customised CF profiles -====================== +---------------------- Iris now provides hooks in the CF-netCDF export process to allow user-defined routines to check and/or modify the representation in the @@ -89,10 +66,13 @@ netCDF file. The following keys within the ``iris.site_configuration`` dictionary have been **reserved** as hooks to *external* user-defined CF profile functions: - * ``cf_profile`` injests a :class:`iris.cube.Cube` for analysis and returns a profile result - * ``cf_patch`` modifies the CF-netCDF file associated with export of the :class:`iris.cube.Cube` + * ``cf_profile`` injests a :class:`iris.cube.Cube` for analysis and returns a + profile result + * ``cf_patch`` modifies the CF-netCDF file associated with export of the + :class:`iris.cube.Cube` -The ``iris.site_configuration`` dictionary should be configured via the ``iris/site_config.py`` file. +The ``iris.site_configuration`` dictionary should be configured via the +``iris/site_config.py`` file. For further implementation details see ``iris/fileformats/netcdf.py``. @@ -100,7 +80,7 @@ For further implementation details see ``iris/fileformats/netcdf.py``. .. _whats-new-concat: Cube concatenation -================== +------------------ Iris now provides initial support for concatenating Cubes along one or more existing dimensions. Currently this will force the data to be @@ -126,3 +106,33 @@ combine these into a single Cube as follows:: As this is an experimental feature, your feedback is especially welcome. +Bugs fixed +========== + +* Printing a Cube now supports Unicode attribute values. + +* PP export now sets LBMIN correctly. + +* Converting between reference times now works correctly for + units with non-Gregorian calendars. + +* Slicing a :class:`~iris.cube.CubeList` now returns a + :class:`~iris.cube.CubeList` instead of a normal list. + + +Deprecations +============ + +* The boolean methods/properties on the :class:`~iris.unit.Unit` class + have been updated to `is_...()` methods, in line with the project's + naming conventions. + + ====================================== =========================================== + Deprecated property/method New method + ====================================== =========================================== + :meth:`~iris.unit.Unit.convertible()` :meth:`~iris.unit.Unit.is_convertible()` + :attr:`~iris.unit.Unit.dimensionless` :meth:`~iris.unit.Unit.is_dimensionless()` + :attr:`~iris.unit.Unit.no_unit` :meth:`~iris.unit.Unit.is_no_unit()` + :attr:`~iris.unit.Unit.time_reference` :meth:`~iris.unit.Unit.is_time_reference()` + :attr:`~iris.unit.Unit.unknown` :meth:`~iris.unit.Unit.is_unknown()` + ====================================== =========================================== diff --git a/docs/iris/src/whatsnew/1.4.rst b/docs/iris/src/whatsnew/1.4.rst index 3586b05a5c..23b70b10c9 100644 --- a/docs/iris/src/whatsnew/1.4.rst +++ b/docs/iris/src/whatsnew/1.4.rst @@ -1,96 +1,114 @@ -What's new in Iris 1.4 -********************** +v1.4 (14 Jun 2013) +****************** -:Release: 1.4.0 -:Date: 14 June 2013 - -This document explains the new/changed features of Iris in version 1.4. +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.4 features -================= -A summary of the main features added with version 1.4: +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== * Multiple cubes can now be exported to a NetCDF file. + * Correct nearest-neighbour calculation with circular coords. + * :ref:`Experimental regridding enhancements`. + * :ref:`Iris-Pandas interoperability`. + * NIMROD level type 12 (levels below ground) can now be loaded. + * :ref:`Load cubes from the internet via OPeNDAP`. + * :ref:`GeoTiff export (experimental)`. + * :ref:`Cube merge update`. + * :ref:`Unambiguous season year naming`. + * NIMROD files with multiple fields and period of interest can now be loaded. + * Missing values are now handled when loading GRIB messages. + * PP export rule to calculate forecast period. + * :func:`~iris.cube.Cube.aggregated_by` now maintains array masking. + * IEEE 32bit fieldsfiles can now be loaded. + * NetCDF transverse mercator and climatology data can now be loaded. + * Polar stereographic GRIB data can now be loaded. + * :ref:`Cubes with no vertical coord can now be exported to GRIB`. + * :ref:`Simplified resource configuration`. + * :ref:`Extended GRIB parameter translation`. + * Added an optimisation for single-valued coordinate constraints. + * :ref:`One dimensional linear interpolation fix`. -* :ref:`Fix for iris.analysis.calculus.differentiate`. -* Fixed pickling of cubes with 2D aux coords from NetCDF. -* Fixed bug which ignored the "coords" keyword for certain plots. -* Use the latest release of Cartopy, v0.8.0. +* :ref:`Fix for iris.analysis.calculus.differentiate`. -Incompatible changes --------------------- -* As part of simplifying the mechanism for accessing test data, - :func:`iris.io.select_data_path`, :data:`iris.config.DATA_REPOSITORY`, - :data:`iris.config.MASTER_DATA_REPOSITORY` and - :data:`iris.config.RESOURCE_DIR` have been removed. +* Fixed pickling of cubes with 2D aux coords from NetCDF. -Deprecations ------------- -* The *add_custom_season_** functions from :mod:`~iris.coord_categorisation` have been deprecated in favour of adding their functionality to the *add_season_** functions +* Fixed bug which ignored the "coords" keyword for certain plots. +* Use the latest release of Cartopy, v0.8.0. .. _OPeNDAP: http://www.opendap.org/about - - .. _exp-regrid: Experimental regridding enhancements -==================================== +------------------------------------ + +Bilinear, area-weighted and area-conservative regridding functions are now +available in :mod:`iris.experimental`. These functions support masked data and +handle derived coordinates such as hybrid height. The final API is still in +development. -Bilinear, area-weighted and area-conservative regridding functions are now available in -:mod:`iris.experimental`. These functions support masked data and handle -derived coordinates such as hybrid height. The final API is still in development. In the meantime: + Bilinear rectilinear regridding ------------------------------- + :func:`~iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid` -can be used to regrid a cube onto a horizontal grid defined in a different coordinate system. -The data values are calculated using bilinear interpolation. +can be used to regrid a cube onto a horizontal grid defined in a differentiate +coordinate system. The data values are calculated using bilinear interpolation. For example:: from iris.experimental.regrid import regrid_bilinear_rectilinear_src_and_grid regridded_cube = regrid_bilinear_rectilinear_src_and_grid(source_cube, target_grid_cube) + Area-weighted regridding ------------------------ -:func:`~iris.experimental.regrid.regrid_area_weighted_rectilinear_src_and_grid` can be used to regrid a cube -such that the data values of the resulting cube are calculated using the -area-weighted mean. + +:func:`~iris.experimental.regrid.regrid_area_weighted_rectilinear_src_and_grid` +can be used to regrid a cube such that the data values of the resulting cube +are calculated using the area-weighted mean. For example:: from iris.experimental.regrid import regrid_area_weighted_rectilinear_src_and_grid as regrid_area_weighted regridded_cube = regrid_area_weighted(source_cube, target_grid_cube) + Area-conservative regridding ---------------------------- + :func:`~iris.experimental.regrid_conservative.regrid_conservative_via_esmpy` -can be used for area-conservative regridding between geographical coordinate systems. -This uses the ESMF library functions, via the ESMPy interface. +can be used for area-conservative regridding between geographical coordinate +systems. This uses the ESMF library functions, via the ESMPy interface. For example:: @@ -101,7 +119,8 @@ For example:: .. _iris-pandas: Iris-Pandas interoperablilty -============================ +---------------------------- + Conversion to and from Pandas Series_ and DataFrames_ is now available. See :mod:`iris.pandas` for more details. @@ -112,7 +131,8 @@ See :mod:`iris.pandas` for more details. .. _load-opendap: Load cubes from the internet via OPeNDAP -======================================== +---------------------------------------- + Cubes can now be loaded directly from the internet, via OPeNDAP_. For example:: @@ -123,8 +143,10 @@ For example:: .. _geotiff_export: GeoTiff export -============== -With this experimental feature, two dimensional cubes can now be exported to GeoTiff files. +-------------- + +With this experimental feature, two dimensional cubes can now be exported to +GeoTiff files. For example:: @@ -139,17 +161,20 @@ For example:: .. _cube-merge-update: Cube merge update -================= +----------------- + Cube merging now favours numerical coordinates over string coordinates to describe a dimension, and :class:`~iris.coords.DimCoord` over :class:`~iris.coords.AuxCoord`. These modifications prevent the error: -*"No functional relationship between separable and inseparable candidate dimensions"*. +*"No functional relationship between separable and inseparable candidate +dimensions"*. .. _season-year-name: Unambiguous season year naming -============================== +------------------------------ + The default names of categorisation coordinates are now less ambiguous. For example, :func:`~iris.coord_categorisation.add_month_number` and :func:`~iris.coord_categorisation.add_month_fullname` now create @@ -159,15 +184,18 @@ For example, :func:`~iris.coord_categorisation.add_month_number` and .. _grib-novert: Cubes with no vertical coord can now be exported to GRIB -======================================================== +-------------------------------------------------------- + Iris can now export cubes with no vertical coord to GRIB. -The solution is still under discussion: See https://github.com/SciTools/iris/issues/519. +The solution is still under discussion: See +https://github.com/SciTools/iris/issues/519. .. _simple_cfg: Simplified resource configuration -================================= +--------------------------------- + A new configuration variable called :data:`iris.config.TEST_DATA_DIR` has been added, replacing the previous combination of :data:`iris.config.MASTER_DATA_REPOSITORY` and @@ -180,7 +208,8 @@ be set by adding a ``test_data_dir`` entry to the ``Resources`` section of .. _grib_params: Extended GRIB parameter translation -=================================== +----------------------------------- + - More GRIB2 params are recognised on input. - Now translates some codes on GRIB2 output. - Some GRIB2 params may load with a different standard_name. @@ -190,16 +219,37 @@ Extended GRIB parameter translation .. _one-d-linear: One dimensional linear interpolation fix -======================================== -:func:`~iris.analysis.interpolate.linear` can now extrapolate from a single point -assuming a gradient of zero. This prevents an issue when loading cross sections -with a hybrid height coordinate, on a staggered grid and only a single orography field. +---------------------------------------- + +:func:`~iris.analysis.interpolate.linear` can now extrapolate from a single +point assuming a gradient of zero. This prevents an issue when loading cross +sections with a hybrid height coordinate, on a staggered grid and only a single +orography field. .. _calc-diff-fix: Fix for iris.analysis.calculus.differentiate -============================================= -A bug in :func:`~iris.analysis.calculus.differentiate` that had the potential to cause -the loss of coordinate metadata when calculating the curl or the derivative of a cube has been fixed. +-------------------------------------------- + +A bug in :func:`~iris.analysis.calculus.differentiate` that had the potential +to cause the loss of coordinate metadata when calculating the curl or the +derivative of a cube has been fixed. + + +Incompatible changes +==================== + +* As part of simplifying the mechanism for accessing test data, + :func:`iris.io.select_data_path`, :data:`iris.config.DATA_REPOSITORY`, + :data:`iris.config.MASTER_DATA_REPOSITORY` and + :data:`iris.config.RESOURCE_DIR` have been removed. + +Deprecations +============ + +* The *add_custom_season_** functions from :mod:`~iris.coord_categorisation` + have been deprecated in favour of adding their functionality to the + *add_season_** functions + diff --git a/docs/iris/src/whatsnew/1.5.rst b/docs/iris/src/whatsnew/1.5.rst index 6a4f418259..c891e6c7ef 100644 --- a/docs/iris/src/whatsnew/1.5.rst +++ b/docs/iris/src/whatsnew/1.5.rst @@ -1,16 +1,21 @@ -What's new in Iris 1.5 -********************** +v1.5 (13 Sep 2013) +****************** -:Release: 1.5.0 -:Date: 12 September 2013 - -This document explains the new/changed features of Iris in version 1.5. +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.5 features -================= + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== + * Scatter plots can now be produced using :func:`iris.plot.scatter` and :func:`iris.quickplot.scatter`. + * The functions :func:`iris.plot.plot` and :func:`iris.quickplot.plot` now take up to two arguments, which may be cubes or coordinates, allowing the user to have full control over what is plotted on each axis. The coords keyword @@ -25,7 +30,9 @@ Iris 1.5 features * :class:`iris.analysis.SUM` is now a weighted aggregator, allowing it to take a weights keyword argument. + * GRIB2 translations added for standard_name 'soil_temperature'. + * :meth:`iris.cube.Cube.slices` can now handle passing dimension index as well as the currently supported types (string, coordinate), in order to slice in cases where there is no coordinate associated with a dimension (a mix of @@ -48,6 +55,7 @@ Iris 1.5 features plt.show() * Support for UM ancillary files truncated with the UM utility ieee + * Complete support for Transverse Mercator with saving to NetCDF also. .. code-block:: python @@ -70,18 +78,26 @@ Iris 1.5 features .. image:: images/transverse_merc.png * Support for loading NAME files (gridded and trajectory data). + * Multi-dimensional coordinate support added for :func:`iris.analysis.cartography.cosine_latitude_weights` + * Added limited packaged GRIB support (bulletin headers). + * In-place keyword added to :func:`iris.analysis.maths.divide` and :func:`iris.analysis.maths.multiply`. + * Performance gains for PP loading of the order of 40%. + * :mod:`iris.quickplot` now has a :func:`~iris.quickplot.show` function to provide convenient access to matplotlib.pyplot.show(). + * :meth:`iris.coords.DimCoord.from_regular` now implemented which creates a :class:`~iris.coords.DimCoord` with regularly spaced points, and optionally bounds. + * Iris can now cope with a missing bounds variable from NetCDF files. + * Added support for bool array indexing on a cube. .. code-block:: python @@ -95,8 +111,10 @@ Iris 1.5 features * Added support for loading fields defined on regular Gaussian grids from GRIB files. + * :func:`iris.analysis.interpolate.extract_nearest_neighbour` now works without needing to load the data (especially relevant to large datasets). + * When using plotting routines from :mod:`iris.plot` or :mod:`iris.quickplot`, the direction of vertical axes will be reversed if the corresponding coordinate has a "positive" attribute set to "down". @@ -105,63 +123,83 @@ Iris 1.5 features * New PP stashcode translations added including 'dewpoint' and 'relative_humidity'. + * Added implied heights for several common PP STASH codes. + * GeoTIFF export capability enhanced for supporting various data types, coord systems and mapping 0 to 360 longitudes to the -180 to 180 range. Bugs fixed ----------- +========== + * NetCDF error handling on save has been extended to capture file path and permission errors. + * Shape of the Earth scale factors are now correctly interpreted by the GRIB loader. They were previously used as a multiplier for the given value but should have been used as a decimal shift. + * OSGB definition corrected. + * Transverse Mercator on load now accepts the following interchangeably due to inconsistencies in CF documentation: - * +scale_factor_at_central_meridian <-> scale_factor_at_projection_origin - * +longitude_of_central_meridian <-> longitude_of_projection_origin - (+recommended encoding) + + * +scale_factor_at_central_meridian <-> scale_factor_at_projection_origin + + * +longitude_of_central_meridian <-> longitude_of_projection_origin + (+recommended encoding) + * Ellipse description now maintained when converting GeogCS to cartopy. + * GeoTIFF export bug fixes. + * Polar axis now set to the North Pole, when a cube with no coordinate system is saved to the PP file-format. + * :meth:`iris.coords.DimCoord.from_coord` and :meth:`iris.coords.AuxCoord.from_coord` now correctly returns a copy of the source coordinate's coordinate system. + * Units part of the axis label is now omitted when the coordinate it represents is given as a time reference (:mod:`iris.quickplot`). + * CF dimension coordinate is now maintained in the resulting cube when a cube with CF dimension coordinate is being aggregated over. + * Units for Lambert conformal and polar stereographic coordinates now defined as meters. + * Various fieldsfile load bugs including failing to read the coordinates from the file have been fixed. + * Coding of maximum and minimum time-stats in GRIB2 saving has been fixed. + * Example code in section 4.1 of the userguide updated so it uses a sample data file that exists. + * Zorder of contour lines drawn by :func:`~iris.plot.contourf` has been changed to address issue of objects appearing in-between line and filled contours. + * Coord comparisons now function correctly when comparing to numpy scalars. + * Cube loading constraints and :meth:`iris.cube.Cube.extract` correctly implement cell equality methods. -Incompatible changes --------------------- -* N/A - Deprecations ------------- +============ + * The coords keyword argument for :func:`iris.plot.plot` and :func:`iris.quickplot.plot` has been deprecated due to the new API which accepts multiple cubes or coordinates. + * :meth:`iris.fileformats.pp.PPField.regular_points` and :meth:`iris.fileformats.pp.PPField.regular_bounds` have now been deprecated in favour of a new factory method :meth:`iris.coords.DimCoord.from_regular()`. + * :func:`iris.fileformats.pp.add_load_rules` and :func:`iris.fileformats.grib.add_load_rules` are now deprecated. diff --git a/docs/iris/src/whatsnew/1.6.rst b/docs/iris/src/whatsnew/1.6.rst index 4b540c6cc9..068311db5f 100644 --- a/docs/iris/src/whatsnew/1.6.rst +++ b/docs/iris/src/whatsnew/1.6.rst @@ -1,14 +1,17 @@ -What's new in Iris 1.6 -********************** +v1.6 (26 Jan 2014) +****************** -:Release: 1.6.1 -:Date: 18th February 2014 - -This document explains the new/changed features of Iris in version 1.6. +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.6 features -================= + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== .. _showcase: @@ -29,9 +32,9 @@ Iris 1.6 features >>> print([str(cell) for cell in coord.cells()]) ['1970-01-01 01:00:00', '1970-01-01 02:00:00', '1970-01-01 03:00:00'] - Note that, either a :class:`datetime.datetime` or :class:`netcdftime.datetime` - object instance will be returned, depending on the calendar of the time - reference coordinate. + Note that, either a :class:`datetime.datetime` or + :class:`netcdftime.datetime` object instance will be returned, depending on + the calendar of the time reference coordinate. This capability permits the ability to express time constraints more naturally when the cell represents a *datetime-like* object. @@ -41,8 +44,10 @@ Iris 1.6 features # Ignore the 1st of January. iris.Constraint(time=lambda cell: cell.point.month != 1 and cell.point.day != 1) - Note that, :class:`iris.Future` also supports a `context manager `_ - which allows multiple sections of code to execute with different run-time behaviour. + Note that, :class:`iris.Future` also supports a + `context manager `_ + which allows multiple sections of code to execute with different run-time + behaviour. .. code-block:: python @@ -63,12 +68,12 @@ Iris 1.6 features :class:`datetime.datetime` or :class:`netcdftime.datetime`. The *year, month, day, hour, minute, second* and *microsecond* attributes of - a :class:`iris.time.PartialDateTime` object may be fully or partially specified - for any given comparison. + a :class:`iris.time.PartialDateTime` object may be fully or partially + specified for any given comparison. This is particularly useful for time based constraints, whilst enabling the - :data:`iris.FUTURE.cell_datetime_objects`, see :ref:`here ` for further - details on this new release feature. + :data:`iris.FUTURE.cell_datetime_objects`, see :ref:`here ` for + further details on this new release feature. .. code-block:: python @@ -85,139 +90,64 @@ Iris 1.6 features * GRIB loading supports latitude/longitude or Gaussian reduced grids for version 1 and version 2. + * :ref:`A new utility function to assist with caching`. + * :ref:`The RMS aggregator supports weights`. + * :ref:`A new experimental function to equalise cube attributes`. + * :ref:`Collapsing a cube provides a tolerance level for missing-data`. + * NAME loading supports vertical coordinates. + * UM land/sea mask de-compression for Fieldsfiles and PP files. + * Lateral boundary condition Fieldsfile support. + * Staggered grid support for Fieldsfiles extended to type 6 (Arakawa C grid with v at poles). + * Extend support for Fieldsfiles with grid codes 11, 26, 27, 28 and 29. + * :ref:`Promoting a scalar coordinate to new leading cube dimension`. + * Interpreting cell methods from NAME. + * GRIB2 export without forecast_period, enabling NAME to GRIB2. + * Loading height levels from GRIB2. + * :func:`iris.coord_categorisation.add_categorised_coord` now supports multi-dimensional coordinate categorisation. -* Fieldsfiles and PP support for loading and saving of air potential temperature. + +* Fieldsfiles and PP support for loading and saving of air potential + temperature. + * :func:`iris.experimental.regrid.regrid_weighted_curvilinear_to_rectilinear` regrids curvilinear point data to a target rectilinear grid using associated area weights. -* Extended capability of the NetCDF saver :meth:`iris.fileformats.netcdf.Saver.write` - for fine-tune control of a :mod:`netCDF4.Variable`. Also allows multiple dimensions - to be nominated as *unlimited*. -* :ref:`A new PEAK aggregator providing spline interpolation`. -* A new utility function :func:`iris.util.broadcast_to_shape`. -* A new utility function :func:`iris.util.as_compatible_shape`. -* Iris tests can now be run on systems where directory write permissions - previously did not allow it. This is achieved by writing to the current working - directory in such cases. -* Support for 365 day calendar PP fields. -* Added phenomenon translation between cf and grib2 for wind (from) direction. -* PP files now retain lbfc value on save, derived from the stash attribute. -Bugs fixed -========== -* :meth:`iris.cube.Cube.rolling_window` has been extended to support masked arrays. -* :meth:`iris.cube.Cube.collapsed` now handles string coordinates. -* Default LBUSER(2) to -99 for Fieldsfile and PP saving. -* :func:`iris.util.monotonic` returns the correct direction. -* File loaders correctly parse filenames containing colons. -* ABF loader now correctly loads the ABF data payload once. -* Support for 1D array :data:`iris.cube.cube.attributes`. -* GRIB bounded level saving fix. -* :func:`iris.analysis.cartography.project` now associates a coordinate system - with the resulting target cube, where applicable. -* :func:`iris.util.array_equal` now correctly ignores any mask if present, - matching the behaviour of :func:`numpy.array_equal` except with string array - support. -* :func:`iris.analysis.interpolate.linear` now retains a mask in the resulting - cube. -* :meth:`iris.coords.DimCoord.from_regular` now correctly returns a coordinate - which will always be regular as indicated by :func:`~iris.util.is_regular`. -* :func:`iris.util.rolling_window` handling of masked arrays (degenerate - masks) fixed. -* Exception no longer raised for any ellipsoid definition in nimrod loading. +* Extended capability of the NetCDF saver + :meth:`iris.fileformats.netcdf.Saver.write` for fine-tune control of a + :mod:`netCDF4.Variable`. Also allows multiple dimensions to be nominated as + *unlimited*. -Incompatible changes -==================== -* The experimental 'concatenate' function is now a method of a - :class:`iris.cube.CubeList`, see :meth:`iris.cube.CubeList.concatenate`. The - functionality is unchanged. -* :meth:`iris.cube.Cube.extract_by_trajectory()` has been removed. - Instead, use :func:`iris.analysis.trajectory.interpolate()`. -* :func:`iris.load_strict()` has been removed. - Instead, use :func:`iris.load_cube()` and :func:`iris.load_cubes()`. -* :meth:`iris.coords.Coord.cos()` and :meth:`iris.coords.Coord.sin()` - have been removed. -* :meth:`iris.coords.Coord.unit_converted()` has been removed. - Instead, make a copy of the coordinate using - :meth:`iris.coords.Coord.copy()` and then call the - :meth:`iris.coords.Coord.convert_units()` method of the new - coordinate. -* Iteration over a :class:`~iris.cube.Cube` has been removed. Instead, - use :meth:`iris.cube.Cube.slices()`. -* The following :class:`~iris.unit.Unit` deprecated methods/properties have been removed. - - ====================================== =========================================== - Removed property/method New method - ====================================== =========================================== - :meth:`~iris.unit.Unit.convertible()` :meth:`~iris.unit.Unit.is_convertible()` - :attr:`~iris.unit.Unit.dimensionless` :meth:`~iris.unit.Unit.is_dimensionless()` - :attr:`~iris.unit.Unit.no_unit` :meth:`~iris.unit.Unit.is_no_unit()` - :attr:`~iris.unit.Unit.time_reference` :meth:`~iris.unit.Unit.is_time_reference()` - :attr:`~iris.unit.Unit.unknown` :meth:`~iris.unit.Unit.is_unknown()` - ====================================== =========================================== -* As a result of deprecating :meth:`iris.cube.Cube.add_history` and removing the - automatic appending of history by operations such as cube arithmetic, - collapsing, and aggregating, the signatures of a number of functions within - :mod:`iris.analysis.maths` have been modified along with that of - :class:`iris.analysis.Aggregator` and :class:`iris.analysis.WeightedAggregator`. -* The experimental ABF and ABL functionality has now been promoted to - core functionality in :mod:`iris.fileformats.abf`. -* The following :mod:`iris.coord_categorisation` deprecated functions have been - removed. +* :ref:`A new PEAK aggregator providing spline interpolation`. - =============================================================== ======================================================= - Removed function New function - =============================================================== ======================================================= - :func:`~iris.coord_categorisation.add_custom_season` :func:`~iris.coord_categorisation.add_season` - :func:`~iris.coord_categorisation.add_custom_season_number` :func:`~iris.coord_categorisation.add_season_number` - :func:`~iris.coord_categorisation.add_custom_season_year` :func:`~iris.coord_categorisation.add_season_year` - :func:`~iris.coord_categorisation.add_custom_season_membership` :func:`~iris.coord_categorisation.add_season_membership` - :func:`~iris.coord_categorisation.add_month_shortname` :func:`~iris.coord_categorisation.add_month` - :func:`~iris.coord_categorisation.add_weekday_shortname` :func:`~iris.coord_categorisation.add_weekday` - :func:`~iris.coord_categorisation.add_season_month_initials` :func:`~iris.coord_categorisation.add_season` - =============================================================== ======================================================= -* When a cube is loaded from PP or GRIB and it has both time and forecast period - coordinates, and the time coordinate has bounds, the forecast period coordinate - will now also have bounds. These bounds will be aligned with the bounds of the - time coordinate taking into account the forecast reference time. Also, - the forecast period point will now be aligned with the time point. +* A new utility function :func:`iris.util.broadcast_to_shape`. -Deprecations -============ -* :meth:`iris.cube.Cube.add_history` has been deprecated in favour - of users modifying/creating the history metadata directly. This is - because the automatic behaviour did not deliver a sufficiently complete, - auditable history and often prevented the merging of cubes. -* :func:`iris.util.broadcast_weights` has been deprecated and replaced - by the new utility function :func:`iris.util.broadcast_to_shape`. -* Callback mechanism `iris.run_callback` has had its deprecation of return - values revoked. The callback can now return cube instances as well as - inplace changes to the cube. +* A new utility function :func:`iris.util.as_compatible_shape`. -New Contributors -================ -Congratulations and thank you to `felicityguest `_, `jkettleb `_, -`kwilliams-mo `_ and `shoyer `_ who all made their first contribution -to Iris! +* Iris tests can now be run on systems where directory write permissions + previously did not allow it. This is achieved by writing to the current + working directory in such cases. +* Support for 365 day calendar PP fields. ----- +* Added phenomenon translation between cf and grib2 for wind (from) direction. +* PP files now retain lbfc value on save, derived from the stash attribute. .. _caching: @@ -249,7 +179,8 @@ consuming processing, or to reap the benefit of fast-loading a pickled cube. .. _rms: The RMS aggregator supports weights -=================================== +----------------------------------- + The :data:`iris.analysis.RMS` aggregator has been extended to allow the use of weights using the new keyword argument :data:`weights`. @@ -264,7 +195,8 @@ For example, an RMS weighted cube collapse is performed as follows: .. _equalise: Equalise cube attributes -======================== +------------------------ + To assist with :class:`iris.cube.Cube` merging, the new experimental in-place function :func:`iris.experimental.equalise_cubes.equalise_attributes` ensures that a sequence of cubes contains a common set of :data:`iris.cube.Cube.attributes`. @@ -276,7 +208,8 @@ have the same attributes. .. _tolerance: Masking a collapsed result by missing-data tolerance -==================================================== +---------------------------------------------------- + The result from collapsing masked cube data may now be completely masked by providing a :data:`mdtol` missing-data tolerance keyword to :meth:`iris.cube.Cube.collapsed`. @@ -289,7 +222,8 @@ less than or equal to the provided tolerance. .. _promote: Promote a scalar coordinate -=========================== +--------------------------- + The new utility function :func:`iris.util.new_axis` creates a new cube with a new leading dimension of size unity. If a scalar coordinate is provided, then the scalar coordinate is promoted to be the dimension coordinate for the new @@ -301,7 +235,8 @@ Note that, this function will load the data payload of the cube. .. _peak: A new PEAK aggregator providing spline interpolation -==================================================== +---------------------------------------------------- + The new :data:`iris.analysis.PEAK` aggregator calculates the global peak value from a spline interpolation of the :class:`iris.cube.Cube` data payload along a nominated coordinate axis. @@ -312,3 +247,138 @@ For example, to calculate the peak time: from iris.analysis import PEAK collapsed_cube = cube.collapsed('time', PEAK) + + +Bugs fixed +========== + +* :meth:`iris.cube.Cube.rolling_window` has been extended to support masked + arrays. + +* :meth:`iris.cube.Cube.collapsed` now handles string coordinates. + +* Default LBUSER(2) to -99 for Fieldsfile and PP saving. + +* :func:`iris.util.monotonic` returns the correct direction. + +* File loaders correctly parse filenames containing colons. + +* ABF loader now correctly loads the ABF data payload once. + +* Support for 1D array :data:`iris.cube.cube.attributes`. + +* GRIB bounded level saving fix. + +* :func:`iris.analysis.cartography.project` now associates a coordinate system + with the resulting target cube, where applicable. + +* :func:`iris.util.array_equal` now correctly ignores any mask if present, + matching the behaviour of :func:`numpy.array_equal` except with string array + support. + +* :func:`iris.analysis.interpolate.linear` now retains a mask in the resulting + cube. + +* :meth:`iris.coords.DimCoord.from_regular` now correctly returns a coordinate + which will always be regular as indicated by :func:`~iris.util.is_regular`. + +* :func:`iris.util.rolling_window` handling of masked arrays (degenerate + masks) fixed. + +* Exception no longer raised for any ellipsoid definition in nimrod loading. + + +Incompatible changes +==================== + +* The experimental 'concatenate' function is now a method of a + :class:`iris.cube.CubeList`, see :meth:`iris.cube.CubeList.concatenate`. The + functionality is unchanged. + +* :meth:`iris.cube.Cube.extract_by_trajectory()` has been removed. + Instead, use :func:`iris.analysis.trajectory.interpolate()`. + +* :func:`iris.load_strict()` has been removed. + Instead, use :func:`iris.load_cube()` and :func:`iris.load_cubes()`. + +* :meth:`iris.coords.Coord.cos()` and :meth:`iris.coords.Coord.sin()` + have been removed. + +* :meth:`iris.coords.Coord.unit_converted()` has been removed. + Instead, make a copy of the coordinate using + :meth:`iris.coords.Coord.copy()` and then call the + :meth:`iris.coords.Coord.convert_units()` method of the new + coordinate. + +* Iteration over a :class:`~iris.cube.Cube` has been removed. Instead, + use :meth:`iris.cube.Cube.slices()`. + +* The following :class:`~iris.unit.Unit` deprecated methods/properties have + been removed. + + ====================================== =========================================== + Removed property/method New method + ====================================== =========================================== + :meth:`~iris.unit.Unit.convertible()` :meth:`~iris.unit.Unit.is_convertible()` + :attr:`~iris.unit.Unit.dimensionless` :meth:`~iris.unit.Unit.is_dimensionless()` + :attr:`~iris.unit.Unit.no_unit` :meth:`~iris.unit.Unit.is_no_unit()` + :attr:`~iris.unit.Unit.time_reference` :meth:`~iris.unit.Unit.is_time_reference()` + :attr:`~iris.unit.Unit.unknown` :meth:`~iris.unit.Unit.is_unknown()` + ====================================== =========================================== + +* As a result of deprecating :meth:`iris.cube.Cube.add_history` and removing the + automatic appending of history by operations such as cube arithmetic, + collapsing, and aggregating, the signatures of a number of functions within + :mod:`iris.analysis.maths` have been modified along with that of + :class:`iris.analysis.Aggregator` and + :class:`iris.analysis.WeightedAggregator`. + +* The experimental ABF and ABL functionality has now been promoted to + core functionality in :mod:`iris.fileformats.abf`. + +* The following :mod:`iris.coord_categorisation` deprecated functions have been + removed. + + =============================================================== ======================================================= + Removed function New function + =============================================================== ======================================================= + :func:`~iris.coord_categorisation.add_custom_season` :func:`~iris.coord_categorisation.add_season` + :func:`~iris.coord_categorisation.add_custom_season_number` :func:`~iris.coord_categorisation.add_season_number` + :func:`~iris.coord_categorisation.add_custom_season_year` :func:`~iris.coord_categorisation.add_season_year` + :func:`~iris.coord_categorisation.add_custom_season_membership` :func:`~iris.coord_categorisation.add_season_membership` + :func:`~iris.coord_categorisation.add_month_shortname` :func:`~iris.coord_categorisation.add_month` + :func:`~iris.coord_categorisation.add_weekday_shortname` :func:`~iris.coord_categorisation.add_weekday` + :func:`~iris.coord_categorisation.add_season_month_initials` :func:`~iris.coord_categorisation.add_season` + =============================================================== ======================================================= + +* When a cube is loaded from PP or GRIB and it has both time and forecast period + coordinates, and the time coordinate has bounds, the forecast period + coordinate will now also have bounds. These bounds will be aligned with the + bounds of the time coordinate taking into account the forecast reference + time. Also, the forecast period point will now be aligned with the time point. + + +Deprecations +============ + +* :meth:`iris.cube.Cube.add_history` has been deprecated in favour + of users modifying/creating the history metadata directly. This is + because the automatic behaviour did not deliver a sufficiently complete, + auditable history and often prevented the merging of cubes. + +* :func:`iris.util.broadcast_weights` has been deprecated and replaced + by the new utility function :func:`iris.util.broadcast_to_shape`. + +* Callback mechanism `iris.run_callback` has had its deprecation of return + values revoked. The callback can now return cube instances as well as + inplace changes to the cube. + + +New Contributors +================ +Congratulations and thank you to +`felicityguest `_, +`jkettleb `_, +`kwilliams-mo `_ and +`shoyer `_ who all made their first contribution +to Iris! diff --git a/docs/iris/src/whatsnew/1.7.rst b/docs/iris/src/whatsnew/1.7.rst index 757d407684..22d9b05257 100644 --- a/docs/iris/src/whatsnew/1.7.rst +++ b/docs/iris/src/whatsnew/1.7.rst @@ -1,22 +1,26 @@ -What's new in Iris 1.7 -********************** +v1.7 (04 Jul 2014) +******************** -This document explains the new/changed features of Iris in version 1.7. +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -:Release: 1.7.4 -:Date: 15th April 2015 -Iris 1.7 features -================= +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== .. _showcase: .. admonition:: Showcase: Iris is making use of Biggus - Iris is now making extensive use of `Biggus `_ - for virtual arrays and lazy array evaluation. In practice this means that analyses - of cubes with data bigger than the available system memory are now possible. + Iris is now making extensive use of + `Biggus `_ for virtual arrays and lazy + array evaluation. In practice this means that analyses of cubes with data + bigger than the available system memory are now possible. Other than the improved functionality the changes are mostly transparent; for example, before the introduction of biggus, MemoryErrors @@ -33,20 +37,20 @@ Iris 1.7 features >>> print(type(result)) - Memory is still a limiting factor if ever the data is desired as a NumPy array - (e.g. via :data:`cube.data `), but additional methods have - been added to the Cube to support querying and subsequently accessing the "lazy" - data form (see :meth:`~iris.cube.Cube.has_lazy_data` and - :meth:`~iris.cube.Cube.lazy_data`). + Memory is still a limiting factor if ever the data is desired as a NumPy + array (e.g. via :data:`cube.data `), but additional + methods have been added to the Cube to support querying and subsequently + accessing the "lazy" data form (see :meth:`~iris.cube.Cube.has_lazy_data` + and :meth:`~iris.cube.Cube.lazy_data`). .. admonition:: Showcase: New interpolation and regridding API - New interpolation and regridding interfaces have been added which simplify and - extend the existing functionality. + New interpolation and regridding interfaces have been added which simplify + and extend the existing functionality. The interfaces are exposed on the cube in the form of the - :meth:`~iris.cube.Cube.interpolate` and :meth:`~iris.cube.Cube.regrid` methods. - Conceptually the signatures of the methods are:: + :meth:`~iris.cube.Cube.interpolate` and :meth:`~iris.cube.Cube.regrid` + methods. Conceptually the signatures of the methods are:: interpolated_cube = cube.interpolate(interpolation_points, interpolation_scheme) @@ -55,16 +59,17 @@ Iris 1.7 features regridded_cube = cube.regrid(target_grid_cube, regridding_scheme) Whilst not all schemes have been migrated to the new interface, - :class:`iris.analysis.Linear` defines both linear interpolation and regridding, - and :class:`iris.analysis.AreaWeighted` defines an area weighted regridding - scheme. + :class:`iris.analysis.Linear` defines both linear interpolation and + regridding, and :class:`iris.analysis.AreaWeighted` defines an area weighted + regridding scheme. .. admonition:: Showcase: Merge and concatenate reporting Merge reporting is designed as an aid to the merge processes. Should merging - a :class:`~iris.cube.CubeList` fail, merge reporting means that a descriptive - error will be raised that details the differences between the cubes in the - :class:`~iris.cube.CubeList` that prevented the merge from being successful. + a :class:`~iris.cube.CubeList` fail, merge reporting means that a + descriptive error will be raised that details the differences between the + cubes in the :class:`~iris.cube.CubeList` that prevented the merge from + being successful. A new :class:`~iris.cube.CubeList` method, called :meth:`~iris.cube.CubeList.merge_cube`, has been introduced. Calling it on a @@ -83,8 +88,8 @@ Iris 1.7 features iris.exceptions.MergeError: failed to merge into a single cube. cube.attributes keys differ: 'foo' - The naming of this new method mirrors that of Iris load functions, where - one would always expect a :class:`~iris.cube.CubeList` from :func:`iris.load` + The naming of this new method mirrors that of Iris load functions, where one + would always expect a :class:`~iris.cube.CubeList` from :func:`iris.load` and a :class:`~iris.cube.Cube` from :func:`iris.load_cube`. Concatenate reporting is the equivalent process for concatenating a @@ -101,10 +106,10 @@ Iris 1.7 features However, the additional richness of Iris coordinate meta-data provides an enhanced capability beyond the basic broadcasting behaviour of NumPy. - This means that when performing cube arithmetic, the dimensionality and shape of - cubes no longer need to match. For example, if the dimensionality of a cube is - reduced by collapsing, then the result can be used to subtract from the original - cube to calculate an anomaly:: + This means that when performing cube arithmetic, the dimensionality and + shape of cubes no longer need to match. For example, if the dimensionality + of a cube is reduced by collapsing, then the result can be used to subtract + from the original cube to calculate an anomaly:: >>> time_mean = original_cube.collapsed('time', iris.analysis.MEAN) >>> mean_anomaly = original_cube - time_mean @@ -117,131 +122,218 @@ Iris 1.7 features >>> zero_cube = original_cube - similar_cube * Merge reporting that raises a descriptive error if the merge process fails. -* Linear interpolation and regridding now make use of SciPy's RegularGridInterpolator - for much faster linear interpolation. + +* Linear interpolation and regridding now make use of SciPy's + RegularGridInterpolator for much faster linear interpolation. + * NAME file loading now handles the "no time averaging" column and translates - height/altitude above ground/sea-level columns into appropriate coordinate metadata. -* The NetCDF saver has been extended to allow saving of cubes with hybrid pressure - auxiliary factories. -* PP/FF loading supports LBLEV of 9999. -* Extended GRIB1 loading to support data on hybrid pressure levels. -* :func:`iris.coord_categorisation.add_day_of_year` can be used to add categorised - day of year coordinates based on time coordinates with non-Gregorian calendars. + height/altitude above ground/sea-level columns into appropriate coordinate + metadata. + +* The NetCDF saver has been extended to allow saving of cubes with hybrid + pressure auxiliary factories. + +* PP/FF loading supports LBLEV of 9999. + +* Extended GRIB1 loading to support data on hybrid pressure levels. + +* :func:`iris.coord_categorisation.add_day_of_year` can be used to add + categorised day of year coordinates based on time coordinates with + non-Gregorian calendars. + * Support for loading data on reduced grids from GRIB files in raw form without automatically interpolating to a regular grid. + * The coordinate systems :class:`iris.coord_systems.Orthographic` and - :class:`iris.coord_systems.VerticalPerspective` (for imagery from geostationary - satellites) have been added. -* Extended NetCDF loading to support the "ocean sigma over z" auxiliary coordinate + :class:`iris.coord_systems.VerticalPerspective` (for imagery from + geostationary satellites) have been added. + +* Extended NetCDF loading to support the "ocean sigma over z" auxiliary + coordinate factory. + * Support added for loading CF-NetCDF data with bounds arrays that are missing a vertex dimension. + * :meth:`iris.cube.Cube.rolling_window` can now be used with string-based :class:`iris.coords.AuxCoord` instances. + * Loading of PP and FF files has been optimised through deferring creation of PPField attributes. + * Automatic association of a coordinate's CF formula terms variable with the data variable associated with that coordinate. -* PP loading translates cross-section height into a dimensional auxiliary coordinate. -* String auxiliary coordinates can now be plotted with the Iris plotting wrappers. -* :func:`iris.analysis.geometry.geometry_area_weights` now allows for the calculation of - normalized cell weights. -* Many new translations between the CF spec and STASH codes or GRIB2 parameter codes. -* PP save rules add the data's UM Version to the attributes of the saved file - when appropriate. + +* PP loading translates cross-section height into a dimensional auxiliary + coordinate. + +* String auxiliary coordinates can now be plotted with the Iris + plotting wrappers. + +* :func:`iris.analysis.geometry.geometry_area_weights` now + allows for the calculation of normalized cell weights. + +* Many new translations between the CF spec and STASH codes or GRIB2 parameter + codes. + +* PP save rules add the data's UM Version to the attributes of the saved + file when appropriate. + * NetCDF reference surface variable promotion available through the :class:`iris.FUTURE` mechanism. -* A speed improvement in calculation of :func:`iris.analysis.geometry.geometry_area_weights`. -* The mdtol keyword was added to area-weighted regridding to allow control of the - tolerance for missing data. For a further description of this concept, see + +* A speed improvement in calculation of + :func:`iris.analysis.geometry.geometry_area_weights`. + +* The mdtol keyword was added to area-weighted regridding to allow control of + the tolerance for missing data. For a further description of this concept, see :class:`iris.analysis.AreaWeighted`. + * Handling for patching of the CF conventions global attribute via a defined cf_patch_conventions function. -* Deferred GRIB data loading has been introduced for reduced memory consumption when - loading GRIB files. + +* Deferred GRIB data loading has been introduced for reduced memory consumption + when loading GRIB files. + * Concatenate reporting that raises a descriptive error if the concatenation process fails. + * A speed improvement when loading PP or FF data and constraining on STASH code. + Bugs fixed ========== + * Data containing more than one reference cube for constructing hybrid height coordinates can now be loaded. + * Removed cause of increased margin of error when interpolating. + * Changed floating-point precision used when wrapping points for interpolation. + * Mappables that can be used to generate colorbars are now returned by Iris plotting wrappers. -* NetCDF load ignores over-specified formula terms on bounded dimensionless vertical - coordinates. + +* NetCDF load ignores over-specified formula terms on bounded dimensionless + vertical coordinates. + * Auxiliary coordinate factory loading now correctly interprets formula term varibles for "atmosphere hybrid sigma pressure" coordinate data. + * Corrected comparison of NumPy NaN values in cube merge process. -* Fixes for :meth:`iris.cube.Cube.intersection` to correct calculating the intersection - of a cube with split bounds, handling of circular coordinates, handling of - monotonically descending bounded coordinats and for finding a wrapped two-point - result and longitude tolerances. -* A bug affecting :meth:`iris.cube.Cube.extract` and :meth:`iris.cube.CubeList.extract` - that led to unexpected behaviour when operating on scalar cubes has been fixed. -* Aggregate_by may now be passed single-value coordinates. -* Making a copy of a :class:`iris.coords.DimCoord` no longer results in the writeable - flag on the copied points and bounds arrays being set to True. -* Can now save to PP a cube that has vertical levels but no orography. + +* Fixes for :meth:`iris.cube.Cube.intersection` to correct calculating the + intersection of a cube with split bounds, handling of circular coordinates, + handling of monotonically descending bounded coordinats and for finding a + wrapped two-point result and longitude tolerances. + +* A bug affecting :meth:`iris.cube.Cube.extract` and + :meth:`iris.cube.CubeList.extract` that led to unexpected behaviour when + operating on scalar cubes has been fixed. + +* Aggregate_by may now be passed single-value coordinates. + +* Making a copy of a :class:`iris.coords.DimCoord` no longer results in the + writeable flag on the copied points and bounds arrays being set to True. + +* Can now save to PP a cube that has vertical levels but no orography. + * Fix a bug causing surface altitude and surface pressure fields to not appear in cubes loaded with a STASH constraint. -* Fixed support for :class:`iris.fileformats.pp.STASH` objects in STASH constraints. -* A fix to avoid a problem where cube attribute names clash with NetCDF reserved attribute names. -* A fix to allow :meth:`iris.cube.CubeList.concatenate` to deal with descending coordinate order. -* Add missing NetCDF attribute `varname` when constructing a new :class:`iris.coords.AuxCoord`. -* The datatype of time arrays converted with :func:`iris.util.unify_time_units` is now preserved. -Bugs fixed in v1.7.3 +* Fixed support for :class:`iris.fileformats.pp.STASH` objects in STASH + constraints. + +* A fix to avoid a problem where cube attribute names clash with + NetCDF reserved attribute names. + +* A fix to allow :meth:`iris.cube.CubeList.concatenate` to deal with descending + coordinate order. + +* Add missing NetCDF attribute `varname` when constructing a new + :class:`iris.coords.AuxCoord`. * The datatype of time arrays converted with + :func:`iris.util.unify_time_units` is now preserved. + + +v1.7.3 (16 Dec 2014) ^^^^^^^^^^^^^^^^^^^^ -* Scalar dimension coordinates can now be concatenated with :meth:`iris.cube.CubeList.concatenate`. -* Arbitrary names can no longer be set for elements of a :class:`iris.fileformats.pp.SplittableInt`. -* Cubes that contain a pseudo-level coordinate can now be saved to PP. -* Fixed a bug in the FieldsFile loader that prevented it always loading all available fields. -Bugs fixed in v1.7.4 +* Scalar dimension coordinates can now be concatenated with + :meth:`iris.cube.CubeList.concatenate`. + +* Arbitrary names can no longer be set + for elements of a :class:`iris.fileformats.pp.SplittableInt`. + +* Cubes that contain a pseudo-level coordinate can now be saved to PP. + +* Fixed a bug in the FieldsFile loader that prevented it always loading all + available fields. + + +v1.7.4 (15 Apr 2015) ^^^^^^^^^^^^^^^^^^^^ + * :meth:`Coord.guess_bounds` can now deal with circular coordinates. + * :meth:`Coord.nearest_neighbour_index` can now work with descending bounds. + * Passing `weights` to :meth:`Cube.rolling_window` no longer prevents other keyword arguments from being passed to the aggregator. + * Several minor fixes to allow use of Iris on Windows. + * Made use of the new standard_parallels keyword in Cartopy's LambertConformal projection (Cartopy v0.12). Older versions of Iris will not be able to create LambertConformal coordinate systems with Cartopy >= 0.12. + Incompatible changes ==================== + * Saving a cube with a STASH attribute to NetCDF now produces a variable with an attribute of "um_stash_source" rather than "ukmo__um_stash_source". -* Cubes saved to NetCDF with a coordinate system referencing a spherical ellipsoid - now result in the grid mapping variable containing only the "earth_radius" attribute, - rather than the "semi_major_axis" and "semi_minor_axis". -* Collapsing a cube over all of its dimensions now results in a scalar cube rather - than a 1d cube. + +* Cubes saved to NetCDF with a coordinate system referencing a spherical + ellipsoid now result in the grid mapping variable containing only the + "earth_radius" attribute, rather than the "semi_major_axis" and + "semi_minor_axis". + +* Collapsing a cube over all of its dimensions now results in a scalar cube + rather than a 1d cube. + Deprecations ============ + * :func:`iris.util.ensure_array` has been deprecated. + * Deprecated the :func:`iris.fileformats.pp.reset_load_rules` and :func:`iris.fileformats.grib.reset_load_rules` functions. + * Matplotlib is no longer a core Iris dependency. -Documentation changes -===================== + +Documentation +============= + * New sections on :ref:`cube broadcasting ` and :doc:`regridding and interpolation ` have been added to the :doc:`user guide `. + * An example demonstrating custom log-scale colouring has been added. See :ref:`sphx_glr_generated_gallery_general_plot_anomaly_log_colouring.py`. + * An example demonstrating the creation of a custom :class:`iris.analysis.Aggregator` has been added. See :ref:`sphx_glr_generated_gallery_general_plot_custom_aggregation.py`. + * An example of reprojecting data from 2D auxiliary spatial coordinates - (such as that from the ORCA grid) has been added. See :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py`. -* A clarification of the behaviour of :func:`iris.analysis.calculus.differentiate`. -* A new :doc:`"Technical Papers" ` section has been added to the documentation along - with the addition of a paper providing an :doc:`overview of the load process for UM-like - fileformats (e.g. PP and Fieldsfile) `. + (such as that from the ORCA grid) has been added. See + :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py`. + +* A clarification of the behaviour of + :func:`iris.analysis.calculus.differentiate`. + +* A new :doc:`"Technical Papers" ` section has been added to + the documentation along with the addition of a paper providing an + :doc:`overview of the load process for UM-like fileformats (e.g. PP and Fieldsfile) `. diff --git a/docs/iris/src/whatsnew/1.8.rst b/docs/iris/src/whatsnew/1.8.rst index 54763a194b..17432d7267 100644 --- a/docs/iris/src/whatsnew/1.8.rst +++ b/docs/iris/src/whatsnew/1.8.rst @@ -1,14 +1,17 @@ -What's new in Iris 1.8 -********************** +v1.8 (14 Apr 2015) +******************** -:Release: 1.8.1 -:Date: 3rd June 2015 - -This document explains the new/changed features of Iris in version 1.8. +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.8 features -================= + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== .. _showcase: @@ -38,14 +41,17 @@ Iris 1.8 features .. admonition:: Showcase: Slices over a coordinate - You can slice over one or more dimensions of a cube using :meth:`iris.cube.Cube.slices_over`. - This provides similar functionality to :meth:`~iris.cube.Cube.slices` but with - almost the opposite outcome. + You can slice over one or more dimensions of a cube using + :meth:`iris.cube.Cube.slices_over`. + This provides similar functionality to :meth:`~iris.cube.Cube.slices` + but with almost the opposite outcome. - Using :meth:`~iris.cube.Cube.slices` to slice a cube on a selected dimension returns - all possible slices of the cube with the selected dimension retaining its dimensionality. - Using :meth:`~iris.cube.Cube.slices_over` to slice a cube on a selected - dimension returns all possible slices of the cube over the selected dimension. + Using :meth:`~iris.cube.Cube.slices` to slice a cube on a selected + dimension returns all possible slices of the cube with the selected + dimension retaining its dimensionality. Using + :meth:`~iris.cube.Cube.slices_over` to slice a cube on a selected + dimension returns all possible slices of the cube over the selected + dimension. To demonstrate this:: @@ -60,42 +66,65 @@ Iris 1.8 features air_potential_temperature / (K) (model_level_number: 10; grid_latitude: 83; grid_longitude: 83) -* :func:`iris.cube.CubeList.concatenate` now works with `biggus `_ arrays and so +* :func:`iris.cube.CubeList.concatenate` now works with + `biggus `_ arrays and so now supports concatenation of cubes with deferred data. + * Improvements to NetCDF saving through using biggus: * A cube's lazy data payload will still be lazy after saving; the data will not be loaded into memory by the save operation. + * Cubes with data payloads larger than system memory can now be saved to NetCDF through biggus streaming the data to disk. -* :func:`iris.util.demote_dim_coord_to_aux_coord` and :func:`iris.util.promote_aux_coord_to_dim_coord` +* :func:`iris.util.demote_dim_coord_to_aux_coord` and + :func:`iris.util.promote_aux_coord_to_dim_coord` allow a coordinate to be easily demoted or promoted within a cube. -* :func:`iris.util.squeeze` removes all length 1 dimensions from a cube, and demotes - any associated squeeze dimension :class:`~iris.coords.DimCoord` to be a scalar coordinate. -* :meth:`iris.cube.Cube.slices_over`, which returns an iterator of all sub-cubes along a given - coordinate or dimension index. + +* :func:`iris.util.squeeze` removes all length 1 dimensions from a cube, and + demotes any associated squeeze dimension :class:`~iris.coords.DimCoord` to be + a scalar coordinate. + +* :meth:`iris.cube.Cube.slices_over`, which returns an iterator of all + sub-cubes along a given coordinate or dimension index. + * :meth:`iris.cube.Cube.interpolate` now accepts datetime.datetime and netcdftime.datetime instances for date or time coordinates. -* Many new and updated translations between CF spec and STASH codes or GRIB2 parameter - codes. -* PP/FF loader creates a height coordinate at 1.5m or 10m for certain relevant stash codes. -* Lazy aggregator support for the :class:`standard deviation ` - aggregator has been added. -* A speed improvement in calculation of :func:`iris.analysis.cartography.area_weights`. -* Experimental support for unstructured grids has been added with :func:`iris.experimental.ugrid`. - This has been implemented using `UGRID `_. -* :meth:`iris.cube.CubeList.extract_overlapping` supports extraction of cubes over - regions where common coordinates overlap, over multiple coordinates. + +* Many new and updated translations between CF spec and STASH codes or GRIB2 + parameter codes. + +* PP/FF loader creates a height coordinate at 1.5m or 10m for certain relevant + stash codes. + +* Lazy aggregator support for the + :class:`standard deviation ` aggregator has been added. + +* A speed improvement in calculation of + :func:`iris.analysis.cartography.area_weights`. + +* Experimental support for unstructured grids has been added with + :func:`iris.experimental.ugrid`. This has been implemented using + `UGRID `_. + +* :meth:`iris.cube.CubeList.extract_overlapping` supports extraction of cubes + over regions where common coordinates overlap, over multiple coordinates. + * Warnings raised due to invalid units in loaded data have been suppressed. -* Experimental low-level read and write access for FieldsFile variants is now supported - via :class:`iris.experimental.um.FieldsFileVariant`. + +* Experimental low-level read and write access for FieldsFile variants is now + supported via :class:`iris.experimental.um.FieldsFileVariant`. + * PP loader will return cubes for all fields prior to a field with a problematic header before raising an exception. -* NetCDF loader skips invalid global attributes, raising a warning rather than raising an - exception. + +* NetCDF loader skips invalid global attributes, raising a warning rather than + raising an exception. + * A warning is now raised rather than an exception when constructing an :class:`~iris.aux_factory.AuxCoordFactory` fails. + * Supported :class:`aux coordinate factories ` have been extended to include: @@ -104,78 +133,104 @@ Iris 1.8 features * ``ocean s coordinate, generic form 1``, and * ``ocean s coordinate, generic form 2``. -* :meth:`iris.cube.Cube.intersection` now supports taking a points-only intersection. - Any bounds on intersected coordinates are ignored but retained. +* :meth:`iris.cube.Cube.intersection` now supports taking a points-only + intersection. Any bounds on intersected coordinates are ignored but retained. + * The FF loader's known handled grids now includes ``Grid 21``. -* A :class:`nearest neighbour ` scheme is now provided for - :meth:`iris.cube.Cube.interpolate` and :meth:`iris.cube.Cube.regrid`. -* :func:`iris.analysis.cartography.rotate_winds` supports transformation of wind vectors - to a different coordinate system. + +* A :class:`nearest neighbour ` scheme is now provided + for :meth:`iris.cube.Cube.interpolate` and :meth:`iris.cube.Cube.regrid`. + +* :func:`iris.analysis.cartography.rotate_winds` supports transformation of + wind vectors to a different coordinate system. + * NumPy universal functions can now be applied to cubes using :func:`iris.analysis.maths.apply_ufunc`. + * Generic functions can be applied to :class:`~iris.cube.Cube` instances using :class:`iris.analysis.maths.IFunc`. -* The :class:`iris.analysis.Linear` scheme now supports regridding as well as interpolation. - This enables :meth:`iris.cube.Cube.regrid` to perform bilinear regridding, which now - replaces the experimental routine "iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid". + +* The :class:`iris.analysis.Linear` scheme now supports regridding as well as + interpolation. This enables :meth:`iris.cube.Cube.regrid` to perform bilinear + regridding, which now replaces the experimental routine + "iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid". + Bugs fixed ========== -1.8.0 ------- * Fix in netCDF loader to correctly determine whether the longitude coordinate (including scalar coordinates) is circular. -* :meth:`iris.cube.Cube.intersection` now supports bounds that extend slightly beyond 360 - degrees. -* Lateral Boundary Condition (LBC) type FieldFiles are now handled correctly by the FF loader. -* Making a copy of a scalar cube with no data now correctly copies the data array. -* Height coordinates in NAME trajectory output files have been changed to match other - NAME output file formats. + +* :meth:`iris.cube.Cube.intersection` now supports bounds that extend slightly + beyond 360 degrees. + +* Lateral Boundary Condition (LBC) type FieldFiles are now handled correctly by + the FF loader. + +* Making a copy of a scalar cube with no data now correctly copies the data + array. + +* Height coordinates in NAME trajectory output files have been changed to match + other NAME output file formats. + * Fixed datatype when loading an ``integer_constants`` array from a FieldsFile. + * FF/PP loader adds appropriate cell methods for ``lbtim.ib = 3`` intervals. + * An exception is raised if the units of the latitude and longitude coordinates of the cube passed into :func:`iris.analysis.cartography.area_weights` are not convertible to radians. + * GRIB1 loader now creates a time coordinate for a time range indicator of 2. + * NetCDF loader now loads units that are empty strings as dimensionless. -1.8.1 ------- -* The PP loader now carefully handles floating point errors in date time conversions to hours. -* The handling fill values for lazy data loaded from NetCDF files is altered, such that the - _FillValue set in the file is preserved through lazy operations. -* The risk that cube intersections could return incorrect results due to floating point - tolerances is reduced. -* The new GRIB2 loading code is altered to enable the loading of various data representation - templates; the data value unpacking is handled by the GRIB API. -* Saving cube collections to NetCDF, where multiple similar aux-factories exist within the cubes, - is now carefully handled such that extra file variables are created where required in some cases. - -1.8.2 ------ -* A fix to prevent the error: *AttributeError: 'module' object has no attribute 'date2num'*. - This was caused by the function :func:`netcdftime.date2num` being removed from the netCDF4 - package in recent versions. + +v1.8.1 (03 Jun 2015) +-------------------- + +* The PP loader now carefully handles floating point errors in date time + conversions to hours. + +* The handling fill values for lazy data loaded from NetCDF files is altered, + such that the _FillValue set in the file is preserved through lazy operations. + +* The risk that cube intersections could return incorrect results due to + floating point tolerances is reduced. + +* The new GRIB2 loading code is altered to enable the loading of various data + representation templates; the data value unpacking is handled by the GRIB API. + +* Saving cube collections to NetCDF, where multiple similar aux-factories exist + within the cubes, is now carefully handled such that extra file variables are + created where required in some cases. + Deprecations ============ + * The original GRIB loader has been deprecated and replaced with a new template-based GRIB loader. + * Deprecated default NetCDF save behaviour of assigning the outermost dimension to be unlimited. Switch to the new behaviour with no auto assignment by setting :data:`iris.FUTURE.netcdf_no_unlimited` to True. + * The former experimental method - "iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid" has been removed, as - :class:`iris.analysis.Linear` now includes this functionality. - -Documentation changes -===================== -* A chapter on :doc:`merge and concatenate ` has been - added to the :doc:`user guide `. -* A section on installing Iris using `conda `_ has been - added to the :doc:`install guide `. + "iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid" has been + removed, as :class:`iris.analysis.Linear` now includes this functionality. + + +Documentation +============= + +* A chapter on :doc:`merge and concatenate ` has + been added to the :doc:`user guide `. + +* A section on installing Iris using `conda `_ has + been added to the :doc:`install guide `. + * Updates to the chapter on :doc:`regridding and interpolation ` have been added to the :doc:`user guide `. - diff --git a/docs/iris/src/whatsnew/1.9.rst b/docs/iris/src/whatsnew/1.9.rst index 7fda661ebc..da3fabe613 100644 --- a/docs/iris/src/whatsnew/1.9.rst +++ b/docs/iris/src/whatsnew/1.9.rst @@ -1,32 +1,48 @@ -What's new in Iris 1.9 -********************** +v1.9 (10 Dec 2015) +******************** -:Release: 1.9.2 -:Date: 28th January 2016 - -This document explains the new/changed features of Iris in version 1.9 +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 1.9 features -================= -* Support for running on Python 3.4 has been added to the whole code base. Some features which - depend on external libraries will not be available until they also support Python 3, namely: + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== + +* Support for running on Python 3.4 has been added to the whole code base. + Some features which depend on external libraries will not be available until + they also support Python 3, namely: * gribapi does not yet provide a Python 3 interface -* Added the UM pseudo level type to the information made available in the STASH_TRANS table in :mod:`iris.fileformats.um._ff_cross_references` -* When reading "cell_methods" attributes from NetCDF files, allow optional whitespace before the colon. - This is not strictly in the CF spec, but is a common occurrence. -* Basic cube arithemetic (plus, minus, times, divide) now supports lazy evaluation. -* :meth:`iris.analysis.cartography.rotate_winds` can now operate much faster on multi-layer (i.e. > 2-dimensional) cubes, - as it calculates rotation coefficients only once and reuses them for additional layers. +* Added the UM pseudo level type to the information made available in the + STASH_TRANS table in :mod:`iris.fileformats.um._ff_cross_references` + +* When reading "cell_methods" attributes from NetCDF files, allow optional + whitespace before the colon. This is not strictly in the CF spec, but is a + common occurrence. -* Linear regridding of a multi-layer (i.e. > 2-dimensional) cube is now much faster, - as it calculates transform coefficients just once and reuses them for additional layers. -* Ensemble statistics can now be saved to GRIB2, using Product Definition Template 4.11. +* Basic cube arithemetic (plus, minus, times, divide) now supports lazy + evaluation. -* Loading of NetCDF data with ocean vertical coordinates now returns a 'depth' in addition to an 'eta' cube. - This operates on specific defined dimensionless coordinates : see CF spec version 1.6, Appendix D. +* :meth:`iris.analysis.cartography.rotate_winds` can now operate much faster + on multi-layer (i.e. > 2-dimensional) cubes, as it calculates rotation + coefficients only once and reuses them for additional layers. + +* Linear regridding of a multi-layer (i.e. > 2-dimensional) cube is now much + faster, as it calculates transform coefficients just once and reuses them for + additional layers. + +* Ensemble statistics can now be saved to GRIB2, using Product Definition + Template 4.11. + +* Loading of NetCDF data with ocean vertical coordinates now returns a 'depth' + in addition to an 'eta' cube. This operates on specific defined + dimensionless coordinates : see CF spec version 1.6, Appendix D. * :func:`iris.analysis.stats.pearsonr` updates: @@ -37,14 +53,21 @@ Iris 1.9 features * Accepts common_mask keyword for restricting calculation to unmasked pairs of cells. -* Added a new point-in-cell regridding scheme, :class:`iris.experimental.regrid.PointInCell`. -* Added :meth:`iris.analysis.WPERCENTILE` - a new weighted aggregator for calculating - percentiles. -* Added cell-method translations for LBPROC=64 and 192 in UM files, encoding 'zonal mean' and 'zonal+time mean'. +* Added a new point-in-cell regridding scheme, + :class:`iris.experimental.regrid.PointInCell`. + +* Added :meth:`iris.analysis.WPERCENTILE` - a new weighted aggregator for + calculating percentiles. + +* Added cell-method translations for LBPROC=64 and 192 in UM files, encoding + 'zonal mean' and 'zonal+time mean'. + +* Support for loading GRIB2 messages defined on a Lambert conformal grid has + been added to the GRIB2 loader. + +* Data on potential-temperature (theta) levels can now be saved to GRIB2, with + a fixed surface type of 107. -* Support for loading GRIB2 messages defined on a Lambert conformal grid has been added to - the GRIB2 loader. -* Data on potential-temperature (theta) levels can now be saved to GRIB2, with a fixed surface type of 107. * Added several new helper functions for file-save customisation, (see also : :doc:`Saving Iris Cubes `): @@ -54,76 +77,130 @@ Iris 1.9 features * :meth:`iris.fileformats.pp.as_pairs` * :meth:`iris.fileformats.pp.as_fields` * :meth:`iris.fileformats.pp.save_fields` -* Loading data from GRIB2 now supports most of the currently defined 'data representation templates' : - code numbers 0, 1, 2, 3, 4, 40, 41, 50, 51 and 61. -* When a Fieldsfile is opened for update as a :class:`iris.experimental.um.FieldsFileVariant`, - unmodified packed data in the file can now be retained in the original form. - Previously it could only be stored in an unpacked form. + +* Loading data from GRIB2 now supports most of the currently defined 'data + representation templates' : code numbers 0, 1, 2, 3, 4, 40, 41, 50, 51 and 61. + +* When a Fieldsfile is opened for update as a + :class:`iris.experimental.um.FieldsFileVariant`, unmodified packed data in + the file can now be retained in the original form. Previously it could only + be stored in an unpacked form. + * When reading and writing NetCDF data, the CF 'flag' attributes, - "flag_masks", "flag_meanings" and "flag_values" are now preserved through Iris load and save. -* `mo_pack `_ was added as an optional dependency. + "flag_masks", "flag_meanings" and "flag_values" are now preserved through + Iris load and save. + +* `mo_pack `_ was added as an optional + dependency. It is used to encode and decode data in WGDOS packed form. -* The :meth:`iris.experimental.um.Field.get_data` method can now be used to read Fieldsfile data - after the original :class:`iris.experimental.um.FieldsFileVariant` has been closed. + +* The :meth:`iris.experimental.um.Field.get_data` method can now be used to + read Fieldsfile data after the original + :class:`iris.experimental.um.FieldsFileVariant` has been closed. Bugs fixed ========== + * Fixed a bug in :meth:`iris.unit.Unit.convert` (and the equivalent in `cf_units `_) - so that it now converts data to the native endianness, without which udunits could not read it correctly. + so that it now converts data to the native endianness, without which udunits + could not read it correctly. + * Fixed a bug with loading WGDOS packed data in :mod:`iris.experimental.um`, which could occasionally crash, with some data. -* Ignore non-numeric suffices in the numpy version string, which would otherwise crash some regridding routines. + +* Ignore non-numeric suffices in the numpy version string, which would + otherwise crash some regridding routines. + * fixed a bug in :mod:`iris.fileformats.um_cf_map` where the standard name - for the stash code m01s12i187 was incorrectly set, such that it is inconsistent - with the stated unit of measure, 'm s-1'. The different name, a long_name - of 'change_over_time_in_upward_air_velocity_due_to_advection' with + for the stash code m01s12i187 was incorrectly set, such that it is + inconsistent with the stated unit of measure, 'm s-1'. The different name, + a long_name of 'change_over_time_in_upward_air_velocity_due_to_advection' with units of 'm s-1' is now used instead. + * Fixed a bug in :meth:`iris.cube.Cube.intersection`. - When edge points were at (base + period), intersection would unnecessarily wrap the data. + When edge points were at (base + period), intersection would unnecessarily + wrap the data. + * Fixed a bug in :mod:`iris.fileformats.pp`. - A previous release removed the ability to pass a partial constraint on STASH attribute. -* :meth:`iris.plot.default_projection_extent` now correctly raises an exception if a cube has X bounds but no Y bounds, or vice versa. - Previously it never failed this, as the test was wrong. -* When loading NetCDF data, a "units" attribute containing unicode characters is now transformed by backslash-replacement. - Previously this caused a crash. Note: unicode units are *not supported in the CF conventions*. -* When saving to NetCDF, factory-derived auxiliary coordinates are now correctly saved with different names when they are not identical. - Previously, such coordinates could be saved with the same name, leading to errors. + A previous release removed the ability to pass a partial constraint on STASH + attribute. + +* :meth:`iris.plot.default_projection_extent` now correctly raises an exception + if a cube has X bounds but no Y bounds, or vice versa. Previously it never + failed this, as the test was wrong. + +* When loading NetCDF data, a "units" attribute containing unicode characters + is now transformed by backslash-replacement. Previously this caused a crash. + Note: unicode units are *not supported in the CF conventions*. + +* When saving to NetCDF, factory-derived auxiliary coordinates are now correctly + saved with different names when they are not identical. Previously, such + coordinates could be saved with the same name, leading to errors. + * Fixed a bug in :meth:`iris.experimental.um.FieldsFileVariant.close`, which now correctly allocates extra blocks for larger lookups when saving. - Previously, when larger files open for update were closed, they could be written out with data overlapping the lookup table. + Previously, when larger files open for update were closed, they could be + written out with data overlapping the lookup table. + * Fixed a bug in :class:`iris.aux_factory.OceanSigmaZFactory` - which sometimes caused crashes when fetching the points of an "ocean sigma z" coordinate. + which sometimes caused crashes when fetching the points of an "ocean sigma z" + coordinate. + -Version 1.9.1 -------------- -* Fixed a unicode bug preventing standard names from being built cleanly when installing in Python3 +v1.9.1 (05 Jan 2016) +-------------------- + +* Fixed a unicode bug preventing standard names from being built cleanly when + installing in Python3 + + +v1.9.2 (28 Jan 2016) +-------------------- + +* New warning regarding data loss if writing to an open file which is also + open to read, with lazy data. -Version 1.9.2 -------------- -* New warning regarding data loss if writing to an open file which is also open to read, with lazy data. * Removal of a warning about data payload loading from concatenate. + * Updates to concatenate documentation. + * Fixed a bug with a name change in the netcdf4-python package. + * Fixed a bug building the documentation examples. -* Fixed a bug avoiding sorting classes directly when :meth:`iris.cube.Cube.coord_system` is used in Python3. + +* Fixed a bug avoiding sorting classes directly when + :meth:`iris.cube.Cube.coord_system` is used in Python3. + * Fixed a bug regarding unsuccessful dot import. + Incompatible changes ==================== -* GRIB message/file reading and writing may not be available for Python 3 due to GRIB API limitations. + +* GRIB message/file reading and writing may not be available for Python 3 due + to GRIB API limitations. + Deprecations ============ -* Deprecated :mod:`iris.unit`, with unit functionality provided by `cf_units `_ instead. -* When loading from NetCDF, a deprecation warning is emitted if there is vertical coordinate information - that *would* produce extra result cubes if :data:`iris.FUTURE.netcdf_promote` were set, - but it is *not* set. + +* Deprecated :mod:`iris.unit`, with unit functionality provided by + `cf_units `_ instead. + +* When loading from NetCDF, a deprecation warning is emitted if there is + vertical coordinate information that *would* produce extra result cubes if + :data:`iris.FUTURE.netcdf_promote` were set, but it is *not* set. + * Deprecated :class:`iris.aux_factory.LazyArray` -Documentation changes -===================== + +Documentation +============= + * A chapter on :doc:`saving iris cubes ` has been added to the :doc:`user guide `. -* Added script and documentation for building a what's new page from developer-submitted contributions. - See :doc:`Contributing a "What's New" entry `. + +* Added script and documentation for building a what's new page from + developer-submitted contributions. See + :doc:`Contributing a "What's New" entry `. diff --git a/docs/iris/src/whatsnew/2.0.rst b/docs/iris/src/whatsnew/2.0.rst index 61568d3a8e..577e8fea22 100644 --- a/docs/iris/src/whatsnew/2.0.rst +++ b/docs/iris/src/whatsnew/2.0.rst @@ -1,16 +1,18 @@ -What's new in Iris 2.0.0 -************************ +v2.0 (14 Feb 2018) +****************** -:Release: 2.0.0rc1 -:Date: 2018-01-11 +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) -This document explains the new/changed features of Iris in version 2.0.0 -(:doc:`View all changes `). +.. contents:: Skip to section: + :local: + :depth: 3 -Iris 2.0.0 features -=================== +Features +======== + .. _showcase: .. admonition:: Dask Integration @@ -209,8 +211,8 @@ Incompatible Changes printed as ``m.s-1``. -Deprecation removals --------------------- +Deprecation +=========== All deprecated functionality that was announced for removal in Iris 2.0 has been removed. In particular: @@ -289,8 +291,8 @@ been removed. In particular: removed from the :class:`iris.fileformats.rules.Loader` constructor. -Documentation changes -===================== +Documentation +============= * A new UserGuide chapter on :doc:`Real and Lazy Data ` has been added, and referenced from key diff --git a/docs/iris/src/whatsnew/2.1.rst b/docs/iris/src/whatsnew/2.1.rst index a82d3b8470..311e8c251b 100644 --- a/docs/iris/src/whatsnew/2.1.rst +++ b/docs/iris/src/whatsnew/2.1.rst @@ -1,37 +1,17 @@ -What's new in Iris 2.1 -********************** +v2.1 (06 Jun 2018) +****************** -:Release: 2.1 -:Date: 2018-06-06 +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) -This document explains the new/changed features of Iris in version 2.1 -(:doc:`older "What's New" release notes can be found here`.) +.. contents:: Skip to section: + :local: + :depth: 3 -Iris 2.1 dependency updates -=========================== -* The `cf_units `_ dependency - was updated to cf_units ``v2.0``. - cf_units v2 is almost entirely backwards compatible with v1. - However the ability to preserve some aliased calendars has been removed. - For this reason, it is possible that NetCDF load of a variable with a - "standard" calendar will result in a saved NetCDF of a "gregorian" - calendar. -* Iris updated its time-handling functionality from the - `netcdf4-python `_ - ``netcdftime`` implementation to the standalone module - `cftime `_. - cftime is entirely compatible with netcdftime, but some issues may - occur where users are constructing their own datetime objects. - In this situation, simply replacing ``netcdftime.datetime`` with - ``cftime.datetime`` should be sufficient. -* Iris now requires version 2 of Matplotlib, and ``>=1.14`` of NumPy. - Full requirements can be seen in the `requirements `_ - directory of the Iris' the source. - -Iris 2.1 features -================= +Features +======== * Added ``repr_html`` functionality to the :class:`~iris.cube.Cube` to provide a rich html representation of cubes in Jupyter notebooks. Existing functionality @@ -42,42 +22,81 @@ Iris 2.1 features * Updated :func:`iris.cube.Cube.name` to return a STASH code if the cube has one and no other valid names are present. This is now consistent with the summary information from :func:`iris.cube.Cube.summary`. + * The partial collapse of multi-dimensional auxiliary coordinates is now supported. Collapsed bounds span the range of the collapsed dimension(s). + * Added new function :func:`iris.cube.CubeList.realise_data` to compute multiple lazy values in a single operation, avoiding repeated re-loading of data or re-calculation of expressions. + * The methods :meth:`iris.cube.Cube.convert_units` and :meth:`iris.coords.Coord.convert_units` no longer forcibly realise the cube data or coordinate points/bounds. The converted values are now lazy arrays if the originals were. + * Added :meth:`iris.analysis.trajectory.interpolate` that allows you to interpolate to find values along a trajectory. + * It is now possible to add an attribute of ``missing_value`` to a cube (:issue:`1588`). + * Iris can now represent data on the Albers Equal Area Projection, and the NetCDF loader and saver were updated to handle this. (:issue:`2943`) + * The :class:`~iris.coord_systems.Mercator` projection has been updated to accept the ``standard_parallel`` keyword argument (:pull:`3041`). + Bugs fixed ========== * All var names being written to NetCDF are now CF compliant. Non alpha-numeric characters are replaced with '_', and var names now always have a leading letter (:pull:`2930`). + * A cube resulting from a regrid operation using the `iris.analysis.AreaWeighted` regridding scheme will now have the smallest floating point data type to which the source cube's data type can be safely converted using NumPy's type promotion rules. + * :mod:`iris.quickplot` labels now honour the axes being drawn to when using the ``axes`` keyword (:pull:`3010`). + Incompatible changes ==================== + * The deprecated :mod:`iris.experimental.um` was removed. Please use consider using `mule `_ as an alternative. + * This release of Iris contains a number of updated metadata translations. - See [this changelist](https://github.com/SciTools/iris/commit/69597eb3d8501ff16ee3d56aef1f7b8f1c2bb316#diff-1680206bdc5cfaa83e14428f5ba0f848) + See this + `changelist `_ for further information. + + +Internal +======== + +* The `cf_units `_ dependency + was updated to cf_units ``v2.0``. + cf_units v2 is almost entirely backwards compatible with v1. + However the ability to preserve some aliased calendars has been removed. + For this reason, it is possible that NetCDF load of a variable with a + "standard" calendar will result in a saved NetCDF of a "gregorian" + calendar. + +* Iris updated its time-handling functionality from the + `netcdf4-python `_ + ``netcdftime`` implementation to the standalone module + `cftime `_. + cftime is entirely compatible with netcdftime, but some issues may + occur where users are constructing their own datetime objects. + In this situation, simply replacing ``netcdftime.datetime`` with + ``cftime.datetime`` should be sufficient. + +* Iris now requires version 2 of Matplotlib, and ``>=1.14`` of NumPy. + Full requirements can be seen in the `requirements `_ + directory of the Iris' the source. \ No newline at end of file diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst index 75be5460b3..314f84355f 100644 --- a/docs/iris/src/whatsnew/2.2.rst +++ b/docs/iris/src/whatsnew/2.2.rst @@ -1,17 +1,18 @@ -What's new in Iris 2.2 -************************ +v2.2 (11 Oct 2018) +****************** -:Release: 2.2.0 -:Date: +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) -This document explains the new/changed features of Iris in the release -of version 2.2 -(:doc:`View all changes `). +.. contents:: Skip to section: + :local: + :depth: 3 -Iris 2.2 features -=================== +Features +======== + .. _showcase: .. admonition:: 2-Dimensional Coordinate Plotting @@ -70,18 +71,6 @@ Iris 2.2 features a NaN-tolerant array comparison. -Iris 2.2 dependency updates -============================= - -* Iris is now using the latest version release of dask (currently 0.19.3) - -* Proj4 has been temporarily pinned to version < 5 while problems with the - Mollweide projection are addressed. - -* Matplotlib has been pinned to version < 3 temporarily while we account for - its changes in all SciTools libraries. - - Bugs fixed ========== @@ -93,7 +82,7 @@ Bugs fixed bound data is actually masked. -Bugs fixed in v2.2.1 +v2.2.1 (28 May 2019) -------------------- * Iris can now correctly unpack a column of header objects when saving a @@ -108,9 +97,20 @@ Bugs fixed in v2.2.1 floating-point arithmetic. +Internal +======== + +* Iris is now using the latest version release of dask (currently 0.19.3) + +* Proj4 has been temporarily pinned to version < 5 while problems with the + Mollweide projection are addressed. + +* Matplotlib has been pinned to version < 3 temporarily while we account for + its changes in all SciTools libraries. + -Documentation changes -===================== +Documentation +============= * Iris' `INSTALL` document has been updated to include guidance for running tests. diff --git a/docs/iris/src/whatsnew/2.3.rst b/docs/iris/src/whatsnew/2.3.rst index 6fb7088339..a515f6daad 100644 --- a/docs/iris/src/whatsnew/2.3.rst +++ b/docs/iris/src/whatsnew/2.3.rst @@ -1,14 +1,18 @@ -What's new in Iris 2.3.0 -************************ +v2.3 (19 Dec 2019) +****************** -:Release: 2.3.0 -:Date: 2019-12-19 - -This document explains the new/changed features of Iris in version 2.3.0 +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 2.3.0 features -=================== + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== + .. _showcase: .. admonition:: Support for CF 1.7 @@ -112,53 +116,67 @@ Iris 2.3.0 features * Added support to render HTML for :class:`~iris.cube.CubeList` in Jupyter Notebooks and JupyterLab. + * Loading CellMeasures with integer values is now supported. + * New coordinate system: :class:`iris.coord_systems.Geostationary`, including load and save support, based on the `CF Geostationary projection definition `_. + * :class:`iris.coord_systems.VerticalPerspective` can now be saved to and loaded from NetCDF files. + * :class:`iris.experimental.regrid.PointInCell` moved to :class:`iris.analysis.PointInCell` to make this regridding scheme public -* Iris now supports standard name modifiers. See `Appendix C, Standard Name Modifiers `_ for more information. + +* Iris now supports standard name modifiers. See + `Appendix C, Standard Name Modifiers `_ + for more information. + * :meth:`iris.cube.Cube.remove_cell_measure` now also allows removal of a cell measure by its name (previously only accepted a CellMeasure object). + * The :data:`iris.analysis.RMS` aggregator now supports a lazy calculation. However, the "weights" keyword is not currently supported by this, so a *weighted* calculation will still return a realised result, *and* force realisation of the original cube data. -* Iris now supports NetCDF Climate and Forecast (CF) Metadata Conventions 1.7 (see `CF 1.7 Conventions Document `_ for more information) + +* Iris now supports NetCDF Climate and Forecast (CF) Metadata Conventions 1.7 + (see `CF 1.7 Conventions Document `_ for more information) + * Updated standard name support to `CF standard name table version 70, 2019-12-10 `_ + * Updated UM STASH translations to `metarelate/metOcean commit 448f2ef, 2019-11-29 `_ -Iris 2.3.0 dependency updates -============================= -* Iris now supports Proj4 up to version 5, but not yet 6 or beyond, pending - `fixes to some cartopy tests `_. -* Iris now requires Dask >= 1.2 to allow for improved coordinate equality - checks. - - Bugs fixed ========== + * Cube equality of boolean data is now handled correctly. + * Fixed a bug where cell measures were incorrect after a cube :meth:`~iris.cube.Cube.transpose` operation. Previously, this resulted in cell-measures that were no longer correctly mapped to the cube dimensions. -* The :class:`~iris.coords.AuxCoord` disregarded masked points and bounds, as did the :class:`~iris.coords.DimCoord`. - Fix permits an :class:`~iris.coords.AuxCoord` to contain masked points/bounds, and a TypeError exception is now - raised when attempting to create or set the points/bounds of a - :class:`~iris.coords.DimCoord` with arrays with missing points. + +* The :class:`~iris.coords.AuxCoord` disregarded masked points and bounds, as + did the :class:`~iris.coords.DimCoord`. Fix permits an + :class:`~iris.coords.AuxCoord` to contain masked points/bounds, and a + TypeError exception is now raised when attempting to create or set the + points/bounds of a :class:`~iris.coords.DimCoord` with arrays with missing + points. + * :class:`iris.coord_systems.VerticalPerspective` coordinate system now uses the `CF Vertical perspective definition `_; had been erroneously using Geostationary. -* :class:`~iris.coords.CellMethod` will now only use valid `NetCDF name tokens`_ to reference the coordinates involved in the statistical operation. + +* :class:`~iris.coords.CellMethod` will now only use valid + `NetCDF name tokens`_ to reference the coordinates involved in the + statistical operation. + * The following var_name properties will now only allow valid `NetCDF name tokens`_ to reference the said NetCDF variable name. Note that names with a leading @@ -174,48 +192,73 @@ Bugs fixed * Rendering a cube in Jupyter will no longer crash for a cube with attributes containing ``\n``. + * NetCDF variables which reference themselves in their ``cell_measures`` attribute can now be read. + * :func:`~iris.plot.quiver` now handles circular coordinates. + * The names of cubes loaded from abf/abl files have been corrected. + * Fixed a bug in UM file loading, where any landsea-mask-compressed fields (i.e. with LBPACK=x2x) would cause an error later, when realising the data. + * :meth:`iris.cube.Cube.collapsed` now handles partial collapsing of multidimensional coordinates that have bounds. + * Fixed a bug in the :data:`~iris.analysis.PROPORTION` aggregator, where cube data in the form of a masked array with ``array.mask=False`` would cause an error, but possibly only later when the values are actually realised. ( Note: since netCDF4 version 1.4.0, this is now a common form for data loaded from netCDF files ). + * Fixed a bug where plotting a cube with a :class:`iris.coord_systems.LambertConformal` coordinate system would result in an error. This would happen if the coordinate system was defined with one standard parallel, rather than two. In these cases, a call to :meth:`~iris.coord_systems.LambertConformal.as_cartopy_crs` would fail. + * :meth:`iris.cube.Cube.aggregated_by` now gives correct values in points and bounds when handling multidimensional coordinates. + * Fixed a bug in the :meth:`iris.cube.Cube.collapsed` operation, which caused the unexpected realization of any attached auxiliary coordinates that were *bounded*. It now correctly produces a lazy result and does not realise the original attached AuxCoords. -Documentation changes -===================== +Internal +======== + +* Iris now supports Proj4 up to version 5, but not yet 6 or beyond, pending + `fixes to some cartopy tests `_. + +* Iris now requires Dask >= 1.2 to allow for improved coordinate equality + checks. + + +Documentation +============= + * Adopted a `new colour logo for Iris <../_static/Iris7_1_trim_full.png>`_ + * Added a gallery example showing how to concatenate NEMO ocean model data, see :ref:`sphx_glr_generated_gallery_oceanography_plot_load_nemo.py`. + * Added an example in the `Loading Iris Cubes: Constraining on Time <../userguide/loading_iris_cubes .html#constraining-on-time>`_ Userguide section, demonstrating how to load data within a specified date range. + * Added notes to the :func:`iris.load` documentation, and the userguide `Loading Iris Cubes <../userguide/loading_iris_cubes.html>`_ chapter, emphasizing that the *order* of the cubes returned by an iris load operation is effectively random and unstable, and should not be relied on. + * Fixed references in the documentation of :func:`iris.util.find_discontiguities` to a nonexistent "mask_discontiguities" routine : these now refer to diff --git a/docs/iris/src/whatsnew/2.4.rst b/docs/iris/src/whatsnew/2.4.rst index 776cc8aa69..0df2a909ff 100644 --- a/docs/iris/src/whatsnew/2.4.rst +++ b/docs/iris/src/whatsnew/2.4.rst @@ -1,23 +1,25 @@ -What's new in Iris 2.4.0 -************************ +v2.4 (20 Feb 2020) +****************** -:Release: 2.4.0 -:Date: 2020-02-20 - -This document explains the new/changed features of Iris in version 2.4.0 +This document explains the changes made to Iris for this release (:doc:`View all changes `.) -Iris 2.4.0 features -=================== +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== .. admonition:: Last python 2 version of Iris - Iris 2.4 is a final extra release of Iris 2, which back-ports specific desired features from - Iris 3 (not yet released). + Iris 2.4 is a final extra release of Iris 2, which back-ports specific + desired features from Iris 3 (not yet released). - The purpose of this is both to support early adoption of certain newer features, - and to provide a final release for Python 2. + The purpose of this is both to support early adoption of certain newer + features, and to provide a final release for Python 2. The next release of Iris will be version 3.0 : a major-version release which introduces breaking API and behavioural changes, and only supports Python 3. @@ -25,35 +27,42 @@ Iris 2.4.0 features * :class:`iris.coord_systems.Geostationary` can now accept creation arguments of `false_easting=None` or `false_northing=None`, equivalent to values of 0. Previously these kwargs could be omitted, but could not be set to `None`. - This also enables loading of netcdf data on a Geostationary grid, where either of these - keys is not present as a grid-mapping variable property : Previously, loading any - such data caused an exception. -* The area weights used when performing area weighted regridding with :class:`iris.analysis.AreaWeighted` - are now cached. - This allows a significant speedup when regridding multiple similar cubes, by repeatedly using - a `'regridder' object <../iris/iris/analysis.html?highlight=regridder#iris.analysis.AreaWeighted.regridder>`_ + This also enables loading of netcdf data on a Geostationary grid, where + either of these keys is not present as a grid-mapping variable + property : Previously, loading any such data caused an exception. + +* The area weights used when performing area weighted regridding with + :class:`iris.analysis.AreaWeighted` are now cached. This allows a + significant speedup when regridding multiple similar cubes, by repeatedly + using a :func:`iris.analysis.AreaWeighted.regridder` objects which you created first. -* Name constraint matching against cubes during loading or extracting has been relaxed from strictly matching - against the :meth:`~iris.cube.Cube.name`, to matching against either the - ``standard_name``, ``long_name``, NetCDF ``var_name``, or ``STASH`` attributes metadata of a cube. -* Cubes and coordinates now have a new ``names`` property that contains a tuple of the - ``standard_name``, ``long_name``, NetCDF ``var_name``, and ``STASH`` attributes metadata. -* The :class:`~iris.NameConstraint` provides richer name constraint matching when loading or extracting - against cubes, by supporting a constraint against any combination of - ``standard_name``, ``long_name``, NetCDF ``var_name`` and ``STASH`` - from the attributes dictionary of a :class:`~iris.cube.Cube`. - - -Iris 2.4.0 dependency updates -============================= -* Iris is now able to use the latest version of matplotlib. + +* Name constraint matching against cubes during loading or extracting has been + relaxed from strictly matching against the :meth:`~iris.cube.Cube.name`, to + matching against either the ``standard_name``, ``long_name``, NetCDF + ``var_name``, or ``STASH`` attributes metadata of a cube. + +* Cubes and coordinates now have a new ``names`` property that contains a tuple + of the ``standard_name``, ``long_name``, NetCDF ``var_name``, and ``STASH`` + attributes metadata. + +* The :class:`~iris.NameConstraint` provides richer name constraint matching + when loading or extracting against cubes, by supporting a constraint against + any combination of ``standard_name``, ``long_name``, NetCDF ``var_name`` and + ``STASH`` from the attributes dictionary of a :class:`~iris.cube.Cube`. Bugs fixed ========== + * Fixed a problem which was causing file loads to fetch *all* field data whenever UM files (PP or Fieldsfiles) were loaded. With large sourcefiles, initial file loads are slow, with large memory usage before any cube data is even fetched. Large enough files will cause a crash. The problem occurs only with Dask versions >= 2.0. + +Internal +======== + +* Iris is now able to use the latest version of matplotlib. diff --git a/docs/iris/src/whatsnew/aggregate_directory.py b/docs/iris/src/whatsnew/aggregate_directory.py deleted file mode 100644 index 6fe92f6764..0000000000 --- a/docs/iris/src/whatsnew/aggregate_directory.py +++ /dev/null @@ -1,337 +0,0 @@ -# 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 -# licensing details. -""" -Build a release file from files in a contributions directory. - -Looks for directories "<...whatsnew>/contributions_". -Takes specified "xx.xx" as version, or latest found (alphabetic). -Writes a file "<...whatsnew>/.rst". - -Valid contributions filenames are of the form: - __summary.txt -Where can be any valid chars, and - is one of : - "newfeature" "bugfix" "incompatiblechange" "deprecate" "docchange", and - is in the style "2001-Jan-23". - -""" - -import datetime -from glob import glob -import os -import re -import argparse -import warnings -from operator import itemgetter -from distutils import version - -# Regular expressions: CONTRIBUTION_REGEX matches the filenames of -# contribution snippets. It is split into three sections separated by _ -# 0. String for the category. 1. ISO8601 date. 2. String for the feature name. -# RELEASE_REGEX matches the directory names, returning the release. -CONTRIBUTION_REGEX_STRING = r"(?P.*)" -CONTRIBUTION_REGEX_STRING += r"_(?P\d{4}-\w{3}-\d{2})" -CONTRIBUTION_REGEX_STRING += r"_(?P.*)\.txt$" -CONTRIBUTION_REGEX = re.compile(CONTRIBUTION_REGEX_STRING) -RELEASEDIR_PREFIX = r"contributions_" -_RELEASEDIR_REGEX_STRING = RELEASEDIR_PREFIX + r"(?P.*)$" -RELEASE_REGEX = re.compile(_RELEASEDIR_REGEX_STRING) -SOFTWARE_NAME = "Iris" -EXTENSION = ".rst" -VALID_CATEGORIES = [ - {"Prefix": "newfeature", "Title": "features"}, - {"Prefix": "bugfix", "Title": "Bugs Fixed"}, - {"Prefix": "incompatiblechange", "Title": "Incompatible Changes"}, - {"Prefix": "deprecate", "Title": "Deprecations"}, - {"Prefix": "docchange", "Title": "Documentation Changes"}, -] -VALID_CATEGORY_PREFIXES = [cat["Prefix"] for cat in VALID_CATEGORIES] - - -def _self_root_directory(): - return os.path.abspath(os.path.dirname(__file__)) - - -def _decode_contribution_filename(file_name): - file_name_elements = CONTRIBUTION_REGEX.match(file_name) - category = file_name_elements.group("category") - if category not in VALID_CATEGORY_PREFIXES: - # This is an error - raise ValueError("Unknown category in contribution filename.") - isodate = file_name_elements.group("isodate") - date_of_item = datetime.datetime.strptime(isodate, "%Y-%b-%d").date() - return category, isodate, date_of_item - - -def is_release_directory(directory_name, release): - """Returns True if a given directory name matches the requested release.""" - result = False - directory_elements = RELEASE_REGEX.match(directory_name) - try: - release_string = directory_elements.group("release") - directory_release = version.StrictVersion(release_string) - except (AttributeError, ValueError): - pass - else: - if directory_release == release: - result = True - return result - - -def is_compiled_release(root_directory, release): - """Returns True if the requested release.rst file exists.""" - result = False - compiled_filename = "{!s}{}".format(release, EXTENSION) - compiled_filepath = os.path.join(root_directory, compiled_filename) - if os.path.exists(compiled_filepath) and os.path.isfile(compiled_filepath): - result = True - return result - - -def get_latest_release(root_directory=None): - """ - Implement default=latest release identification. - - Returns a valid release code. - - """ - if root_directory is None: - root_directory = _self_root_directory() - directory_contents = os.listdir(root_directory) - # Default release to latest visible dir. - possible_release_dirs = [ - releasedir_name - for releasedir_name in directory_contents - if RELEASE_REGEX.match(releasedir_name) - ] - if len(possible_release_dirs) == 0: - dirspec = os.path.join(root_directory, RELEASEDIR_PREFIX + "*") - msg = "No valid release directories found, i.e. {!r}." - raise ValueError(msg.format(dirspec)) - release_dirname = sorted(possible_release_dirs)[-1] - release = RELEASE_REGEX.match(release_dirname).group("release") - return release - - -def find_release_directory( - root_directory, release=None, fail_on_existing=True -): - """ - Returns the matching contribution directory or raises an exception. - - Defaults to latest-found release (from release directory names). - Optionally, fail if the matching release file already exists. - *Always* fail if no release directory exists. - - """ - if release is None: - # Default to latest release. - release = get_latest_release(root_directory) - - if fail_on_existing: - compiled_release = is_compiled_release(root_directory, release) - if compiled_release: - msg = ( - "Specified release {!r} is already compiled : " - "{!r} already exists." - ) - compiled_filename = str(release) + EXTENSION - raise ValueError(msg.format(release, compiled_filename)) - - directory_contents = os.listdir(root_directory) - result = None - for inode in directory_contents: - node_path = os.path.join(root_directory, inode) - if os.path.isdir(node_path): - release_directory = is_release_directory(inode, release) - if release_directory: - result = os.path.join(root_directory, inode) - break - if not result: - msg = "Contribution folder for release {!s} does not exist : no {!r}." - release_dirname = RELEASEDIR_PREFIX + str(release) + "/" - release_dirpath = os.path.join(root_directory, release_dirname) - raise ValueError(msg.format(release, release_dirpath)) - return result - - -def generate_header(release, unreleased=False): - """Return a list of text lines that make up a header for the document.""" - if unreleased: - isodatestamp = "" - else: - isodatestamp = datetime.date.today().strftime("%Y-%m-%d") - header_text = [] - title_template = "What's new in {} {!s}\n" - title_line = title_template.format(SOFTWARE_NAME, release) - title_underline = ("*" * (len(title_line) - 1)) + "\n" - header_text.append(title_line) - header_text.append(title_underline) - header_text.append("\n") - header_text.append(":Release: {!s}\n".format(release)) - header_text.append(":Date: {}\n".format(isodatestamp)) - header_text.append("\n") - description_template = ( - "This document explains the new/changed features " - "of {} in version {!s}\n" - ) - header_text.append(description_template.format(SOFTWARE_NAME, release)) - header_text.append("(:doc:`View all changes `.)") - header_text.append("\n") - return header_text - - -def read_directory(directory_path): - """Parse the items in a specified directory and return their metadata.""" - directory_contents = os.listdir(directory_path) - compilable_files_unsorted = [] - misnamed_files = [] - for file_name in directory_contents: - try: - category, isodate, date_of_item = _decode_contribution_filename( - file_name - ) - except (AttributeError, ValueError): - misnamed_files.append(file_name) - continue - compilable_files_unsorted.append( - {"Category": category, "Date": date_of_item, "FileName": file_name} - ) - compilable_files = sorted( - compilable_files_unsorted, key=itemgetter("Date"), reverse=True - ) - if misnamed_files: - msg = "Found contribution file(s) with unexpected names :" - for filename in misnamed_files: - full_path = os.path.join(directory_path, filename) - msg += "\n {}".format(full_path) - warnings.warn(msg, UserWarning) - - return compilable_files - - -def compile_directory(directory, release, unreleased=False): - """Read in source files in date order and compile the text into a list.""" - if unreleased: - release = "" - source_text = read_directory(directory) - compiled_text = [] - header_text = generate_header(release, unreleased) - compiled_text.extend(header_text) - for count, category in enumerate(VALID_CATEGORIES): - category_text = [] - subtitle_line = "" - if count == 0: - subtitle_line += "{} {!s} ".format(SOFTWARE_NAME, release) - subtitle_line += category["Title"] + "\n" - subtitle_underline = ("=" * (len(subtitle_line) - 1)) + "\n" - category_text.append("\n") - category_text.append(subtitle_line) - category_text.append(subtitle_underline) - category_items = [ - item - for item in source_text - if item["Category"] == category["Prefix"] - ] - if not category_items: - continue - for file_description in category_items: - entry_path = os.path.join(directory, file_description["FileName"]) - with open(entry_path, "r") as content_object: - text = content_object.readlines() - if not text[-1].endswith("\n"): - text[-1] += "\n" - category_text.extend(text) - category_text.append("\n----\n\n") - compiled_text.extend(category_text) - return compiled_text - - -def check_all_contributions_valid(release=None, quiet=False, unreleased=False): - """"Scan the contributions directory for badly-named files.""" - root_directory = _self_root_directory() - # Check there are *some* contributions directory(s), else silently pass. - contribs_spec = os.path.join(root_directory, RELEASEDIR_PREFIX + "*") - if len(glob(contribs_spec)) > 0: - # There are some contributions directories: check latest / specified. - if release is None: - release = get_latest_release() - if not quiet: - msg = 'Checking whatsnew contributions for release "{!s}".' - print(msg.format(release)) - release_directory = find_release_directory( - root_directory, release, fail_on_existing=False - ) - # Run the directory scan, but convert any warning into an error. - with warnings.catch_warnings(): - warnings.simplefilter("error") - compile_directory(release_directory, release, unreleased) - if not quiet: - print("done.") - - -def run_compilation(release=None, quiet=False, unreleased=False): - """Write a draft release.rst file given a specified uncompiled release.""" - if release is None: - # This must exist ! - release = get_latest_release() - if not quiet: - msg = 'Building release document for release "{!s}".' - print(msg.format(release)) - root_directory = _self_root_directory() - release_directory = find_release_directory(root_directory, release) - compiled_text = compile_directory(release_directory, release, unreleased) - if unreleased: - compiled_filename = "latest" + EXTENSION - else: - compiled_filename = str(release) + EXTENSION - compiled_filepath = os.path.join(root_directory, compiled_filename) - with open(compiled_filepath, "w") as output_object: - for string_line in compiled_text: - output_object.write(string_line) - if not quiet: - print("done.") - - -if __name__ == "__main__": - PARSER = argparse.ArgumentParser() - PARSER.add_argument( - "release", - help="Release number to be compiled", - nargs="?", - type=version.StrictVersion, - ) - PARSER.add_argument( - "-c", - "--checkonly", - action="store_true", - help="Check contribution file names, do not build.", - ) - PARSER.add_argument( - "-u", - "--unreleased", - action="store_true", - help=( - "Label the release version as '', " - "and its date as ''." - ), - ) - PARSER.add_argument( - "-q", - "--quiet", - action="store_true", - help="Do not print progress messages.", - ) - ARGUMENTS = PARSER.parse_args() - release = ARGUMENTS.release - unreleased = ARGUMENTS.unreleased - quiet = ARGUMENTS.quiet - if ARGUMENTS.checkonly: - check_all_contributions_valid( - release, quiet=quiet, unreleased=unreleased - ) - else: - run_compilation(release, quiet=quiet, unreleased=unreleased) diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Dec-02_cell_measure_concatenate.txt b/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Dec-02_cell_measure_concatenate.txt deleted file mode 100644 index 151341d9af..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Dec-02_cell_measure_concatenate.txt +++ /dev/null @@ -1,2 +0,0 @@ -* Concatenating cubes along an axis shared by cell measures would cause concatenation to inappropriately fail. - These cell measures are now concatenated together in the resulting cube. \ No newline at end of file diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Nov-14_cell_measure_positional_argument.txt b/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Nov-14_cell_measure_positional_argument.txt deleted file mode 100644 index d43b5c2d44..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Nov-14_cell_measure_positional_argument.txt +++ /dev/null @@ -1,4 +0,0 @@ -* A :class:`iris.coords.CellMeasure` requires a string ``measure`` attribute to be defined, which can only have a value - of ``area`` or ``volume``. Previously, the ``measure`` was provided as a keyword argument to - :class:`~iris.coords.CellMeasure` with an default value of ``None``, which caused a ``TypeError`` when no - ``measure`` was provided. The default value of ``area`` is now used. \ No newline at end of file diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Nov-19_cell_measure_copy_loss.txt b/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Nov-19_cell_measure_copy_loss.txt deleted file mode 100644 index 3a0bbfaf56..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2019-Nov-19_cell_measure_copy_loss.txt +++ /dev/null @@ -1,2 +0,0 @@ -* Copying a cube would previously ignore any attached class:`iris.coords.CellMeasure`. - These are now copied over. \ No newline at end of file diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Feb-13_cube_iter_remove.txt b/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Feb-13_cube_iter_remove.txt deleted file mode 100644 index 082cd8acc8..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Feb-13_cube_iter_remove.txt +++ /dev/null @@ -1,3 +0,0 @@ -* The `__iter__()` method in class:`iris.cube.Cube` was set to `None`. - `TypeError` is still raised if a `Cube` is iterated over but - `isinstance(cube, collections.Iterable)` now behaves as expected. \ No newline at end of file diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Jun-15_remove_aux_factories_with_remove_coord.txt b/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Jun-15_remove_aux_factories_with_remove_coord.txt deleted file mode 100644 index f5d88ab357..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/bugfix_2020-Jun-15_remove_aux_factories_with_remove_coord.txt +++ /dev/null @@ -1,2 +0,0 @@ -* The method :meth:`~iris.Cube.cube.remove_coord` would fail to remove derived - coordinates, will now remove derived coordinates by removing aux_factories. \ No newline at end of file diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/deprecate_2019-Oct-11_remove_LBProc_flag_attributes.txt b/docs/iris/src/whatsnew/contributions_3.0.0/deprecate_2019-Oct-11_remove_LBProc_flag_attributes.txt deleted file mode 100644 index 56c1435316..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/deprecate_2019-Oct-11_remove_LBProc_flag_attributes.txt +++ /dev/null @@ -1,2 +0,0 @@ -* :attr:`iris.fileformats.pp.PPField.lbproc` is now an `int`. The - deprecated attributes `flag1`, `flag2` etc. have been removed from it. \ No newline at end of file diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/deprecate_2019-Oct-14_remove_deprecated_future_flags.txt b/docs/iris/src/whatsnew/contributions_3.0.0/deprecate_2019-Oct-14_remove_deprecated_future_flags.txt deleted file mode 100644 index 3bf515187b..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/deprecate_2019-Oct-14_remove_deprecated_future_flags.txt +++ /dev/null @@ -1,3 +0,0 @@ -* The deprecated :class:`iris.Future` flags `cell_date_time_objects`, - `netcdf_promote`, `netcdf_no_unlimited` and `clip_latitudes` have - been removed. \ No newline at end of file diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2019-Dec-04_black_code_formatting.txt b/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2019-Dec-04_black_code_formatting.txt deleted file mode 100644 index 500a215bb9..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2019-Dec-04_black_code_formatting.txt +++ /dev/null @@ -1,6 +0,0 @@ -* Added support for the `black `_ code formatter. - This is now automatically checked on GitHub PRs, replacing the older, unittest-based - "iris.tests.test_coding_standards.TestCodeFormat". - Black provides automatic code format correction for most IDEs. - See the new developer guide section on this : - https://scitools-docs.github.io/iris/master/developers_guide/code_format.html. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-10_modernise_documentation_using_themes_and_readthedocs.txt b/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-10_modernise_documentation_using_themes_and_readthedocs.txt deleted file mode 100644 index 5ff8c001e8..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-10_modernise_documentation_using_themes_and_readthedocs.txt +++ /dev/null @@ -1,2 +0,0 @@ -* Updated documentation to use a modern sphinx theme and be served from - https://scitools-iris.readthedocs.io/en/latest/. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-23_moved_gallery_entry.txt b/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-23_moved_gallery_entry.txt deleted file mode 100644 index d73021dcc9..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/docchange_2020-Jul-23_moved_gallery_entry.txt +++ /dev/null @@ -1,2 +0,0 @@ -* Moved the :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` - from the general part of the gallery to oceanography. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-12_remove_experimental_concatenate_module.txt b/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-12_remove_experimental_concatenate_module.txt deleted file mode 100644 index 418377aabc..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-12_remove_experimental_concatenate_module.txt +++ /dev/null @@ -1,3 +0,0 @@ -* The :mod:`iris.experimental.concatenate` module has now been removed. In ``v1.6.0`` the experimental `concatenate` - functionality was moved to the :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the - :func:`iris.experimental.concatenate.concatenate` function raised an exception. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-13_move_experimental_equalise_cubes.txt b/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-13_move_experimental_equalise_cubes.txt deleted file mode 100644 index a7ddaa441b..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-13_move_experimental_equalise_cubes.txt +++ /dev/null @@ -1,3 +0,0 @@ -* The :func:`iris.experimental.equalise_cubes.equalise_attributes` function has been moved from the - :mod:`iris.experimental` module into the :mod:`iris.util` module. Please use the :func:`iris.util.equalise_attributes` - function instead. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-26_remove_coord_comparison.txt b/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-26_remove_coord_comparison.txt deleted file mode 100644 index a8ba4131d0..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2019-Nov-26_remove_coord_comparison.txt +++ /dev/null @@ -1 +0,0 @@ -* The former function "iris.analysis.coord_comparison" has been removed. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2020-May-22_cubelist_extract_cubes.txt b/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2020-May-22_cubelist_extract_cubes.txt deleted file mode 100644 index ed8e6a8e2c..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/incompatiblechange_2020-May-22_cubelist_extract_cubes.txt +++ /dev/null @@ -1,10 +0,0 @@ -* The method :meth:`~iris.cube.CubeList.extract_strict`, and the 'strict' - keyword to :meth:`~iris.cube.CubeList.extract` method have been removed, and - are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` and - :meth:`~iris.cube.CubeList.extract_cubes`. - The new routines perform the same operation, but in a style more like other - Iris functions such as :meth:`iris.load_cube` and :meth:`iris.load_cubes`. - Unlike 'strict extraction', the type of return value is now completely - consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a cube, - and :meth:`~iris.cube.CubeList.extract_cubes` always returns a CubeList of a - length equal to the number of constraints. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Dec-20_cache_area_weights.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Dec-20_cache_area_weights.txt deleted file mode 100644 index 8c9b7b95d2..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Dec-20_cache_area_weights.txt +++ /dev/null @@ -1,5 +0,0 @@ -* The area weights used when performing area weighted regridding with :class:`iris.analysis.AreaWeighted` - are now cached. - This allows a significant speedup when regridding multiple similar cubes, by repeatedly using - a `'regridder' object <../iris/iris/analysis.html?highlight=regridder#iris.analysis.AreaWeighted.regridder>`_ - which you created first. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Nov-27_cell_measure_statistics.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Nov-27_cell_measure_statistics.txt deleted file mode 100644 index cf8c83e594..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Nov-27_cell_measure_statistics.txt +++ /dev/null @@ -1,5 +0,0 @@ -* Statistical operations :meth:`iris.cube.Cube.collapsed`, - :meth:`iris.cube.Cube.aggregated_by` and :meth:`iris.cube.Cube.rolling_window` - previously removed every :class:`iris.coord.CellMeasure` attached to the cube. - Now, a :class:`iris.coord.CellMeasure` will only be removed if it is associated - with an axis over which the statistic is being run. \ No newline at end of file diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-14_cf_ancillary_data.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-14_cf_ancillary_data.txt deleted file mode 100644 index ea70702f38..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-14_cf_ancillary_data.txt +++ /dev/null @@ -1 +0,0 @@ -* CF Ancillary Variables are now supported in cubes. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_nameconstraint.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_nameconstraint.txt deleted file mode 100644 index eeb40990e2..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_nameconstraint.txt +++ /dev/null @@ -1 +0,0 @@ -* The :class:`~iris.NameConstraint` provides richer name constraint matching when loading or extracting against cubes, by supporting a constraint against any combination of ``standard_name``, ``long_name``, NetCDF ``var_name`` and ``STASH`` from the attributes dictionary of a :class:`~iris.cube.Cube`. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_names_property.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_names_property.txt deleted file mode 100644 index a092631152..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_names_property.txt +++ /dev/null @@ -1 +0,0 @@ -* Cubes and coordinates now have a new ``names`` property that contains a tuple of the ``standard_name``, ``long_name``, NetCDF ``var_name``, and ``STASH`` attributes metadata. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_relaxed_name_loading.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_relaxed_name_loading.txt deleted file mode 100644 index 6773ac28b1..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-15_relaxed_name_loading.txt +++ /dev/null @@ -1 +0,0 @@ -* Name constraint matching against cubes during loading or extracting has been relaxed from strictly matching against the :meth:`~iris.cube.Cube.name`, to matching against either the ``standard_name``, ``long_name``, NetCDF ``var_name``, or ``STASH`` attributes metadata of a cube. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-17_unpin_mpl.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-17_unpin_mpl.txt deleted file mode 100644 index bbee87037a..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2019-Oct-17_unpin_mpl.txt +++ /dev/null @@ -1,2 +0,0 @@ -* Supporting Iris for both Python2 and Python3 resulted in pinning our dependency on matplotlib at v2.x. - Now that Python2 support has been dropped, Iris is free to use the latest version of matplotlib. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-06_relax_geostationary.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-06_relax_geostationary.txt deleted file mode 100644 index e1113c838c..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-06_relax_geostationary.txt +++ /dev/null @@ -1,6 +0,0 @@ -* :class:`iris.coord_systems.Geostationary` can now accept creation arguments of - `false_easting=None` or `false_northing=None`, equivalent to values of 0. - Previously these kwargs could be omitted, but could not be set to `None`. - This also enables loading netcdf data on a Geostationary grid, where either of these - keys is not present as a grid-mapping variable property : Previously, loading any - such data caused an exception. diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt deleted file mode 100644 index 18378691cb..0000000000 --- a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt +++ /dev/null @@ -1,3 +0,0 @@ -* The :class:`~iris.fileformats.nimrod` provides richer meta-data translation - when loading Nimrod-format data into cubes. This covers most known operational - use-cases. diff --git a/docs/iris/src/whatsnew/index.rst b/docs/iris/src/whatsnew/index.rst index 00b925a48e..a574e7a689 100644 --- a/docs/iris/src/whatsnew/index.rst +++ b/docs/iris/src/whatsnew/index.rst @@ -6,11 +6,11 @@ What's new in Iris These "What's new" pages describe the important changes between major Iris versions. + .. toctree:: :maxdepth: 1 latest.rst - 3.0.0.rst 2.4.rst 2.3.rst 2.2.rst diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst new file mode 100644 index 0000000000..fd6bf2fc1f --- /dev/null +++ b/docs/iris/src/whatsnew/latest.rst @@ -0,0 +1,145 @@ + +************ + +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) + + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== + +* The :class:`~iris.fileformats.nimrod` provides richer meta-data translation + when loading Nimrod-format data into cubes. This covers most known + operational use-cases. + +* :class:`iris.coord_systems.Geostationary` can now accept creation arguments + of `false_easting=None` or `false_northing=None`, equivalent to values of 0. + Previously these kwargs could be omitted, but could not be set to `None`. + This also enables loading netcdf data on a Geostationary grid, where either + of these keys is not present as a grid-mapping variable property: + Previously, loading any such data caused an exception. + +* The area weights used when performing area weighted regridding with + :class:`iris.analysis.AreaWeighted` are now cached. This allows a + significant speedup when regridding multiple similar cubes, by repeatedly + using a `'regridder' object <../iris/iris/analysis.html?highlight=regridder#iris.analysis.AreaWeighted.regridder>`_ + which you created first. + +* Statistical operations :meth:`iris.cube.Cube.collapsed`, + :meth:`iris.cube.Cube.aggregated_by` and :meth:`iris.cube.Cube.rolling_window` + previously removed every :class:`iris.coord.CellMeasure` attached to the + cube. Now, a :class:`iris.coord.CellMeasure` will only be removed if it is + associated with an axis over which the statistic is being run. + +* Supporting Iris for both Python2 and Python3 resulted in pinning our + dependency on matplotlib at v2.x. Now that Python2 support has been dropped, + Iris is free to use the latest version of matplotlib. + +* The :class:`~iris.NameConstraint` provides richer name constraint matching + when loading or extracting against cubes, by supporting a constraint against + any combination of ``standard_name``, ``long_name``, NetCDF ``var_name`` + and ``STASH`` from the attributes dictionary of a :class:`~iris.cube.Cube`. + +* Cubes and coordinates now have a new ``names`` property that contains a + tuple of the ``standard_name``, ``long_name``, NetCDF ``var_name``, and + ``STASH`` attributes metadata. + +* Name constraint matching against cubes during loading or extracting has been + relaxed from strictly matching against the :meth:`~iris.cube.Cube.name`, to + matching against either the ``standard_name``, ``long_name``, + NetCDF ``var_name``, or ``STASH`` attributes metadata of a cube. + +* CF Ancillary Variables are now supported in cubes. + + +Bugs Fixed +========== + +* The method :meth:`~iris.Cube.cube.remove_coord` would fail to remove derived + coordinates, will now remove derived coordinates by removing aux_factories. + +* The `__iter__()` method in class:`iris.cube.Cube` was set to `None`. + `TypeError` is still raised if a `Cube` is iterated over but + `isinstance(cube, collections.Iterable)` now behaves as expected. + +* Concatenating cubes along an axis shared by cell measures would cause + concatenation to inappropriately fail. These cell measures are now + concatenated together in the resulting cube. + +* Copying a cube would previously ignore any attached + class:`iris.coords.CellMeasure`. These are now copied over. + +* A :class:`iris.coords.CellMeasure` requires a string ``measure`` attribute + to be defined, which can only have a value of ``area`` or ``volume``. + Previously, the ``measure`` was provided as a keyword argument to + :class:`~iris.coords.CellMeasure` with an default value of ``None``, which + caused a ``TypeError`` when no ``measure`` was provided. The default value + of ``area`` is now used. + + +Incompatible Changes +==================== + +* The method :meth:`~iris.cube.CubeList.extract_strict`, and the 'strict' + keyword to :meth:`~iris.cube.CubeList.extract` method have been removed, and + are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` and + :meth:`~iris.cube.CubeList.extract_cubes`. + The new routines perform the same operation, but in a style more like other + Iris functions such as :meth:`iris.load_cube` and :meth:`iris.load_cubes`. + Unlike 'strict extraction', the type of return value is now completely + consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a cube, + and :meth:`~iris.cube.CubeList.extract_cubes` always returns a CubeList of a + length equal to the number of constraints. + +* The former function "iris.analysis.coord_comparison" has been removed. + +* The :func:`iris.experimental.equalise_cubes.equalise_attributes` function + has been moved from the :mod:`iris.experimental` module into the + :mod:`iris.util` module. Please use the :func:`iris.util.equalise_attributes` + function instead. + +* The :mod:`iris.experimental.concatenate` module has now been removed. In + ``v1.6.0`` the experimental `concatenate` functionality was moved to the + :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the + :func:`iris.experimental.concatenate.concatenate` function raised an + exception. + + +Deprecations +============ + +* The deprecated :class:`iris.Future` flags `cell_date_time_objects`, + `netcdf_promote`, `netcdf_no_unlimited` and `clip_latitudes` have + been removed. + +* :attr:`iris.fileformats.pp.PPField.lbproc` is now an `int`. The + deprecated attributes `flag1`, `flag2` etc. have been removed from it. + + +Documentation +============= + +* Moved the :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` + from the general part of the gallery to oceanography. + +* Updated documentation to use a modern sphinx theme and be served from + https://scitools-iris.readthedocs.io/en/latest/. + +* Added support for the `black `_ code + formatter. This is now automatically checked on GitHub PRs, replacing the + older, unittest-based "iris.tests.test_coding_standards.TestCodeFormat". + Black provides automatic code format correction for most IDEs. See the new + developer guide section on :ref:`iris_code_format` + +* Refreshed the :ref:`whats_new_contributions` for the :ref:`iris_whatsnew`. + This includes always creating the ``latest`` what's new page so it appears + on the latest documentation at + https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves + :issue:`2104` and :issue:`3451` + + diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/iris/src/whatsnew/latest.rst.template new file mode 100644 index 0000000000..82f87d9e5a --- /dev/null +++ b/docs/iris/src/whatsnew/latest.rst.template @@ -0,0 +1,46 @@ + +************ + +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) + + +.. contents:: Skip to section: + :local: + :depth: 3 + + +Features +======== + +* N/A + + +Bugs Fixed +========== + +* N/A + + +Incompatible Changes +==================== + +* N/A + + +Dependencies +============ + +* N/A + + +Internal +======== + +* N/A + + +Documentation +============= + +* N/A \ No newline at end of file From 84d1330f3b033fd1c24ad5c41ef94819913195c0 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Wed, 5 Aug 2020 09:46:38 +0100 Subject: [PATCH 24/32] whatsnew latest update (#3771) --- docs/iris/src/whatsnew/latest.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index fd6bf2fc1f..4e7d60693a 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -134,12 +134,17 @@ Documentation formatter. This is now automatically checked on GitHub PRs, replacing the older, unittest-based "iris.tests.test_coding_standards.TestCodeFormat". Black provides automatic code format correction for most IDEs. See the new - developer guide section on :ref:`iris_code_format` + developer guide section on :ref:`iris_code_format`. * Refreshed the :ref:`whats_new_contributions` for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` what's new page so it appears on the latest documentation at https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves - :issue:`2104` and :issue:`3451` - + :issue:`2104` and :issue:`3451`. Also updated the + :ref:`iris_development_releases_steps` to follow when making a release. +* Enabled the pdf creation of the documentation on the `Read the Docs`_ service. + The pdf may be accessed by clicking on the version at the bottom of the side + bar, then selecting **pdf** from the downloads section. + +.. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ From d16f6767d4d775e2afd691de9c6514782ba2a90d Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Fri, 7 Aug 2020 09:37:34 +0100 Subject: [PATCH 25/32] remove duplicate whats new entries from the last release (#3773) --- docs/iris/src/whatsnew/latest.rst | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 4e7d60693a..a8057cf870 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -17,19 +17,6 @@ Features when loading Nimrod-format data into cubes. This covers most known operational use-cases. -* :class:`iris.coord_systems.Geostationary` can now accept creation arguments - of `false_easting=None` or `false_northing=None`, equivalent to values of 0. - Previously these kwargs could be omitted, but could not be set to `None`. - This also enables loading netcdf data on a Geostationary grid, where either - of these keys is not present as a grid-mapping variable property: - Previously, loading any such data caused an exception. - -* The area weights used when performing area weighted regridding with - :class:`iris.analysis.AreaWeighted` are now cached. This allows a - significant speedup when regridding multiple similar cubes, by repeatedly - using a `'regridder' object <../iris/iris/analysis.html?highlight=regridder#iris.analysis.AreaWeighted.regridder>`_ - which you created first. - * Statistical operations :meth:`iris.cube.Cube.collapsed`, :meth:`iris.cube.Cube.aggregated_by` and :meth:`iris.cube.Cube.rolling_window` previously removed every :class:`iris.coord.CellMeasure` attached to the @@ -39,21 +26,7 @@ Features * Supporting Iris for both Python2 and Python3 resulted in pinning our dependency on matplotlib at v2.x. Now that Python2 support has been dropped, Iris is free to use the latest version of matplotlib. - -* The :class:`~iris.NameConstraint` provides richer name constraint matching - when loading or extracting against cubes, by supporting a constraint against - any combination of ``standard_name``, ``long_name``, NetCDF ``var_name`` - and ``STASH`` from the attributes dictionary of a :class:`~iris.cube.Cube`. - -* Cubes and coordinates now have a new ``names`` property that contains a - tuple of the ``standard_name``, ``long_name``, NetCDF ``var_name``, and - ``STASH`` attributes metadata. - -* Name constraint matching against cubes during loading or extracting has been - relaxed from strictly matching against the :meth:`~iris.cube.Cube.name`, to - matching against either the ``standard_name``, ``long_name``, - NetCDF ``var_name``, or ``STASH`` attributes metadata of a cube. - + * CF Ancillary Variables are now supported in cubes. From 556e71e01bbc4efb3f24a25705f6cfcb4e3c3c6f Mon Sep 17 00:00:00 2001 From: Bill Little Date: Fri, 14 Aug 2020 13:40:52 +0100 Subject: [PATCH 26/32] [PI-3478] Merge cube arithmetic feature branch (#3785) * PI-3478: Common metadata API (#3583) * common metadata api * rationalise _cube_coord_common into common * move state into metadata * MetadataFactory test coverage * temporarily pin back iris-grib * test coverage for iris.common.metadata._BaseMeta * test coverage for iris.common.mixin.LimitedAttributeDict * remove temporary iris-grib pin * review actions * Update lib/iris/tests/unit/common/metadata/test_BaseMetadata.py Co-Authored-By: lbdreyer * [FB] [PI-3478] Lenient metadata (#3739) * add lenient infra-structure * add metadata lenient __eq__ support * complete __eq__, combine and difference support * explicit inherited lenient_service + support equal convenience * fix attributes difference + lenient kwargs * make lenient public + minor tidy * rename MetadataManagerFactory to metadata_manager_factory * extend lenient_client decorator to support services registration * add lenient test coverage * purge qualname usage in metadata.py * support global enable for lenient services * support partial mapping metadata assignment * purge Lenient.__setattr__ from api * add BaseMetadata compare test coverage * metadata rationalisation * add BaseMetadata difference test coverage * added context manager ephemeral comment clarification * add BaseMetadata __ne__ test coverage * standardise lenient decorator closure names * add BaseMetadata equal test coverage * half dunder context * add AncillaryVariableMetadata test coverage * add additional AncillaryVariableMetadata test coverage * add CellMeasureMetadata test coverage * Clarify lenient_service operation + simplify code. * add CoordMetadata test coverage * add CubeMetadata test coverage * metadata tests use self.cls * fix typo * fix context manager ephemeral services * add logging * Pin pillow to make graphics tests work again. (#3630) * Fixed tests since Numpy 1.18 deprecation of non-int num arguments for linspace. (#3655) * Switched use of datetime.weekday() to datetime.dayofwk. (#3687) * New image hashes for mpl 3x2 (#3682) * New image hash for iris.test.test_plot.TestSymbols.test_cloud_cover with matplotlib 3.2.0. * Further images changes for mpl3x2. * Yet more updated image results. * fix sentinel uniqueness test failure * remove redundant cdm mapping test * difference returns None for no difference * protect Lenient and LENIENT private * privitise lenient framework and add API veneer * add explicit maths feature default * review actions * review actions * trexfeathers review actions * stephenworsley review actions Co-authored-by: Patrick Peglar Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> * [FB] [PI-3478] Lenient cube arithmetic (#3774) * initial cube arithmetic * support in-place cube resolve * fix non in-place broadcasting * remove temporary resolve scenario test * lenient/strict support for attributes dicts with numpy arrays * lenient/strict treatment of scalar coordinates * strict points/bounds matching * lenient/strict prepare local dim/aux/scalar coordinates * support extended broadcasting * always raise exception on points/bounds mismatch * ignore scalar points/bounds mismatches, lenient only * remove todos * tidy logger debugs * qualify src/tgt cube references in debug * Numpy rounding fix (#3758) ensure rounding is numpy like (maintains type) * avoid unittest.mock.sentinel copy issue * fast load np.int32 * fix cube maths doctest * fix iris.common.resolve logging configuration * fix prepare points/bounds + extra metadata cml * support mapping reversal based on free dims * var_name fix for lenient equality * add support for DimCoordMetadata * fix circular flag + support CoordMetadata and DimCoordMetadata exchange * fix circular issue for concatenate DimCoord->AuxCoord demotion * fix concatenate._CubeSignature sorted * minor tweaks * keep lenient_client private in maths * tidy maths * tidy iris.analysis.maths.IFunc * refactor IFunc test * polish in-place support * tidy metadata_resolve Co-authored-by: stephenworsley <49274989+stephenworsley@users.noreply.github.com> * rebase master fix-up for cube arithmetic * add missing new dependency to readthedocs.yml requirements Co-authored-by: lbdreyer Co-authored-by: Patrick Peglar Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Co-authored-by: stephenworsley <49274989+stephenworsley@users.noreply.github.com> --- .flake8 | 6 + ci/requirements/readthedocs.yml | 1 + docs/iris/src/userguide/cube_maths.rst | 4 + lib/iris/_concatenate.py | 19 +- lib/iris/_constraints.py | 2 +- lib/iris/_merge.py | 23 +- lib/iris/analysis/__init__.py | 2 +- lib/iris/analysis/maths.py | 504 ++--- lib/iris/aux_factory.py | 123 +- lib/iris/common/__init__.py | 11 + lib/iris/common/lenient.py | 661 +++++++ lib/iris/common/metadata.py | 1477 +++++++++++++++ .../mixin.py} | 211 +-- lib/iris/common/resolve.py | 1542 ++++++++++++++++ lib/iris/config.py | 11 + lib/iris/coords.py | 283 +-- lib/iris/cube.py | 132 +- lib/iris/etc/logging.yaml | 39 + .../fileformats/_pyke_rules/fc_rules_cf.krb | 6 +- lib/iris/iterate.py | 9 +- lib/iris/plot.py | 4 +- lib/iris/tests/__init__.py | 73 + .../integration/fast_load/test_fast_load.py | 5 +- lib/iris/tests/results/analysis/abs.cml | 3 + lib/iris/tests/results/analysis/addition.cml | 3 + .../results/analysis/addition_coord_x.cml | 3 + .../results/analysis/addition_coord_y.cml | 3 + .../analysis/addition_different_std_name.cml | 3 + .../results/analysis/addition_in_place.cml | 3 + .../analysis/addition_in_place_coord.cml | 3 + .../results/analysis/addition_scalar.cml | 3 + .../tests/results/analysis/apply_ifunc.cml | 3 + .../analysis/apply_ifunc_frompyfunc.cml | 3 + .../tests/results/analysis/apply_ufunc.cml | 3 + .../analysis/apply_ufunc_frompyfunc.cml | 3 + lib/iris/tests/results/analysis/division.cml | 3 + .../results/analysis/division_by_array.cml | 3 + .../results/analysis/division_by_latitude.cml | 3 + .../analysis/division_by_longitude.cml | 3 + .../analysis/division_by_singular_coord.cml | 3 + .../results/analysis/division_scalar.cml | 3 + .../tests/results/analysis/exponentiate.cml | 3 + lib/iris/tests/results/analysis/log.cml | 3 + lib/iris/tests/results/analysis/log10.cml | 3 + lib/iris/tests/results/analysis/log2.cml | 3 + lib/iris/tests/results/analysis/multiply.cml | 3 + .../analysis/multiply_different_std_name.cml | 3 + lib/iris/tests/results/analysis/sqrt.cml | 3 + lib/iris/tests/results/analysis/subtract.cml | 3 + .../tests/results/analysis/subtract_array.cml | 3 + .../results/analysis/subtract_coord_x.cml | 3 + .../results/analysis/subtract_coord_y.cml | 3 + .../results/analysis/subtract_scalar.cml | 3 + .../TestBroadcasting/collapse_all_dims.cml | 3 + .../TestBroadcasting/collapse_last_dims.cml | 3 + .../TestBroadcasting/collapse_middle_dim.cml | 3 + .../TestBroadcasting/collapse_zeroth_dim.cml | 3 + .../maths/add/TestBroadcasting/slice.cml | 3 + .../maths/add/TestBroadcasting/transposed.cml | 3 + .../TestBroadcasting/collapse_all_dims.cml | 3 + .../TestBroadcasting/collapse_last_dims.cml | 3 + .../TestBroadcasting/collapse_middle_dim.cml | 3 + .../TestBroadcasting/collapse_zeroth_dim.cml | 3 + .../maths/divide/TestBroadcasting/slice.cml | 3 + .../divide/TestBroadcasting/transposed.cml | 3 + .../TestBroadcasting/collapse_all_dims.cml | 3 + .../TestBroadcasting/collapse_last_dims.cml | 3 + .../TestBroadcasting/collapse_middle_dim.cml | 3 + .../TestBroadcasting/collapse_zeroth_dim.cml | 3 + .../maths/multiply/TestBroadcasting/slice.cml | 3 + .../multiply/TestBroadcasting/transposed.cml | 3 + .../TestBroadcasting/collapse_all_dims.cml | 3 + .../TestBroadcasting/collapse_last_dims.cml | 3 + .../TestBroadcasting/collapse_middle_dim.cml | 3 + .../TestBroadcasting/collapse_zeroth_dim.cml | 3 + .../maths/subtract/TestBroadcasting/slice.cml | 3 + .../subtract/TestBroadcasting/transposed.cml | 3 + lib/iris/tests/test_basic_maths.py | 23 +- lib/iris/tests/test_cdm.py | 8 - lib/iris/tests/test_coord_api.py | 10 +- .../{cube_coord_common => common}/__init__.py | 2 +- .../tests/unit/common/lenient/__init__.py | 6 + .../tests/unit/common/lenient/test_Lenient.py | 182 ++ .../unit/common/lenient/test__Lenient.py | 835 +++++++++ .../common/lenient/test__lenient_client.py | 182 ++ .../common/lenient/test__lenient_service.py | 116 ++ .../unit/common/lenient/test__qualname.py | 66 + .../tests/unit/common/metadata/__init__.py | 6 + .../test_AncillaryVariableMetadata.py | 494 +++++ .../unit/common/metadata/test_BaseMetadata.py | 1636 +++++++++++++++++ .../metadata/test_CellMeasureMetadata.py | 663 +++++++ .../common/metadata/test_CoordMetadata.py | 724 ++++++++ .../unit/common/metadata/test_CubeMetadata.py | 831 +++++++++ .../common/metadata/test__NamedTupleMeta.py | 148 ++ .../unit/common/metadata/test__hexdigest.py | 179 ++ .../metadata/test_metadata_manager_factory.py | 210 +++ lib/iris/tests/unit/common/mixin/__init__.py | 6 + .../unit/common/mixin/test_CFVariableMixin.py | 364 ++++ .../common/mixin/test_LimitedAttributeDict.py | 69 + .../mixin/test__get_valid_standard_name.py} | 26 +- lib/iris/tests/unit/coords/test_CellMethod.py | 6 +- lib/iris/tests/unit/coords/test_Coord.py | 11 + .../cube_coord_common/test_CFVariableMixin.py | 199 -- .../experimental/stratify/test_relevel.py | 5 +- .../netcdf/test__load_aux_factory.py | 14 +- lib/iris/util.py | 2 +- requirements/core.txt | 1 + 107 files changed, 11329 insertions(+), 1005 deletions(-) create mode 100644 lib/iris/common/__init__.py create mode 100644 lib/iris/common/lenient.py create mode 100644 lib/iris/common/metadata.py rename lib/iris/{_cube_coord_common.py => common/mixin.py} (51%) create mode 100644 lib/iris/common/resolve.py create mode 100644 lib/iris/etc/logging.yaml rename lib/iris/tests/unit/{cube_coord_common => common}/__init__.py (75%) create mode 100644 lib/iris/tests/unit/common/lenient/__init__.py create mode 100644 lib/iris/tests/unit/common/lenient/test_Lenient.py create mode 100644 lib/iris/tests/unit/common/lenient/test__Lenient.py create mode 100644 lib/iris/tests/unit/common/lenient/test__lenient_client.py create mode 100644 lib/iris/tests/unit/common/lenient/test__lenient_service.py create mode 100644 lib/iris/tests/unit/common/lenient/test__qualname.py create mode 100644 lib/iris/tests/unit/common/metadata/__init__.py create mode 100644 lib/iris/tests/unit/common/metadata/test_AncillaryVariableMetadata.py create mode 100644 lib/iris/tests/unit/common/metadata/test_BaseMetadata.py create mode 100644 lib/iris/tests/unit/common/metadata/test_CellMeasureMetadata.py create mode 100644 lib/iris/tests/unit/common/metadata/test_CoordMetadata.py create mode 100644 lib/iris/tests/unit/common/metadata/test_CubeMetadata.py create mode 100644 lib/iris/tests/unit/common/metadata/test__NamedTupleMeta.py create mode 100644 lib/iris/tests/unit/common/metadata/test__hexdigest.py create mode 100644 lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py create mode 100644 lib/iris/tests/unit/common/mixin/__init__.py create mode 100644 lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py create mode 100644 lib/iris/tests/unit/common/mixin/test_LimitedAttributeDict.py rename lib/iris/tests/unit/{cube_coord_common/test_get_valid_standard_name.py => common/mixin/test__get_valid_standard_name.py} (70%) delete mode 100644 lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py diff --git a/.flake8 b/.flake8 index 38cd1d82f7..131b6eb1ff 100644 --- a/.flake8 +++ b/.flake8 @@ -15,6 +15,8 @@ ignore = E402, # E501: line too long E501, + # E731: do not assign a lambda expression, use a def + E731, # W503: line break before binary operator W503, # W504: line break after binary operator @@ -38,3 +40,7 @@ exclude = # ignore third-party files # gitwash_dumper.py, + # + # convenience imports + # + lib/iris/common/__init__.py diff --git a/ci/requirements/readthedocs.yml b/ci/requirements/readthedocs.yml index 5a7e3975f7..4a1df9cc7b 100644 --- a/ci/requirements/readthedocs.yml +++ b/ci/requirements/readthedocs.yml @@ -23,6 +23,7 @@ dependencies: - netcdf4 - numpy>=1.14 - scipy + - python-xxhash # Dependencies needed to run the iris tests #------------------------------------------ diff --git a/docs/iris/src/userguide/cube_maths.rst b/docs/iris/src/userguide/cube_maths.rst index 6af4d5b3a6..0ac2b8da74 100644 --- a/docs/iris/src/userguide/cube_maths.rst +++ b/docs/iris/src/userguide/cube_maths.rst @@ -60,6 +60,10 @@ but with the data representing their difference: Scalar coordinates: forecast_reference_time: 1859-09-01 06:00:00 height: 1.5 m + Attributes: + Conventions: CF-1.5 + Model scenario: E1 + source: Data from Met Office Unified Model 6.05 .. note:: diff --git a/lib/iris/_concatenate.py b/lib/iris/_concatenate.py index 32dc87d65b..6bda3aa274 100644 --- a/lib/iris/_concatenate.py +++ b/lib/iris/_concatenate.py @@ -68,7 +68,7 @@ class _CoordMetaData( Args: * defn: - The :class:`iris.coords.CoordDefn` metadata that represents a + The :class:`iris.common.CoordMetadata` metadata that represents a coordinate. * dims: @@ -85,7 +85,7 @@ class _CoordMetaData( """ - def __new__(cls, coord, dims): + def __new__(mcs, coord, dims): """ Create a new :class:`_CoordMetaData` instance. @@ -101,7 +101,7 @@ def __new__(cls, coord, dims): The new class instance. """ - defn = coord._as_defn() + defn = coord.metadata points_dtype = coord.points.dtype bounds_dtype = coord.bounds.dtype if coord.bounds is not None else None kwargs = {} @@ -120,7 +120,7 @@ def __new__(cls, coord, dims): order = _DECREASING kwargs["order"] = order metadata = super().__new__( - cls, defn, dims, points_dtype, bounds_dtype, kwargs + mcs, defn, dims, points_dtype, bounds_dtype, kwargs ) return metadata @@ -194,7 +194,7 @@ def __new__(cls, ancil, dims): The new class instance. """ - defn = ancil._as_defn() + defn = ancil.metadata metadata = super().__new__(cls, defn, dims) return metadata @@ -403,11 +403,11 @@ def __init__(self, cube): axes = dict(T=0, Z=1, Y=2, X=3) # Coordinate sort function - by guessed coordinate axis, then - # by coordinate definition, then by dimensions, in ascending order. + # by coordinate name, then by dimensions, in ascending order. def key_func(coord): return ( axes.get(guess_coord_axis(coord), len(axes) + 1), - coord._as_defn(), + coord.name(), cube.coord_dims(coord), ) @@ -422,7 +422,7 @@ def key_func(coord): self.scalar_coords.append(coord) def meta_key_func(dm): - return (dm._as_defn(), dm.cube_dims(cube)) + return (dm.metadata, dm.cube_dims(cube)) for cm in sorted(cube.cell_measures(), key=meta_key_func): dims = cube.cell_measure_dims(cm) @@ -990,6 +990,9 @@ def _build_aux_coordinates(self): points, bounds=bnds, **kwargs ) except ValueError: + # Ensure to remove the "circular" kwarg, which may be + # present in the defn of a DimCoord being demoted. + _ = kwargs.pop("circular", None) coord = iris.coords.AuxCoord( points, bounds=bnds, **kwargs ) diff --git a/lib/iris/_constraints.py b/lib/iris/_constraints.py index 4746425bb3..0f6a8ab6c6 100644 --- a/lib/iris/_constraints.py +++ b/lib/iris/_constraints.py @@ -131,7 +131,7 @@ def _coordless_match(self, cube): if self._name: # Require to also check against cube.name() for the fallback # "unknown" default case, when there is no name metadata available. - match = self._name in cube.names or self._name == cube.name() + match = self._name in cube._names or self._name == cube.name() if match and self._cube_func: match = self._cube_func(cube) return match diff --git a/lib/iris/_merge.py b/lib/iris/_merge.py index 9ea07e54b2..ed6dd784f2 100644 --- a/lib/iris/_merge.py +++ b/lib/iris/_merge.py @@ -22,8 +22,9 @@ is_lazy_data, multidim_lazy_stack, ) -import iris.cube import iris.coords +from iris.common import CoordMetadata, CubeMetadata +import iris.cube import iris.exceptions import iris.util @@ -115,7 +116,7 @@ class _ScalarCoordPayload( Args: * defns: - A list of scalar coordinate definitions :class:`iris.coords.CoordDefn` + A list of scalar coordinate metadata :class:`iris.common.CoordMetadata` belonging to a :class:`iris.cube.Cube`. * values: @@ -1478,9 +1479,7 @@ def axis_and_name(name): ) else: bounds = None - kwargs = dict( - zip(iris.coords.CoordDefn._fields, defns[name]) - ) + kwargs = dict(zip(CoordMetadata._fields, defns[name])) kwargs.update(metadata[name].kwargs) def name_in_independents(): @@ -1560,7 +1559,7 @@ def name_in_independents(): if bounds is not None: bounds[index] = name_value.bound - kwargs = dict(zip(iris.coords.CoordDefn._fields, defns[name])) + kwargs = dict(zip(CoordMetadata._fields, defns[name])) self._aux_templates.append( _Template(dims, points, bounds, kwargs) ) @@ -1594,7 +1593,7 @@ def _get_cube(self, data): (deepcopy(coord), dims) for coord, dims in self._aux_coords_and_dims ] - kwargs = dict(zip(iris.cube.CubeMetadata._fields, signature.defn)) + kwargs = dict(zip(CubeMetadata._fields, signature.defn)) cms_and_dims = [ (deepcopy(cm), dims) for cm, dims in self._cell_measures_and_dims @@ -1794,7 +1793,7 @@ def _extract_coord_payload(self, cube): # Coordinate sort function. # NB. This makes use of two properties which don't end up in - # the CoordDefn used by scalar_defns: `coord.points.dtype` and + # the metadata used by scalar_defns: `coord.points.dtype` and # `type(coord)`. def key_func(coord): points_dtype = coord.dtype @@ -1805,14 +1804,14 @@ def key_func(coord): axis_dict.get( iris.util.guess_coord_axis(coord), len(axis_dict) + 1 ), - coord._as_defn(), + coord.metadata, ) # Order the coordinates by hints, axis, and definition. for coord in sorted(coords, key=key_func): if not cube.coord_dims(coord) and coord.shape == (1,): # Extract the scalar coordinate data and metadata. - scalar_defns.append(coord._as_defn()) + scalar_defns.append(coord.metadata) # Because we know there's a single Cell in the # coordinate, it's quicker to roll our own than use # Coord.cell(). @@ -1844,14 +1843,14 @@ def key_func(coord): factory_defns = [] for factory in sorted( - cube.aux_factories, key=lambda factory: factory._as_defn() + cube.aux_factories, key=lambda factory: factory.metadata ): dependency_defns = [] dependencies = factory.dependencies for key in sorted(dependencies): coord = dependencies[key] if coord is not None: - dependency_defns.append((key, coord._as_defn())) + dependency_defns.append((key, coord.metadata)) factory_defn = _FactoryDefn(type(factory), dependency_defns) factory_defns.append(factory_defn) diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 0d4d3bfdab..12560cefda 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -319,7 +319,7 @@ def _dimensional_metadata_comparison(*cubes, object_get=None): eq = ( other_coord is coord or other_coord.name() == coord.name() - and other_coord._as_defn() == coord._as_defn() + and other_coord.metadata == coord.metadata ) if eq: coord_to_add_to_group = other_coord diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py index 0de97b02f3..3a38b3b283 100644 --- a/lib/iris/analysis/maths.py +++ b/lib/iris/analysis/maths.py @@ -10,22 +10,27 @@ from functools import lru_cache import inspect +import logging import math import operator import warnings import cf_units +import dask.array as da import numpy as np from numpy import ma import iris.analysis +from iris.common import SERVICES, Resolve +from iris.common.lenient import _lenient_client import iris.coords import iris.cube import iris.exceptions import iris.util -import dask.array as da -from dask.array.core import broadcast_shapes + +# Configure the logger. +logger = logging.getLogger(__name__) @lru_cache(maxsize=128, typed=True) @@ -115,7 +120,9 @@ def abs(cube, in_place=False): _assert_is_cube(cube) new_dtype = _output_dtype(np.abs, cube.dtype, in_place=in_place) op = da.absolute if cube.has_lazy_data() else np.abs - return _math_op_common(cube, op, cube.units, new_dtype, in_place=in_place) + return _math_op_common( + cube, op, cube.units, new_dtype=new_dtype, in_place=in_place + ) def intersection_of_cubes(cube, other_cube): @@ -179,43 +186,7 @@ def _assert_is_cube(cube): ) -def _assert_compatible(cube, other): - """ - Checks to see if cube.data and another array can be broadcast to - the same shape. - - """ - try: - new_shape = broadcast_shapes(cube.shape, other.shape) - except ValueError as err: - # re-raise - raise ValueError( - "The array was not broadcastable to the cube's data " - "shape. The error message when " - "broadcasting:\n{}\nThe cube's shape was {} and the " - "array's shape was {}".format(err, cube.shape, other.shape) - ) - - if cube.shape != new_shape: - raise ValueError( - "The array operation would increase the size or " - "dimensionality of the cube. The new cube's data " - "would have had to become: {}".format(new_shape) - ) - - -def _assert_matching_units(cube, other, operation_name): - """ - Check that the units of the cube and the other item are the same, or if - the other does not have a unit, skip this test - """ - if cube.units != getattr(other, "units", cube.units): - msg = "Cannot use {!r} with differing units ({} & {})".format( - operation_name, cube.units, other.units - ) - raise iris.exceptions.NotYetImplementedError(msg) - - +@_lenient_client(services=SERVICES) def add(cube, other, dim=None, in_place=False): """ Calculate the sum of two cubes, or the sum of a cube and a @@ -249,7 +220,10 @@ def add(cube, other, dim=None, in_place=False): """ _assert_is_cube(cube) new_dtype = _output_dtype( - operator.add, cube.dtype, _get_dtype(other), in_place=in_place + operator.add, + cube.dtype, + second_dtype=_get_dtype(other), + in_place=in_place, ) if in_place: _inplace_common_checks(cube, other, "addition") @@ -261,6 +235,7 @@ def add(cube, other, dim=None, in_place=False): ) +@_lenient_client(services=SERVICES) def subtract(cube, other, dim=None, in_place=False): """ Calculate the difference between two cubes, or the difference between @@ -294,7 +269,10 @@ def subtract(cube, other, dim=None, in_place=False): """ _assert_is_cube(cube) new_dtype = _output_dtype( - operator.sub, cube.dtype, _get_dtype(other), in_place=in_place + operator.sub, + cube.dtype, + second_dtype=_get_dtype(other), + in_place=in_place, ) if in_place: _inplace_common_checks(cube, other, "subtraction") @@ -335,30 +313,15 @@ def _add_subtract_common( """ _assert_is_cube(cube) - _assert_matching_units(cube, other, operation_name) - - if isinstance(other, iris.cube.Cube): - # get a coordinate comparison of this cube and the cube to do the - # operation with - coord_comp = iris.analysis._dimensional_metadata_comparison( - cube, other - ) - bad_coord_grps = ( - coord_comp["ungroupable_and_dimensioned"] - + coord_comp["resamplable"] + if cube.units != getattr(other, "units", cube.units): + emsg = ( + f"Cannot use {operation_name!r} with differing units " + f"({cube.units} & {other.units})" ) - if bad_coord_grps: - raise ValueError( - "This operation cannot be performed as there are " - "differing coordinates (%s) remaining " - "which cannot be ignored." - % ", ".join({coord_grp.name() for coord_grp in bad_coord_grps}) - ) - else: - coord_comp = None + raise iris.exceptions.NotYetImplementedError(emsg) - new_cube = _binary_op_common( + result = _binary_op_common( operation_function, operation_name, cube, @@ -369,17 +332,10 @@ def _add_subtract_common( in_place=in_place, ) - if coord_comp: - # If a coordinate is to be ignored - remove it - ignore = filter( - None, [coord_grp[0] for coord_grp in coord_comp["ignorable"]] - ) - for coord in ignore: - new_cube.remove_coord(coord) - - return new_cube + return result +@_lenient_client(services=SERVICES) def multiply(cube, other, dim=None, in_place=False): """ Calculate the product of a cube and another cube or coordinate. @@ -403,38 +359,23 @@ def multiply(cube, other, dim=None, in_place=False): """ _assert_is_cube(cube) + new_dtype = _output_dtype( - operator.mul, cube.dtype, _get_dtype(other), in_place=in_place + operator.mul, + cube.dtype, + second_dtype=_get_dtype(other), + in_place=in_place, ) other_unit = getattr(other, "units", "1") new_unit = cube.units * other_unit + if in_place: _inplace_common_checks(cube, other, "multiplication") op = operator.imul else: op = operator.mul - if isinstance(other, iris.cube.Cube): - # get a coordinate comparison of this cube and the cube to do the - # operation with - coord_comp = iris.analysis._dimensional_metadata_comparison( - cube, other - ) - bad_coord_grps = ( - coord_comp["ungroupable_and_dimensioned"] - + coord_comp["resamplable"] - ) - if bad_coord_grps: - raise ValueError( - "This operation cannot be performed as there are " - "differing coordinates (%s) remaining " - "which cannot be ignored." - % ", ".join({coord_grp.name() for coord_grp in bad_coord_grps}) - ) - else: - coord_comp = None - - new_cube = _binary_op_common( + result = _binary_op_common( op, "multiply", cube, @@ -445,15 +386,7 @@ def multiply(cube, other, dim=None, in_place=False): in_place=in_place, ) - if coord_comp: - # If a coordinate is to be ignored - remove it - ignore = filter( - None, [coord_grp[0] for coord_grp in coord_comp["ignorable"]] - ) - for coord in ignore: - new_cube.remove_coord(coord) - - return new_cube + return result def _inplace_common_checks(cube, other, math_op): @@ -475,6 +408,7 @@ def _inplace_common_checks(cube, other, math_op): ) +@_lenient_client(services=SERVICES) def divide(cube, other, dim=None, in_place=False): """ Calculate the division of a cube by a cube or coordinate. @@ -498,44 +432,29 @@ def divide(cube, other, dim=None, in_place=False): """ _assert_is_cube(cube) + new_dtype = _output_dtype( - operator.truediv, cube.dtype, _get_dtype(other), in_place=in_place + operator.truediv, + cube.dtype, + second_dtype=_get_dtype(other), + in_place=in_place, ) other_unit = getattr(other, "units", "1") new_unit = cube.units / other_unit + if in_place: if cube.dtype.kind in "iu": # Cannot coerce float result from inplace division back to int. - aemsg = ( - "Cannot perform inplace division of cube {!r} " + emsg = ( + f"Cannot perform inplace division of cube {cube.name()!r} " "with integer data." ) - raise ArithmeticError(aemsg) + raise ArithmeticError(emsg) op = operator.itruediv else: op = operator.truediv - if isinstance(other, iris.cube.Cube): - # get a coordinate comparison of this cube and the cube to do the - # operation with - coord_comp = iris.analysis._dimensional_metadata_comparison( - cube, other - ) - bad_coord_grps = ( - coord_comp["ungroupable_and_dimensioned"] - + coord_comp["resamplable"] - ) - if bad_coord_grps: - raise ValueError( - "This operation cannot be performed as there are " - "differing coordinates (%s) remaining " - "which cannot be ignored." - % ", ".join({coord_grp.name() for coord_grp in bad_coord_grps}) - ) - else: - coord_comp = None - - new_cube = _binary_op_common( + result = _binary_op_common( op, "divide", cube, @@ -546,15 +465,7 @@ def divide(cube, other, dim=None, in_place=False): in_place=in_place, ) - if coord_comp: - # If a coordinate is to be ignored - remove it - ignore = filter( - None, [coord_grp[0] for coord_grp in coord_comp["ignorable"]] - ) - for coord in ignore: - new_cube.remove_coord(coord) - - return new_cube + return result def exponentiate(cube, exponent, in_place=False): @@ -585,7 +496,10 @@ def exponentiate(cube, exponent, in_place=False): """ _assert_is_cube(cube) new_dtype = _output_dtype( - operator.pow, cube.dtype, _get_dtype(exponent), in_place=in_place + operator.pow, + cube.dtype, + second_dtype=_get_dtype(exponent), + in_place=in_place, ) if cube.has_lazy_data(): @@ -598,7 +512,11 @@ def power(data, out=None): return np.power(data, exponent, out) return _math_op_common( - cube, power, cube.units ** exponent, new_dtype, in_place=in_place + cube, + power, + cube.units ** exponent, + new_dtype=new_dtype, + in_place=in_place, ) @@ -628,7 +546,7 @@ def exp(cube, in_place=False): new_dtype = _output_dtype(np.exp, cube.dtype, in_place=in_place) op = da.exp if cube.has_lazy_data() else np.exp return _math_op_common( - cube, op, cf_units.Unit("1"), new_dtype, in_place=in_place + cube, op, cf_units.Unit("1"), new_dtype=new_dtype, in_place=in_place ) @@ -654,7 +572,11 @@ def log(cube, in_place=False): new_dtype = _output_dtype(np.log, cube.dtype, in_place=in_place) op = da.log if cube.has_lazy_data() else np.log return _math_op_common( - cube, op, cube.units.log(math.e), new_dtype, in_place=in_place + cube, + op, + cube.units.log(math.e), + new_dtype=new_dtype, + in_place=in_place, ) @@ -680,7 +602,7 @@ def log2(cube, in_place=False): new_dtype = _output_dtype(np.log2, cube.dtype, in_place=in_place) op = da.log2 if cube.has_lazy_data() else np.log2 return _math_op_common( - cube, op, cube.units.log(2), new_dtype, in_place=in_place + cube, op, cube.units.log(2), new_dtype=new_dtype, in_place=in_place ) @@ -706,12 +628,12 @@ def log10(cube, in_place=False): new_dtype = _output_dtype(np.log10, cube.dtype, in_place=in_place) op = da.log10 if cube.has_lazy_data() else np.log10 return _math_op_common( - cube, op, cube.units.log(10), new_dtype, in_place=in_place + cube, op, cube.units.log(10), new_dtype=new_dtype, in_place=in_place ) def apply_ufunc( - ufunc, cube, other_cube=None, new_unit=None, new_name=None, in_place=False + ufunc, cube, other=None, new_unit=None, new_name=None, in_place=False ): """ Apply a `numpy universal function @@ -735,7 +657,7 @@ def apply_ufunc( Kwargs: - * other_cube: + * other: An instance of :class:`iris.cube.Cube` to be given as the second argument to :func:`numpy.ufunc`. @@ -758,51 +680,59 @@ def apply_ufunc( """ if not isinstance(ufunc, np.ufunc): - name = getattr(ufunc, "__name__", "function passed to apply_ufunc") - - raise TypeError( - "{} is not recognised (it is not an instance of " - "numpy.ufunc)".format(name) + ufunc_name = getattr( + ufunc, "__name__", "function passed to apply_ufunc" ) + emsg = f"{ufunc_name} is not recognised, it is not an instance of numpy.ufunc" + raise TypeError(emsg) + + ufunc_name = ufunc.__name__ if ufunc.nout != 1: - raise ValueError( - "{} returns {} objects, apply_ufunc currently " - "only supports ufunc functions returning a single " - "object.".format(ufunc.__name__, ufunc.nout) + emsg = ( + f"{ufunc_name} returns {ufunc.nout} objects, apply_ufunc currently " + "only supports numpy.ufunc functions returning a single object." ) + raise ValueError(emsg) - if ufunc.nin == 2: - if other_cube is None: - raise ValueError( - "{} requires two arguments, so other_cube " - "must also be passed to apply_ufunc".format(ufunc.__name__) + if ufunc.nin == 1: + if other is not None: + dmsg = ( + "ignoring surplus 'other' argument to apply_ufunc, " + f"provided ufunc {ufunc_name!r} only requires 1 input" ) + logger.debug(dmsg) - _assert_is_cube(other_cube) + new_dtype = _output_dtype(ufunc, cube.dtype, in_place=in_place) + + new_cube = _math_op_common( + cube, ufunc, new_unit, new_dtype=new_dtype, in_place=in_place + ) + elif ufunc.nin == 2: + if other is None: + emsg = ( + f"{ufunc_name} requires two arguments, another cube " + "must also be passed to apply_ufunc." + ) + raise ValueError(emsg) + + _assert_is_cube(other) new_dtype = _output_dtype( - ufunc, cube.dtype, other_cube.dtype, in_place=in_place + ufunc, cube.dtype, second_dtype=other.dtype, in_place=in_place ) new_cube = _binary_op_common( ufunc, - ufunc.__name__, + ufunc_name, cube, - other_cube, + other, new_unit, new_dtype=new_dtype, in_place=in_place, ) - - elif ufunc.nin == 1: - new_dtype = _output_dtype(ufunc, cube.dtype, in_place=in_place) - - new_cube = _math_op_common( - cube, ufunc, new_unit, new_dtype, in_place=in_place - ) - else: - raise ValueError(ufunc.__name__ + ".nin should be 1 or 2.") + emsg = f"Provided ufunc '{ufunc_name}.nin' must be 1 or 2." + raise ValueError(emsg) new_cube.rename(new_name) @@ -838,39 +768,63 @@ def _binary_op_common( `cube` and `cube.data` """ _assert_is_cube(cube) + + # Flag to notify the _math_op_common function to simply wrap the resultant + # data of the maths operation in a cube with no metadata. + skeleton_cube = False + if isinstance(other, iris.coords.Coord): - other = _broadcast_cube_coord_data(cube, other, operation_name, dim) + # The rhs must be an array. + rhs = _broadcast_cube_coord_data(cube, other, operation_name, dim=dim) elif isinstance(other, iris.cube.Cube): - try: - broadcast_shapes(cube.shape, other.shape) - except ValueError: - other = iris.util.as_compatible_shape(other, cube) - other = other.core_data() - else: - other = np.asanyarray(other) + # Prepare to resolve the cube operands and associated coordinate + # metadata into the resultant cube. + resolver = Resolve(cube, other) + + # Get the broadcast, auto-transposed safe versions of the cube operands. + cube = resolver.lhs_cube_resolved + other = resolver.rhs_cube_resolved - # don't worry about checking for other data types (such as scalars or - # np.ndarrays) because _assert_compatible validates that they are broadcast - # compatible with cube.data - _assert_compatible(cube, other) + # Flag that it's safe to wrap the resultant data of the math operation + # in a cube with no metadata, as all of the metadata of the resultant + # cube is being managed by the resolver. + skeleton_cube = True - def unary_func(x): - ret = operation_function(x, other) - if ret is NotImplemented: - # explicitly raise the TypeError, so it gets raised even if, for + # The rhs must be an array. + rhs = other.core_data() + else: + # The rhs must be an array. + rhs = np.asanyarray(other) + + def unary_func(lhs): + data = operation_function(lhs, rhs) + if data is NotImplemented: + # Explicitly raise the TypeError, so it gets raised even if, for # example, `iris.analysis.maths.multiply(cube, other)` is called - # directly instead of `cube * other` - raise TypeError( - "cannot %s %r and %r objects" - % ( - operation_function.__name__, - type(x).__name__, - type(other).__name__, - ) + # directly instead of `cube * other`. + emsg = ( + f"Cannot {operation_function.__name__} {type(lhs).__name__!r} " + f"and {type(rhs).__name__} objects." ) - return ret + raise TypeError(emsg) + return data + + result = _math_op_common( + cube, + unary_func, + new_unit, + new_dtype=new_dtype, + in_place=in_place, + skeleton_cube=skeleton_cube, + ) - return _math_op_common(cube, unary_func, new_unit, new_dtype, in_place) + if isinstance(other, iris.cube.Cube): + # Insert the resultant data from the maths operation + # within the resolved cube. + result = resolver.cube(result.core_data(), in_place=in_place) + _sanitise_metadata(result, new_unit) + + return result def _broadcast_cube_coord_data(cube, other, operation_name, dim=None): @@ -915,26 +869,64 @@ def _broadcast_cube_coord_data(cube, other, operation_name, dim=None): return points +def _sanitise_metadata(cube, unit): + """ + As part of the maths metadata contract, clear the necessary or + unsupported metadata from the resultant cube of the maths operation. + + """ + # Clear the cube names. + cube.rename(None) + + # Clear the cube cell methods. + cube.cell_methods = None + + # Clear the cell measures. + for cm in cube.cell_measures(): + cube.remove_cell_measure(cm) + + # Clear the ancillary variables. + for av in cube.ancillary_variables(): + cube.remove_ancillary_variable(av) + + # Clear the STASH attribute, if present. + if "STASH" in cube.attributes: + del cube.attributes["STASH"] + + # Set the cube units. + cube.units = unit + + def _math_op_common( - cube, operation_function, new_unit, new_dtype=None, in_place=False + cube, + operation_function, + new_unit, + new_dtype=None, + in_place=False, + skeleton_cube=False, ): _assert_is_cube(cube) - if in_place: - new_cube = cube + if in_place and not skeleton_cube: if cube.has_lazy_data(): - new_cube.data = operation_function(cube.lazy_data()) + cube.data = operation_function(cube.lazy_data()) else: try: operation_function(cube.data, out=cube.data) except TypeError: - # Non ufunc function + # Non-ufunc function operation_function(cube.data) + new_cube = cube else: - new_cube = cube.copy(data=operation_function(cube.core_data())) + data = operation_function(cube.core_data()) + if skeleton_cube: + # Simply wrap the resultant data in a cube, as no + # cube metadata is required by the caller. + new_cube = iris.cube.Cube(data) + else: + new_cube = cube.copy(data) - # If the result of the operation is scalar and masked, we need to fix up - # the dtype + # If the result of the operation is scalar and masked, we need to fix-up the dtype. if ( new_dtype is not None and not new_cube.has_lazy_data() @@ -943,8 +935,8 @@ def _math_op_common( ): new_cube.data = ma.masked_array(0, 1, dtype=new_dtype) - iris.analysis.clear_phenomenon_identity(new_cube) - new_cube.units = new_unit + _sanitise_metadata(new_cube, new_unit) + return new_cube @@ -965,12 +957,12 @@ def __init__(self, data_func, units_func): are given as positional arguments. Should return another data array, with the same shape as the first array. - Can also have keyword arguments. + May also have keyword arguments. * units_func: - Function to calculate the unit of the resulting cube. - Should take the cube(s) as input and return + Function to calculate the units of the resulting cube. + Should take the cube/s as input and return an instance of :class:`cf_units.Unit`. Returns: @@ -1008,6 +1000,22 @@ def ws_units_func(u_cube, v_cube): cs_cube = cs_ifunc(cube, axis=1) """ + self._data_func_name = getattr( + data_func, "__name__", "data_func argument passed to IFunc" + ) + + if not callable(data_func): + emsg = f"{self._data_func_name} is not callable." + raise TypeError(emsg) + + self._unit_func_name = getattr( + units_func, "__name__", "units_func argument passed to IFunc" + ) + + if not callable(units_func): + emsg = f"{self._unit_func_name} is not callable." + raise TypeError(emsg) + if hasattr(data_func, "nin"): self.nin = data_func.nin else: @@ -1023,39 +1031,38 @@ def ws_units_func(u_cube, v_cube): self.nin = len(args) if self.nin not in [1, 2]: - msg = ( - "{} requires {} input data arrays, the IFunc class " - "currently only supports functions requiring 1 or two " - "data arrays as input." + emsg = ( + f"{self._data_func_name} requires {self.nin} input data " + "arrays, the IFunc class currently only supports functions " + "requiring 1 or 2 data arrays as input." ) - raise ValueError(msg.format(data_func.__name__, self.nin)) + raise ValueError(emsg) if hasattr(data_func, "nout"): if data_func.nout != 1: - msg = ( - "{} returns {} objects, the IFunc class currently " - "only supports functions returning a single object." - ) - raise ValueError( - msg.format(data_func.__name__, data_func.nout) + emsg = ( + f"{self._data_func_name} returns {data_func.nout} objects, " + "the IFunc class currently only supports functions " + "returning a single object." ) + raise ValueError(emsg) self.data_func = data_func - self.units_func = units_func def __repr__(self): - return "iris.analysis.maths.IFunc({}, {})".format( - self.data_func.__name__, self.units_func.__name__ + result = ( + f"iris.analysis.maths.IFunc({self._data_func_name}, " + f"{self._unit_func_name})" ) + return result def __str__(self): - return ( - "IFunc constructed from the data function {} " - "and the units function {}".format( - self.data_func.__name__, self.units_func.__name__ - ) + result = ( + f"IFunc constructed from the data function {self._data_func_name} " + f"and the units function {self._unit_func_name}" ) + return result def __call__( self, @@ -1105,11 +1112,27 @@ def wrap_data_func(*args, **kwargs): return self.data_func(*args, **kwargs_combined) - if self.nin == 2: + if self.nin == 1: + if other is not None: + dmsg = ( + "ignoring surplus 'other' argument to IFunc.__call__, " + f"provided data_func {self._data_func_name!r} only requires " + "1 input" + ) + logger.debug(dmsg) + + new_unit = self.units_func(cube) + + new_cube = _math_op_common( + cube, wrap_data_func, new_unit, in_place=in_place + ) + else: if other is None: - raise ValueError( - self.data_func.__name__ + " requires two arguments" + emsg = ( + f"{self._data_func_name} requires two arguments, another " + "cube must also be passed to IFunc.__call__." ) + raise ValueError(emsg) new_unit = self.units_func(cube, other) @@ -1123,21 +1146,6 @@ def wrap_data_func(*args, **kwargs): in_place=in_place, ) - elif self.nin == 1: - if other is not None: - raise ValueError( - self.data_func.__name__ + " requires one argument" - ) - - new_unit = self.units_func(cube) - - new_cube = _math_op_common( - cube, wrap_data_func, new_unit, in_place=in_place - ) - - else: - raise ValueError("self.nin should be 1 or 2.") - if new_name is not None: new_cube.rename(new_name) diff --git a/lib/iris/aux_factory.py b/lib/iris/aux_factory.py index 11148188fa..0cc6bf068f 100644 --- a/lib/iris/aux_factory.py +++ b/lib/iris/aux_factory.py @@ -14,7 +14,11 @@ import dask.array as da import numpy as np -from iris._cube_coord_common import CFVariableMixin +from iris.common import ( + CFVariableMixin, + CoordMetadata, + metadata_manager_factory, +) import iris.coords @@ -33,14 +37,40 @@ class AuxCoordFactory(CFVariableMixin, metaclass=ABCMeta): """ def __init__(self): + # Configure the metadata manager. + if not hasattr(self, "_metadata_manager"): + self._metadata_manager = metadata_manager_factory(CoordMetadata) + #: Descriptive name of the coordinate made by the factory self.long_name = None #: netCDF variable name for the coordinate made by the factory self.var_name = None - #: Coordinate system (if any) of the coordinate made by the factory self.coord_system = None + # See the climatological property getter. + self._metadata_manager.climatological = False + + @property + def coord_system(self): + """ + The coordinate-system (if any) of the coordinate made by the factory. + + """ + return self._metadata_manager.coord_system + + @coord_system.setter + def coord_system(self, value): + self._metadata_manager.coord_system = value + + @property + def climatological(self): + """ + Always returns False, as a factory itself can never have points/bounds + and therefore can never be climatological by definition. + + """ + return self._metadata_manager.climatological @property @abstractmethod @@ -51,20 +81,6 @@ def dependencies(self): """ - def _as_defn(self): - defn = iris.coords.CoordDefn( - self.standard_name, - self.long_name, - self.var_name, - self.units, - self.attributes, - self.coord_system, - # Slot for Coord 'climatological' property, which this - # doesn't have. - False, - ) - return defn - @abstractmethod def make_coord(self, coord_dims_func): """ @@ -372,6 +388,8 @@ def __init__(self, delta=None, sigma=None, orography=None): The coordinate providing the `orog` term. """ + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() if delta and delta.nbounds not in (0, 2): @@ -395,21 +413,24 @@ def __init__(self, delta=None, sigma=None, orography=None): self.standard_name = "altitude" if delta is None and orography is None: - raise ValueError( - "Unable to determine units: no delta or orography" - " available." + emsg = ( + "Unable to determine units: no delta or orography " + "available." ) + raise ValueError(emsg) if delta and orography and delta.units != orography.units: - raise ValueError( - "Incompatible units: delta and orography must" - " have the same units." + emsg = ( + "Incompatible units: delta and orography must have " + "the same units." ) + raise ValueError(emsg) self.units = (delta and delta.units) or orography.units if not self.units.is_convertible("m"): - raise ValueError( - "Invalid units: delta and/or orography" - " must be expressed in length units." + emsg = ( + "Invalid units: delta and/or orography must be expressed " + "in length units." ) + raise ValueError(emsg) self.attributes = {"positive": "up"} @property @@ -556,10 +577,13 @@ def __init__(self, delta=None, sigma=None, surface_air_pressure=None): The coordinate providing the `ps` term. """ + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coords meet necessary conditions. self._check_dependencies(delta, sigma, surface_air_pressure) + self.units = (delta and delta.units) or surface_air_pressure.units self.delta = delta self.sigma = sigma @@ -568,20 +592,12 @@ def __init__(self, delta=None, sigma=None, surface_air_pressure=None): self.standard_name = "air_pressure" self.attributes = {} - @property - def units(self): - if self.delta is not None: - units = self.delta.units - else: - units = self.surface_air_pressure.units - return units - @staticmethod def _check_dependencies(delta, sigma, surface_air_pressure): # Check for sufficient coordinates. if delta is None and (sigma is None or surface_air_pressure is None): msg = ( - "Unable to contruct hybrid pressure coordinate factory " + "Unable to construct hybrid pressure coordinate factory " "due to insufficient source coordinates." ) raise ValueError(msg) @@ -753,7 +769,7 @@ def __init__( zlev=None, ): """ - Creates a ocean sigma over z coordinate factory with the formula: + Creates an ocean sigma over z coordinate factory with the formula: if k < nsigma: z(n, k, j, i) = eta(n, j, i) + sigma(k) * @@ -766,10 +782,13 @@ def __init__( either `eta`, or 'sigma' and `depth` and `depth_c` coordinates. """ + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. self._check_dependencies(sigma, eta, depth, depth_c, nsigma, zlev) + self.units = zlev.units self.sigma = sigma self.eta = eta @@ -781,16 +800,12 @@ def __init__( self.standard_name = "sea_surface_height_above_reference_ellipsoid" self.attributes = {"positive": "up"} - @property - def units(self): - return self.zlev.units - @staticmethod def _check_dependencies(sigma, eta, depth, depth_c, nsigma, zlev): # Check for sufficient factory coordinates. if zlev is None: raise ValueError( - "Unable to determine units: " "no zlev coordinate available." + "Unable to determine units: no zlev coordinate available." ) if nsigma is None: raise ValueError("Missing nsigma coordinate.") @@ -1068,10 +1083,13 @@ def __init__(self, sigma=None, eta=None, depth=None): (depth(j, i) + eta(n, j, i)) """ + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. self._check_dependencies(sigma, eta, depth) + self.units = depth.units self.sigma = sigma self.eta = eta @@ -1080,10 +1098,6 @@ def __init__(self, sigma=None, eta=None, depth=None): self.standard_name = "sea_surface_height_above_reference_ellipsoid" self.attributes = {"positive": "up"} - @property - def units(self): - return self.depth.units - @staticmethod def _check_dependencies(sigma, eta, depth): # Check for sufficient factory coordinates. @@ -1252,10 +1266,13 @@ def __init__(self, s=None, c=None, eta=None, depth=None, depth_c=None): S(k,j,i) = depth_c * s(k) + (depth(j,i) - depth_c) * C(k) """ + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. self._check_dependencies(s, c, eta, depth, depth_c) + self.units = depth.units self.s = s self.c = c @@ -1266,10 +1283,6 @@ def __init__(self, s=None, c=None, eta=None, depth=None, depth_c=None): self.standard_name = "sea_surface_height_above_reference_ellipsoid" self.attributes = {"positive": "up"} - @property - def units(self): - return self.depth.units - @staticmethod def _check_dependencies(s, c, eta, depth, depth_c): # Check for sufficient factory coordinates. @@ -1476,10 +1489,13 @@ def __init__( b * [tanh(a * (s(k) + 0.5)) / (2 * tanh(0.5*a)) - 0.5] """ + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. self._check_dependencies(s, eta, depth, a, b, depth_c) + self.units = depth.units self.s = s self.eta = eta @@ -1491,10 +1507,6 @@ def __init__( self.standard_name = "sea_surface_height_above_reference_ellipsoid" self.attributes = {"positive": "up"} - @property - def units(self): - return self.depth.units - @staticmethod def _check_dependencies(s, eta, depth, a, b, depth_c): # Check for sufficient factory coordinates. @@ -1695,10 +1707,13 @@ def __init__(self, s=None, c=None, eta=None, depth=None, depth_c=None): (depth_c + depth(j,i)) """ + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(CoordMetadata) super().__init__() # Check that provided coordinates meet necessary conditions. self._check_dependencies(s, c, eta, depth, depth_c) + self.units = depth.units self.s = s self.c = c @@ -1709,10 +1724,6 @@ def __init__(self, s=None, c=None, eta=None, depth=None, depth_c=None): self.standard_name = "sea_surface_height_above_reference_ellipsoid" self.attributes = {"positive": "up"} - @property - def units(self): - return self.depth.units - @staticmethod def _check_dependencies(s, c, eta, depth, depth_c): # Check for sufficient factory coordinates. diff --git a/lib/iris/common/__init__.py b/lib/iris/common/__init__.py new file mode 100644 index 0000000000..c540d81bc0 --- /dev/null +++ b/lib/iris/common/__init__.py @@ -0,0 +1,11 @@ +# 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 +# licensing details. + + +from .lenient import * +from .metadata import * +from .mixin import * +from .resolve import * diff --git a/lib/iris/common/lenient.py b/lib/iris/common/lenient.py new file mode 100644 index 0000000000..802d854554 --- /dev/null +++ b/lib/iris/common/lenient.py @@ -0,0 +1,661 @@ +# 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 +# licensing details. + +from collections.abc import Iterable +from contextlib import contextmanager +from copy import deepcopy +from functools import wraps +from inspect import getmodule +import threading + + +__all__ = [ + "LENIENT", + "Lenient", +] + + +#: Default _Lenient services global activation state. +_LENIENT_ENABLE_DEFAULT = True + +#: Default Lenient maths feature state. +_LENIENT_MATHS_DEFAULT = True + +#: Protected _Lenient internal non-client, non-service keys. +_LENIENT_PROTECTED = ("active", "enable") + + +def _lenient_client(*dargs, services=None): + """ + Decorator that allows a client function/method to declare at runtime that + it is executing and requires lenient behaviour from a prior registered + lenient service function/method. + + This decorator supports being called with no arguments e.g., + + @_lenient_client() + def func(): + pass + + This is equivalent to using it as a simple naked decorator e.g., + + @_lenient_client + def func() + pass + + Alternatively, this decorator supports the lenient client explicitly + declaring the lenient services that it wishes to use e.g., + + @_lenient_client(services=(service1, service2, ...) + def func(): + pass + + Args: + + * dargs (tuple of callable): + A tuple containing the callable lenient client function/method to be + wrapped by the decorator. This is automatically populated by Python + through the decorator interface. No argument requires to be manually + provided. + + Kwargs: + + * services (callable or str or iterable of callable/str) + Zero or more function/methods, or equivalent fully qualified string names, of + lenient service function/methods. + + Returns: + Closure wrapped function/method. + + """ + ndargs = len(dargs) + + if ndargs: + assert ( + ndargs == 1 + ), f"Invalid lenient client arguments, expecting 1 got {ndargs}." + assert callable( + dargs[0] + ), "Invalid lenient client argument, expecting a callable." + + assert not ( + ndargs and services + ), "Invalid lenient client, got both arguments and keyword arguments." + + if ndargs: + # The decorator has been used as a simple naked decorator. + (func,) = dargs + + @wraps(func) + def lenient_client_inner_naked(*args, **kwargs): + """ + Closure wrapper function to register the wrapped function/method + as active at runtime before executing it. + + """ + with _LENIENT.context(active=_qualname(func)): + result = func(*args, **kwargs) + return result + + result = lenient_client_inner_naked + else: + # The decorator has been called with None, zero or more explicit lenient services. + if services is None: + services = () + + if isinstance(services, str) or not isinstance(services, Iterable): + services = (services,) + + def lenient_client_outer(func): + @wraps(func) + def lenient_client_inner(*args, **kwargs): + """ + Closure wrapper function to register the wrapped function/method + as active at runtime before executing it. + + """ + with _LENIENT.context(*services, active=_qualname(func)): + result = func(*args, **kwargs) + return result + + return lenient_client_inner + + result = lenient_client_outer + + return result + + +def _lenient_service(*dargs): + """ + Decorator that allows a function/method to declare that it supports lenient + behaviour as a service. + + Registration is at Python interpreter parse time. + + The decorator supports being called with no arguments e.g., + + @_lenient_service() + def func(): + pass + + This is equivalent to using it as a simple naked decorator e.g., + + @_lenient_service + def func(): + pass + + Args: + + * dargs (tuple of callable): + A tuple containing the callable lenient service function/method to be + wrapped by the decorator. This is automatically populated by Python + through the decorator interface. No argument requires to be manually + provided. + + Returns: + Closure wrapped function/method. + + """ + ndargs = len(dargs) + + if ndargs: + assert ( + ndargs == 1 + ), f"Invalid lenient service arguments, expecting 1 got {ndargs}." + assert callable( + dargs[0] + ), "Invalid lenient service argument, expecting a callable." + + if ndargs: + # The decorator has been used as a simple naked decorator. + # Thus the (single) argument is a function to be wrapped. + # We just register the argument function as a lenient service, and + # return it unchanged + (func,) = dargs + + _LENIENT.register_service(func) + + # This decorator registers 'func': the func itself is unchanged. + result = func + + else: + # The decorator has been called with no arguments. + # Return a decorator, to apply to 'func' immediately following. + def lenient_service_outer(func): + _LENIENT.register_service(func) + + # Decorator registers 'func', but func itself is unchanged. + return func + + result = lenient_service_outer + + return result + + +def _qualname(func): + """ + Return the fully qualified function/method string name. + + Args: + + * func (callable): + Callable function/method. Non-callable arguments are simply + passed through. + + .. note:: + Inherited methods will be qualified with the base class that + defines the method. + + """ + result = func + if callable(func): + module = getmodule(func) + result = f"{module.__name__}.{func.__qualname__}" + + return result + + +class Lenient(threading.local): + def __init__(self, **kwargs): + """ + A container for managing the run-time lenient features and options. + + Kwargs: + + * kwargs (dict) + Mapping of lenient key/value options to enable/disable. Note that, + only the lenient "maths" options is available, which controls + lenient/strict cube arithmetic. + + For example:: + + Lenient(maths=False) + + Note that, the values of these options are thread-specific. + + """ + # Configure the initial default lenient state. + self._init() + + if not kwargs: + # If not specified, set the default behaviour of the maths lenient feature. + kwargs = dict(maths=_LENIENT_MATHS_DEFAULT) + + # Configure the provided (or default) lenient features. + for feature, state in kwargs.items(): + self[feature] = state + + def __contains__(self, key): + return key in self.__dict__ + + def __getitem__(self, key): + if key not in self.__dict__: + cls = self.__class__.__name__ + emsg = f"Invalid {cls!r} option, got {key!r}." + raise KeyError(emsg) + return self.__dict__[key] + + def __repr__(self): + cls = self.__class__.__name__ + msg = f"{cls}(maths={self.__dict__['maths']!r})" + return msg + + def __setitem__(self, key, value): + cls = self.__class__.__name__ + + if key not in self.__dict__: + emsg = f"Invalid {cls!r} option, got {key!r}." + raise KeyError(emsg) + + if not isinstance(value, bool): + emsg = f"Invalid {cls!r} option {key!r} value, got {value!r}." + raise ValueError(emsg) + + self.__dict__[key] = value + # Toggle the (private) lenient behaviour. + _LENIENT.enable = value + + def _init(self): + """Configure the initial default lenient state.""" + # This is the only public supported lenient feature i.e., cube arithmetic + self.__dict__["maths"] = None + + @contextmanager + def context(self, **kwargs): + """ + Return a context manager which allows temporary modification of the + lenient option state within the scope of the context manager. + + On entry to the context manager, all provided keyword arguments are + applied. On exit from the context manager, the previous lenient + option state is restored. + + For example:: + with iris.common.Lenient.context(maths=False): + pass + + """ + + def configure_state(state): + for feature, value in state.items(): + self[feature] = value + + # Save the original state. + original_state = deepcopy(self.__dict__) + + # Configure the provided lenient features. + configure_state(kwargs) + + try: + yield + finally: + # Restore the original state. + self.__dict__.clear() + self._init() + configure_state(original_state) + + +############################################################################### + + +class _Lenient(threading.local): + def __init__(self, *args, **kwargs): + """ + A container for managing the run-time lenient services and client + options for pre-defined functions/methods. + + Args: + + * args (callable or str or iterable of callable/str) + A function/method or fully qualified string name of the function/method + acting as a lenient service. + + Kwargs: + + * kwargs (dict of callable/str or iterable of callable/str) + Mapping of lenient client function/method, or fully qualified string name + of the function/method, to one or more lenient service + function/methods or fully qualified string name of function/methods. + + For example:: + + _Lenient(service1, service2, client1=service1, client2=(service1, service2)) + + Note that, the values of these options are thread-specific. + + """ + # The executing lenient client at runtime. + self.__dict__["active"] = None + # The global lenient services state activation switch. + self.__dict__["enable"] = _LENIENT_ENABLE_DEFAULT + + for service in args: + self.register_service(service) + + for client, services in kwargs.items(): + self.register_client(client, services) + + def __call__(self, func): + """ + Determine whether it is valid for the function/method to provide a + lenient service at runtime to the actively executing lenient client. + + Args: + + * func (callable or str): + A function/method or fully qualified string name of the function/method. + + Returns: + Boolean. + + """ + result = False + if self.__dict__["enable"]: + service = _qualname(func) + if service in self and self.__dict__[service]: + active = self.__dict__["active"] + if active is not None and active in self: + services = self.__dict__[active] + if isinstance(services, str) or not isinstance( + services, Iterable + ): + services = (services,) + result = service in services + return result + + def __contains__(self, name): + name = _qualname(name) + return name in self.__dict__ + + def __getattr__(self, name): + if name not in self.__dict__: + cls = self.__class__.__name__ + emsg = f"Invalid {cls!r} option, got {name!r}." + raise AttributeError(emsg) + return self.__dict__[name] + + def __getitem__(self, name): + name = _qualname(name) + if name not in self.__dict__: + cls = self.__class__.__name__ + emsg = f"Invalid {cls!r} option, got {name!r}." + raise KeyError(emsg) + return self.__dict__[name] + + def __repr__(self): + cls = self.__class__.__name__ + width = len(cls) + 1 + kwargs = [ + "{}={!r}".format(name, self.__dict__[name]) + for name in sorted(self.__dict__.keys()) + ] + joiner = ",\n{}".format(" " * width) + return "{}({})".format(cls, joiner.join(kwargs)) + + def __setitem__(self, name, value): + name = _qualname(name) + cls = self.__class__.__name__ + + if name not in self.__dict__: + emsg = f"Invalid {cls!r} option, got {name!r}." + raise KeyError(emsg) + + if name == "active": + value = _qualname(value) + if not isinstance(value, str) and value is not None: + emsg = f"Invalid {cls!r} option {name!r}, expected a registered {cls!r} client, got {value!r}." + raise ValueError(emsg) + self.__dict__[name] = value + elif name == "enable": + self.enable = value + else: + if isinstance(value, str) or callable(value): + value = (value,) + if isinstance(value, Iterable): + value = tuple([_qualname(item) for item in value]) + self.__dict__[name] = value + + @contextmanager + def context(self, *args, **kwargs): + """ + Return a context manager which allows temporary modification of + the lenient option state for the active thread. + + On entry to the context manager, all provided keyword arguments are + applied. On exit from the context manager, the previous lenient option + state is restored. + + For example:: + with iris._LENIENT.context(example_lenient_flag=False): + # ... code that expects some non-lenient behaviour + + .. note:: + iris._LENIENT.example_lenient_flag does not exist and is + provided only as an example. + + """ + + def update_client(client, services): + if client in self.__dict__: + existing_services = self.__dict__[client] + else: + existing_services = () + + self.__dict__[client] = tuple(set(existing_services + services)) + + # Save the original state. + original_state = deepcopy(self.__dict__) + + # Temporarily update the state with the kwargs first. + for name, value in kwargs.items(): + self[name] = value + + # Get the active client. + active = self.__dict__["active"] + + if args: + # Update the client with the provided services. + new_services = tuple([_qualname(arg) for arg in args]) + + if active is None: + # Ensure not to use "context" as the ephemeral name + # of the context manager runtime "active" lenient client, + # as this causes a namespace clash with this method + # i.e., _Lenient.context, via _Lenient.__getattr__ + active = "__context" + self.__dict__["active"] = active + self.__dict__[active] = new_services + else: + # Append provided services to any pre-existing services of the active client. + update_client(active, new_services) + else: + # Append previous ephemeral services (for non-specific client) to the active client. + if ( + active is not None + and active != "__context" + and "__context" in self.__dict__ + ): + new_services = self.__dict__["__context"] + update_client(active, new_services) + + try: + yield + finally: + # Restore the original state. + self.__dict__.clear() + self.__dict__.update(original_state) + + @property + def enable(self): + """Return the activation state of the lenient services.""" + return self.__dict__["enable"] + + @enable.setter + def enable(self, state): + """ + Set the activate state of the lenient services. + + Setting the state to `False` disables all lenient services, and + setting the state to `True` enables all lenient services. + + Args: + + * state (bool): + Activate state for lenient services. + + """ + if not isinstance(state, bool): + cls = self.__class__.__name__ + emsg = f"Invalid {cls!r} option 'enable', expected a {type(True)!r}, got {state!r}." + raise ValueError(emsg) + self.__dict__["enable"] = state + + def register_client(self, func, services, append=False): + """ + Add the provided mapping of lenient client function/method to + required lenient service function/methods. + + Args: + + * func (callable or str): + A client function/method or fully qualified string name of the + client function/method. + + * services (callable or str or iterable of callable/str): + One or more service function/methods or fully qualified string names + of the required service function/method. + + Kwargs: + + * append (bool): + If True, append the lenient services to any pre-registered lenient + services for the provided lenient client. Default is False. + + """ + func = _qualname(func) + cls = self.__class__.__name__ + + if func in _LENIENT_PROTECTED: + emsg = ( + f"Cannot register {cls!r} client. " + f"Please rename your client to be something other than {func!r}." + ) + raise ValueError(emsg) + if isinstance(services, str) or not isinstance(services, Iterable): + services = (services,) + if not len(services): + emsg = f"Require at least one {cls!r} client service." + raise ValueError(emsg) + services = tuple([_qualname(service) for service in services]) + if append: + # The original provided service order is not significant. There is + # no requirement to preserve it, so it's safe to sort. + existing = self.__dict__[func] if func in self else () + services = tuple(sorted(set(existing) | set(services))) + self.__dict__[func] = services + + def register_service(self, func): + """ + Add the provided function/method as providing a lenient service and + activate it. + + Args: + + * func (callable or str): + A service function/method or fully qualified string name of the + service function/method. + + """ + func = _qualname(func) + if func in _LENIENT_PROTECTED: + cls = self.__class__.__name__ + emsg = ( + f"Cannot register {cls!r} service. " + f"Please rename your service to be something other than {func!r}." + ) + raise ValueError(emsg) + self.__dict__[func] = True + + def unregister_client(self, func): + """ + Remove the provided function/method as a lenient client using lenient services. + + Args: + + * func (callable or str): + A function/method of fully qualified string name of the function/method. + + """ + func = _qualname(func) + cls = self.__class__.__name__ + + if func in _LENIENT_PROTECTED: + emsg = f"Cannot unregister {cls!r} client, as {func!r} is a protected {cls!r} option." + raise ValueError(emsg) + + if func in self.__dict__: + value = self.__dict__[func] + if isinstance(value, bool): + emsg = f"Cannot unregister {cls!r} client, as {func!r} is not a valid {cls!r} client." + raise ValueError(emsg) + del self.__dict__[func] + else: + emsg = f"Cannot unregister unknown {cls!r} client {func!r}." + raise ValueError(emsg) + + def unregister_service(self, func): + """ + Remove the provided function/method as providing a lenient service. + + Args: + + * func (callable or str): + A function/method or fully qualified string name of the function/method. + + """ + func = _qualname(func) + cls = self.__class__.__name__ + + if func in _LENIENT_PROTECTED: + emsg = f"Cannot unregister {cls!r} service, as {func!r} is a protected {cls!r} option." + raise ValueError(emsg) + + if func in self.__dict__: + value = self.__dict__[func] + if not isinstance(value, bool): + emsg = f"Cannot unregister {cls!r} service, as {func!r} is not a valid {cls!r} service." + raise ValueError(emsg) + del self.__dict__[func] + else: + emsg = f"Cannot unregister unknown {cls!r} service {func!r}." + raise ValueError(emsg) + + +#: (Private) Instance that manages all Iris run-time lenient client and service options. +_LENIENT = _Lenient() + +#: (Public) Instance that manages all Iris run-time lenient features. +LENIENT = Lenient() diff --git a/lib/iris/common/metadata.py b/lib/iris/common/metadata.py new file mode 100644 index 0000000000..af097ab4ec --- /dev/null +++ b/lib/iris/common/metadata.py @@ -0,0 +1,1477 @@ +# 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 +# licensing details. + +from abc import ABCMeta +from collections import namedtuple +from collections.abc import Iterable, Mapping +from copy import deepcopy +from functools import wraps +import logging +import re + +import numpy as np +import numpy.ma as ma +from xxhash import xxh64_hexdigest + +from .lenient import _LENIENT +from .lenient import _lenient_service as lenient_service +from .lenient import _qualname as qualname + + +__all__ = [ + "SERVICES_COMBINE", + "SERVICES_DIFFERENCE", + "SERVICES_EQUAL", + "SERVICES", + "AncillaryVariableMetadata", + "BaseMetadata", + "CellMeasureMetadata", + "CoordMetadata", + "CubeMetadata", + "DimCoordMetadata", + "metadata_manager_factory", +] + + +# https://www.unidata.ucar.edu/software/netcdf/docs/netcdf_data_set_components.html#object_name +_TOKEN_PARSE = re.compile(r"""^[a-zA-Z0-9][\w\.\+\-@]*$""") + +# Configure the logger. +logger = logging.getLogger(__name__) + + +def _hexdigest(value): + """ + Return a hexidecimal string hash representation of the provided value. + + Calculates a 64-bit non-cryptographic hash of the provided value, + and returns the hexdigest string representation of the calculated hash. + + """ + # Special case: deal with numpy arrays. + if ma.isMaskedArray(value): + parts = ( + value.shape, + xxh64_hexdigest(value.data), + xxh64_hexdigest(value.mask), + ) + value = str(parts) + elif isinstance(value, np.ndarray): + parts = (value.shape, xxh64_hexdigest(value)) + value = str(parts) + + try: + # Calculate single-shot hash to avoid allocating state on the heap + result = xxh64_hexdigest(value) + except TypeError: + # xxhash expects a bytes-like object, so try hashing the + # string representation of the provided value instead, but + # also fold in the object type... + parts = (type(value), value) + result = xxh64_hexdigest(str(parts)) + + return result + + +class _NamedTupleMeta(ABCMeta): + """ + Meta-class to support the convenience of creating a namedtuple from + names/members of the metadata class hierarchy. + + """ + + def __new__(mcs, name, bases, namespace): + names = [] + + for base in bases: + if hasattr(base, "_fields"): + base_names = getattr(base, "_fields") + is_abstract = getattr( + base_names, "__isabstractmethod__", False + ) + if not is_abstract: + if (not isinstance(base_names, Iterable)) or isinstance( + base_names, str + ): + base_names = (base_names,) + names.extend(base_names) + + if "_members" in namespace and not getattr( + namespace["_members"], "__isabstractmethod__", False + ): + namespace_names = namespace["_members"] + + if (not isinstance(namespace_names, Iterable)) or isinstance( + namespace_names, str + ): + namespace_names = (namespace_names,) + + names.extend(namespace_names) + + if names: + item = namedtuple(f"{name}Namedtuple", names) + bases = list(bases) + # Influence the appropriate MRO. + bases.insert(0, item) + bases = tuple(bases) + + return super().__new__(mcs, name, bases, namespace) + + +class BaseMetadata(metaclass=_NamedTupleMeta): + """ + Container for common metadata. + + """ + + DEFAULT_NAME = "unknown" # the fall-back name for metadata identity + + _members = ( + "standard_name", + "long_name", + "var_name", + "units", + "attributes", + ) + + __slots__ = () + + @lenient_service + def __eq__(self, other): + """ + Determine whether the associated metadata members are equivalent. + + Args: + + * other (metadata): + A metadata instance of the same type. + + Returns: + Boolean. + + """ + result = NotImplemented + # Only perform equivalence with similar class instances. + if hasattr(other, "__class__") and other.__class__ is self.__class__: + if _LENIENT(self.__eq__) or _LENIENT(self.equal): + # Perform "lenient" equality. + logger.debug( + "lenient", extra=dict(cls=self.__class__.__name__) + ) + result = self._compare_lenient(other) + else: + # Perform "strict" equality. + logger.debug("strict", extra=dict(cls=self.__class__.__name__)) + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + if self._is_attributes(field, left, right): + result = self._compare_strict_attributes(left, right) + else: + result = left == right + return result + + # Note that, for strict we use "_fields" not "_members". + # The "circular" member does not participate in strict equivalence. + fields = filter( + lambda field: field != "circular", self._fields + ) + result = all([func(field) for field in fields]) + + return result + + def __lt__(self, other): + # + # Support Python2 behaviour for a "<" operation involving a + # "NoneType" operand. + # + if not isinstance(other, self.__class__): + return NotImplemented + + def _sort_key(item): + keys = [] + for field in item._fields: + if field != "attributes": + value = getattr(item, field) + keys.extend((value is not None, value)) + return tuple(keys) + + return _sort_key(self) < _sort_key(other) + + def __ne__(self, other): + result = self.__eq__(other) + if result is not NotImplemented: + result = not result + + return result + + def _api_common( + self, other, func_service, func_operation, action, lenient=None + ): + """ + Common entry-point for lenient metadata API methods. + + Args: + + * other (metadata): + A metadata instance of the same type. + + * func_service (callable): + The parent service method offering the API entry-point to the service. + + * func_operation (callable): + The parent service method that provides the actual service. + + * action (str): + The verb describing the service operation. + + Kwargs: + + * lenient (boolean): + Enable/disable the lenient service operation. The default is to automatically + detect whether this lenient service operation is enabled. + + Returns: + The result of the service operation to the parent service caller. + + """ + # Ensure that we have similar class instances. + if ( + not hasattr(other, "__class__") + or other.__class__ is not self.__class__ + ): + emsg = "Cannot {} {!r} with {!r}." + raise TypeError( + emsg.format(action, self.__class__.__name__, type(other)) + ) + + if lenient is None: + result = func_operation(other) + else: + if lenient: + # Use qualname to disassociate from the instance bounded method. + args, kwargs = (qualname(func_service),), dict() + else: + # Use qualname to guarantee that the instance bounded method + # is a hashable key. + args, kwargs = (), {qualname(func_service): False} + + with _LENIENT.context(*args, **kwargs): + result = func_operation(other) + + return result + + def _combine(self, other): + """Perform associated metadata member combination.""" + if _LENIENT(self.combine): + # Perform "lenient" combine. + logger.debug("lenient", extra=dict(cls=self.__class__.__name__)) + values = self._combine_lenient(other) + else: + # Perform "strict" combine. + logger.debug("strict", extra=dict(cls=self.__class__.__name__)) + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + if self._is_attributes(field, left, right): + result = self._combine_strict_attributes(left, right) + else: + result = left if left == right else None + return result + + # Note that, for strict we use "_fields" not "_members". + values = [func(field) for field in self._fields] + + return values + + def _combine_lenient(self, other): + """ + Perform lenient combination of metadata members. + + Args: + + * other (BaseMetadata): + The other metadata participating in the lenient combination. + + Returns: + A list of combined metadata member values. + + """ + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + result = None + if field == "units": + # Perform "strict" combination for "units". + result = left if left == right else None + elif self._is_attributes(field, left, right): + result = self._combine_lenient_attributes(left, right) + else: + if left == right: + result = left + elif left is None: + result = right + elif right is None: + result = left + return result + + # Note that, we use "_members" not "_fields". + return [func(field) for field in BaseMetadata._members] + + @staticmethod + def _combine_lenient_attributes(left, right): + """Leniently combine the dictionary members together.""" + # Copy the dictionaries. + left = deepcopy(left) + right = deepcopy(right) + # Use xxhash to perform an extremely fast non-cryptographic hash of + # each dictionary key rvalue, thus ensuring that the dictionary is + # completely hashable, as required by a set. + sleft = {(k, _hexdigest(v)) for k, v in left.items()} + sright = {(k, _hexdigest(v)) for k, v in right.items()} + # Intersection of common items. + common = sleft & sright + # Items in sleft different from sright. + dsleft = dict(sleft - sright) + # Items in sright different from sleft. + dsright = dict(sright - sleft) + # Intersection of common item keys with different values. + keys = set(dsleft.keys()) & set(dsright.keys()) + # Remove (in-place) common item keys with different values. + [dsleft.pop(key) for key in keys] + [dsright.pop(key) for key in keys] + # Now bring the result together. + result = {k: left[k] for k, _ in common} + result.update({k: left[k] for k in dsleft.keys()}) + result.update({k: right[k] for k in dsright.keys()}) + + return result + + @staticmethod + def _combine_strict_attributes(left, right): + """Perform strict combination of the dictionary members.""" + # Copy the dictionaries. + left = deepcopy(left) + right = deepcopy(right) + # Use xxhash to perform an extremely fast non-cryptographic hash of + # each dictionary key rvalue, thus ensuring that the dictionary is + # completely hashable, as required by a set. + sleft = {(k, _hexdigest(v)) for k, v in left.items()} + sright = {(k, _hexdigest(v)) for k, v in right.items()} + # Intersection of common items. + common = sleft & sright + # Now bring the result together. + result = {k: left[k] for k, _ in common} + + return result + + def _compare_lenient(self, other): + """ + Perform lenient equality of metadata members. + + Args: + + * other (BaseMetadata): + The other metadata participating in the lenient comparison. + + Returns: + Boolean. + + """ + result = False + + # Use the "name" method to leniently compare "standard_name", + # "long_name", and "var_name" in a well defined way. + if self.name() == other.name(): + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + if field == "units": + # Perform "strict" compare for "units". + result = left == right + elif self._is_attributes(field, left, right): + result = self._compare_lenient_attributes(left, right) + else: + # Perform "lenient" compare for members. + result = (left == right) or left is None or right is None + return result + + # Note that, we use "_members" not "_fields". + # Lenient equality explicitly ignores the "var_name" member. + result = all( + [ + func(field) + for field in BaseMetadata._members + if field != "var_name" + ] + ) + + return result + + @staticmethod + def _compare_lenient_attributes(left, right): + """Perform lenient compare between the dictionary members.""" + # Use xxhash to perform an extremely fast non-cryptographic hash of + # each dictionary key rvalue, thus ensuring that the dictionary is + # completely hashable, as required by a set. + sleft = {(k, _hexdigest(v)) for k, v in left.items()} + sright = {(k, _hexdigest(v)) for k, v in right.items()} + # Items in sleft different from sright. + dsleft = dict(sleft - sright) + # Items in sright different from sleft. + dsright = dict(sright - sleft) + # Intersection of common item keys with different values. + keys = set(dsleft.keys()) & set(dsright.keys()) + + return not bool(keys) + + @staticmethod + def _compare_strict_attributes(left, right): + """Perform strict compare between the dictionary members.""" + # Use xxhash to perform an extremely fast non-cryptographic hash of + # each dictionary key rvalue, thus ensuring that the dictionary is + # completely hashable, as required by a set. + sleft = {(k, _hexdigest(v)) for k, v in left.items()} + sright = {(k, _hexdigest(v)) for k, v in right.items()} + + return sleft == sright + + def _difference(self, other): + """Perform associated metadata member difference.""" + if _LENIENT(self.difference): + # Perform "lenient" difference. + logger.debug("lenient", extra=dict(cls=self.__class__.__name__)) + values = self._difference_lenient(other) + else: + # Perform "strict" difference. + logger.debug("strict", extra=dict(cls=self.__class__.__name__)) + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + if self._is_attributes(field, left, right): + result = self._difference_strict_attributes(left, right) + else: + result = None if left == right else (left, right) + return result + + # Note that, for strict we use "_fields" not "_members". + values = [func(field) for field in self._fields] + + return values + + def _difference_lenient(self, other): + """ + Perform lenient difference of metadata members. + + Args: + + * other (BaseMetadata): + The other metadata participating in the lenient difference. + + Returns: + A list of difference metadata member values. + + """ + + def func(field): + left = getattr(self, field) + right = getattr(other, field) + if field == "units": + # Perform "strict" difference for "units". + result = None if left == right else (left, right) + elif self._is_attributes(field, left, right): + result = self._difference_lenient_attributes(left, right) + else: + # Perform "lenient" difference for members. + result = ( + (left, right) + if left is not None and right is not None and left != right + else None + ) + return result + + # Note that, we use "_members" not "_fields". + return [func(field) for field in BaseMetadata._members] + + @staticmethod + def _difference_lenient_attributes(left, right): + """Perform lenient difference between the dictionary members.""" + # Use xxhash to perform an extremely fast non-cryptographic hash of + # each dictionary key rvalue, thus ensuring that the dictionary is + # completely hashable, as required by a set. + sleft = {(k, _hexdigest(v)) for k, v in left.items()} + sright = {(k, _hexdigest(v)) for k, v in right.items()} + # Items in sleft different from sright. + dsleft = dict(sleft - sright) + # Items in sright different from sleft. + dsright = dict(sright - sleft) + # Intersection of common item keys with different values. + keys = set(dsleft.keys()) & set(dsright.keys()) + # Keep (in-place) common item keys with different values. + [dsleft.pop(key) for key in list(dsleft.keys()) if key not in keys] + [dsright.pop(key) for key in list(dsright.keys()) if key not in keys] + + if not bool(dsleft) and not bool(dsright): + result = None + else: + # Replace hash-rvalue with original rvalue. + dsleft = {k: left[k] for k in dsleft.keys()} + dsright = {k: right[k] for k in dsright.keys()} + result = (dsleft, dsright) + + return result + + @staticmethod + def _difference_strict_attributes(left, right): + """Perform strict difference between the dictionary members.""" + # Use xxhash to perform an extremely fast non-cryptographic hash of + # each dictionary key rvalue, thus ensuring that the dictionary is + # completely hashable, as required by a set. + sleft = {(k, _hexdigest(v)) for k, v in left.items()} + sright = {(k, _hexdigest(v)) for k, v in right.items()} + # Items in sleft different from sright. + dsleft = dict(sleft - sright) + # Items in sright different from sleft. + dsright = dict(sright - sleft) + + if not bool(dsleft) and not bool(dsright): + result = None + else: + # Replace hash-rvalue with original rvalue. + dsleft = {k: left[k] for k in dsleft.keys()} + dsright = {k: right[k] for k in dsright.keys()} + result = (dsleft, dsright) + + return result + + @staticmethod + def _is_attributes(field, left, right): + """Determine whether we have two 'attributes' dictionaries.""" + return ( + field == "attributes" + and isinstance(left, Mapping) + and isinstance(right, Mapping) + ) + + @lenient_service + def combine(self, other, lenient=None): + """ + Return a new metadata instance created by combining each of the + associated metadata members. + + Args: + + * other (metadata): + A metadata instance of the same type. + + Kwargs: + + * lenient (boolean): + Enable/disable lenient combination. The default is to automatically + detect whether this lenient operation is enabled. + + Returns: + Metadata instance. + + """ + result = self._api_common( + other, self.combine, self._combine, "combine", lenient=lenient + ) + return self.__class__(*result) + + @lenient_service + def difference(self, other, lenient=None): + """ + Return a new metadata instance created by performing a difference + comparison between each of the associated metadata members. + + A metadata member returned with a value of "None" indicates that there + is no difference between the members being compared. Otherwise, a tuple + of the different values is returned. + + Args: + + * other (metadata): + A metadata instance of the same type. + + Kwargs: + + * lenient (boolean): + Enable/disable lenient difference. The default is to automatically + detect whether this lenient operation is enabled. + + Returns: + Metadata instance of member differences or None. + + """ + result = self._api_common( + other, self.difference, self._difference, "differ", lenient=lenient + ) + result = ( + None + if all([item is None for item in result]) + else self.__class__(*result) + ) + return result + + @lenient_service + def equal(self, other, lenient=None): + """ + Determine whether the associated metadata members are equivalent. + + Args: + + * other (metadata): + A metadata instance of the same type. + + Kwargs: + + * lenient (boolean): + Enable/disable lenient equivalence. The default is to automatically + detect whether this lenient operation is enabled. + + Returns: + Boolean. + + """ + result = self._api_common( + other, self.equal, self.__eq__, "compare", lenient=lenient + ) + return result + + @classmethod + def from_metadata(cls, other): + result = None + if isinstance(other, BaseMetadata): + if other.__class__ is cls: + result = other + else: + kwargs = {field: None for field in cls._fields} + fields = set(cls._fields) & set(other._fields) + for field in fields: + kwargs[field] = getattr(other, field) + result = cls(**kwargs) + return result + + def name(self, default=None, token=False): + """ + Returns a string name representing the identity of the metadata. + + First it tries standard name, then it tries the long name, then + the NetCDF variable name, before falling-back to a default value, + which itself defaults to the string 'unknown'. + + Kwargs: + + * default: + The fall-back string representing the default name. Defaults to + the string 'unknown'. + * token: + If True, ensures that the name returned satisfies the criteria for + the characters required by a valid NetCDF name. If it is not + possible to return a valid name, then a ValueError exception is + raised. Defaults to False. + + Returns: + String. + + """ + + def _check(item): + return self.token(item) if token else item + + default = self.DEFAULT_NAME if default is None else default + + result = ( + _check(self.standard_name) + or _check(self.long_name) + or _check(self.var_name) + or _check(default) + ) + + if token and result is None: + emsg = "Cannot retrieve a valid name token from {!r}" + raise ValueError(emsg.format(self)) + + return result + + @classmethod + def token(cls, name): + """ + Determine whether the provided name is a valid NetCDF name and thus + safe to represent a single parsable token. + + Args: + + * name: + The string name to verify + + Returns: + The provided name if valid, otherwise None. + + """ + if name is not None: + result = _TOKEN_PARSE.match(name) + name = result if result is None else name + + return name + + +class AncillaryVariableMetadata(BaseMetadata): + """ + Metadata container for a :class:`~iris.coords.AncillaryVariableMetadata`. + + """ + + __slots__ = () + + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + + +class CellMeasureMetadata(BaseMetadata): + """ + Metadata container for a :class:`~iris.coords.CellMeasure`. + + """ + + _members = "measure" + + __slots__ = () + + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) + + def _combine_lenient(self, other): + """ + Perform lenient combination of metadata members for cell measures. + + Args: + + * other (CellMeasureMetadata): + The other cell measure metadata participating in the lenient + combination. + + Returns: + A list of combined metadata member values. + + """ + # Perform "strict" combination for "measure". + value = self.measure if self.measure == other.measure else None + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.append(value) + + return result + + def _compare_lenient(self, other): + """ + Perform lenient equality of metadata members for cell measures. + + Args: + + * other (CellMeasureMetadata): + The other cell measure metadata participating in the lenient + comparison. + + Returns: + Boolean. + + """ + # Perform "strict" comparison for "measure". + result = self.measure == other.measure + if result: + # Perform lenient comparison of the other parent members. + result = super()._compare_lenient(other) + + return result + + def _difference_lenient(self, other): + """ + Perform lenient difference of metadata members for cell measures. + + Args: + + * other (CellMeasureMetadata): + The other cell measure metadata participating in the lenient + difference. + + Returns: + A list of difference metadata member values. + + """ + # Perform "strict" difference for "measure". + value = ( + None + if self.measure == other.measure + else (self.measure, other.measure) + ) + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.append(value) + + return result + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + + +class CoordMetadata(BaseMetadata): + """ + Metadata container for a :class:`~iris.coords.Coord`. + + """ + + _members = ("coord_system", "climatological") + + __slots__ = () + + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + # Convert a DimCoordMetadata instance to a CoordMetadata instance. + if ( + self.__class__ is CoordMetadata + and hasattr(other, "__class__") + and other.__class__ is DimCoordMetadata + ): + other = self.from_metadata(other) + return super().__eq__(other) + + def __lt__(self, other): + # + # Support Python2 behaviour for a "<" operation involving a + # "NoneType" operand. + # + if not isinstance(other, BaseMetadata): + return NotImplemented + + if other.__class__ is DimCoordMetadata: + other = self.from_metadata(other) + + if not isinstance(other, self.__class__): + return NotImplemented + + def _sort_key(item): + keys = [] + for field in item._fields: + if field not in ("attributes", "coord_system"): + value = getattr(item, field) + keys.extend((value is not None, value)) + return tuple(keys) + + return _sort_key(self) < _sort_key(other) + + def _combine_lenient(self, other): + """ + Perform lenient combination of metadata members for coordinates. + + Args: + + * other (CoordMetadata): + The other coordinate metadata participating in the lenient + combination. + + Returns: + A list of combined metadata member values. + + """ + # Perform "strict" combination for "coord_system" and "climatological". + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return left if left == right else None + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in CoordMetadata._members] + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.extend(values) + + return result + + def _compare_lenient(self, other): + """ + Perform lenient equality of metadata members for coordinates. + + Args: + + * other (CoordMetadata): + The other coordinate metadata participating in the lenient + comparison. + + Returns: + Boolean. + + """ + # Perform "strict" comparison for "coord_system" and "climatological". + result = all( + [ + getattr(self, field) == getattr(other, field) + for field in CoordMetadata._members + ] + ) + if result: + # Perform lenient comparison of the other parent members. + result = super()._compare_lenient(other) + + return result + + def _difference_lenient(self, other): + """ + Perform lenient difference of metadata members for coordinates. + + Args: + + * other (CoordMetadata): + The other coordinate metadata participating in the lenient + difference. + + Returns: + A list of difference metadata member values. + + """ + # Perform "strict" difference for "coord_system" and "climatological". + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return None if left == right else (left, right) + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in CoordMetadata._members] + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.extend(values) + + return result + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + # Convert a DimCoordMetadata instance to a CoordMetadata instance. + if ( + self.__class__ is CoordMetadata + and hasattr(other, "__class__") + and other.__class__ is DimCoordMetadata + ): + other = self.from_metadata(other) + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + # Convert a DimCoordMetadata instance to a CoordMetadata instance. + if ( + self.__class__ is CoordMetadata + and hasattr(other, "__class__") + and other.__class__ is DimCoordMetadata + ): + other = self.from_metadata(other) + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + # Convert a DimCoordMetadata instance to a CoordMetadata instance. + if ( + self.__class__ is CoordMetadata + and hasattr(other, "__class__") + and other.__class__ is DimCoordMetadata + ): + other = self.from_metadata(other) + return super().equal(other, lenient=lenient) + + +class CubeMetadata(BaseMetadata): + """ + Metadata container for a :class:`~iris.cube.Cube`. + + """ + + _members = "cell_methods" + + __slots__ = () + + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) + + def __lt__(self, other): + # + # Support Python2 behaviour for a "<" operation involving a + # "NoneType" operand. + # + if not isinstance(other, self.__class__): + return NotImplemented + + def _sort_key(item): + keys = [] + for field in item._fields: + if field not in ("attributes", "cell_methods"): + value = getattr(item, field) + keys.extend((value is not None, value)) + return tuple(keys) + + return _sort_key(self) < _sort_key(other) + + def _combine_lenient(self, other): + """ + Perform lenient combination of metadata members for cubes. + + Args: + + * other (CubeMetadata): + The other cube metadata participating in the lenient combination. + + Returns: + A list of combined metadata member values. + + """ + # Perform "strict" combination for "cell_methods". + value = ( + self.cell_methods + if self.cell_methods == other.cell_methods + else None + ) + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.append(value) + + return result + + def _compare_lenient(self, other): + """ + Perform lenient equality of metadata members for cubes. + + Args: + + * other (CubeMetadata): + The other cube metadata participating in the lenient comparison. + + Returns: + Boolean. + + """ + # Perform "strict" comparison for "cell_methods". + result = self.cell_methods == other.cell_methods + if result: + result = super()._compare_lenient(other) + + return result + + def _difference_lenient(self, other): + """ + Perform lenient difference of metadata members for cubes. + + Args: + + * other (CubeMetadata): + The other cube metadata participating in the lenient difference. + + Returns: + A list of difference metadata member values. + + """ + # Perform "strict" difference for "cell_methods". + value = ( + None + if self.cell_methods == other.cell_methods + else (self.cell_methods, other.cell_methods) + ) + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.append(value) + + return result + + @property + def _names(self): + """ + A tuple containing the value of each name participating in the identity + of a :class:`iris.cube.Cube`. This includes the standard name, + long name, NetCDF variable name, and the STASH from the attributes + dictionary. + + """ + standard_name = self.standard_name + long_name = self.long_name + var_name = self.var_name + + # Defensive enforcement of attributes being a dictionary. + if not isinstance(self.attributes, Mapping): + try: + self.attributes = dict() + except AttributeError: + emsg = "Invalid '{}.attributes' member, must be a mapping." + raise AttributeError(emsg.format(self.__class__.__name__)) + + stash_name = self.attributes.get("STASH") + if stash_name is not None: + stash_name = str(stash_name) + + return standard_name, long_name, var_name, stash_name + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + + @wraps(BaseMetadata.name) + def name(self, default=None, token=False): + def _check(item): + return self.token(item) if token else item + + default = self.DEFAULT_NAME if default is None else default + + # Defensive enforcement of attributes being a dictionary. + if not isinstance(self.attributes, Mapping): + try: + self.attributes = dict() + except AttributeError: + emsg = "Invalid '{}.attributes' member, must be a mapping." + raise AttributeError(emsg.format(self.__class__.__name__)) + + result = ( + _check(self.standard_name) + or _check(self.long_name) + or _check(self.var_name) + or _check(str(self.attributes.get("STASH", ""))) + or _check(default) + ) + + if token and result is None: + emsg = "Cannot retrieve a valid name token from {!r}" + raise ValueError(emsg.format(self)) + + return result + + +class DimCoordMetadata(CoordMetadata): + """ + Metadata container for a :class:`~iris.coords.DimCoord" + + """ + + # The "circular" member is stateful only, and does not participate + # in lenient/strict equivalence. + _members = ("circular",) + + __slots__ = () + + @wraps(CoordMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + # Convert a CoordMetadata instance to a DimCoordMetadata instance. + if hasattr(other, "__class__") and other.__class__ is CoordMetadata: + other = self.from_metadata(other) + return super().__eq__(other) + + def __lt__(self, other): + # + # Support Python2 behaviour for a "<" operation involving a + # "NoneType" operand. + # + if not isinstance(other, BaseMetadata): + return NotImplemented + + if other.__class__ is CoordMetadata: + other = self.from_metadata(other) + + if not isinstance(other, self.__class__): + return NotImplemented + + def _sort_key(item): + keys = [] + for field in item._fields: + if field not in ("attributes", "coord_system"): + value = getattr(item, field) + keys.extend((value is not None, value)) + return tuple(keys) + + return _sort_key(self) < _sort_key(other) + + @wraps(CoordMetadata._combine_lenient, assigned=("__doc__",), updated=()) + def _combine_lenient(self, other): + # Perform "strict" combination for "circular". + value = self.circular if self.circular == other.circular else None + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.append(value) + + return result + + @wraps(CoordMetadata._compare_lenient, assigned=("__doc__",), updated=()) + def _compare_lenient(self, other): + # The "circular" member is not part of lenient equivalence. + return super()._compare_lenient(other) + + @wraps( + CoordMetadata._difference_lenient, assigned=("__doc__",), updated=() + ) + def _difference_lenient(self, other): + # Perform "strict" difference for "circular". + value = ( + None + if self.circular == other.circular + else (self.circular, other.circular) + ) + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.append(value) + + return result + + @wraps(CoordMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + # Convert a CoordMetadata instance to a DimCoordMetadata instance. + if hasattr(other, "__class__") and other.__class__ is CoordMetadata: + other = self.from_metadata(other) + return super().combine(other, lenient=lenient) + + @wraps(CoordMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + # Convert a CoordMetadata instance to a DimCoordMetadata instance. + if hasattr(other, "__class__") and other.__class__ is CoordMetadata: + other = self.from_metadata(other) + return super().difference(other, lenient=lenient) + + @wraps(CoordMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + # Convert a CoordMetadata instance to a DimCoordMetadata instance. + if hasattr(other, "__class__") and other.__class__ is CoordMetadata: + other = self.from_metadata(other) + return super().equal(other, lenient=lenient) + + +def metadata_manager_factory(cls, **kwargs): + """ + A class instance factory function responsible for manufacturing + metadata instances dynamically at runtime. + + The factory instances returned by the factory are capable of managing + their metadata state, which can be proxied by the owning container. + + Args: + + * cls: + A subclass of :class:`~iris.common.metadata.BaseMetadata`, defining + the metadata to be managed. + + Kwargs: + + * kwargs: + Initial values for the manufactured metadata instance. Unspecified + fields will default to a value of 'None'. + + """ + + def __init__(self, cls, **kwargs): + # Restrict to only dealing with appropriate metadata classes. + if not issubclass(cls, BaseMetadata): + emsg = "Require a subclass of {!r}, got {!r}." + raise TypeError(emsg.format(BaseMetadata.__name__, cls)) + + #: The metadata class to be manufactured by this factory. + self.cls = cls + + # Initialise the metadata class fields in the instance. + for field in self.fields: + setattr(self, field, None) + + # Populate with provided kwargs, which have already been verified + # by the factory. + for field, value in kwargs.items(): + setattr(self, field, value) + + def __eq__(self, other): + if not hasattr(other, "cls"): + return NotImplemented + match = self.cls is other.cls + if match: + match = self.values == other.values + + return match + + def __getstate__(self): + """Return the instance state to be pickled.""" + return {field: getattr(self, field) for field in self.fields} + + def __ne__(self, other): + match = self.__eq__(other) + if match is not NotImplemented: + match = not match + + return match + + def __reduce__(self): + """ + Dynamically created classes at runtime cannot be pickled, due to not + being defined at the top level of a module. As a result, we require to + use the __reduce__ interface to allow 'pickle' to recreate this class + instance, and dump and load instance state successfully. + + """ + return metadata_manager_factory, (self.cls,), self.__getstate__() + + def __repr__(self): + args = ", ".join( + [ + "{}={!r}".format(field, getattr(self, field)) + for field in self.fields + ] + ) + return "{}({})".format(self.__class__.__name__, args) + + def __setstate__(self, state): + """Set the instance state when unpickling.""" + for field, value in state.items(): + setattr(self, field, value) + + @property + def fields(self): + """Return the name of the metadata members.""" + # Proxy for built-in namedtuple._fields property. + return self.cls._fields + + @property + def values(self): + fields = {field: getattr(self, field) for field in self.fields} + return self.cls(**fields) + + # Restrict factory to appropriate metadata classes only. + if not issubclass(cls, BaseMetadata): + emsg = "Require a subclass of {!r}, got {!r}." + raise TypeError(emsg.format(BaseMetadata.__name__, cls)) + + # Check whether kwargs have valid fields for the specified metadata. + if kwargs: + extra = [field for field in kwargs.keys() if field not in cls._fields] + if extra: + bad = ", ".join(map(lambda field: "{!r}".format(field), extra)) + emsg = "Invalid {!r} field parameters, got {}." + raise ValueError(emsg.format(cls.__name__, bad)) + + # Define the name, (inheritance) bases and namespace of the dynamic class. + name = "MetadataManager" + bases = () + namespace = { + "DEFAULT_NAME": cls.DEFAULT_NAME, + "__init__": __init__, + "__eq__": __eq__, + "__getstate__": __getstate__, + "__ne__": __ne__, + "__reduce__": __reduce__, + "__repr__": __repr__, + "__setstate__": __setstate__, + "fields": fields, + "name": cls.name, + "token": cls.token, + "values": values, + } + + # Account for additional "CubeMetadata" specialised class behaviour. + if cls is CubeMetadata: + namespace["_names"] = cls._names + + # Dynamically create the class. + Metadata = type(name, bases, namespace) + # Now manufacture an instance of that class. + metadata = Metadata(cls, **kwargs) + + return metadata + + +#: Convenience collection of lenient metadata combine services. +SERVICES_COMBINE = ( + AncillaryVariableMetadata.combine, + BaseMetadata.combine, + CellMeasureMetadata.combine, + CoordMetadata.combine, + CubeMetadata.combine, + DimCoordMetadata.combine, +) + + +#: Convenience collection of lenient metadata difference services. +SERVICES_DIFFERENCE = ( + AncillaryVariableMetadata.difference, + BaseMetadata.difference, + CellMeasureMetadata.difference, + CoordMetadata.difference, + CubeMetadata.difference, + DimCoordMetadata.difference, +) + + +#: Convenience collection of lenient metadata equality services. +SERVICES_EQUAL = ( + AncillaryVariableMetadata.__eq__, + AncillaryVariableMetadata.equal, + BaseMetadata.__eq__, + BaseMetadata.equal, + CellMeasureMetadata.__eq__, + CellMeasureMetadata.equal, + CoordMetadata.__eq__, + CoordMetadata.equal, + CubeMetadata.__eq__, + CubeMetadata.equal, + DimCoordMetadata.__eq__, + DimCoordMetadata.equal, +) + + +#: Convenience collection of lenient metadata services. +SERVICES = SERVICES_COMBINE + SERVICES_DIFFERENCE + SERVICES_EQUAL diff --git a/lib/iris/_cube_coord_common.py b/lib/iris/common/mixin.py similarity index 51% rename from lib/iris/_cube_coord_common.py rename to lib/iris/common/mixin.py index 541780ca15..50ef561036 100644 --- a/lib/iris/_cube_coord_common.py +++ b/lib/iris/common/mixin.py @@ -5,43 +5,20 @@ # licensing details. -from collections import namedtuple +from collections.abc import Mapping +from functools import wraps import re import cf_units +from iris.common import BaseMetadata import iris.std_names -# https://www.unidata.ucar.edu/software/netcdf/docs/netcdf_data_set_components.html#object_name -_TOKEN_PARSE = re.compile(r"""^[a-zA-Z0-9][\w\.\+\-@]*$""") +__all__ = ["CFVariableMixin"] -class Names( - namedtuple("Names", ["standard_name", "long_name", "var_name", "STASH"]) -): - """ - Immutable container for name metadata. - - Args: - - * standard_name: - A string representing the CF Conventions and Metadata standard name, or - None. - * long_name: - A string representing the CF Conventions and Metadata long name, or - None - * var_name: - A string representing the associated NetCDF variable name, or None. - * STASH: - A string representing the `~iris.fileformats.pp.STASH` code, or None. - - """ - - __slots__ = () - - -def get_valid_standard_name(name): +def _get_valid_standard_name(name): # Standard names are optionally followed by a standard name # modifier, separated by one or more blank spaces @@ -100,7 +77,7 @@ def __init__(self, *args, **kwargs): # Check validity of keys for key in self.keys(): if key in self._forbidden_keys: - raise ValueError("%r is not a permitted attribute" % key) + raise ValueError(f"{key!r} is not a permitted attribute") def __eq__(self, other): # Extend equality to allow for NumPy arrays. @@ -121,7 +98,7 @@ def __ne__(self, other): def __setitem__(self, key, value): if key in self._forbidden_keys: - raise ValueError("%r is not a permitted attribute" % key) + raise ValueError(f"{key!r} is not a permitted attribute") dict.__setitem__(self, key, value) def update(self, other, **kwargs): @@ -137,92 +114,15 @@ def update(self, other, **kwargs): # Check validity of keys for key in keys: if key in self._forbidden_keys: - raise ValueError("%r is not a permitted attribute" % key) + raise ValueError(f"{key!r} is not a permitted attribute") dict.update(self, other, **kwargs) class CFVariableMixin: - - _DEFAULT_NAME = "unknown" # the name default string - - @staticmethod - def token(name): - """ - Determine whether the provided name is a valid NetCDF name and thus - safe to represent a single parsable token. - - Args: - - * name: - The string name to verify - - Returns: - The provided name if valid, otherwise None. - - """ - if name is not None: - result = _TOKEN_PARSE.match(name) - name = result if result is None else name - return name - - def name(self, default=None, token=False): - """ - Returns a human-readable name. - - First it tries :attr:`standard_name`, then 'long_name', then - 'var_name', then the STASH attribute before falling back to - the value of `default` (which itself defaults to 'unknown'). - - Kwargs: - - * default: - The value of the default name. - * token: - If true, ensure that the name returned satisfies the criteria for - the characters required by a valid NetCDF name. If it is not - possible to return a valid name, then a ValueError exception is - raised. - - Returns: - String. - - """ - - def _check(item): - return self.token(item) if token else item - - default = self._DEFAULT_NAME if default is None else default - - result = ( - _check(self.standard_name) - or _check(self.long_name) - or _check(self.var_name) - or _check(str(self.attributes.get("STASH", ""))) - or _check(default) - ) - - if token and result is None: - emsg = "Cannot retrieve a valid name token from {!r}" - raise ValueError(emsg.format(self)) - - return result - - @property - def names(self): - """ - A tuple containing all of the metadata names. This includes the - standard name, long name, NetCDF variable name, and attributes - STASH name. - - """ - standard_name = self.standard_name - long_name = self.long_name - var_name = self.var_name - stash_name = self.attributes.get("STASH") - if stash_name is not None: - stash_name = str(stash_name) - return Names(standard_name, long_name, var_name, stash_name) + @wraps(BaseMetadata.name) + def name(self, default=None, token=None): + return self._metadata_manager.name(default=default, token=token) def rename(self, name): """ @@ -245,40 +145,99 @@ def rename(self, name): @property def standard_name(self): - """The standard name for the Cube's data.""" - return self._standard_name + """The CF Metadata standard name for the object.""" + return self._metadata_manager.standard_name @standard_name.setter def standard_name(self, name): - self._standard_name = get_valid_standard_name(name) + self._metadata_manager.standard_name = _get_valid_standard_name(name) @property - def units(self): - """The :mod:`~cf_units.Unit` instance of the object.""" - return self._units + def long_name(self): + """The CF Metadata long name for the object.""" + return self._metadata_manager.long_name - @units.setter - def units(self, unit): - self._units = cf_units.as_unit(unit) + @long_name.setter + def long_name(self, name): + self._metadata_manager.long_name = name @property def var_name(self): - """The netCDF variable name for the object.""" - return self._var_name + """The NetCDF variable name for the object.""" + return self._metadata_manager.var_name @var_name.setter def var_name(self, name): if name is not None: - result = self.token(name) + result = self._metadata_manager.token(name) if result is None or not name: emsg = "{!r} is not a valid NetCDF variable name." raise ValueError(emsg.format(name)) - self._var_name = name + self._metadata_manager.var_name = name + + @property + def units(self): + """The S.I. unit of the object.""" + return self._metadata_manager.units + + @units.setter + def units(self, unit): + self._metadata_manager.units = cf_units.as_unit(unit) @property def attributes(self): - return self._attributes + return self._metadata_manager.attributes @attributes.setter def attributes(self, attributes): - self._attributes = LimitedAttributeDict(attributes or {}) + self._metadata_manager.attributes = LimitedAttributeDict( + attributes or {} + ) + + @property + def metadata(self): + return self._metadata_manager.values + + @metadata.setter + def metadata(self, metadata): + cls = self._metadata_manager.cls + fields = self._metadata_manager.fields + arg = metadata + + try: + # Try dict-like initialisation... + metadata = cls(**metadata) + except TypeError: + try: + # Try iterator/namedtuple-like initialisation... + metadata = cls(*metadata) + except TypeError: + if hasattr(metadata, "_asdict"): + metadata = metadata._asdict() + + if isinstance(metadata, Mapping): + fields = [field for field in fields if field in metadata] + else: + # Generic iterable/container with no associated keys. + missing = [ + field + for field in fields + if not hasattr(metadata, field) + ] + + if missing: + missing = ", ".join( + map(lambda i: "{!r}".format(i), missing) + ) + emsg = "Invalid {!r} metadata, require {} to be specified." + raise TypeError(emsg.format(type(arg), missing)) + + for field in fields: + if hasattr(metadata, field): + value = getattr(metadata, field) + else: + value = metadata[field] + + # Ensure to always set state through the individual mixin/container + # setter functions. + setattr(self, field, value) diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py new file mode 100644 index 0000000000..7098eaa65e --- /dev/null +++ b/lib/iris/common/resolve.py @@ -0,0 +1,1542 @@ +# 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 +# licensing details. + +from collections import namedtuple +from collections.abc import Iterable +import logging + +from dask.array.core import broadcast_shapes +import numpy as np + +from iris.common import LENIENT + + +__all__ = ["Resolve"] + + +# Configure the logger. +logger = logging.getLogger(__name__) + + +_AuxCoverage = namedtuple( + "AuxCoverage", + [ + "cube", + "common_items_aux", + "common_items_scalar", + "local_items_aux", + "local_items_scalar", + "dims_common", + "dims_local", + "dims_free", + ], +) + +_CategoryItems = namedtuple( + "CategoryItems", ["items_dim", "items_aux", "items_scalar"], +) + +_DimCoverage = namedtuple( + "DimCoverage", + ["cube", "metadata", "coords", "dims_common", "dims_local", "dims_free"], +) + +_Item = namedtuple("Item", ["metadata", "coord", "dims"]) + +_PreparedFactory = namedtuple("PreparedFactory", ["container", "dependencies"]) + +_PreparedItem = namedtuple( + "PreparedItem", ["metadata", "points", "bounds", "dims", "container"], +) + +_PreparedMetadata = namedtuple("PreparedMetadata", ["combined", "src", "tgt"]) + + +class Resolve: + def __init__(self, lhs=None, rhs=None): + if lhs is not None or rhs is not None: + self(lhs, rhs) + + def __call__(self, lhs, rhs): + self._init(lhs, rhs) + + self._metadata_resolve() + self._metadata_coverage() + + if self._debug: + self._debug_items(self.lhs_cube_category_local, title="LHS local") + self._debug_items(self.rhs_cube_category_local, title="RHS local") + self._debug_items(self.category_common, title="common") + logger.debug(f"map_rhs_to_lhs={self.map_rhs_to_lhs}") + + self._metadata_mapping() + self._metadata_prepare() + + def _as_compatible_cubes(self): + from iris.cube import Cube + + src_cube = self._src_cube + tgt_cube = self._tgt_cube + + # Use the mapping to calculate the new src cube shape. + new_src_shape = [1] * tgt_cube.ndim + for src_dim, tgt_dim in self.mapping.items(): + new_src_shape[tgt_dim] = src_cube.shape[src_dim] + new_src_shape = tuple(new_src_shape) + dmsg = ( + f"new src {self._src_cube_position} cube shape {new_src_shape}, " + f"actual shape {src_cube.shape}" + ) + logger.debug(dmsg) + + try: + # Determine whether the tgt cube shape and proposed new src + # cube shape will successfully broadcast together. + self._broadcast_shape = broadcast_shapes( + tgt_cube.shape, new_src_shape + ) + except ValueError: + emsg = ( + "Cannot resolve cubes, as a suitable transpose of the " + f"{self._src_cube_position} cube {src_cube.name()!r} " + f"will not broadcast with the {self._tgt_cube_position} cube " + f"{tgt_cube.name()!r}." + ) + raise ValueError(emsg) + + new_src_data = src_cube.core_data().copy() + + # Use the mapping to determine the transpose sequence of + # src dimensions in increasing tgt dimension order. + order = [ + src_dim + for src_dim, tgt_dim in sorted( + self.mapping.items(), key=lambda pair: pair[1] + ) + ] + + # Determine whether a transpose of the src cube is necessary. + if order != sorted(order): + new_src_data = new_src_data.transpose(order) + logger.debug( + f"transpose src {self._src_cube_position} cube with order {order}" + ) + + # Determine whether a reshape is necessary. + if new_src_shape != new_src_data.shape: + new_src_data = new_src_data.reshape(new_src_shape) + logger.debug( + f"reshape src {self._src_cube_position} cube to new shape {new_src_shape}" + ) + + # Create the new src cube. + new_src_cube = Cube(new_src_data) + new_src_cube.metadata = src_cube.metadata + + def add_coord(coord, dim_coord=False): + src_dims = src_cube.coord_dims(coord) + tgt_dims = [self.mapping[src_dim] for src_dim in src_dims] + if dim_coord: + new_src_cube.add_dim_coord(coord, tgt_dims) + else: + new_src_cube.add_aux_coord(coord, tgt_dims) + + # Add the dim coordinates to the new src cube. + for coord in src_cube.dim_coords: + add_coord(coord, dim_coord=True) + + # Add the aux and scalar coordinates to the new src cube. + for coord in src_cube.aux_coords: + add_coord(coord) + + # Add the aux factories to the new src cube. + for factory in src_cube.aux_factories: + new_src_cube.add_aux_factory(factory) + + # Set the resolved cubes. + self._src_cube_resolved = new_src_cube + self._tgt_cube_resolved = tgt_cube + + @staticmethod + def _aux_coverage( + cube, + cube_items_aux, + cube_items_scalar, + common_aux_metadata, + common_scalar_metadata, + ): + common_items_aux = [] + common_items_scalar = [] + local_items_aux = [] + local_items_scalar = [] + dims_common = [] + dims_local = [] + dims_free = set(range(cube.ndim)) + + for item in cube_items_aux: + [dims_free.discard(dim) for dim in item.dims] + + if item.metadata in common_aux_metadata: + common_items_aux.append(item) + dims_common.extend(item.dims) + else: + local_items_aux.append(item) + dims_local.extend(item.dims) + + for item in cube_items_scalar: + if item.metadata in common_scalar_metadata: + common_items_scalar.append(item) + else: + local_items_scalar.append(item) + + return _AuxCoverage( + cube=cube, + common_items_aux=common_items_aux, + common_items_scalar=common_items_scalar, + local_items_aux=local_items_aux, + local_items_scalar=local_items_scalar, + dims_common=sorted(set(dims_common)), + dims_local=sorted(set(dims_local)), + dims_free=sorted(dims_free), + ) + + def _aux_mapping(self, src_coverage, tgt_coverage): + for tgt_item in tgt_coverage.common_items_aux: + # Search for a src aux metadata match. + tgt_metadata = tgt_item.metadata + src_items = tuple( + filter( + lambda src_item: src_item.metadata == tgt_metadata, + src_coverage.common_items_aux, + ) + ) + if src_items: + # Multiple matching src metadata must cover the same src + # dimensions. + src_dims = src_items[0].dims + if all(map(lambda item: item.dims == src_dims, src_items)): + # Ensure src and tgt have equal rank. + tgt_dims = tgt_item.dims + if len(src_dims) == len(tgt_dims): + for src_dim, tgt_dim in zip(src_dims, tgt_dims): + self.mapping[src_dim] = tgt_dim + logger.debug(f"{src_dim}->{tgt_dim}") + else: + # This situation can only occur due to a systemic internal + # failure to correctly identify common aux coordinate metadata + # coverage between the cubes. + emsg = ( + "Failed to map common aux coordinate metadata from " + "source cube {!r} to target cube {!r}, using {!r} on " + "target cube dimension{} {}." + ) + raise ValueError( + emsg.format( + src_coverage.cube.name(), + tgt_coverage.cube.name(), + tgt_metadata, + "s" if len(tgt_item.dims) > 1 else "", + tgt_item.dims, + ) + ) + + @staticmethod + def _categorise_items(cube): + category = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + + # Categorise the dim coordinates of the cube. + for coord in cube.dim_coords: + item = _Item( + metadata=coord.metadata, + coord=coord, + dims=cube.coord_dims(coord), + ) + category.items_dim.append(item) + + # Categorise the aux and scalar coordinates of the cube. + for coord in cube.aux_coords: + dims = cube.coord_dims(coord) + item = _Item(metadata=coord.metadata, coord=coord, dims=dims) + if dims: + category.items_aux.append(item) + else: + category.items_scalar.append(item) + + return category + + @staticmethod + def _create_prepared_item(coord, dims, src=None, tgt=None): + if src is not None and tgt is not None: + combined = src.combine(tgt) + else: + combined = src or tgt + if not isinstance(dims, Iterable): + dims = (dims,) + prepared_metadata = _PreparedMetadata( + combined=combined, src=src, tgt=tgt + ) + bounds = coord.bounds + result = _PreparedItem( + metadata=prepared_metadata, + points=coord.points.copy(), + bounds=bounds if bounds is None else bounds.copy(), + dims=dims, + container=type(coord), + ) + return result + + @property + def _debug(self): + result = False + level = logger.getEffectiveLevel() + if level != logging.NOTSET: + result = logging.DEBUG >= level + return result + + @staticmethod + def _debug_items(items, title=None): + def _show(items, heading): + logger.debug(f"{title}{heading}:") + for item in items: + dmsg = f"metadata={item.metadata}, dims={item.dims}, bounds={item.coord.has_bounds()}" + logger.debug(dmsg) + + title = f"{title} " if title else "" + _show(items.items_dim, "dim") + _show(items.items_aux, "aux") + _show(items.items_scalar, "scalar") + + @staticmethod + def _dim_coverage(cube, cube_items_dim, common_dim_metadata): + ndim = cube.ndim + metadata = [None] * ndim + coords = [None] * ndim + dims_common = [] + dims_local = [] + dims_free = set(range(ndim)) + + for item in cube_items_dim: + (dim,) = item.dims + dims_free.discard(dim) + metadata[dim] = item.metadata + coords[dim] = item.coord + if item.metadata in common_dim_metadata: + dims_common.append(dim) + else: + dims_local.append(dim) + + return _DimCoverage( + cube=cube, + metadata=metadata, + coords=coords, + dims_common=sorted(dims_common), + dims_local=sorted(dims_local), + dims_free=sorted(dims_free), + ) + + def _dim_mapping(self, src_coverage, tgt_coverage): + for tgt_dim in tgt_coverage.dims_common: + # Search for a src dim metadata match. + tgt_metadata = tgt_coverage.metadata[tgt_dim] + try: + src_dim = src_coverage.metadata.index(tgt_metadata) + self.mapping[src_dim] = tgt_dim + logger.debug(f"{src_dim}->{tgt_dim}") + except ValueError: + # This exception can only occur due to a systemic internal + # failure to correctly identify common dim coordinate metadata + # coverage between the cubes. + emsg = ( + "Failed to map common dim coordinate metadata from " + "source cube {!r} to target cube {!r}, using {!r} on " + "target cube dimension {}." + ) + raise ValueError( + emsg.format( + src_coverage.cube.name(), + tgt_coverage.cube.name(), + tgt_metadata, + tuple([tgt_dim]), + ) + ) + + def _free_mapping( + self, + src_dim_coverage, + tgt_dim_coverage, + src_aux_coverage, + tgt_aux_coverage, + ): + src_cube = src_dim_coverage.cube + tgt_cube = tgt_dim_coverage.cube + src_ndim = src_cube.ndim + tgt_ndim = tgt_cube.ndim + + # mapping src to tgt, involving free dimensions on either the src/tgt. + free_mapping = {} + + # Determine the src/tgt dimensions that are not mapped, + # and not covered by any metadata. + src_free = set(src_dim_coverage.dims_free) & set( + src_aux_coverage.dims_free + ) + tgt_free = set(tgt_dim_coverage.dims_free) & set( + tgt_aux_coverage.dims_free + ) + + if src_free or tgt_free: + # Determine the src/tgt dimensions that are not mapped. + src_unmapped = set(range(src_ndim)) - set(self.mapping) + tgt_unmapped = set(range(tgt_ndim)) - set(self.mapping.values()) + + # Determine the src/tgt dimensions that are not mapped, + # but are covered by a src/tgt local coordinate. + src_unmapped_local = src_unmapped - src_free + tgt_unmapped_local = tgt_unmapped - tgt_free + + src_shape = src_cube.shape + tgt_shape = tgt_cube.shape + src_max, tgt_max = max(src_shape), max(tgt_shape) + + def assign_mapping(extent, unmapped_local_items, free_items=None): + result = None + if free_items is None: + free_items = [] + if extent == 1: + if unmapped_local_items: + result, _ = unmapped_local_items.pop(0) + elif free_items: + result, _ = free_items.pop(0) + else: + + def _filter(items): + return list( + filter(lambda item: item[1] == extent, items) + ) + + def _pop(item, items): + result, _ = item + index = items.index(item) + items.pop(index) + return result + + items = _filter(unmapped_local_items) + if items: + result = _pop(items[0], unmapped_local_items) + else: + items = _filter(free_items) + if items: + result = _pop(items[0], free_items) + return result + + if src_free: + # Attempt to map src free dimensions to tgt unmapped local or free dimensions. + tgt_unmapped_local_items = [ + (dim, tgt_shape[dim]) for dim in tgt_unmapped_local + ] + tgt_free_items = [(dim, tgt_shape[dim]) for dim in tgt_free] + + for src_dim in sorted( + src_free, key=lambda dim: (src_max - src_shape[dim], dim) + ): + tgt_dim = assign_mapping( + src_shape[src_dim], + tgt_unmapped_local_items, + tgt_free_items, + ) + if tgt_dim is None: + # Failed to map the src free dimension + # to a suitable tgt local/free dimension. + dmsg = ( + f"failed to map src free dimension ({src_dim},) from " + f"{self._src_cube_position} cube {src_cube.name()!r} to " + f"{self._tgt_cube_position} cube {tgt_cube.name()!r}." + ) + logger.debug(dmsg) + break + free_mapping[src_dim] = tgt_dim + else: + # Attempt to map tgt free dimensions to src unmapped local dimensions. + src_unmapped_local_items = [ + (dim, src_shape[dim]) for dim in src_unmapped_local + ] + + for tgt_dim in sorted( + tgt_free, key=lambda dim: (tgt_max - tgt_shape[dim], dim) + ): + src_dim = assign_mapping( + tgt_shape[tgt_dim], src_unmapped_local_items + ) + if src_dim is not None: + free_mapping[src_dim] = tgt_dim + if not src_unmapped_local_items: + # There are no more src unmapped local dimensions. + break + + # Determine whether there are still unmapped src dimensions. + src_unmapped = ( + set(range(src_cube.ndim)) - set(self.mapping) - set(free_mapping) + ) + + if src_unmapped: + plural = "s" if len(src_unmapped) > 1 else "" + emsg = ( + "Insufficient matching coordinate metadata to resolve cubes, " + f"cannot map dimension{plural} {tuple(sorted(src_unmapped))} " + f"of the {self._src_cube_position} cube {src_cube.name()!r} " + f"to the {self._tgt_cube_position} cube {tgt_cube.name()!r}." + ) + raise ValueError(emsg) + + # Update the mapping. + self.mapping.update(free_mapping) + logger.debug(f"mapping free dimensions gives, mapping={self.mapping}") + + def _init(self, lhs, rhs): + from iris.cube import Cube + + emsg = ( + "{cls} requires {arg!r} argument to be a 'Cube', got {actual!r}." + ) + clsname = self.__class__.__name__ + + if not isinstance(lhs, Cube): + raise TypeError( + emsg.format(cls=clsname, arg="LHS", actual=type(lhs)) + ) + + if not isinstance(rhs, Cube): + raise TypeError( + emsg.format(cls=clsname, arg="RHS", actual=type(rhs)) + ) + + # The LHS cube to be resolved into the resultant cube. + self.lhs_cube = lhs + # The RHS cube to be resolved into the resultant cube. + self.rhs_cube = rhs + + # The transposed/reshaped (if required) LHS cube, which + # can be broadcast with RHS cube. + self.lhs_cube_resolved = None + # The transposed/reshaped (if required) RHS cube, which + # can be broadcast with LHS cube. + self.rhs_cube_resolved = None + + # Categorised dim, aux and scalar coordinate items for LHS cube. + self.lhs_cube_category = None + # Categorised dim, aux and scalar coordinate items for RHS cube. + self.rhs_cube_category = None + + # Categorised dim, aux and scalar coordinate items local to LHS cube only. + self.lhs_cube_category_local = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # Categorised dim, aux and scalar coordinate items local to RHS cube only. + self.rhs_cube_category_local = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # Categorised dim, aux and scalar coordinate items common to both + # LHS cube and RHS cube. + self.category_common = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + + # Analysis of dim coordinates spanning LHS cube. + self.lhs_cube_dim_coverage = None + # Analysis of aux and scalar coordinates spanning LHS cube. + self.lhs_cube_aux_coverage = None + # Analysis of dim coordinates spanning RHS cube. + self.rhs_cube_dim_coverage = None + # Analysis of aux and scalar coordinates spanning RHS cube. + self.rhs_cube_aux_coverage = None + + # Map common metadata from RHS cube to LHS cube if LHS-rank >= RHS-rank, + # otherwise map common metadata from LHS cube to RHS cube. + if self.lhs_cube.ndim >= self.rhs_cube.ndim: + self.map_rhs_to_lhs = True + else: + self.map_rhs_to_lhs = False + + # Mapping of the dimensions between common metadata for the cubes, + # where the direction of the mapping is governed by map_rhs_to_lhs. + self.mapping = None + + # Cache containing a list of dim, aux and scalar coordinates prepared + # and ready for creating and attaching to the resultant cube. + self.prepared_category = None + + # Cache containing a list of aux factories prepared and ready for + # creating and attaching to the resultant cube. + self.prepared_factories = None + + # The shape of the resultant resolved cube. + self._broadcast_shape = None + + def _metadata_coverage(self): + # Determine the common dim coordinate metadata coverage. + common_dim_metadata = [ + item.metadata for item in self.category_common.items_dim + ] + + self.lhs_cube_dim_coverage = self._dim_coverage( + self.lhs_cube, + self.lhs_cube_category.items_dim, + common_dim_metadata, + ) + self.rhs_cube_dim_coverage = self._dim_coverage( + self.rhs_cube, + self.rhs_cube_category.items_dim, + common_dim_metadata, + ) + + # Determine the common aux and scalar coordinate metadata coverage. + common_aux_metadata = [ + item.metadata for item in self.category_common.items_aux + ] + common_scalar_metadata = [ + item.metadata for item in self.category_common.items_scalar + ] + + self.lhs_cube_aux_coverage = self._aux_coverage( + self.lhs_cube, + self.lhs_cube_category.items_aux, + self.lhs_cube_category.items_scalar, + common_aux_metadata, + common_scalar_metadata, + ) + self.rhs_cube_aux_coverage = self._aux_coverage( + self.rhs_cube, + self.rhs_cube_category.items_aux, + self.rhs_cube_category.items_scalar, + common_aux_metadata, + common_scalar_metadata, + ) + + def _metadata_mapping(self): + # Initialise the state. + self.mapping = {} + + # Map RHS cube to LHS cube, or smaller to larger cube rank. + if self.map_rhs_to_lhs: + src_cube = self.rhs_cube + src_dim_coverage = self.rhs_cube_dim_coverage + src_aux_coverage = self.rhs_cube_aux_coverage + tgt_cube = self.lhs_cube + tgt_dim_coverage = self.lhs_cube_dim_coverage + tgt_aux_coverage = self.lhs_cube_aux_coverage + else: + src_cube = self.lhs_cube + src_dim_coverage = self.lhs_cube_dim_coverage + src_aux_coverage = self.lhs_cube_aux_coverage + tgt_cube = self.rhs_cube + tgt_dim_coverage = self.rhs_cube_dim_coverage + tgt_aux_coverage = self.rhs_cube_aux_coverage + + # Use the dim coordinates to fully map the + # src cube dimensions to the tgt cube dimensions. + self._dim_mapping(src_dim_coverage, tgt_dim_coverage) + logger.debug( + f"mapping common dim coordinates gives, mapping={self.mapping}" + ) + + # If necessary, use the aux coordinates to fully map the + # src cube dimensions to the tgt cube dimensions. + if not self.mapped: + self._aux_mapping(src_aux_coverage, tgt_aux_coverage) + logger.debug( + f"mapping common aux coordinates, mapping={self.mapping}" + ) + + if not self.mapped: + # Attempt to complete the mapping using src/tgt free dimensions. + # Note that, this may not be possible and result in an exception. + self._free_mapping( + src_dim_coverage, + tgt_dim_coverage, + src_aux_coverage, + tgt_aux_coverage, + ) + + # Attempt to transpose/reshape the cubes into compatible broadcast shapes. + # Note that, this may not be possible and result in an exception. + self._as_compatible_cubes() + + # Given the resultant broadcast shape, determine whether the + # mapping requires to be reversed. + broadcast_flip = ( + src_cube.ndim == tgt_cube.ndim + and self._tgt_cube_resolved.shape != self.shape + and self._src_cube_resolved.shape == self.shape + ) + + # Given the number of free dimensions, determine whether the + # mapping requires to be reversed. + src_free = set(src_dim_coverage.dims_free) & set( + src_aux_coverage.dims_free + ) + tgt_free = set(tgt_dim_coverage.dims_free) & set( + tgt_aux_coverage.dims_free + ) + free_flip = len(tgt_free) > len(src_free) + + # Reverse the mapping direction. + if broadcast_flip or free_flip: + flip_mapping = { + tgt_dim: src_dim for src_dim, tgt_dim in self.mapping.items() + } + self.map_rhs_to_lhs = not self.map_rhs_to_lhs + dmsg = ( + f"reversing the mapping from {self.mapping} to {flip_mapping}, " + f"now map_rhs_to_lhs={self.map_rhs_to_lhs}" + ) + logger.debug(dmsg) + self.mapping = flip_mapping + # Now require to transpose/reshape the cubes into compatible + # broadcast cubes again, due to possible non-commutative behaviour + # after reversing the mapping direction. + self._as_compatible_cubes() + + def _metadata_prepare(self): + # Initialise the state. + self.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.prepared_factories = [] + + # Map RHS cube to LHS cube, or smaller to larger cube rank. + if self.map_rhs_to_lhs: + src_cube = self.rhs_cube + src_category_local = self.rhs_cube_category_local + src_dim_coverage = self.rhs_cube_dim_coverage + src_aux_coverage = self.rhs_cube_aux_coverage + tgt_cube = self.lhs_cube + tgt_category_local = self.lhs_cube_category_local + tgt_dim_coverage = self.lhs_cube_dim_coverage + tgt_aux_coverage = self.lhs_cube_aux_coverage + else: + src_cube = self.lhs_cube + src_category_local = self.lhs_cube_category_local + src_dim_coverage = self.lhs_cube_dim_coverage + src_aux_coverage = self.lhs_cube_aux_coverage + tgt_cube = self.rhs_cube + tgt_category_local = self.rhs_cube_category_local + tgt_dim_coverage = self.rhs_cube_dim_coverage + tgt_aux_coverage = self.rhs_cube_aux_coverage + + # Determine the resultant cube dim coordinate/s. + self._prepare_common_dim_payload(src_dim_coverage, tgt_dim_coverage) + + # Determine the resultant cube aux coordinate/s. + self._prepare_common_aux_payload( + src_aux_coverage.common_items_aux, # input + tgt_aux_coverage.common_items_aux, # input + self.prepared_category.items_aux, # output + ) + + # Determine the resultant cube scalar coordinate/s. + self._prepare_common_aux_payload( + src_aux_coverage.common_items_scalar, # input + tgt_aux_coverage.common_items_scalar, # input + self.prepared_category.items_scalar, # output + ignore_mismatch=True, + ) + + self._prepare_local_payload( + src_dim_coverage, + src_aux_coverage, + tgt_dim_coverage, + tgt_aux_coverage, + ) + + self._prepare_factory_payload( + tgt_cube, tgt_category_local, from_src=False + ) + self._prepare_factory_payload(src_cube, src_category_local) + + def _metadata_resolve(self): + """ + Categorise the coordinate metadata of the cubes into three distinct + groups; metadata from coordinates only available (local) on the LHS + cube, metadata from coordinates only available (local) on the RHS + cube, and metadata from coordinates common to both the LHS and RHS + cubes. + + This is only applicable to coordinates that are members of the + 'aux_coords' or 'dim_coords' of the participating cubes. + + """ + + # Determine the cube dim, aux and scalar coordinate items + # for each individual cube. + self.lhs_cube_category = self._categorise_items(self.lhs_cube) + self.rhs_cube_category = self._categorise_items(self.rhs_cube) + + def _categorise( + lhs_items, + rhs_items, + lhs_local_items, + rhs_local_items, + common_items, + ): + rhs_items_metadata = [item.metadata for item in rhs_items] + # Track common metadata here as a temporary convenience. + common_metadata = [] + + # Determine items local to the lhs, and shared items + # common to both lhs and rhs. + for item in lhs_items: + metadata = item.metadata + if metadata in rhs_items_metadata: + # The metadata is common between lhs and rhs. + if metadata not in common_metadata: + common_items.append(item) + common_metadata.append(metadata) + else: + # The metadata is local to the lhs. + lhs_local_items.append(item) + + # Determine items local to the rhs. + for item in rhs_items: + if item.metadata not in common_metadata: + rhs_local_items.append(item) + + # Determine local and common dim category items. + _categorise( + self.lhs_cube_category.items_dim, # input + self.rhs_cube_category.items_dim, # input + self.lhs_cube_category_local.items_dim, # output + self.rhs_cube_category_local.items_dim, # output + self.category_common.items_dim, # output + ) + + # Determine local and common aux category items. + _categorise( + self.lhs_cube_category.items_aux, # input + self.rhs_cube_category.items_aux, # input + self.lhs_cube_category_local.items_aux, # output + self.rhs_cube_category_local.items_aux, # output + self.category_common.items_aux, # output + ) + + # Determine local and common scalar category items. + _categorise( + self.lhs_cube_category.items_scalar, # input + self.rhs_cube_category.items_scalar, # input + self.lhs_cube_category_local.items_scalar, # output + self.rhs_cube_category_local.items_scalar, # output + self.category_common.items_scalar, # output + ) + + # Sort the resultant categories by metadata name for consistency, + # in-place. + categories = ( + self.lhs_cube_category, + self.rhs_cube_category, + self.lhs_cube_category_local, + self.rhs_cube_category_local, + self.category_common, + ) + key_func = lambda item: item.metadata.name() + + for category in categories: + category.items_dim.sort(key=key_func) + category.items_aux.sort(key=key_func) + category.items_scalar.sort(key=key_func) + + def _prepare_common_aux_payload( + self, + src_common_items, + tgt_common_items, + prepared_items, + ignore_mismatch=None, + ): + from iris.coords import AuxCoord + + if ignore_mismatch is None: + # Configure ability to ignore coordinate points/bounds + # mismatches between common items. + ignore_mismatch = False + + for src_item in src_common_items: + src_metadata = src_item.metadata + tgt_items = tuple( + filter( + lambda tgt_item: tgt_item.metadata == src_metadata, + tgt_common_items, + ) + ) + if not tgt_items: + dmsg = ( + f"ignoring src {self._src_cube_position} cube aux coordinate " + f"{src_metadata}, does not match any common tgt " + f"{self._tgt_cube_position} cube aux coordinate metadata" + ) + logger.debug(dmsg) + elif len(tgt_items) > 1: + dmsg = ( + f"ignoring src {self._src_cube_position} cube aux coordinate " + f"{src_metadata}, matches multiple [{len(tgt_items)}] common " + f"tgt {self._tgt_cube_position} cube aux coordinate metadata" + ) + logger.debug(dmsg) + else: + (tgt_item,) = tgt_items + src_coord = src_item.coord + tgt_coord = tgt_item.coord + points, bounds = self._prepare_points_and_bounds( + src_coord, + tgt_coord, + src_item.dims, + tgt_item.dims, + ignore_mismatch=ignore_mismatch, + ) + if points is not None: + src_type = type(src_coord) + tgt_type = type(tgt_coord) + # Downcast to aux if there are mixed container types. + container = src_type if src_type is tgt_type else AuxCoord + prepared_metadata = _PreparedMetadata( + combined=src_metadata.combine(tgt_item.metadata), + src=src_metadata, + tgt=tgt_item.metadata, + ) + prepared_item = _PreparedItem( + metadata=prepared_metadata, + points=points.copy(), + bounds=bounds if bounds is None else bounds.copy(), + dims=tgt_item.dims, + container=container, + ) + prepared_items.append(prepared_item) + + def _prepare_common_dim_payload( + self, src_coverage, tgt_coverage, ignore_mismatch=None + ): + from iris.coords import DimCoord + + if ignore_mismatch is None: + # Configure ability to ignore coordinate points/bounds + # mismatches between common items. + ignore_mismatch = False + + for src_dim in src_coverage.dims_common: + src_metadata = src_coverage.metadata[src_dim] + src_coord = src_coverage.coords[src_dim] + + tgt_dim = self.mapping[src_dim] + tgt_metadata = tgt_coverage.metadata[tgt_dim] + tgt_coord = tgt_coverage.coords[tgt_dim] + + points, bounds = self._prepare_points_and_bounds( + src_coord, + tgt_coord, + src_dim, + tgt_dim, + ignore_mismatch=ignore_mismatch, + ) + + if points is not None: + prepared_metadata = _PreparedMetadata( + combined=src_metadata.combine(tgt_metadata), + src=src_metadata, + tgt=tgt_metadata, + ) + prepared_item = _PreparedItem( + metadata=prepared_metadata, + points=points.copy(), + bounds=bounds if bounds is None else bounds.copy(), + dims=(tgt_dim,), + container=DimCoord, + ) + self.prepared_category.items_dim.append(prepared_item) + + def _prepare_factory_payload(self, cube, category_local, from_src=True): + def _get_prepared_item(metadata, from_src=True, from_local=False): + result = None + if from_local: + category = category_local + match = lambda item: item.metadata == metadata + else: + category = self.prepared_category + if from_src: + match = lambda item: item.metadata.src == metadata + else: + match = lambda item: item.metadata.tgt == metadata + for member in category._fields: + category_items = getattr(category, member) + matched_items = tuple(filter(match, category_items)) + if matched_items: + if len(matched_items) > 1: + dmsg = ( + f"ignoring factory dependency {metadata}, multiple {'src' if from_src else 'tgt'} " + f"{'local' if from_local else 'prepared'} metadata matches" + ) + logger.debug(dmsg) + else: + (item,) = matched_items + if from_local: + src = tgt = None + if from_src: + src = item.metadata + dims = tuple( + [self.mapping[dim] for dim in item.dims] + ) + else: + tgt = item.metadata + dims = item.dims + result = self._create_prepared_item( + item.coord, dims, src=src, tgt=tgt + ) + getattr(self.prepared_category, member).append( + result + ) + else: + result = item + break + return result + + for factory in cube.aux_factories: + container = type(factory) + dependencies = {} + prepared_item = None + + if tuple( + filter( + lambda item: item.container is container, + self.prepared_factories, + ) + ): + # debug: skipping, factory already exists + dmsg = ( + f"ignoring {'src' if from_src else 'tgt'} {container}, " + f"a similar factory has already been prepared" + ) + logger.debug(dmsg) + continue + + for ( + dependency_name, + dependency_coord, + ) in factory.dependencies.items(): + metadata = dependency_coord.metadata + prepared_item = _get_prepared_item(metadata, from_src=from_src) + if prepared_item is None: + prepared_item = _get_prepared_item( + metadata, from_src=from_src, from_local=True + ) + if prepared_item is None: + dmsg = f"cannot find matching {metadata} for {container} dependency {dependency_name}" + logger.debug(dmsg) + break + dependencies[dependency_name] = prepared_item.metadata + + if prepared_item is not None: + prepared_factory = _PreparedFactory( + container=container, dependencies=dependencies + ) + self.prepared_factories.append(prepared_factory) + else: + dmsg = f"ignoring {'src' if from_src else 'tgt'} {container}, cannot find all dependencies" + logger.debug(dmsg) + + def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): + # Determine whether there are tgt dimensions not mapped to by an + # associated src dimension, and thus may be covered by any local + # tgt aux coordinates. + extra_tgt_dims = set(range(tgt_aux_coverage.cube.ndim)) - set( + self.mapping.values() + ) + + if LENIENT["maths"]: + mapped_src_dims = set(self.mapping.keys()) + mapped_tgt_dims = set(self.mapping.values()) + + # Add local src aux coordinates. + for item in src_aux_coverage.local_items_aux: + if all([dim in mapped_src_dims for dim in item.dims]): + tgt_dims = tuple([self.mapping[dim] for dim in item.dims]) + prepared_item = self._create_prepared_item( + item.coord, tgt_dims, src=item.metadata + ) + self.prepared_category.items_aux.append(prepared_item) + else: + dmsg = ( + f"ignoring local src {self._src_cube_position} cube " + f"aux coordinate {item.metadata}, as not all src " + f"dimensions {item.dims} are mapped" + ) + logger.debug(dmsg) + else: + # For strict maths, only local tgt aux coordinates covering + # the extra dimensions of the tgt cube may be added. + mapped_tgt_dims = set() + + # Add local tgt aux coordinates. + for item in tgt_aux_coverage.local_items_aux: + tgt_dims = item.dims + if all([dim in mapped_tgt_dims for dim in tgt_dims]) or any( + [dim in extra_tgt_dims for dim in tgt_dims] + ): + prepared_item = self._create_prepared_item( + item.coord, tgt_dims, tgt=item.metadata + ) + self.prepared_category.items_aux.append(prepared_item) + else: + dmsg = ( + f"ignoring local tgt {self._tgt_cube_position} cube " + f"aux coordinate {item.metadata}, as not all tgt " + f"dimensions {tgt_dims} are mapped" + ) + logger.debug(dmsg) + + def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): + mapped_tgt_dims = self.mapping.values() + + # Determine whether there are tgt dimensions not mapped to by an + # associated src dimension, and thus may be covered by any local + # tgt dim coordinates. + extra_tgt_dims = set(range(tgt_dim_coverage.cube.ndim)) - set( + mapped_tgt_dims + ) + + if LENIENT["maths"]: + tgt_dims_conflict = set() + + # Add local src dim coordinates. + for src_dim in src_dim_coverage.dims_local: + tgt_dim = self.mapping[src_dim] + # Only add the local src dim coordinate iff there is no + # associated local tgt dim coordinate. + if tgt_dim not in tgt_dim_coverage.dims_local: + metadata = src_dim_coverage.metadata[src_dim] + coord = src_dim_coverage.coords[src_dim] + prepared_item = self._create_prepared_item( + coord, tgt_dim, src=metadata + ) + self.prepared_category.items_dim.append(prepared_item) + else: + tgt_dims_conflict.add(tgt_dim) + if self._debug: + src_metadata = src_dim_coverage.metadata[src_dim] + tgt_metadata = tgt_dim_coverage.metadata[tgt_dim] + dmsg = ( + f"ignoring local src {self._src_cube_position} cube " + f"dim coordinate {src_metadata}, as conflicts with " + f"tgt {self._tgt_cube_position} cube dim coordinate " + f"{tgt_metadata}, mapping ({src_dim},)->({tgt_dim},)" + ) + logger.debug(dmsg) + + # Determine whether there are any tgt dims free to be mapped + # by an available local tgt dim coordinate. + tgt_dims_unmapped = ( + set(tgt_dim_coverage.dims_local) - tgt_dims_conflict + ) + else: + # For strict maths, only local tgt dim coordinates covering + # the extra dimensions of the tgt cube may be added. + tgt_dims_unmapped = extra_tgt_dims + + # Add local tgt dim coordinates. + for tgt_dim in tgt_dims_unmapped: + if tgt_dim in mapped_tgt_dims or tgt_dim in extra_tgt_dims: + metadata = tgt_dim_coverage.metadata[tgt_dim] + if metadata is not None: + coord = tgt_dim_coverage.coords[tgt_dim] + prepared_item = self._create_prepared_item( + coord, tgt_dim, tgt=metadata + ) + self.prepared_category.items_dim.append(prepared_item) + + def _prepare_local_payload_scalar( + self, src_aux_coverage, tgt_aux_coverage + ): + # Add all local tgt scalar coordinates iff the src cube is a + # scalar cube with no local src scalar coordinates. + # Only for strict maths. + src_scalar_cube = ( + not LENIENT["maths"] + and src_aux_coverage.cube.ndim == 0 + and len(src_aux_coverage.local_items_scalar) == 0 + ) + + if src_scalar_cube or LENIENT["maths"]: + # Add any local src scalar coordinates, if available. + for item in src_aux_coverage.local_items_scalar: + prepared_item = self._create_prepared_item( + item.coord, item.dims, src=item.metadata + ) + self.prepared_category.items_scalar.append(prepared_item) + + # Add any local tgt scalar coordinates, if available. + for item in tgt_aux_coverage.local_items_scalar: + prepared_item = self._create_prepared_item( + item.coord, item.dims, tgt=item.metadata + ) + self.prepared_category.items_scalar.append(prepared_item) + + def _prepare_local_payload( + self, + src_dim_coverage, + src_aux_coverage, + tgt_dim_coverage, + tgt_aux_coverage, + ): + # Add local src/tgt dim coordinates. + self._prepare_local_payload_dim(src_dim_coverage, tgt_dim_coverage) + + # Add local src/tgt aux coordinates. + self._prepare_local_payload_aux(src_aux_coverage, tgt_aux_coverage) + + # Add local src/tgt scalar coordinates. + self._prepare_local_payload_scalar(src_aux_coverage, tgt_aux_coverage) + + def _prepare_points_and_bounds( + self, src_coord, tgt_coord, src_dims, tgt_dims, ignore_mismatch=None + ): + from iris.util import array_equal + + if ignore_mismatch is None: + # Configure ability to ignore coordinate points/bounds + # mismatches between common items. + ignore_mismatch = False + + points, bounds = None, None + + if not isinstance(src_dims, Iterable): + src_dims = (src_dims,) + + if not isinstance(tgt_dims, Iterable): + tgt_dims = (tgt_dims,) + + # Deal with coordinates that have been sliced. + if src_coord.ndim != tgt_coord.ndim: + if tgt_coord.ndim > src_coord.ndim: + # Use the tgt coordinate points/bounds. + points = tgt_coord.points + bounds = tgt_coord.bounds + else: + # Use the src coordinate points/bounds. + points = src_coord.points + bounds = src_coord.bounds + + # Deal with coordinates spanning broadcast dimensions. + if ( + points is None + and bounds is None + and src_coord.shape != tgt_coord.shape + ): + # Check whether the src coordinate is broadcasting. + dims = tuple([self.mapping[dim] for dim in src_dims]) + src_shape_broadcast = tuple([self.shape[dim] for dim in dims]) + src_cube_shape = self._src_cube.shape + src_shape = tuple([src_cube_shape[dim] for dim in src_dims]) + src_broadcasting = src_shape != src_shape_broadcast + + # Check whether the tgt coordinate is broadcasting. + tgt_shape_broadcast = tuple([self.shape[dim] for dim in tgt_dims]) + tgt_cube_shape = self._tgt_cube.shape + tgt_shape = tuple([tgt_cube_shape[dim] for dim in tgt_dims]) + tgt_broadcasting = tgt_shape != tgt_shape_broadcast + + if src_broadcasting and tgt_broadcasting: + emsg = ( + f"Cannot broadcast the coordinate {src_coord.name()!r} on " + f"{self._src_cube_position} cube {self._src_cube.name()!r} and " + f"coordinate {tgt_coord.name()!r} on " + f"{self._tgt_cube_position} cube {self._tgt_cube.name()!r} to " + f"broadcast shape {tgt_shape_broadcast}." + ) + raise ValueError(emsg) + elif src_broadcasting: + # Use the tgt coordinate points/bounds. + points = tgt_coord.points + bounds = tgt_coord.bounds + elif tgt_broadcasting: + # Use the src coordinate points/bounds. + points = src_coord.points + bounds = src_coord.bounds + + if points is None and bounds is None: + # Note that, this also ensures shape equality. + eq_points = array_equal( + src_coord.points, tgt_coord.points, withnans=True + ) + if eq_points: + points = src_coord.points + src_has_bounds = src_coord.has_bounds() + tgt_has_bounds = tgt_coord.has_bounds() + + if src_has_bounds and tgt_has_bounds: + src_bounds = src_coord.bounds + eq_bounds = array_equal( + src_bounds, tgt_coord.bounds, withnans=True + ) + + if eq_bounds: + bounds = src_bounds + else: + if LENIENT["maths"] and ignore_mismatch: + # For lenient, ignore coordinate with mis-matched bounds. + dmsg = ( + f"ignoring src {self._src_cube_position} cube " + f"{src_coord.metadata}, unequal bounds with " + f"tgt {self._tgt_cube_position} cube, " + f"{src_dims}->{tgt_dims}" + ) + logger.debug(dmsg) + else: + emsg = ( + f"Coordinate {src_coord.name()!r} has different bounds for the " + f"LHS cube {self.lhs_cube.name()!r} and " + f"RHS cube {self.rhs_cube.name()!r}." + ) + raise ValueError(emsg) + else: + # For lenient, use either of the coordinate bounds, if they exist. + if LENIENT["maths"]: + if src_has_bounds: + dmsg = ( + f"using src {self._src_cube_position} cube " + f"{src_coord.metadata} bounds, tgt has no bounds" + ) + logger.debug(dmsg) + bounds = src_coord.bounds + else: + dmsg = ( + f"using tgt {self._tgt_cube_position} cube " + f"{tgt_coord.metadata} bounds, src has no bounds" + ) + logger.debug(dmsg) + bounds = tgt_coord.bounds + else: + # For strict, both coordinates must have bounds, or both + # coordinates must not have bounds. + if src_has_bounds: + emsg = ( + f"Coordinate {src_coord.name()!r} has bounds for the " + f"{self._src_cube_position} cube {self._src_cube.name()!r}, " + f"but not the {self._tgt_cube_position} cube {self._tgt_cube.name()!r}." + ) + raise ValueError(emsg) + if tgt_has_bounds: + emsg = ( + f"Coordinate {tgt_coord.name()!r} has bounds for the " + f"{self._tgt_cube_position} cube {self._tgt_cube.name()!r}, " + f"but not the {self._src_cube_position} cube {self._src_cube.name()!r}." + ) + raise ValueError(emsg) + else: + if LENIENT["maths"] and ignore_mismatch: + # For lenient, ignore coordinate with mis-matched points. + dmsg = ( + f"ignoring src {self._src_cube_position} cube " + f"{src_coord.metadata}, unequal points with tgt " + f"{src_dims}->{tgt_dims}" + ) + logger.debug(dmsg) + else: + emsg = ( + f"Coordinate {src_coord.name()!r} has different points for the " + f"LHS cube {self.lhs_cube.name()!r} and " + f"RHS cube {self.rhs_cube.name()!r}." + ) + raise ValueError(emsg) + + return points, bounds + + @property + def _src_cube(self): + if self.map_rhs_to_lhs: + result = self.rhs_cube + else: + result = self.lhs_cube + return result + + @property + def _src_cube_position(self): + if self.map_rhs_to_lhs: + result = "RHS" + else: + result = "LHS" + return result + + @property + def _src_cube_resolved(self): + if self.map_rhs_to_lhs: + result = self.rhs_cube_resolved + else: + result = self.lhs_cube_resolved + return result + + @_src_cube_resolved.setter + def _src_cube_resolved(self, cube): + if self.map_rhs_to_lhs: + self.rhs_cube_resolved = cube + else: + self.lhs_cube_resolved = cube + + @property + def _tgt_cube(self): + if self.map_rhs_to_lhs: + result = self.lhs_cube + else: + result = self.rhs_cube + return result + + @property + def _tgt_cube_position(self): + if self.map_rhs_to_lhs: + result = "LHS" + else: + result = "RHS" + return result + + @property + def _tgt_cube_resolved(self): + if self.map_rhs_to_lhs: + result = self.lhs_cube_resolved + else: + result = self.rhs_cube_resolved + return result + + @_tgt_cube_resolved.setter + def _tgt_cube_resolved(self, cube): + if self.map_rhs_to_lhs: + self.lhs_cube_resolved = cube + else: + self.rhs_cube_resolved = cube + + def _tgt_cube_prepare(self, data): + cube = self._tgt_cube + + # Replace existing tgt cube data with the provided data. + cube.data = data + + # Clear the aux factories. + for factory in cube.aux_factories: + cube.remove_aux_factory(factory) + + # Clear the cube coordinates. + for coord in cube.coords(): + cube.remove_coord(coord) + + # Clear the cube cell measures. + for cm in cube.cell_measures(): + cube.remove_cell_measure(cm) + + # Clear the ancillary variables. + for av in cube.ancillary_variables(): + cube.remove_ancillary_variable(av) + + def cube(self, data, in_place=False): + from iris.cube import Cube + + expected_shape = self.shape + + # Ensure that we have been provided with candidate cubes, which are + # now resolved and metadata is prepared, ready and awaiting the + # resultant resolved cube. + if expected_shape is None: + emsg = ( + "Cannot resolve resultant cube, as no candidate cubes have " + "been provided." + ) + raise ValueError(emsg) + + if not hasattr(data, "shape"): + data = np.asanyarray(data) + + # Ensure that the shape of the provided data is the expected + # shape of the resultant resolved cube. + if data.shape != expected_shape: + emsg = ( + "Cannot resolve resultant cube, as the provided data must " + f"have shape {expected_shape}, got data shape {data.shape}." + ) + raise ValueError(emsg) + + if in_place: + result = self._tgt_cube + + if result.shape != expected_shape: + emsg = ( + "Cannot resolve resultant cube in-place, as the " + f"{self._tgt_cube_position} tgt cube {result.name()!r} " + f"requires data with shape {result.shape}, got data " + f"shape {data.shape}. Suggest not performing this " + "operation in-place." + ) + raise ValueError(emsg) + + # Prepare target cube for in-place population with the prepared + # metadata content and the provided data. + self._tgt_cube_prepare(data) + else: + # Create the resultant resolved cube with provided data. + result = Cube(data) + + # Add the combined cube metadata from both the candidate cubes. + result.metadata = self.lhs_cube.metadata.combine( + self.rhs_cube.metadata + ) + + # Add the prepared dim coordinates. + for item in self.prepared_category.items_dim: + coord = item.container(item.points, bounds=item.bounds) + coord.metadata = item.metadata.combined + result.add_dim_coord(coord, item.dims) + + # Add the prepared aux and scalar coordinates. + prepared_aux_coords = ( + self.prepared_category.items_aux + + self.prepared_category.items_scalar + ) + for item in prepared_aux_coords: + coord = item.container(item.points, bounds=item.bounds) + coord.metadata = item.metadata.combined + try: + result.add_aux_coord(coord, item.dims) + except ValueError as err: + scalar = dims = "" + if item.dims: + plural = "s" if len(item.dims) > 1 else "" + dims = f" with tgt dim{plural} {item.dims}" + else: + scalar = "scalar " + dmsg = ( + f"ignoring prepared {scalar}coordinate " + f"{coord.metadata}{dims}, got {err!r}" + ) + logger.debug(dmsg) + + # Add the prepared aux factories. + for prepared_factory in self.prepared_factories: + dependencies = dict() + for ( + dependency_name, + prepared_metadata, + ) in prepared_factory.dependencies.items(): + coord = result.coord(prepared_metadata.combined) + dependencies[dependency_name] = coord + factory = prepared_factory.container(**dependencies) + result.add_aux_factory(factory) + + return result + + @property + def mapped(self): + """ + Returns the state of whether all src cube dimensions have been + associated with relevant tgt cube dimensions. + + """ + return self._src_cube.ndim == len(self.mapping) + + @property + def shape(self): + """Returns the shape of the resultant resolved cube.""" + return getattr(self, "_broadcast_shape", None) diff --git a/lib/iris/config.py b/lib/iris/config.py index e1d7dee29d..eeef1873f9 100644 --- a/lib/iris/config.py +++ b/lib/iris/config.py @@ -32,8 +32,11 @@ import configparser import contextlib +import logging.config import os.path +import pathlib import warnings +import yaml # Returns simple string options @@ -81,6 +84,14 @@ def get_dir_option(section, option, default=None): config = configparser.ConfigParser() config.read([os.path.join(CONFIG_PATH, "site.cfg")]) +# Configure logging. +fname_logging = pathlib.Path(CONFIG_PATH) / "logging.yaml" +if not fname_logging.exists(): + emsg = f"Logging configuration file '{fname_logging!s}' does not exist." + raise FileNotFoundError(emsg) +with open(fname_logging) as fi: + logging.config.dictConfig(yaml.safe_load(fi)) +del fname_logging ################## # Resource options diff --git a/lib/iris/coords.py b/lib/iris/coords.py index b5392579c8..8fbe1abf56 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -25,13 +25,19 @@ from iris._data_manager import DataManager import iris._lazy_data as _lazy import iris.aux_factory +from iris.common import ( + AncillaryVariableMetadata, + BaseMetadata, + CFVariableMixin, + CellMeasureMetadata, + CoordMetadata, + DimCoordMetadata, + metadata_manager_factory, +) import iris.exceptions import iris.time import iris.util -from iris._cube_coord_common import CFVariableMixin -from iris.util import points_step - class _DimensionalMetadata(CFVariableMixin, metaclass=ABCMeta): """ @@ -92,6 +98,10 @@ def __init__( # its __init__ or __copy__ methods. The only bounds-related behaviour # it provides is a 'has_bounds()' method, which always returns False. + # Configure the metadata manager. + if not hasattr(self, "_metadata_manager"): + self._metadata_manager = metadata_manager_factory(BaseMetadata) + #: CF standard name of the quantity that the metadata represents. self.standard_name = standard_name @@ -340,9 +350,9 @@ def __eq__(self, other): # If the other object has a means of getting its definition, then do # the comparison, otherwise return a NotImplemented to let Python try # to resolve the operator elsewhere. - if hasattr(other, "_as_defn"): + if hasattr(other, "metadata"): # metadata comparison - eq = self._as_defn() == other._as_defn() + eq = self.metadata == other.metadata # data values comparison if eq and eq is not NotImplemented: eq = iris.util.array_equal( @@ -367,17 +377,6 @@ def __ne__(self, other): result = not result return result - def _as_defn(self): - defn = _DMDefn( - self.standard_name, - self.long_name, - self.var_name, - self.units, - self.attributes, - ) - - return defn - # Must supply __hash__ as Python 3 does not enable it if __eq__ is defined. # NOTE: Violates "objects which compare equal must have the same hash". # We ought to remove this, as equality of two dimensional metadata can @@ -714,6 +713,12 @@ def __init__( A dictionary containing other cf and user-defined attributes. """ + # Configure the metadata manager. + if not hasattr(self, "_metadata_manager"): + self._metadata_manager = metadata_manager_factory( + AncillaryVariableMetadata + ) + super().__init__( values=data, standard_name=standard_name, @@ -821,6 +826,9 @@ def __init__( 'area' and 'volume'. The default is 'area'. """ + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(CellMeasureMetadata) + super().__init__( data=data, standard_name=standard_name, @@ -838,14 +846,14 @@ def __init__( @property def measure(self): - return self._measure + return self._metadata_manager.measure @measure.setter def measure(self, measure): if measure not in ["area", "volume"]: emsg = f"measure must be 'area' or 'volume', got {measure!r}" raise ValueError(emsg) - self._measure = measure + self._metadata_manager.measure = measure def __str__(self): result = repr(self) @@ -864,17 +872,6 @@ def __repr__(self): ) return result - def _as_defn(self): - defn = CellMeasureDefn( - self.standard_name, - self.long_name, - self.var_name, - self.units, - self.attributes, - self.measure, - ) - return defn - def cube_dims(self, cube): """ Return the cube dimensions of this CellMeasure. @@ -895,160 +892,6 @@ def xml_element(self, doc): return element -class CoordDefn( - namedtuple( - "CoordDefn", - [ - "standard_name", - "long_name", - "var_name", - "units", - "attributes", - "coord_system", - "climatological", - ], - ) -): - """ - Criterion for identifying a specific type of :class:`DimCoord` or - :class:`AuxCoord` based on its metadata. - - """ - - __slots__ = () - - def name(self, default="unknown"): - """ - Returns a human-readable name. - - First it tries self.standard_name, then it tries the 'long_name' - attribute, then the 'var_name' attribute, before falling back to - the value of `default` (which itself defaults to 'unknown'). - - """ - return self.standard_name or self.long_name or self.var_name or default - - def __lt__(self, other): - if not isinstance(other, CoordDefn): - return NotImplemented - - def _sort_key(defn): - # Emulate Python 2 behaviour with None - return ( - defn.standard_name is not None, - defn.standard_name, - defn.long_name is not None, - defn.long_name, - defn.var_name is not None, - defn.var_name, - defn.units is not None, - defn.units, - defn.coord_system is not None, - defn.coord_system, - ) - - return _sort_key(self) < _sort_key(other) - - -class CellMeasureDefn( - namedtuple( - "CellMeasureDefn", - [ - "standard_name", - "long_name", - "var_name", - "units", - "attributes", - "measure", - ], - ) -): - """ - Criterion for identifying a specific type of :class:`CellMeasure` - based on its metadata. - - """ - - __slots__ = () - - def name(self, default="unknown"): - """ - Returns a human-readable name. - - First it tries self.standard_name, then it tries the 'long_name' - attribute, then the 'var_name' attribute, before falling back to - the value of `default` (which itself defaults to 'unknown'). - - """ - return self.standard_name or self.long_name or self.var_name or default - - def __lt__(self, other): - if not isinstance(other, CellMeasureDefn): - return NotImplemented - - def _sort_key(defn): - # Emulate Python 2 behaviour with None - return ( - defn.standard_name is not None, - defn.standard_name, - defn.long_name is not None, - defn.long_name, - defn.var_name is not None, - defn.var_name, - defn.units is not None, - defn.units, - defn.measure is not None, - defn.measure, - ) - - return _sort_key(self) < _sort_key(other) - - -class _DMDefn( - namedtuple( - "DMDefn", - ["standard_name", "long_name", "var_name", "units", "attributes",], - ) -): - """ - Criterion for identifying a specific type of :class:`_DimensionalMetadata` - based on its metadata. - - """ - - __slots__ = () - - def name(self, default="unknown"): - """ - Returns a human-readable name. - - First it tries self.standard_name, then it tries the 'long_name' - attribute, then the 'var_name' attribute, before falling back to - the value of `default` (which itself defaults to 'unknown'). - - """ - return self.standard_name or self.long_name or self.var_name or default - - def __lt__(self, other): - if not isinstance(other, _DMDefn): - return NotImplemented - - def _sort_key(defn): - # Emulate Python 2 behaviour with None - return ( - defn.standard_name is not None, - defn.standard_name, - defn.long_name is not None, - defn.long_name, - defn.var_name is not None, - defn.var_name, - defn.units is not None, - defn.units, - ) - - return _sort_key(self) < _sort_key(other) - - class CoordExtent( namedtuple( "_CoordExtent", @@ -1490,7 +1333,12 @@ def __init__( Will set to True when a climatological time axis is loaded from NetCDF. Always False if no bounds exist. + """ + # Configure the metadata manager. + if not hasattr(self, "_metadata_manager"): + self._metadata_manager = metadata_manager_factory(CoordMetadata) + super().__init__( values=points, standard_name=standard_name, @@ -1589,7 +1437,7 @@ def bounds(self, bounds): # Ensure the bounds are a compatible shape. if bounds is None: self._bounds_dm = None - self._climatological = False + self.climatological = False else: bounds = self._sanitise_array(bounds, 2) if self.shape != bounds.shape[:-1]: @@ -1605,6 +1453,15 @@ def bounds(self, bounds): else: self._bounds_dm.data = bounds + @property + def coord_system(self): + """The coordinate-system of the coordinate.""" + return self._metadata_manager.coord_system + + @coord_system.setter + def coord_system(self, value): + self._metadata_manager.coord_system = value + @property def climatological(self): """ @@ -1615,8 +1472,13 @@ def climatological(self): Always reads as False if there are no bounds. On set, the input value is cast to a boolean, exceptions raised if units are not time units or if there are no bounds. + """ - return self._climatological if self.has_bounds() else False + if not self.has_bounds(): + self._metadata_manager.climatological = False + if not self.units.is_time_reference(): + self._metadata_manager.climatological = False + return self._metadata_manager.climatological @climatological.setter def climatological(self, value): @@ -1634,7 +1496,7 @@ def climatological(self, value): emsg = "Cannot set climatological coordinate, no bounds exist." raise ValueError(emsg) - self._climatological = value + self._metadata_manager.climatological = value def lazy_points(self): """ @@ -1722,18 +1584,6 @@ def _repr_other_metadata(self): result += ", climatological={}".format(self.climatological) return result - def _as_defn(self): - defn = CoordDefn( - self.standard_name, - self.long_name, - self.var_name, - self.units, - self.attributes, - self.coord_system, - self.climatological, - ) - return defn - # Must supply __hash__ as Python 3 does not enable it if __eq__ is defined. # NOTE: Violates "objects which compare equal must have the same hash". # We ought to remove this, as equality of two coords can *change*, so they @@ -1986,8 +1836,9 @@ def is_compatible(self, other, ignore=None): Args: * other: - An instance of :class:`iris.coords.Coord` or - :class:`iris.coords.CoordDefn`. + An instance of :class:`iris.coords.Coord`, + :class:`iris.common.CoordMetadata` or + :class:`iris.common.DimCoordMetadata`. * ignore: A single attribute key or iterable of attribute keys to ignore when comparing the coordinates. Default is None. To ignore all @@ -2442,7 +2293,7 @@ def from_regular( """ points = (zeroth + step) + step * np.arange(count, dtype=np.float32) - _, regular = points_step(points) + _, regular = iris.util.points_step(points) if not regular: points = (zeroth + step) + step * np.arange( count, dtype=np.float64 @@ -2486,6 +2337,9 @@ def __init__( read-only points and bounds. """ + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(DimCoordMetadata) + super().__init__( points, standard_name=standard_name, @@ -2499,7 +2353,7 @@ def __init__( ) #: Whether the coordinate wraps by ``coord.units.modulus``. - self.circular = bool(circular) + self.circular = circular def __deepcopy__(self, memo): """ @@ -2515,6 +2369,14 @@ def __deepcopy__(self, memo): new_coord._bounds_dm.data.flags.writeable = False return new_coord + @property + def circular(self): + return self._metadata_manager.circular + + @circular.setter + def circular(self, circular): + self._metadata_manager.circular = bool(circular) + def copy(self, points=None, bounds=None): new_coord = super().copy(points=points, bounds=bounds) # Make the arrays read-only. @@ -2524,13 +2386,13 @@ def copy(self, points=None, bounds=None): return new_coord def __eq__(self, other): - # TODO investigate equality of AuxCoord and DimCoord if circular is - # False. result = NotImplemented if isinstance(other, DimCoord): - result = ( - Coord.__eq__(self, other) and self.circular == other.circular - ) + # The "circular" member participates in DimCoord to DimCoord + # equivalence. We require to do this explicitly here + # as the "circular" member does NOT participate in + # DimCoordMetadata to DimCoordMetadata equivalence. + result = self.circular == other.circular and super().__eq__(other) return result # The __ne__ operator from Coord implements the not __eq__ method. @@ -2779,19 +2641,20 @@ def __init__(self, method, coords=None, intervals=None, comments=None): "'method' must be a string - got a '%s'" % type(method) ) - default_name = CFVariableMixin._DEFAULT_NAME + default_name = BaseMetadata.DEFAULT_NAME _coords = [] + if coords is None: pass elif isinstance(coords, Coord): _coords.append(coords.name(token=True)) elif isinstance(coords, str): - _coords.append(CFVariableMixin.token(coords) or default_name) + _coords.append(BaseMetadata.token(coords) or default_name) else: normalise = ( lambda coord: coord.name(token=True) if isinstance(coord, Coord) - else CFVariableMixin.token(coord) or default_name + else BaseMetadata.token(coord) or default_name ) _coords.extend([normalise(coord) for coord in coords]) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 964e56c313..7c28018512 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -9,7 +9,7 @@ """ -from collections import namedtuple, OrderedDict +from collections import OrderedDict from collections.abc import ( Iterable, Container, @@ -29,56 +29,29 @@ import numpy as np import numpy.ma as ma -from iris._cube_coord_common import CFVariableMixin import iris._concatenate import iris._constraints from iris._data_manager import DataManager import iris._lazy_data as _lazy - import iris._merge import iris.analysis from iris.analysis.cartography import wrap_lons import iris.analysis.maths import iris.aux_factory +from iris.common import ( + CFVariableMixin, + CoordMetadata, + CubeMetadata, + DimCoordMetadata, + metadata_manager_factory, +) import iris.coord_systems import iris.coords import iris.exceptions import iris.util -__all__ = ["Cube", "CubeList", "CubeMetadata"] - - -class CubeMetadata( - namedtuple( - "CubeMetadata", - [ - "standard_name", - "long_name", - "var_name", - "units", - "attributes", - "cell_methods", - ], - ) -): - """ - Represents the phenomenon metadata for a single :class:`Cube`. - - """ - - __slots__ = () - - def name(self, default="unknown"): - """ - Returns a human-readable name. - - First it tries self.standard_name, then it tries the 'long_name' - attribute, then the 'var_name' attribute, before falling back to - the value of `default` (which itself defaults to 'unknown'). - - """ - return self.standard_name or self.long_name or self.var_name or default +__all__ = ["Cube", "CubeList"] # The XML namespace to use for CubeML documents @@ -864,6 +837,9 @@ def __init__( if isinstance(data, str): raise TypeError("Invalid data type: {!r}.".format(data)) + # Configure the metadata manager. + self._metadata_manager = metadata_manager_factory(CubeMetadata) + # Initialise the cube data manager. self._data_manager = DataManager(data) @@ -930,43 +906,15 @@ def __init__( self.add_ancillary_variable(ancillary_variable, dims) @property - def metadata(self): + def _names(self): """ - An instance of :class:`CubeMetadata` describing the phenomenon. - - This property can be updated with any of: - - another :class:`CubeMetadata` instance, - - a tuple/dict which can be used to make a :class:`CubeMetadata`, - - or any object providing the attributes exposed by - :class:`CubeMetadata`. + A tuple containing the value of each name participating in the identity + of a :class:`iris.cube.Cube`. This includes the standard name, + long name, NetCDF variable name, and the STASH from the attributes + dictionary. """ - return CubeMetadata( - self.standard_name, - self.long_name, - self.var_name, - self.units, - self.attributes, - self.cell_methods, - ) - - @metadata.setter - def metadata(self, value): - try: - value = CubeMetadata(**value) - except TypeError: - try: - value = CubeMetadata(*value) - except TypeError: - missing_attrs = [ - field - for field in CubeMetadata._fields - if not hasattr(value, field) - ] - if missing_attrs: - raise TypeError("Invalid/incomplete metadata") - for name in CubeMetadata._fields: - setattr(self, name, getattr(value, name)) + return self._metadata_manager._names def is_compatible(self, other, ignore=None): """ @@ -1186,7 +1134,7 @@ def add_cell_measure(self, cell_measure, data_dims=None): data_dims = self._check_multi_dim_metadata(cell_measure, data_dims) self._cell_measures_and_dims.append((cell_measure, data_dims)) self._cell_measures_and_dims.sort( - key=lambda cm_dims: (cm_dims[0]._as_defn(), cm_dims[1]) + key=lambda cm_dims: (cm_dims[0].metadata, cm_dims[1]) ) def add_ancillary_variable(self, ancillary_variable, data_dims=None): @@ -1219,7 +1167,7 @@ def add_ancillary_variable(self, ancillary_variable, data_dims=None): (ancillary_variable, data_dims) ) self._ancillary_variables_and_dims.sort( - key=lambda av_dims: (av_dims[0]._as_defn(), av_dims[1]) + key=lambda av_dims: (av_dims[0].metadata, av_dims[1]) ) def add_dim_coord(self, dim_coord, data_dim): @@ -1304,7 +1252,7 @@ def _remove_coord(self, coord): if coord_ is not coord ] for aux_factory in self.aux_factories: - if coord._as_defn() == aux_factory._as_defn(): + if coord.metadata == aux_factory.metadata: self.remove_aux_factory(aux_factory) def remove_coord(self, coord): @@ -1338,7 +1286,7 @@ def remove_cell_measure(self, cell_measure): (a) a :attr:`standard_name`, :attr:`long_name`, or :attr:`var_name`. Defaults to value of `default` (which itself defaults to `unknown`) as defined in - :class:`iris._cube_coord_common.CFVariableMixin`. + :class:`iris.common.CFVariableMixin`. (b) a cell_measure instance with metadata equal to that of the desired cell_measures. @@ -1431,11 +1379,11 @@ def coord_dims(self, coord): ] # Search derived aux coords - target_defn = coord._as_defn() if not matches: + target_metadata = coord.metadata def match(factory): - return factory._as_defn() == target_defn + return factory.metadata == target_metadata factories = filter(match, self._aux_factories) matches = [ @@ -1591,13 +1539,14 @@ def coords( (a) a :attr:`standard_name`, :attr:`long_name`, or :attr:`var_name`. Defaults to value of `default` (which itself defaults to `unknown`) as defined in - :class:`iris._cube_coord_common.CFVariableMixin`. + :class:`iris.common.CFVariableMixin`. (b) a coordinate instance with metadata equal to that of the desired coordinates. Accepts either a :class:`iris.coords.DimCoord`, :class:`iris.coords.AuxCoord`, - :class:`iris.aux_factory.AuxCoordFactory` - or :class:`iris.coords.CoordDefn`. + :class:`iris.aux_factory.AuxCoordFactory`, + :class:`iris.common.CoordMetadata` or + :class:`iris.common.DimCoordMetadata`. * standard_name The CF standard name of the desired coordinate. If None, does not check for standard name. @@ -1715,14 +1664,17 @@ def attr_filter(coord_): ] if coord is not None: - if isinstance(coord, iris.coords.CoordDefn): - defn = coord + if hasattr(coord, "__class__") and coord.__class__ in ( + CoordMetadata, + DimCoordMetadata, + ): + target_metadata = coord else: - defn = coord._as_defn() + target_metadata = coord.metadata coords_and_factories = [ coord_ for coord_ in coords_and_factories - if coord_._as_defn() == defn + if coord_.metadata == target_metadata ] if contains_dimension is not None: @@ -1888,7 +1840,7 @@ def cell_measures(self, name_or_cell_measure=None): (a) a :attr:`standard_name`, :attr:`long_name`, or :attr:`var_name`. Defaults to value of `default` (which itself defaults to `unknown`) as defined in - :class:`iris._cube_coord_common.CFVariableMixin`. + :class:`iris.common.CFVariableMixin`. (b) a cell_measure instance with metadata equal to that of the desired cell_measures. @@ -1971,7 +1923,7 @@ def ancillary_variables(self, name_or_ancillary_variable=None): (a) a :attr:`standard_name`, :attr:`long_name`, or :attr:`var_name`. Defaults to value of `default` (which itself defaults to `unknown`) as defined in - :class:`iris._cube_coord_common.CFVariableMixin`. + :class:`iris.common.CFVariableMixin`. (b) a ancillary_variable instance with metadata equal to that of the desired ancillary_variables. @@ -2052,11 +2004,13 @@ def cell_methods(self): done on the phenomenon. """ - return self._cell_methods + return self._metadata_manager.cell_methods @cell_methods.setter def cell_methods(self, cell_methods): - self._cell_methods = tuple(cell_methods) if cell_methods else tuple() + self._metadata_manager.cell_methods = ( + tuple(cell_methods) if cell_methods else tuple() + ) def core_data(self): """ @@ -4084,7 +4038,7 @@ def aggregated_by(self, coords, aggregator, **kwargs): ) coords = self._as_list_of_coords(coords) - for coord in sorted(coords, key=lambda coord: coord._as_defn()): + for coord in sorted(coords, key=lambda coord: coord.metadata): if coord.ndim > 1: msg = ( "Cannot aggregate_by coord %s as it is " @@ -4200,7 +4154,7 @@ def aggregated_by(self, coords, aggregator, **kwargs): for coord in groupby.coords: if ( dim_coord is not None - and dim_coord._as_defn() == coord._as_defn() + and dim_coord.metadata == coord.metadata and isinstance(coord, iris.coords.DimCoord) ): aggregateby_cube.add_dim_coord( diff --git a/lib/iris/etc/logging.yaml b/lib/iris/etc/logging.yaml new file mode 100644 index 0000000000..5671916ff9 --- /dev/null +++ b/lib/iris/etc/logging.yaml @@ -0,0 +1,39 @@ +version: 1 + +formatters: + basic: + format: "%(asctime)s %(name)s %(levelname)s - %(message)s" + datefmt: "%d-%m-%Y %H:%M:%S" + basic-cls-func: + format: "%(asctime)s %(name)s %(levelname)s - %(message)s [%(cls)s.%(funcName)s]" + datefmt: "%d-%m-%Y %H:%M:%S" + basic-func: + format: "%(asctime)s %(name)s %(levelname)s - %(message)s [%(funcName)s]" + +handlers: + console: + class: logging.StreamHandler + formatter: basic + stream: ext://sys.stdout + console-cls-func: + class: logging.StreamHandler + formatter: basic-cls-func + stream: ext://sys.stdout + console-func: + class: logging.StreamHandler + formatter: basic-func + stream: ext://sys.stdout + +loggers: + iris.common.metadata: + level: INFO + handlers: [console-cls-func] + propagate: no + iris.common.resolve: + level: INFO + handlers: [console-func] + propagate: no + +root: + level: INFO + handlers: [console] diff --git a/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb b/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb index 5ecfeb77b1..ad2c181b0b 100644 --- a/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb +++ b/lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb @@ -1173,6 +1173,7 @@ fc_extras import numpy.ma as ma import iris.aux_factory + from iris.common.mixin import _get_valid_standard_name import iris.coords import iris.coord_systems import iris.fileformats.cf as cf @@ -1182,7 +1183,6 @@ fc_extras import iris.exceptions import iris.std_names import iris.util - from iris._cube_coord_common import get_valid_standard_name from iris._lazy_data import as_lazy_data @@ -1298,7 +1298,7 @@ fc_extras if standard_name is not None: try: - cube.standard_name = get_valid_standard_name(standard_name) + cube.standard_name = _get_valid_standard_name(standard_name) except ValueError: if cube.long_name is not None: cube.attributes['invalid_standard_name'] = standard_name @@ -1693,7 +1693,7 @@ fc_extras if standard_name is not None: try: - standard_name = get_valid_standard_name(standard_name) + standard_name = _get_valid_standard_name(standard_name) except ValueError: if long_name is not None: attributes['invalid_standard_name'] = standard_name diff --git a/lib/iris/iterate.py b/lib/iris/iterate.py index 6cca135d21..ea2d939280 100644 --- a/lib/iris/iterate.py +++ b/lib/iris/iterate.py @@ -302,12 +302,13 @@ def __init__(self, coord): self._coord = coord # Methods of contained class we need to expose/use. - def _as_defn(self): - return self._coord._as_defn() + @property + def metadata(self): + return self._coord.metadata - # Methods of contained class we want to overide/customise. + # Methods of contained class we want to override/customise. def __eq__(self, other): - return self._coord._as_defn() == other._as_defn() + return self._coord.metadata == other.metadata # Force use of __eq__ for set operations. def __hash__(self): diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 9dff582bc4..36afe906dc 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -168,7 +168,7 @@ def guess_axis(coord): if isinstance(coord, iris.coords.DimCoord) ] if aux_coords: - aux_coords.sort(key=lambda coord: coord._as_defn()) + aux_coords.sort(key=lambda coord: coord.metadata) coords[dim] = aux_coords[0] # If plotting a 2 dimensional plot, check for 2d coordinates @@ -183,7 +183,7 @@ def guess_axis(coord): coord for coord in two_dim_coords if coord.ndim == 2 ] if len(two_dim_coords) >= 2: - two_dim_coords.sort(key=lambda coord: coord._as_defn()) + two_dim_coords.sort(key=lambda coord: coord.metadata) coords = two_dim_coords[:2] if mode == iris.coords.POINT_MODE: diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index 9132e16680..b5b80a97ef 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -21,6 +21,7 @@ import codecs import collections +from collections.abc import Mapping import contextlib import datetime import difflib @@ -1004,6 +1005,78 @@ def assertArrayShapeStats(self, result, shape, mean, std_dev, rtol=1e-6): self.assertArrayAllClose(result.data.mean(), mean, rtol=rtol) self.assertArrayAllClose(result.data.std(), std_dev, rtol=rtol) + def assertDictEqual(self, lhs, rhs, msg=None): + """ + This method overrides unittest.TestCase.assertDictEqual (new in Python3.1) + in order to cope with dictionary comparison where the value of a key may + be a numpy array. + + """ + if not isinstance(lhs, Mapping): + emsg = ( + f"Provided LHS argument is not a 'Mapping', got {type(lhs)}." + ) + self.fail(emsg) + + if not isinstance(rhs, Mapping): + emsg = ( + f"Provided RHS argument is not a 'Mapping', got {type(rhs)}." + ) + self.fail(emsg) + + if set(lhs.keys()) != set(rhs.keys()): + emsg = f"{lhs!r} != {rhs!r}." + self.fail(emsg) + + for key in lhs.keys(): + lvalue, rvalue = lhs[key], rhs[key] + + if ma.isMaskedArray(lvalue) or ma.isMaskedArray(rvalue): + if not ma.isMaskedArray(lvalue): + emsg = ( + f"Dictionary key {key!r} values are not equal, " + f"the LHS value has type {type(lvalue)} and " + f"the RHS value has type {ma.core.MaskedArray}." + ) + raise AssertionError(emsg) + + if not ma.isMaskedArray(rvalue): + emsg = ( + f"Dictionary key {key!r} values are not equal, " + f"the LHS value has type {ma.core.MaskedArray} and " + f"the RHS value has type {type(lvalue)}." + ) + raise AssertionError(emsg) + + self.assertMaskedArrayEqual(lvalue, rvalue) + elif isinstance(lvalue, np.ndarray) or isinstance( + rvalue, np.ndarray + ): + if not isinstance(lvalue, np.ndarray): + emsg = ( + f"Dictionary key {key!r} values are not equal, " + f"the LHS value has type {type(lvalue)} and " + f"the RHS value has type {np.ndarray}." + ) + raise AssertionError(emsg) + + if not isinstance(rvalue, np.ndarray): + emsg = ( + f"Dictionary key {key!r} values are not equal, " + f"the LHS value has type {np.ndarray} and " + f"the RHS value has type {type(rvalue)}." + ) + raise AssertionError(emsg) + + self.assertArrayEqual(lvalue, rvalue) + else: + if lvalue != rvalue: + emsg = ( + f"Dictionary key {key!r} values are not equal, " + f"{lvalue!r} != {rvalue!r}." + ) + raise AssertionError(emsg) + # An environment variable controls whether test timings are output. # diff --git a/lib/iris/tests/integration/fast_load/test_fast_load.py b/lib/iris/tests/integration/fast_load/test_fast_load.py index 0a4d186b39..ba50e389a8 100644 --- a/lib/iris/tests/integration/fast_load/test_fast_load.py +++ b/lib/iris/tests/integration/fast_load/test_fast_load.py @@ -9,7 +9,7 @@ # before importing anything else. import iris.tests as tests -from collections import Iterable +from collections.abc import Iterable import tempfile import shutil @@ -377,7 +377,8 @@ def callback(cube, collation, filename): # Make an 'expected' from selected fields, with the expected attribute. expected = CubeList([flds[1], flds[3]]).merge() if not self.do_fast_loads: - expected[0].attributes["LBVC"] = 8 + # This is actually a NumPy int32, so honour that here. + expected[0].attributes["LBVC"] = np.int32(8) else: expected[0].attributes["A_LBVC"] = [8, 8] diff --git a/lib/iris/tests/results/analysis/abs.cml b/lib/iris/tests/results/analysis/abs.cml index e92f96e1cb..b0a37b6074 100644 --- a/lib/iris/tests/results/analysis/abs.cml +++ b/lib/iris/tests/results/analysis/abs.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/addition.cml b/lib/iris/tests/results/analysis/addition.cml index d673e73bb3..4f9600694d 100644 --- a/lib/iris/tests/results/analysis/addition.cml +++ b/lib/iris/tests/results/analysis/addition.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/addition_coord_x.cml b/lib/iris/tests/results/analysis/addition_coord_x.cml index af0c5ecc91..a086b8ad8b 100644 --- a/lib/iris/tests/results/analysis/addition_coord_x.cml +++ b/lib/iris/tests/results/analysis/addition_coord_x.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/addition_coord_y.cml b/lib/iris/tests/results/analysis/addition_coord_y.cml index ba8547b617..266e81c912 100644 --- a/lib/iris/tests/results/analysis/addition_coord_y.cml +++ b/lib/iris/tests/results/analysis/addition_coord_y.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/addition_different_std_name.cml b/lib/iris/tests/results/analysis/addition_different_std_name.cml index cb77adde99..14b0b42dd8 100644 --- a/lib/iris/tests/results/analysis/addition_different_std_name.cml +++ b/lib/iris/tests/results/analysis/addition_different_std_name.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/addition_in_place.cml b/lib/iris/tests/results/analysis/addition_in_place.cml index d673e73bb3..4f9600694d 100644 --- a/lib/iris/tests/results/analysis/addition_in_place.cml +++ b/lib/iris/tests/results/analysis/addition_in_place.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/addition_in_place_coord.cml b/lib/iris/tests/results/analysis/addition_in_place_coord.cml index 6ec39571c1..00dee609eb 100644 --- a/lib/iris/tests/results/analysis/addition_in_place_coord.cml +++ b/lib/iris/tests/results/analysis/addition_in_place_coord.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/addition_scalar.cml b/lib/iris/tests/results/analysis/addition_scalar.cml index d65d7492fe..daf0050069 100644 --- a/lib/iris/tests/results/analysis/addition_scalar.cml +++ b/lib/iris/tests/results/analysis/addition_scalar.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/apply_ifunc.cml b/lib/iris/tests/results/analysis/apply_ifunc.cml index f2bac40826..fe0e394ee6 100644 --- a/lib/iris/tests/results/analysis/apply_ifunc.cml +++ b/lib/iris/tests/results/analysis/apply_ifunc.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/apply_ifunc_frompyfunc.cml b/lib/iris/tests/results/analysis/apply_ifunc_frompyfunc.cml index 2faa06f4a5..29cb6f611e 100644 --- a/lib/iris/tests/results/analysis/apply_ifunc_frompyfunc.cml +++ b/lib/iris/tests/results/analysis/apply_ifunc_frompyfunc.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/apply_ufunc.cml b/lib/iris/tests/results/analysis/apply_ufunc.cml index f2bac40826..fe0e394ee6 100644 --- a/lib/iris/tests/results/analysis/apply_ufunc.cml +++ b/lib/iris/tests/results/analysis/apply_ufunc.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/apply_ufunc_frompyfunc.cml b/lib/iris/tests/results/analysis/apply_ufunc_frompyfunc.cml index d4239acbad..7b1511f028 100644 --- a/lib/iris/tests/results/analysis/apply_ufunc_frompyfunc.cml +++ b/lib/iris/tests/results/analysis/apply_ufunc_frompyfunc.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/division.cml b/lib/iris/tests/results/analysis/division.cml index bbe6c1eb90..762f51ec0a 100644 --- a/lib/iris/tests/results/analysis/division.cml +++ b/lib/iris/tests/results/analysis/division.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/division_by_array.cml b/lib/iris/tests/results/analysis/division_by_array.cml index cb77adde99..14b0b42dd8 100644 --- a/lib/iris/tests/results/analysis/division_by_array.cml +++ b/lib/iris/tests/results/analysis/division_by_array.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/division_by_latitude.cml b/lib/iris/tests/results/analysis/division_by_latitude.cml index 3e2abf69cd..42437d1e36 100644 --- a/lib/iris/tests/results/analysis/division_by_latitude.cml +++ b/lib/iris/tests/results/analysis/division_by_latitude.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/division_by_longitude.cml b/lib/iris/tests/results/analysis/division_by_longitude.cml index b1a0228dc8..264ce9b793 100644 --- a/lib/iris/tests/results/analysis/division_by_longitude.cml +++ b/lib/iris/tests/results/analysis/division_by_longitude.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/division_by_singular_coord.cml b/lib/iris/tests/results/analysis/division_by_singular_coord.cml index 7f7835a1be..4c9c58d760 100644 --- a/lib/iris/tests/results/analysis/division_by_singular_coord.cml +++ b/lib/iris/tests/results/analysis/division_by_singular_coord.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/division_scalar.cml b/lib/iris/tests/results/analysis/division_scalar.cml index cb77adde99..14b0b42dd8 100644 --- a/lib/iris/tests/results/analysis/division_scalar.cml +++ b/lib/iris/tests/results/analysis/division_scalar.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/exponentiate.cml b/lib/iris/tests/results/analysis/exponentiate.cml index a13c6be151..bb825f6714 100644 --- a/lib/iris/tests/results/analysis/exponentiate.cml +++ b/lib/iris/tests/results/analysis/exponentiate.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/log.cml b/lib/iris/tests/results/analysis/log.cml index 33214d01f1..c24e071dc5 100644 --- a/lib/iris/tests/results/analysis/log.cml +++ b/lib/iris/tests/results/analysis/log.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/log10.cml b/lib/iris/tests/results/analysis/log10.cml index fbee8f73f0..abd4065526 100644 --- a/lib/iris/tests/results/analysis/log10.cml +++ b/lib/iris/tests/results/analysis/log10.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/log2.cml b/lib/iris/tests/results/analysis/log2.cml index 6371f3925b..d121ad9a9d 100644 --- a/lib/iris/tests/results/analysis/log2.cml +++ b/lib/iris/tests/results/analysis/log2.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/multiply.cml b/lib/iris/tests/results/analysis/multiply.cml index 44996a9138..8fb8658f5d 100644 --- a/lib/iris/tests/results/analysis/multiply.cml +++ b/lib/iris/tests/results/analysis/multiply.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/multiply_different_std_name.cml b/lib/iris/tests/results/analysis/multiply_different_std_name.cml index 49f1779b77..2d89e5882f 100644 --- a/lib/iris/tests/results/analysis/multiply_different_std_name.cml +++ b/lib/iris/tests/results/analysis/multiply_different_std_name.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/sqrt.cml b/lib/iris/tests/results/analysis/sqrt.cml index 3a7bff138c..0dd0fe20b3 100644 --- a/lib/iris/tests/results/analysis/sqrt.cml +++ b/lib/iris/tests/results/analysis/sqrt.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/subtract.cml b/lib/iris/tests/results/analysis/subtract.cml index 7b0740888d..3466578756 100644 --- a/lib/iris/tests/results/analysis/subtract.cml +++ b/lib/iris/tests/results/analysis/subtract.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/subtract_array.cml b/lib/iris/tests/results/analysis/subtract_array.cml index cb77adde99..14b0b42dd8 100644 --- a/lib/iris/tests/results/analysis/subtract_array.cml +++ b/lib/iris/tests/results/analysis/subtract_array.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/subtract_coord_x.cml b/lib/iris/tests/results/analysis/subtract_coord_x.cml index c7aee8395b..060814c6ba 100644 --- a/lib/iris/tests/results/analysis/subtract_coord_x.cml +++ b/lib/iris/tests/results/analysis/subtract_coord_x.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/subtract_coord_y.cml b/lib/iris/tests/results/analysis/subtract_coord_y.cml index 355692b27b..4a9351cf6f 100644 --- a/lib/iris/tests/results/analysis/subtract_coord_y.cml +++ b/lib/iris/tests/results/analysis/subtract_coord_y.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/analysis/subtract_scalar.cml b/lib/iris/tests/results/analysis/subtract_scalar.cml index ab8e9d0d60..f458364143 100644 --- a/lib/iris/tests/results/analysis/subtract_scalar.cml +++ b/lib/iris/tests/results/analysis/subtract_scalar.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_all_dims.cml b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_all_dims.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_all_dims.cml +++ b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_all_dims.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_last_dims.cml b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_last_dims.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_last_dims.cml +++ b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_last_dims.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_middle_dim.cml b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_middle_dim.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_middle_dim.cml +++ b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_middle_dim.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_zeroth_dim.cml b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_zeroth_dim.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_zeroth_dim.cml +++ b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/collapse_zeroth_dim.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/slice.cml b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/slice.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/slice.cml +++ b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/slice.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/transposed.cml b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/transposed.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/transposed.cml +++ b/lib/iris/tests/results/unit/analysis/maths/add/TestBroadcasting/transposed.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_all_dims.cml b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_all_dims.cml index 940661c230..d4a90d37ac 100644 --- a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_all_dims.cml +++ b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_all_dims.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_last_dims.cml b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_last_dims.cml index 940661c230..d4a90d37ac 100644 --- a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_last_dims.cml +++ b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_last_dims.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_middle_dim.cml b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_middle_dim.cml index 940661c230..d4a90d37ac 100644 --- a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_middle_dim.cml +++ b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_middle_dim.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_zeroth_dim.cml b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_zeroth_dim.cml index 940661c230..d4a90d37ac 100644 --- a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_zeroth_dim.cml +++ b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/collapse_zeroth_dim.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/slice.cml b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/slice.cml index 940661c230..d4a90d37ac 100644 --- a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/slice.cml +++ b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/slice.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/transposed.cml b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/transposed.cml index 940661c230..d4a90d37ac 100644 --- a/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/transposed.cml +++ b/lib/iris/tests/results/unit/analysis/maths/divide/TestBroadcasting/transposed.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_all_dims.cml b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_all_dims.cml index b646e8b550..7ae36e51c3 100644 --- a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_all_dims.cml +++ b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_all_dims.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_last_dims.cml b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_last_dims.cml index b646e8b550..7ae36e51c3 100644 --- a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_last_dims.cml +++ b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_last_dims.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_middle_dim.cml b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_middle_dim.cml index b646e8b550..7ae36e51c3 100644 --- a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_middle_dim.cml +++ b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_middle_dim.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_zeroth_dim.cml b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_zeroth_dim.cml index b646e8b550..7ae36e51c3 100644 --- a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_zeroth_dim.cml +++ b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/collapse_zeroth_dim.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/slice.cml b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/slice.cml index b646e8b550..7ae36e51c3 100644 --- a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/slice.cml +++ b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/slice.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/transposed.cml b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/transposed.cml index b646e8b550..7ae36e51c3 100644 --- a/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/transposed.cml +++ b/lib/iris/tests/results/unit/analysis/maths/multiply/TestBroadcasting/transposed.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_all_dims.cml b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_all_dims.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_all_dims.cml +++ b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_all_dims.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_last_dims.cml b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_last_dims.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_last_dims.cml +++ b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_last_dims.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_middle_dim.cml b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_middle_dim.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_middle_dim.cml +++ b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_middle_dim.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_zeroth_dim.cml b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_zeroth_dim.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_zeroth_dim.cml +++ b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/collapse_zeroth_dim.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/slice.cml b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/slice.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/slice.cml +++ b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/slice.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/transposed.cml b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/transposed.cml index c6e6271a63..bea6795b38 100644 --- a/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/transposed.cml +++ b/lib/iris/tests/results/unit/analysis/maths/subtract/TestBroadcasting/transposed.cml @@ -1,6 +1,9 @@ + + + diff --git a/lib/iris/tests/test_basic_maths.py b/lib/iris/tests/test_basic_maths.py index 94880d6ed1..a559ee0e8a 100644 --- a/lib/iris/tests/test_basic_maths.py +++ b/lib/iris/tests/test_basic_maths.py @@ -235,7 +235,11 @@ def test_addition_different_attributes(self): b.attributes["my attribute"] = "foobar" c = a + b self.assertIsNone(c.standard_name) - self.assertEqual(c.attributes, {}) + expected = { + "my attribute": "foobar", + "source": "Data from Met Office Unified Model", + } + self.assertEqual(expected, c.attributes) def test_apply_ufunc(self): a = self.cube @@ -344,10 +348,13 @@ def test_ifunc_call_fail(self): my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units ** 2) - # should fail because giving 2 arguments to an ifunc that expects - # only one - with self.assertRaises(ValueError): - my_ifunc(a, a) + # should now NOT fail because giving 2 arguments to an ifunc that + # expects only one will now ignore the surplus argument and raise + # a logging message instead, and go on to perform the operation. + emsg = "ValueError not raised" + with self.assertRaisesRegex(AssertionError, emsg): + with self.assertRaises(ValueError): + my_ifunc(a, a) my_ifunc = iris.analysis.maths.IFunc( np.multiply, lambda a: cf_units.Unit("1") @@ -509,7 +516,11 @@ def test_multiplication_different_attributes(self): b.attributes["my attribute"] = "foobar" c = a * b self.assertIsNone(c.standard_name) - self.assertEqual(c.attributes, {}) + expected = { + "source": "Data from Met Office Unified Model", + "my attribute": "foobar", + } + self.assertEqual(expected, c.attributes) def test_multiplication_in_place(self): a = self.cube.copy() diff --git a/lib/iris/tests/test_cdm.py b/lib/iris/tests/test_cdm.py index ab27ad6040..bbaae1a8de 100644 --- a/lib/iris/tests/test_cdm.py +++ b/lib/iris/tests/test_cdm.py @@ -1022,14 +1022,6 @@ def test_metadata_fail(self): (), ) with self.assertRaises(TypeError): - self.t.metadata = { - "standard_name": "air_pressure", - "long_name": "foo", - "var_name": "bar", - "units": "", - "attributes": {"random": "12"}, - } - with self.assertRaises(TypeError): class Metadata: pass diff --git a/lib/iris/tests/test_coord_api.py b/lib/iris/tests/test_coord_api.py index 053b6b509b..bdc6fcc609 100644 --- a/lib/iris/tests/test_coord_api.py +++ b/lib/iris/tests/test_coord_api.py @@ -944,11 +944,11 @@ def test_circular(self): r.circular = False self.assertTrue(r.is_compatible(self.dim_coord)) - def test_defn(self): - coord_defn = self.aux_coord._as_defn() - self.assertTrue(self.aux_coord.is_compatible(coord_defn)) - coord_defn = self.dim_coord._as_defn() - self.assertTrue(self.dim_coord.is_compatible(coord_defn)) + def test_metadata(self): + metadata = self.aux_coord.metadata + self.assertTrue(self.aux_coord.is_compatible(metadata)) + metadata = self.dim_coord.metadata + self.assertTrue(self.dim_coord.is_compatible(metadata)) def test_is_ignore(self): r = self.aux_coord.copy() diff --git a/lib/iris/tests/unit/cube_coord_common/__init__.py b/lib/iris/tests/unit/common/__init__.py similarity index 75% rename from lib/iris/tests/unit/cube_coord_common/__init__.py rename to lib/iris/tests/unit/common/__init__.py index 4390f95921..5380785042 100644 --- a/lib/iris/tests/unit/cube_coord_common/__init__.py +++ b/lib/iris/tests/unit/common/__init__.py @@ -3,4 +3,4 @@ # 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 # licensing details. -"""Unit tests for the :mod:`iris._cube_coord_common` module.""" +"""Unit tests for the :mod:`iris.common` module.""" diff --git a/lib/iris/tests/unit/common/lenient/__init__.py b/lib/iris/tests/unit/common/lenient/__init__.py new file mode 100644 index 0000000000..2a99e7a4c2 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/__init__.py @@ -0,0 +1,6 @@ +# 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 +# licensing details. +"""Unit tests for the :mod:`iris.common.lenient` package.""" diff --git a/lib/iris/tests/unit/common/lenient/test_Lenient.py b/lib/iris/tests/unit/common/lenient/test_Lenient.py new file mode 100644 index 0000000000..8ca98342ca --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test_Lenient.py @@ -0,0 +1,182 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.lenient.Lenient`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from unittest.mock import sentinel + +from iris.common.lenient import Lenient, _LENIENT + + +class Test___init__(tests.IrisTest): + def test_default(self): + lenient = Lenient() + expected = dict(maths=True) + self.assertEqual(expected, lenient.__dict__) + + def test_kwargs(self): + lenient = Lenient(maths=False) + expected = dict(maths=False) + self.assertEqual(expected, lenient.__dict__) + + def test_kwargs_invalid(self): + emsg = "Invalid .* option, got 'merge'." + with self.assertRaisesRegex(KeyError, emsg): + _ = Lenient(merge=True) + + +class Test___contains__(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test_in(self): + self.assertTrue("maths", self.lenient) + + def test_not_in(self): + self.assertTrue(("concatenate", self.lenient)) + + +class Test___getitem__(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test_in(self): + self.assertTrue(self.lenient["maths"]) + + def test_not_in(self): + emsg = "Invalid .* option, got 'MATHS'." + with self.assertRaisesRegex(KeyError, emsg): + _ = self.lenient["MATHS"] + + +class Test___repr__(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test(self): + expected = "Lenient(maths=True)" + self.assertEqual(expected, repr(self.lenient)) + + +class Test___setitem__(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test_key_invalid(self): + emsg = "Invalid .* option, got 'MATHS." + with self.assertRaisesRegex(KeyError, emsg): + self.lenient["MATHS"] = False + + def test_maths_value_invalid(self): + value = sentinel.value + emsg = f"Invalid .* option 'maths' value, got {value!r}." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient["maths"] = value + + def test_maths_disable__lenient_enable_true(self): + self.assertTrue(_LENIENT.enable) + self.lenient["maths"] = False + self.assertFalse(self.lenient.__dict__["maths"]) + self.assertFalse(_LENIENT.enable) + + def test_maths_disable__lenient_enable_false(self): + _LENIENT.__dict__["enable"] = False + self.assertFalse(_LENIENT.enable) + self.lenient["maths"] = False + self.assertFalse(self.lenient.__dict__["maths"]) + self.assertFalse(_LENIENT.enable) + + def test_maths_enable__lenient_enable_true(self): + self.assertTrue(_LENIENT.enable) + self.lenient["maths"] = True + self.assertTrue(self.lenient.__dict__["maths"]) + self.assertTrue(_LENIENT.enable) + + def test_maths_enable__lenient_enable_false(self): + _LENIENT.__dict__["enable"] = False + self.assertFalse(_LENIENT.enable) + self.lenient["maths"] = True + self.assertTrue(self.lenient.__dict__["maths"]) + self.assertTrue(_LENIENT.enable) + + +class Test_context(tests.IrisTest): + def setUp(self): + self.lenient = Lenient() + + def test_nop(self): + self.assertTrue(self.lenient["maths"]) + + with self.lenient.context(): + self.assertTrue(self.lenient["maths"]) + + self.assertTrue(self.lenient["maths"]) + + def test_maths_disable__lenient_true(self): + # synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + with self.lenient.context(maths=False): + # still synchronised + self.assertFalse(_LENIENT.enable) + self.assertFalse(self.lenient["maths"]) + + # still synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + def test_maths_disable__lenient_false(self): + # not synchronised + _LENIENT.__dict__["enable"] = False + self.assertFalse(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + with self.lenient.context(maths=False): + # now synchronised + self.assertFalse(_LENIENT.enable) + self.assertFalse(self.lenient["maths"]) + + # still synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + def test_maths_enable__lenient_true(self): + # not synchronised + self.assertTrue(_LENIENT.enable) + self.lenient.__dict__["maths"] = False + self.assertFalse(self.lenient["maths"]) + + with self.lenient.context(maths=True): + # now synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + # still synchronised + self.assertFalse(_LENIENT.enable) + self.assertFalse(self.lenient["maths"]) + + def test_maths_enable__lenient_false(self): + # synchronised + _LENIENT.__dict__["enable"] = False + self.assertFalse(_LENIENT.enable) + self.lenient.__dict__["maths"] = False + self.assertFalse(self.lenient["maths"]) + + with self.lenient.context(maths=True): + # still synchronised + self.assertTrue(_LENIENT.enable) + self.assertTrue(self.lenient["maths"]) + + # still synchronised + self.assertFalse(_LENIENT.enable) + self.assertFalse(self.lenient["maths"]) diff --git a/lib/iris/tests/unit/common/lenient/test__Lenient.py b/lib/iris/tests/unit/common/lenient/test__Lenient.py new file mode 100644 index 0000000000..d6bc2882d6 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test__Lenient.py @@ -0,0 +1,835 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.lenient._Lenient`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from collections.abc import Iterable + +from iris.common.lenient import ( + _LENIENT_ENABLE_DEFAULT, + _LENIENT_PROTECTED, + _Lenient, + _qualname, +) + + +class Test___init__(tests.IrisTest): + def setUp(self): + self.expected = dict(active=None, enable=_LENIENT_ENABLE_DEFAULT) + + def test_default(self): + lenient = _Lenient() + self.assertEqual(self.expected, lenient.__dict__) + + def test_args_service_str(self): + service = "service1" + lenient = _Lenient(service) + self.expected.update(dict(service1=True)) + self.assertEqual(self.expected, lenient.__dict__) + + def test_args_services_str(self): + services = ("service1", "service2") + lenient = _Lenient(*services) + self.expected.update(dict(service1=True, service2=True)) + self.assertEqual(self.expected, lenient.__dict__) + + def test_args_services_callable(self): + def service1(): + pass + + def service2(): + pass + + services = (service1, service2) + lenient = _Lenient(*services) + self.expected.update( + {_qualname(service1): True, _qualname(service2): True,} + ) + self.assertEqual(self.expected, lenient.__dict__) + + def test_kwargs_client_str(self): + client = dict(client1="service1") + lenient = _Lenient(**client) + self.expected.update(dict(client1=("service1",))) + self.assertEqual(self.expected, lenient.__dict__) + + def test_kwargs_clients_str(self): + clients = dict(client1="service1", client2="service2") + lenient = _Lenient(**clients) + self.expected.update( + dict(client1=("service1",), client2=("service2",)) + ) + self.assertEqual(self.expected, lenient.__dict__) + + def test_kwargs_clients_callable(self): + def client1(): + pass + + def client2(): + pass + + def service1(): + pass + + def service2(): + pass + + qualname_client1 = _qualname(client1) + qualname_client2 = _qualname(client2) + clients = { + qualname_client1: service1, + qualname_client2: (service1, service2), + } + lenient = _Lenient(**clients) + self.expected.update( + { + _qualname(client1): (_qualname(service1),), + _qualname(client2): (_qualname(service1), _qualname(service2)), + } + ) + self.assertEqual(self.expected, lenient.__dict__) + + +class Test___call__(tests.IrisTest): + def setUp(self): + self.client = "myclient" + self.lenient = _Lenient() + + def test_missing_service_str(self): + self.assertFalse(self.lenient("myservice")) + + def test_missing_service_callable(self): + def myservice(): + pass + + self.assertFalse(self.lenient(myservice)) + + def test_disabled_service_str(self): + service = "myservice" + self.lenient.__dict__[service] = False + self.assertFalse(self.lenient(service)) + + def test_disable_service_callable(self): + def myservice(): + pass + + qualname_service = _qualname(myservice) + self.lenient.__dict__[qualname_service] = False + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_no_active_client(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.assertFalse(self.lenient(service)) + + def test_service_callable_with_no_active_client(self): + def myservice(): + pass + + qualname_service = _qualname(myservice) + self.lenient.__dict__[qualname_service] = True + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_active_client_with_no_registered_services(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.assertFalse(self.lenient(service)) + + def test_service_callable_with_active_client_with_no_registered_services( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = _qualname(myclient) + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_active_client_with_unmatched_registered_services( + self, + ): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = ("service1", "service2") + self.assertFalse(self.lenient(service)) + + def test_service_callable_with_active_client_with_unmatched_registered_services( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + qualname_client = _qualname(myclient) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = qualname_client + self.lenient.__dict__[qualname_client] = ("service1", "service2") + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_active_client_with_registered_services(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = ("service1", "service2", service) + self.assertTrue(self.lenient(service)) + + def test_service_callable_with_active_client_with_registered_services( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + qualname_client = _qualname(myclient) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = qualname_client + self.lenient.__dict__[qualname_client] = ( + "service1", + "service2", + qualname_service, + ) + self.assertTrue(self.lenient(myservice)) + + def test_service_str_with_active_client_with_unmatched_registered_service_str( + self, + ): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = "serviceXXX" + self.assertFalse(self.lenient(service)) + + def test_service_callable_with_active_client_with_unmatched_registered_service_str( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + qualname_client = _qualname(myclient) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = qualname_client + self.lenient.__dict__[qualname_client] = f"{qualname_service}XXX" + self.assertFalse(self.lenient(myservice)) + + def test_service_str_with_active_client_with_registered_service_str(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = service + self.assertTrue(self.lenient(service)) + + def test_service_callable_with_active_client_with_registered_service_str( + self, + ): + def myservice(): + pass + + def myclient(): + pass + + qualname_service = _qualname(myservice) + qualname_client = _qualname(myclient) + self.lenient.__dict__[qualname_service] = True + self.lenient.__dict__["active"] = qualname_client + self.lenient.__dict__[qualname_client] = qualname_service + self.assertTrue(self.lenient(myservice)) + + def test_enable(self): + service = "myservice" + self.lenient.__dict__[service] = True + self.lenient.__dict__["active"] = self.client + self.lenient.__dict__[self.client] = service + self.assertTrue(self.lenient(service)) + self.lenient.__dict__["enable"] = False + self.assertFalse(self.lenient(service)) + + +class Test___contains__(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_in(self): + self.assertIn("active", self.lenient) + + def test_not_in(self): + self.assertNotIn("ACTIVATE", self.lenient) + + def test_in_qualname(self): + def func(): + pass + + qualname_func = _qualname(func) + lenient = _Lenient() + lenient.__dict__[qualname_func] = None + self.assertIn(func, lenient) + self.assertIn(qualname_func, lenient) + + +class Test___getattr__(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_in(self): + self.assertIsNone(self.lenient.active) + + def test_not_in(self): + emsg = "Invalid .* option, got 'wibble'." + with self.assertRaisesRegex(AttributeError, emsg): + _ = self.lenient.wibble + + +class Test__getitem__(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_in(self): + self.assertIsNone(self.lenient["active"]) + + def test_in_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + self.lenient.__dict__[qualname_service] = True + self.assertTrue(self.lenient[service]) + + def test_not_in(self): + emsg = "Invalid .* option, got 'wibble'." + with self.assertRaisesRegex(KeyError, emsg): + _ = self.lenient["wibble"] + + def test_not_in_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + emsg = f"Invalid .* option, got '{qualname_service}'." + with self.assertRaisesRegex(KeyError, emsg): + _ = self.lenient[service] + + +class Test___setitem__(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_not_in(self): + emsg = "Invalid .* option, got 'wibble'." + with self.assertRaisesRegex(KeyError, emsg): + self.lenient["wibble"] = None + + def test_in_value_str(self): + client = "client" + service = "service" + self.lenient.__dict__[client] = None + self.lenient[client] = service + self.assertEqual(self.lenient.__dict__[client], (service,)) + + def test_callable_in_value_str(self): + def client(): + pass + + service = "service" + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = None + self.lenient[client] = service + self.assertEqual(self.lenient.__dict__[qualname_client], (service,)) + + def test_in_value_callable(self): + def service(): + pass + + client = "client" + qualname_service = _qualname(service) + self.lenient.__dict__[client] = None + self.lenient[client] = service + self.assertEqual(self.lenient.__dict__[client], (qualname_service,)) + + def test_callable_in_value_callable(self): + def client(): + pass + + def service(): + pass + + qualname_client = _qualname(client) + qualname_service = _qualname(service) + self.lenient.__dict__[qualname_client] = None + self.lenient[client] = service + self.assertEqual( + self.lenient.__dict__[qualname_client], (qualname_service,) + ) + + def test_in_value_bool(self): + client = "client" + self.lenient.__dict__[client] = None + self.lenient[client] = True + self.assertTrue(self.lenient.__dict__[client]) + self.assertFalse(isinstance(self.lenient.__dict__[client], Iterable)) + + def test_callable_in_value_bool(self): + def client(): + pass + + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = None + self.lenient[client] = True + self.assertTrue(self.lenient.__dict__[qualname_client]) + self.assertFalse( + isinstance(self.lenient.__dict__[qualname_client], Iterable) + ) + + def test_in_value_iterable(self): + client = "client" + services = ("service1", "service2") + self.lenient.__dict__[client] = None + self.lenient[client] = services + self.assertEqual(self.lenient.__dict__[client], services) + + def test_callable_in_value_iterable(self): + def client(): + pass + + qualname_client = _qualname(client) + services = ("service1", "service2") + self.lenient.__dict__[qualname_client] = None + self.lenient[client] = services + self.assertEqual(self.lenient.__dict__[qualname_client], services) + + def test_in_value_iterable_callable(self): + def service1(): + pass + + def service2(): + pass + + client = "client" + self.lenient.__dict__[client] = None + qualname_services = (_qualname(service1), _qualname(service2)) + self.lenient[client] = (service1, service2) + self.assertEqual(self.lenient.__dict__[client], qualname_services) + + def test_callable_in_value_iterable_callable(self): + def client(): + pass + + def service1(): + pass + + def service2(): + pass + + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = None + qualname_services = (_qualname(service1), _qualname(service2)) + self.lenient[client] = (service1, service2) + self.assertEqual( + self.lenient.__dict__[qualname_client], qualname_services + ) + + def test_active_iterable(self): + active = "active" + self.assertIsNone(self.lenient.__dict__[active]) + emsg = "Invalid .* option 'active'" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient[active] = (None,) + + def test_active_str(self): + active = "active" + client = "client1" + self.assertIsNone(self.lenient.__dict__[active]) + self.lenient[active] = client + self.assertEqual(self.lenient.__dict__[active], client) + + def test_active_callable(self): + def client(): + pass + + active = "active" + qualname_client = _qualname(client) + self.assertIsNone(self.lenient.__dict__[active]) + self.lenient[active] = client + self.assertEqual(self.lenient.__dict__[active], qualname_client) + + def test_enable(self): + enable = "enable" + self.assertEqual( + self.lenient.__dict__[enable], _LENIENT_ENABLE_DEFAULT + ) + self.lenient[enable] = True + self.assertTrue(self.lenient.__dict__[enable]) + self.lenient[enable] = False + self.assertFalse(self.lenient.__dict__[enable]) + + def test_enable_invalid(self): + emsg = "Invalid .* option 'enable'" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient["enable"] = None + + +class Test_context(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + self.default = dict(active=None, enable=_LENIENT_ENABLE_DEFAULT) + + def copy(self): + return self.lenient.__dict__.copy() + + def test_nop(self): + pre = self.copy() + with self.lenient.context(): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + self.assertEqual(context, self.default) + self.assertEqual(post, self.default) + + def test_active_str(self): + client = "client" + pre = self.copy() + with self.lenient.context(active=client): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=client)) + self.assertEqual(context, expected) + self.assertEqual(post, self.default) + + def test_active_callable(self): + def client(): + pass + + pre = self.copy() + with self.lenient.context(active=client): + context = self.copy() + post = self.copy() + qualname_client = _qualname(client) + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=qualname_client)) + self.assertEqual(context, expected) + self.assertEqual(post, self.default) + + def test_kwargs(self): + client = "client" + self.lenient.__dict__["service1"] = False + self.lenient.__dict__["service2"] = False + pre = self.copy() + with self.lenient.context(active=client, service1=True, service2=True): + context = self.copy() + post = self.copy() + self.default.update(dict(service1=False, service2=False)) + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=client, service1=True, service2=True)) + self.assertEqual(context, expected) + self.assertEqual(post, self.default) + + def test_args_str(self): + client = "client" + services = ("service1", "service2") + pre = self.copy() + with self.lenient.context(*services, active=client): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=client, client=services)) + self.assertEqual(context["active"], expected["active"]) + self.assertEqual(set(context["client"]), set(expected["client"])) + self.assertEqual(post, self.default) + + def test_args_callable(self): + def service1(): + pass + + def service2(): + pass + + client = "client" + services = (service1, service2) + pre = self.copy() + with self.lenient.context(*services, active=client): + context = self.copy() + post = self.copy() + qualname_services = tuple([_qualname(service) for service in services]) + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active=client, client=qualname_services)) + self.assertEqual(context["active"], expected["active"]) + self.assertEqual(set(context["client"]), set(expected["client"])) + self.assertEqual(post, self.default) + + def test_context_runtime(self): + services = ("service1", "service2") + pre = self.copy() + with self.lenient.context(*services): + context = self.copy() + post = self.copy() + self.assertEqual(pre, self.default) + expected = self.default.copy() + expected.update(dict(active="__context", __context=services)) + self.assertEqual(context, expected) + self.assertEqual(post, self.default) + + +class Test_enable(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_getter(self): + self.assertEqual(self.lenient.enable, _LENIENT_ENABLE_DEFAULT) + + def test_setter_invalid(self): + emsg = "Invalid .* option 'enable'" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.enable = 0 + + def test_setter(self): + self.assertEqual(self.lenient.enable, _LENIENT_ENABLE_DEFAULT) + self.lenient.enable = False + self.assertFalse(self.lenient.enable) + + +class Test_register_client(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_not_protected(self): + emsg = "Cannot register .* client" + for protected in _LENIENT_PROTECTED: + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.register_client(protected, "service") + + def test_str_service_str(self): + client = "client" + services = "service" + self.lenient.register_client(client, services) + self.assertIn(client, self.lenient.__dict__) + self.assertEqual(self.lenient.__dict__[client], (services,)) + + def test_str_services_str(self): + client = "client" + services = ("service1", "service2") + self.lenient.register_client(client, services) + self.assertIn(client, self.lenient.__dict__) + self.assertEqual(self.lenient.__dict__[client], services) + + def test_callable_service_callable(self): + def client(): + pass + + def service(): + pass + + qualname_client = _qualname(client) + qualname_service = _qualname(service) + self.lenient.register_client(client, service) + self.assertIn(qualname_client, self.lenient.__dict__) + self.assertEqual( + self.lenient.__dict__[qualname_client], (qualname_service,) + ) + + def test_callable_services_callable(self): + def client(): + pass + + def service1(): + pass + + def service2(): + pass + + qualname_client = _qualname(client) + qualname_services = (_qualname(service1), _qualname(service2)) + self.lenient.register_client(client, (service1, service2)) + self.assertIn(qualname_client, self.lenient.__dict__) + self.assertEqual( + self.lenient.__dict__[qualname_client], qualname_services + ) + + def test_services_empty(self): + emsg = "Require at least one .* client service." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.register_client("client", ()) + + def test_services_overwrite(self): + client = "client" + services = ("service1", "service2") + self.lenient.__dict__[client] = services + self.assertEqual(self.lenient[client], services) + new_services = ("service3", "service4") + self.lenient.register_client(client, services=new_services) + self.assertEqual(self.lenient[client], new_services) + + def test_services_append(self): + client = "client" + services = ("service1", "service2") + self.lenient.__dict__[client] = services + self.assertEqual(self.lenient[client], services) + new_services = ("service3", "service4") + self.lenient.register_client( + client, services=new_services, append=True + ) + expected = set(services + new_services) + self.assertEqual(set(self.lenient[client]), expected) + + +class Test_register_service(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_str(self): + service = "service" + self.assertNotIn(service, self.lenient.__dict__) + self.lenient.register_service(service) + self.assertIn(service, self.lenient.__dict__) + self.assertFalse(isinstance(self.lenient.__dict__[service], Iterable)) + self.assertTrue(self.lenient.__dict__[service]) + + def test_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + self.assertNotIn(qualname_service, self.lenient.__dict__) + self.lenient.register_service(service) + self.assertIn(qualname_service, self.lenient.__dict__) + self.assertFalse( + isinstance(self.lenient.__dict__[qualname_service], Iterable) + ) + self.assertTrue(self.lenient.__dict__[qualname_service]) + + def test_not_protected(self): + emsg = "Cannot register .* service" + for protected in _LENIENT_PROTECTED: + self.lenient.__dict__[protected] = None + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.register_service("active") + + +class Test_unregister_client(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_not_protected(self): + emsg = "Cannot unregister .* client, as .* is a protected .* option." + for protected in _LENIENT_PROTECTED: + self.lenient.__dict__[protected] = None + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_client(protected) + + def test_not_in(self): + emsg = "Cannot unregister unknown .* client" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_client("client") + + def test_not_client(self): + client = "client" + self.lenient.__dict__[client] = True + emsg = "Cannot unregister .* client, as .* is not a valid .* client." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_client(client) + + def test_not_client_callable(self): + def client(): + pass + + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = True + emsg = "Cannot unregister .* client, as .* is not a valid .* client." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_client(client) + + def test_str(self): + client = "client" + self.lenient.__dict__[client] = (None,) + self.lenient.unregister_client(client) + self.assertNotIn(client, self.lenient.__dict__) + + def test_callable(self): + def client(): + pass + + qualname_client = _qualname(client) + self.lenient.__dict__[qualname_client] = (None,) + self.lenient.unregister_client(client) + self.assertNotIn(qualname_client, self.lenient.__dict__) + + +class Test_unregister_service(tests.IrisTest): + def setUp(self): + self.lenient = _Lenient() + + def test_not_protected(self): + emsg = "Cannot unregister .* service, as .* is a protected .* option." + for protected in _LENIENT_PROTECTED: + self.lenient.__dict__[protected] = None + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_service(protected) + + def test_not_in(self): + emsg = "Cannot unregister unknown .* service" + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_service("service") + + def test_not_service(self): + service = "service" + self.lenient.__dict__[service] = (None,) + emsg = "Cannot unregister .* service, as .* is not a valid .* service." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_service(service) + + def test_not_service_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + self.lenient.__dict__[qualname_service] = (None,) + emsg = "Cannot unregister .* service, as .* is not a valid .* service." + with self.assertRaisesRegex(ValueError, emsg): + self.lenient.unregister_service(service) + + def test_str(self): + service = "service" + self.lenient.__dict__[service] = True + self.lenient.unregister_service(service) + self.assertNotIn(service, self.lenient.__dict__) + + def test_callable(self): + def service(): + pass + + qualname_service = _qualname(service) + self.lenient.__dict__[qualname_service] = True + self.lenient.unregister_service(service) + self.assertNotIn(qualname_service, self.lenient.__dict__) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/lenient/test__lenient_client.py b/lib/iris/tests/unit/common/lenient/test__lenient_client.py new file mode 100644 index 0000000000..29cf5e7f82 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test__lenient_client.py @@ -0,0 +1,182 @@ +# 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 +# licensing details. +""" +Unit tests for the :func:`iris.common.lenient._lenient_client`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from inspect import getmodule +from unittest.mock import sentinel + +from iris.common.lenient import _LENIENT, _lenient_client + + +class Test(tests.IrisTest): + def setUp(self): + module_name = getmodule(self).__name__ + self.client = f"{module_name}" + ".Test.{}..myclient" + self.service = f"{module_name}" + ".Test.{}..myservice" + self.active = "active" + self.args_in = sentinel.arg1, sentinel.arg2 + self.kwargs_in = dict(kwarg1=sentinel.kwarg1, kwarg2=sentinel.kwarg2) + + def test_args_too_many(self): + emsg = "Invalid lenient client arguments, expecting 1" + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_client(None, None) + + def test_args_not_callable(self): + emsg = "Invalid lenient client argument, expecting a callable" + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_client(None) + + def test_args_and_kwargs(self): + def func(): + pass + + emsg = ( + "Invalid lenient client, got both arguments and keyword arguments" + ) + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_client(func, services=func) + + def test_call_naked(self): + @_lenient_client + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_naked") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_naked_alternative(self): + def myclient(): + return _LENIENT.__dict__.copy() + + result = _lenient_client(myclient)() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_naked_alternative") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_naked_client_args_kwargs(self): + @_lenient_client + def myclient(*args, **kwargs): + return args, kwargs + + args_out, kwargs_out = myclient(*self.args_in, **self.kwargs_in) + self.assertEqual(args_out, self.args_in) + self.assertEqual(kwargs_out, self.kwargs_in) + + def test_call_naked_doc(self): + @_lenient_client + def myclient(): + """myclient doc-string""" + + self.assertEqual(myclient.__doc__, "myclient doc-string") + + def test_call_no_kwargs(self): + @_lenient_client() + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_no_kwargs") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_no_kwargs_alternative(self): + def myclient(): + return _LENIENT.__dict__.copy() + + result = (_lenient_client())(myclient)() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_no_kwargs_alternative") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_kwargs_none(self): + @_lenient_client(services=None) + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_kwargs_none") + self.assertEqual(result[self.active], qualname_client) + self.assertNotIn(qualname_client, result) + + def test_call_kwargs_single(self): + service = sentinel.service + + @_lenient_client(services=service) + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_kwargs_single") + self.assertEqual(result[self.active], qualname_client) + self.assertIn(qualname_client, result) + self.assertEqual(result[qualname_client], (service,)) + + def test_call_kwargs_single_callable(self): + def myservice(): + pass + + @_lenient_client(services=myservice) + def myclient(): + return _LENIENT.__dict__.copy() + + test_name = "test_call_kwargs_single_callable" + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format(test_name) + self.assertEqual(result[self.active], qualname_client) + self.assertIn(qualname_client, result) + qualname_services = (self.service.format(test_name),) + self.assertEqual(result[qualname_client], qualname_services) + + def test_call_kwargs_iterable(self): + services = (sentinel.service1, sentinel.service2) + + @_lenient_client(services=services) + def myclient(): + return _LENIENT.__dict__.copy() + + result = myclient() + self.assertIn(self.active, result) + qualname_client = self.client.format("test_call_kwargs_iterable") + self.assertEqual(result[self.active], qualname_client) + self.assertIn(qualname_client, result) + self.assertEqual(set(result[qualname_client]), set(services)) + + def test_call_client_args_kwargs(self): + @_lenient_client() + def myclient(*args, **kwargs): + return args, kwargs + + args_out, kwargs_out = myclient(*self.args_in, **self.kwargs_in) + self.assertEqual(args_out, self.args_in) + self.assertEqual(kwargs_out, self.kwargs_in) + + def test_call_doc(self): + @_lenient_client() + def myclient(): + """myclient doc-string""" + + self.assertEqual(myclient.__doc__, "myclient doc-string") + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/lenient/test__lenient_service.py b/lib/iris/tests/unit/common/lenient/test__lenient_service.py new file mode 100644 index 0000000000..3b019c9de5 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test__lenient_service.py @@ -0,0 +1,116 @@ +# 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 +# licensing details. +""" +Unit tests for the :func:`iris.common.lenient._lenient_service`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from inspect import getmodule +from unittest.mock import sentinel + +from iris.common.lenient import _LENIENT, _lenient_service + + +class Test(tests.IrisTest): + def setUp(self): + module_name = getmodule(self).__name__ + self.service = f"{module_name}" + ".Test.{}..myservice" + self.args_in = sentinel.arg1, sentinel.arg2 + self.kwargs_in = dict(kwarg1=sentinel.kwarg1, kwarg2=sentinel.kwarg2) + + def test_args_too_many(self): + emsg = "Invalid lenient service arguments, expecting 1" + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_service(None, None) + + def test_args_not_callable(self): + emsg = "Invalid lenient service argument, expecting a callable" + with self.assertRaisesRegex(AssertionError, emsg): + _lenient_service(None) + + def test_call_naked(self): + @_lenient_service + def myservice(): + return _LENIENT.__dict__.copy() + + qualname_service = self.service.format("test_call_naked") + state = _LENIENT.__dict__ + self.assertIn(qualname_service, state) + self.assertTrue(state[qualname_service]) + result = myservice() + self.assertIn(qualname_service, result) + self.assertTrue(result[qualname_service]) + + def test_call_naked_alternative(self): + def myservice(): + return _LENIENT.__dict__.copy() + + qualname_service = self.service.format("test_call_naked_alternative") + result = _lenient_service(myservice)() + self.assertIn(qualname_service, result) + self.assertTrue(result[qualname_service]) + + def test_call_naked_service_args_kwargs(self): + @_lenient_service + def myservice(*args, **kwargs): + return args, kwargs + + args_out, kwargs_out = myservice(*self.args_in, **self.kwargs_in) + self.assertEqual(args_out, self.args_in) + self.assertEqual(kwargs_out, self.kwargs_in) + + def test_call_naked_doc(self): + @_lenient_service + def myservice(): + """myservice doc-string""" + + self.assertEqual(myservice.__doc__, "myservice doc-string") + + def test_call(self): + @_lenient_service() + def myservice(): + return _LENIENT.__dict__.copy() + + qualname_service = self.service.format("test_call") + state = _LENIENT.__dict__ + self.assertIn(qualname_service, state) + self.assertTrue(state[qualname_service]) + result = myservice() + self.assertIn(qualname_service, result) + self.assertTrue(result[qualname_service]) + + def test_call_alternative(self): + def myservice(): + return _LENIENT.__dict__.copy() + + qualname_service = self.service.format("test_call_alternative") + result = (_lenient_service())(myservice)() + self.assertIn(qualname_service, result) + self.assertTrue(result[qualname_service]) + + def test_call_service_args_kwargs(self): + @_lenient_service() + def myservice(*args, **kwargs): + return args, kwargs + + args_out, kwargs_out = myservice(*self.args_in, **self.kwargs_in) + self.assertEqual(args_out, self.args_in) + self.assertEqual(kwargs_out, self.kwargs_in) + + def test_call_doc(self): + @_lenient_service() + def myservice(): + """myservice doc-string""" + + self.assertEqual(myservice.__doc__, "myservice doc-string") + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/lenient/test__qualname.py b/lib/iris/tests/unit/common/lenient/test__qualname.py new file mode 100644 index 0000000000..e233b2ac78 --- /dev/null +++ b/lib/iris/tests/unit/common/lenient/test__qualname.py @@ -0,0 +1,66 @@ +# 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 +# licensing details. +""" +Unit tests for the :func:`iris.common.lenient._qualname`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from inspect import getmodule +from unittest.mock import sentinel + +from iris.common.lenient import _qualname + + +class Test(tests.IrisTest): + def setUp(self): + module_name = getmodule(self).__name__ + self.locals = f"{module_name}" + ".Test.{}..{}" + + def test_pass_thru_non_callable(self): + func = sentinel.func + result = _qualname(func) + self.assertEqual(result, func) + + def test_callable_function_local(self): + def myfunc(): + pass + + qualname_func = self.locals.format( + "test_callable_function_local", "myfunc" + ) + result = _qualname(myfunc) + self.assertEqual(result, qualname_func) + + def test_callable_function(self): + import iris + + result = _qualname(iris.load) + self.assertEqual(result, "iris.load") + + def test_callable_method_local(self): + class MyClass: + def mymethod(self): + pass + + qualname_method = self.locals.format( + "test_callable_method_local", "MyClass.mymethod" + ) + result = _qualname(MyClass.mymethod) + self.assertEqual(result, qualname_method) + + def test_callable_method(self): + import iris + + result = _qualname(iris.cube.Cube.add_ancillary_variable) + self.assertEqual(result, "iris.cube.Cube.add_ancillary_variable") + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/metadata/__init__.py b/lib/iris/tests/unit/common/metadata/__init__.py new file mode 100644 index 0000000000..aba33c8312 --- /dev/null +++ b/lib/iris/tests/unit/common/metadata/__init__.py @@ -0,0 +1,6 @@ +# 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 +# licensing details. +"""Unit tests for the :mod:`iris.common.metadata` package.""" diff --git a/lib/iris/tests/unit/common/metadata/test_AncillaryVariableMetadata.py b/lib/iris/tests/unit/common/metadata/test_AncillaryVariableMetadata.py new file mode 100644 index 0000000000..0e2ca52c47 --- /dev/null +++ b/lib/iris/tests/unit/common/metadata/test_AncillaryVariableMetadata.py @@ -0,0 +1,494 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.metadata.AncillaryVariableMetadata`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from copy import deepcopy +import unittest.mock as mock +from unittest.mock import sentinel + +from iris.common.lenient import _LENIENT, _qualname +from iris.common.metadata import BaseMetadata, AncillaryVariableMetadata + + +class Test(tests.IrisTest): + def setUp(self): + self.standard_name = mock.sentinel.standard_name + self.long_name = mock.sentinel.long_name + self.var_name = mock.sentinel.var_name + self.units = mock.sentinel.units + self.attributes = mock.sentinel.attributes + self.cls = AncillaryVariableMetadata + + def test_repr(self): + metadata = self.cls( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + units=self.units, + attributes=self.attributes, + ) + fmt = ( + "AncillaryVariableMetadata(standard_name={!r}, long_name={!r}, " + "var_name={!r}, units={!r}, attributes={!r})" + ) + expected = fmt.format( + self.standard_name, + self.long_name, + self.var_name, + self.units, + self.attributes, + ) + self.assertEqual(expected, repr(metadata)) + + def test__fields(self): + expected = ( + "standard_name", + "long_name", + "var_name", + "units", + "attributes", + ) + self.assertEqual(self.cls._fields, expected) + + def test_bases(self): + self.assertTrue(issubclass(self.cls, BaseMetadata)) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.dummy = sentinel.dummy + self.cls = AncillaryVariableMetadata + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.__eq__.__doc__, self.cls.__eq__.__doc__, + ) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_call(self): + other = sentinel.other + return_value = sentinel.return_value + metadata = self.cls(*(None,) * len(self.cls._fields)) + with mock.patch.object( + BaseMetadata, "__eq__", return_value=return_value + ) as mocker: + result = metadata.__eq__(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + +class Test___lt__(tests.IrisTest): + def setUp(self): + self.cls = AncillaryVariableMetadata + self.one = self.cls(1, 1, 1, 1, 1) + self.two = self.cls(1, 1, 1, 2, 1) + self.none = self.cls(1, 1, 1, None, 1) + self.attributes = self.cls(1, 1, 1, 1, 10) + + def test__ascending_lt(self): + result = self.one < self.two + self.assertTrue(result) + + def test__descending_lt(self): + result = self.two < self.one + self.assertFalse(result) + + def test__none_rhs_operand(self): + result = self.one < self.none + self.assertFalse(result) + + def test__none_lhs_operand(self): + result = self.none < self.one + self.assertTrue(result) + + def test__ignore_attributes(self): + result = self.one < self.attributes + self.assertFalse(result) + result = self.attributes < self.one + self.assertFalse(result) + + +class Test_combine(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.dummy = sentinel.dummy + self.cls = AncillaryVariableMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.combine.__doc__, self.cls.combine.__doc__, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["units"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + +class Test_difference(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.dummy = sentinel.dummy + self.cls = AncillaryVariableMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.difference.__doc__, self.cls.difference.__doc__, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["units"] = (left["units"], right["units"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["units"] = lexpected["units"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_strict_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + +class Test_equal(tests.IrisTest): + def setUp(self): + self.cls = AncillaryVariableMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual(BaseMetadata.equal.__doc__, self.cls.equal.__doc__) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue(_LENIENT[self.cls.equal]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/metadata/test_BaseMetadata.py b/lib/iris/tests/unit/common/metadata/test_BaseMetadata.py new file mode 100644 index 0000000000..eb0ee9d659 --- /dev/null +++ b/lib/iris/tests/unit/common/metadata/test_BaseMetadata.py @@ -0,0 +1,1636 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.metadata.BaseMetadata`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from collections import OrderedDict +import unittest.mock as mock +from unittest.mock import sentinel + +import numpy.ma as ma +import numpy as np + +from iris.common.lenient import _LENIENT, _qualname +from iris.common.metadata import BaseMetadata, CubeMetadata + + +class Test(tests.IrisTest): + def setUp(self): + self.standard_name = mock.sentinel.standard_name + self.long_name = mock.sentinel.long_name + self.var_name = mock.sentinel.var_name + self.units = mock.sentinel.units + self.attributes = mock.sentinel.attributes + self.cls = BaseMetadata + + def test_repr(self): + metadata = self.cls( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + units=self.units, + attributes=self.attributes, + ) + fmt = ( + "BaseMetadata(standard_name={!r}, long_name={!r}, " + "var_name={!r}, units={!r}, attributes={!r})" + ) + expected = fmt.format( + self.standard_name, + self.long_name, + self.var_name, + self.units, + self.attributes, + ) + self.assertEqual(expected, repr(metadata)) + + def test__fields(self): + expected = ( + "standard_name", + "long_name", + "var_name", + "units", + "attributes", + ) + self.assertEqual(expected, self.cls._fields) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.kwargs = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.cls = BaseMetadata + self.metadata = self.cls(**self.kwargs) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_cannot_compare_non_class(self): + result = self.metadata.__eq__(None) + self.assertIs(NotImplemented, result) + + def test_cannot_compare_different_class(self): + other = CubeMetadata(*(None,) * len(CubeMetadata._fields)) + result = self.metadata.__eq__(other) + self.assertIs(NotImplemented, result) + + def test_lenient(self): + return_value = sentinel.return_value + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ) as mlenient: + with mock.patch.object( + self.cls, "_compare_lenient", return_value=return_value + ) as mcompare: + result = self.metadata.__eq__(self.metadata) + + self.assertEqual(return_value, result) + self.assertEqual(1, mcompare.call_count) + (arg,), kwargs = mcompare.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + self.assertEqual(1, mlenient.call_count) + (arg,), kwargs = mlenient.call_args + self.assertEqual(_qualname(self.cls.__eq__), _qualname(arg)) + self.assertEqual(dict(), kwargs) + + def test_strict_same(self): + self.assertTrue(self.metadata.__eq__(self.metadata)) + other = self.cls(**self.kwargs) + self.assertTrue(self.metadata.__eq__(other)) + self.assertTrue(other.__eq__(self.metadata)) + + def test_strict_different(self): + self.kwargs["var_name"] = None + other = self.cls(**self.kwargs) + self.assertFalse(self.metadata.__eq__(other)) + self.assertFalse(other.__eq__(self.metadata)) + + +class Test___lt__(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.one = self.cls(1, 1, 1, 1, 1) + self.two = self.cls(1, 1, 1, 2, 1) + self.none = self.cls(1, 1, 1, None, 1) + self.attributes = self.cls(1, 1, 1, 1, 10) + + def test__ascending_lt(self): + result = self.one < self.two + self.assertTrue(result) + + def test__descending_lt(self): + result = self.two < self.one + self.assertFalse(result) + + def test__none_rhs_operand(self): + result = self.one < self.none + self.assertFalse(result) + + def test__none_lhs_operand(self): + result = self.none < self.one + self.assertTrue(result) + + def test__ignore_attributes(self): + result = self.one < self.attributes + self.assertFalse(result) + result = self.attributes < self.one + self.assertFalse(result) + + +class Test___ne__(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.other = sentinel.other + + def test_notimplemented(self): + return_value = NotImplemented + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as mocker: + result = self.metadata.__ne__(self.other) + + self.assertIs(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(self.other, arg) + self.assertEqual(dict(), kwargs) + + def test_negate_true(self): + return_value = True + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as mocker: + result = self.metadata.__ne__(self.other) + + self.assertFalse(result) + (arg,), kwargs = mocker.call_args + self.assertEqual(self.other, arg) + self.assertEqual(dict(), kwargs) + + def test_negate_false(self): + return_value = False + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as mocker: + result = self.metadata.__ne__(self.other) + + self.assertTrue(result) + (arg,), kwargs = mocker.call_args + self.assertEqual(self.other, arg) + self.assertEqual(dict(), kwargs) + + +class Test__combine(tests.IrisTest): + def setUp(self): + self.kwargs = dict( + standard_name="standard_name", + long_name="long_name", + var_name="var_name", + units="units", + attributes=dict(one=sentinel.one, two=sentinel.two), + ) + self.cls = BaseMetadata + self.metadata = self.cls(**self.kwargs) + + def test_lenient(self): + return_value = sentinel._combine_lenient + other = sentinel.other + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ) as mlenient: + with mock.patch.object( + self.cls, "_combine_lenient", return_value=return_value + ) as mcombine: + result = self.metadata._combine(other) + + self.assertEqual(1, mlenient.call_count) + (arg,), kwargs = mlenient.call_args + self.assertEqual(self.metadata.combine, arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(return_value, result) + self.assertEqual(1, mcombine.call_count) + (arg,), kwargs = mcombine.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_strict(self): + dummy = sentinel.dummy + values = self.kwargs.copy() + values["standard_name"] = dummy + values["var_name"] = dummy + values["attributes"] = dummy + other = self.cls(**values) + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + result = self.metadata._combine(other) + + expected = [ + None if values[field] == dummy else values[field] + for field in self.cls._fields + ] + self.assertEqual(expected, result) + + +class Test__combine_lenient(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.none = self.cls(*(None,) * len(self.cls._fields))._asdict() + self.names = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + ) + + def test_strict_units(self): + left = self.none.copy() + left["units"] = "K" + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(left.values()) + self.assertEqual(expected, lmetadata._combine_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._combine_lenient(lmetadata)) + + def test_strict_units_different(self): + left = self.none.copy() + right = self.none.copy() + left["units"] = "K" + right["units"] = "km" + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._combine_lenient(rmetadata) + expected = list(self.none.values()) + self.assertEqual(expected, result) + result = rmetadata._combine_lenient(lmetadata) + self.assertEqual(expected, result) + + def test_strict_units_different_none(self): + left = self.none.copy() + right = self.none.copy() + left["units"] = "K" + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._combine_lenient(rmetadata) + expected = list(self.none.values()) + self.assertEqual(expected, result) + + result = rmetadata._combine_lenient(lmetadata) + self.assertEqual(expected, result) + + def test_attributes(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + rdict = dict(item=sentinel.right) + left["attributes"] = ldict + right["attributes"] = rdict + rmetadata = self.cls(**right) + return_value = sentinel.return_value + with mock.patch.object( + self.cls, "_combine_lenient_attributes", return_value=return_value, + ) as mocker: + lmetadata = self.cls(**left) + result = lmetadata._combine_lenient(rmetadata) + + expected = self.none.copy() + expected["attributes"] = return_value + expected = list(expected.values()) + self.assertEqual(expected, result) + + self.assertEqual(1, mocker.call_count) + args, kwargs = mocker.call_args + expected = (ldict, rdict) + self.assertEqual(expected, args) + self.assertEqual(dict(), kwargs) + + def test_attributes_non_mapping_different(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + rdict = sentinel.right + left["attributes"] = ldict + right["attributes"] = rdict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(self.none.copy().values()) + self.assertEqual(expected, lmetadata._combine_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._combine_lenient(lmetadata)) + + def test_attributes_non_mapping_different_none(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + left["attributes"] = ldict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._combine_lenient(rmetadata) + expected = self.none.copy() + expected["attributes"] = ldict + expected = list(expected.values()) + self.assertEqual(expected, result) + + result = rmetadata._combine_lenient(lmetadata) + self.assertEqual(expected, result) + + def test_names(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(left.values()) + self.assertEqual(expected, lmetadata._combine_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._combine_lenient(lmetadata)) + + def test_names_different(self): + dummy = sentinel.dummy + left = self.none.copy() + right = self.none.copy() + left.update(self.names) + right["standard_name"] = dummy + right["long_name"] = dummy + right["var_name"] = dummy + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(self.none.copy().values()) + self.assertEqual(expected, lmetadata._combine_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._combine_lenient(lmetadata)) + + def test_names_different_none(self): + left = self.none.copy() + right = self.none.copy() + left.update(self.names) + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._combine_lenient(rmetadata) + expected = list(left.values()) + self.assertEqual(expected, result) + + result = rmetadata._combine_lenient(lmetadata) + self.assertEqual(expected, result) + + +class Test__combine_lenient_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one="one", + two="two", + three=np.int16(123), + four=np.arange(10), + five=ma.arange(10), + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + result = self.metadata._combine_lenient_attributes(left, right) + expected = left + self.assertDictEqual(expected, result) + + result = self.metadata._combine_lenient_attributes(right, left) + self.assertDictEqual(expected, result) + + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["two"] = left["four"] = self.dummy + + result = self.metadata._combine_lenient_attributes(left, right) + expected = self.values.copy() + for key in ["two", "four"]: + del expected[key] + self.assertDictEqual(expected, result) + + result = self.metadata._combine_lenient_attributes(right, left) + self.assertDictEqual(expected, result) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + result = self.metadata._combine_lenient_attributes(left, right) + expected = self.values.copy() + for key in ["one", "three", "five"]: + del expected[key] + self.assertDictEqual(expected, result) + + result = self.metadata._combine_lenient_attributes(right, left) + self.assertDictEqual(expected, result) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = "extra_left" + right["extra_right"] = "extra_right" + + result = self.metadata._combine_lenient_attributes(left, right) + expected = self.values.copy() + expected["extra_left"] = left["extra_left"] + expected["extra_right"] = right["extra_right"] + self.assertDictEqual(expected, result) + + result = self.metadata._combine_lenient_attributes(right, left) + self.assertDictEqual(expected, result) + + +class Test__combine_strict_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one="one", + two="two", + three=np.int32(123), + four=np.arange(10), + five=ma.arange(10), + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + result = self.metadata._combine_strict_attributes(left, right) + expected = left + self.assertDictEqual(expected, result) + + result = self.metadata._combine_strict_attributes(right, left) + self.assertDictEqual(expected, result) + + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = self.dummy + + result = self.metadata._combine_strict_attributes(left, right) + expected = self.values.copy() + for key in ["one", "three"]: + del expected[key] + self.assertDictEqual(expected, result) + + result = self.metadata._combine_strict_attributes(right, left) + self.assertDictEqual(expected, result) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + result = self.metadata._combine_strict_attributes(left, right) + expected = self.values.copy() + for key in ["one", "three", "five"]: + del expected[key] + self.assertDictEqual(expected, result) + + result = self.metadata._combine_strict_attributes(right, left) + self.assertDictEqual(expected, result) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = "extra_left" + right["extra_right"] = "extra_right" + + result = self.metadata._combine_strict_attributes(left, right) + expected = self.values.copy() + self.assertDictEqual(expected, result) + + result = self.metadata._combine_strict_attributes(right, left) + self.assertDictEqual(expected, result) + + +class Test__compare_lenient(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.none = self.cls(*(None,) * len(self.cls._fields))._asdict() + self.names = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + ) + + def test_name_same(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object( + self.cls, "_is_attributes", return_value=False + ) as mocker: + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._compare_lenient(lmetadata)) + + # mocker not called for "units" nor "var_name" members. + expected = (len(self.cls._fields) - 2) * 2 + self.assertEqual(expected, mocker.call_count) + + def test_name_same_lenient_false__long_name_different(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + right["long_name"] = sentinel.dummy + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object( + self.cls, "_is_attributes", return_value=False + ) as mocker: + self.assertFalse(lmetadata._compare_lenient(rmetadata)) + self.assertFalse(rmetadata._compare_lenient(lmetadata)) + + # mocker not called for "units" nor "var_name" members. + expected = (len(self.cls._fields) - 2) * 2 + self.assertEqual(expected, mocker.call_count) + + def test_name_same_lenient_true__var_name_different(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + right["var_name"] = sentinel.dummy + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object( + self.cls, "_is_attributes", return_value=False + ) as mocker: + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._compare_lenient(lmetadata)) + + # mocker not called for "units" nor "var_name" members. + expected = (len(self.cls._fields) - 2) * 2 + self.assertEqual(expected, mocker.call_count) + + def test_name_different(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + right["standard_name"] = None + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object(self.cls, "_is_attributes") as mocker: + self.assertFalse(lmetadata._compare_lenient(rmetadata)) + self.assertFalse(rmetadata._compare_lenient(lmetadata)) + + self.assertEqual(0, mocker.call_count) + + def test_strict_units(self): + left = self.none.copy() + left.update(self.names) + left["units"] = "K" + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object( + self.cls, "_is_attributes", return_value=False + ) as mocker: + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._compare_lenient(lmetadata)) + + # mocker not called for "units" nor "var_name" members. + expected = (len(self.cls._fields) - 2) * 2 + self.assertEqual(expected, mocker.call_count) + + def test_strict_units_different(self): + left = self.none.copy() + left.update(self.names) + left["units"] = "K" + right = left.copy() + right["units"] = "m" + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + with mock.patch.object( + self.cls, "_is_attributes", return_value=False + ) as mocker: + self.assertFalse(lmetadata._compare_lenient(rmetadata)) + self.assertFalse(rmetadata._compare_lenient(lmetadata)) + + # mocker not called for "units" nor "var_name" members. + expected = (len(self.cls._fields) - 2) * 2 + self.assertEqual(expected, mocker.call_count) + + def test_attributes(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + ldict = dict(item=sentinel.left) + rdict = dict(item=sentinel.right) + left["attributes"] = ldict + right["attributes"] = rdict + rmetadata = self.cls(**right) + with mock.patch.object( + self.cls, "_compare_lenient_attributes", return_value=True, + ) as mocker: + lmetadata = self.cls(**left) + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._compare_lenient(lmetadata)) + + self.assertEqual(2, mocker.call_count) + expected = [((ldict, rdict),), ((rdict, ldict),)] + self.assertEqual(expected, mocker.call_args_list) + + def test_attributes_non_mapping_different(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + ldict = dict(item=sentinel.left) + rdict = sentinel.right + left["attributes"] = ldict + right["attributes"] = rdict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + self.assertFalse(lmetadata._compare_lenient(rmetadata)) + self.assertFalse(rmetadata._compare_lenient(lmetadata)) + + def test_attributes_non_mapping_different_none(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + ldict = dict(item=sentinel.left) + left["attributes"] = ldict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._compare_lenient(lmetadata)) + + def test_names(self): + left = self.none.copy() + left.update(self.names) + left["long_name"] = None + right = self.none.copy() + right["long_name"] = left["standard_name"] + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + self.assertTrue(lmetadata._compare_lenient(rmetadata)) + self.assertTrue(rmetadata._combine_lenient(lmetadata)) + + +class Test__compare_lenient_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one=sentinel.one, + two=sentinel.two, + three=np.int16(123), + four=np.arange(10), + five=ma.arange(5), + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + self.assertTrue(self.metadata._compare_lenient_attributes(left, right)) + self.assertTrue(self.metadata._compare_lenient_attributes(right, left)) + + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["two"] = left["four"] = self.dummy + + self.assertFalse( + self.metadata._compare_lenient_attributes(left, right) + ) + self.assertFalse( + self.metadata._compare_lenient_attributes(right, left) + ) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + self.assertFalse( + self.metadata._compare_lenient_attributes(left, right) + ) + self.assertFalse( + self.metadata._compare_lenient_attributes(right, left) + ) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = sentinel.extra_left + right["extra_right"] = sentinel.extra_right + + self.assertTrue(self.metadata._compare_lenient_attributes(left, right)) + self.assertTrue(self.metadata._compare_lenient_attributes(right, left)) + + +class Test__compare_strict_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one=sentinel.one, + two=sentinel.two, + three=np.int16(123), + four=np.arange(10), + five=ma.arange(5), + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + self.assertTrue(self.metadata._compare_strict_attributes(left, right)) + self.assertTrue(self.metadata._compare_strict_attributes(right, left)) + + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["two"] = left["four"] = self.dummy + + self.assertFalse(self.metadata._compare_strict_attributes(left, right)) + self.assertFalse(self.metadata._compare_strict_attributes(right, left)) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + self.assertFalse(self.metadata._compare_strict_attributes(left, right)) + self.assertFalse(self.metadata._compare_strict_attributes(right, left)) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = sentinel.extra_left + right["extra_right"] = sentinel.extra_right + + self.assertFalse(self.metadata._compare_strict_attributes(left, right)) + self.assertFalse(self.metadata._compare_strict_attributes(right, left)) + + +class Test__difference(tests.IrisTest): + def setUp(self): + self.kwargs = dict( + standard_name="standard_name", + long_name="long_name", + var_name="var_name", + units="units", + attributes=dict(one=sentinel.one, two=sentinel.two), + ) + self.cls = BaseMetadata + self.metadata = self.cls(**self.kwargs) + + def test_lenient(self): + return_value = sentinel._difference_lenient + other = sentinel.other + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ) as mlenient: + with mock.patch.object( + self.cls, "_difference_lenient", return_value=return_value + ) as mdifference: + result = self.metadata._difference(other) + + self.assertEqual(1, mlenient.call_count) + (arg,), kwargs = mlenient.call_args + self.assertEqual(self.metadata.difference, arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(return_value, result) + self.assertEqual(1, mdifference.call_count) + (arg,), kwargs = mdifference.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_strict(self): + dummy = sentinel.dummy + values = self.kwargs.copy() + values["long_name"] = dummy + values["units"] = dummy + other = self.cls(**values) + method = "_difference_strict_attributes" + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + with mock.patch.object( + self.cls, method, return_value=None + ) as mdifference: + result = self.metadata._difference(other) + + expected = [ + (self.kwargs[field], dummy) if values[field] == dummy else None + for field in self.cls._fields + ] + self.assertEqual(expected, result) + self.assertEqual(1, mdifference.call_count) + args, kwargs = mdifference.call_args + expected = (self.kwargs["attributes"], values["attributes"]) + self.assertEqual(expected, args) + self.assertEqual(dict(), kwargs) + + with mock.patch.object( + self.cls, method, return_value=None + ) as mdifference: + result = other._difference(self.metadata) + + expected = [ + (dummy, self.kwargs[field]) if values[field] == dummy else None + for field in self.cls._fields + ] + self.assertEqual(expected, result) + self.assertEqual(1, mdifference.call_count) + args, kwargs = mdifference.call_args + expected = (self.kwargs["attributes"], values["attributes"]) + self.assertEqual(expected, args) + self.assertEqual(dict(), kwargs) + + +class Test__difference_lenient(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.none = self.cls(*(None,) * len(self.cls._fields))._asdict() + self.names = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + ) + + def test_strict_units(self): + left = self.none.copy() + left["units"] = "km" + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + expected = list(self.none.values()) + self.assertEqual(expected, lmetadata._difference_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._difference_lenient(lmetadata)) + + def test_strict_units_different(self): + left = self.none.copy() + right = self.none.copy() + lunits, runits = "m", "km" + left["units"] = lunits + right["units"] = runits + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = self.none.copy() + expected["units"] = (lunits, runits) + expected = list(expected.values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + expected = self.none.copy() + expected["units"] = (runits, lunits) + expected = list(expected.values()) + self.assertEqual(expected, result) + + def test_strict_units_different_none(self): + left = self.none.copy() + right = self.none.copy() + lunits, runits = "m", None + left["units"] = lunits + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = self.none.copy() + expected["units"] = (lunits, runits) + expected = list(expected.values()) + + self.assertEqual(expected, result) + result = rmetadata._difference_lenient(lmetadata) + expected = self.none.copy() + expected["units"] = (runits, lunits) + expected = list(expected.values()) + self.assertEqual(expected, result) + + def test_attributes(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + rdict = dict(item=sentinel.right) + left["attributes"] = ldict + right["attributes"] = rdict + rmetadata = self.cls(**right) + return_value = sentinel.return_value + with mock.patch.object( + self.cls, + "_difference_lenient_attributes", + return_value=return_value, + ) as mocker: + lmetadata = self.cls(**left) + result = lmetadata._difference_lenient(rmetadata) + + expected = self.none.copy() + expected["attributes"] = return_value + expected = list(expected.values()) + self.assertEqual(expected, result) + + self.assertEqual(1, mocker.call_count) + args, kwargs = mocker.call_args + expected = (ldict, rdict) + self.assertEqual(expected, args) + self.assertEqual(dict(), kwargs) + + def test_attributes_non_mapping_different(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + rdict = sentinel.right + left["attributes"] = ldict + right["attributes"] = rdict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = self.none.copy() + expected["attributes"] = (ldict, rdict) + expected = list(expected.values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + expected = self.none.copy() + expected["attributes"] = (rdict, ldict) + expected = list(expected.values()) + self.assertEqual(expected, result) + + def test_attributes_non_mapping_different_none(self): + left = self.none.copy() + right = self.none.copy() + ldict = dict(item=sentinel.left) + left["attributes"] = ldict + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = list(self.none.copy().values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + self.assertEqual(expected, result) + + def test_names(self): + left = self.none.copy() + left.update(self.names) + right = left.copy() + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + expected = list(self.none.values()) + self.assertEqual(expected, lmetadata._difference_lenient(rmetadata)) + self.assertEqual(expected, rmetadata._difference_lenient(lmetadata)) + + def test_names_different(self): + dummy = sentinel.dummy + left = self.none.copy() + right = self.none.copy() + left.update(self.names) + right["standard_name"] = dummy + right["long_name"] = dummy + right["var_name"] = dummy + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = self.none.copy() + expected["standard_name"] = ( + left["standard_name"], + right["standard_name"], + ) + expected["long_name"] = (left["long_name"], right["long_name"]) + expected["var_name"] = (left["var_name"], right["var_name"]) + expected = list(expected.values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + expected = self.none.copy() + expected["standard_name"] = ( + right["standard_name"], + left["standard_name"], + ) + expected["long_name"] = (right["long_name"], left["long_name"]) + expected["var_name"] = (right["var_name"], left["var_name"]) + expected = list(expected.values()) + self.assertEqual(expected, result) + + def test_names_different_none(self): + left = self.none.copy() + right = self.none.copy() + left.update(self.names) + lmetadata = self.cls(**left) + rmetadata = self.cls(**right) + + result = lmetadata._difference_lenient(rmetadata) + expected = list(self.none.values()) + self.assertEqual(expected, result) + + result = rmetadata._difference_lenient(lmetadata) + self.assertEqual(expected, result) + + +class Test__difference_lenient_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one=sentinel.one, + two=sentinel.two, + three=np.float(3.14), + four=np.arange(10, dtype=np.float), + five=ma.arange(10, dtype=np.int16), + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + result = self.metadata._difference_lenient_attributes(left, right) + self.assertIsNone(result) + + result = self.metadata._difference_lenient_attributes(right, left) + self.assertIsNone(result) + + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["two"] = left["four"] = self.dummy + + result = self.metadata._difference_lenient_attributes(left, right) + for key in ["one", "three", "five"]: + del left[key] + del right[key] + expected_left, expected_right = (left, right) + result_left, result_right = result + self.assertDictEqual(expected_left, result_left) + self.assertDictEqual(expected_right, result_right) + + result = self.metadata._difference_lenient_attributes(right, left) + result_left, result_right = result + self.assertDictEqual(expected_right, result_left) + self.assertDictEqual(expected_left, result_right) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + result = self.metadata._difference_lenient_attributes(left, right) + for key in ["two", "four"]: + del left[key] + del right[key] + expected_left, expected_right = (left, right) + result_left, result_right = result + self.assertDictEqual(expected_left, result_left) + self.assertDictEqual(expected_right, result_right) + + result = self.metadata._difference_lenient_attributes(right, left) + result_left, result_right = result + self.assertDictEqual(expected_right, result_left) + self.assertDictEqual(expected_left, result_right) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = sentinel.extra_left + right["extra_right"] = sentinel.extra_right + result = self.metadata._difference_lenient_attributes(left, right) + self.assertIsNone(result) + + result = self.metadata._difference_lenient_attributes(right, left) + self.assertIsNone(result) + + +class Test__difference_strict_attributes(tests.IrisTest): + def setUp(self): + self.values = OrderedDict( + one=sentinel.one, + two=sentinel.two, + three=np.int32(123), + four=np.arange(10), + five=ma.arange(10), + ) + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.dummy = sentinel.dummy + + def test_same(self): + left = self.values.copy() + right = self.values.copy() + + result = self.metadata._difference_strict_attributes(left, right) + self.assertIsNone(result) + result = self.metadata._difference_strict_attributes(right, left) + self.assertIsNone(result) + + def test_different(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = self.dummy + + result = self.metadata._difference_strict_attributes(left, right) + expected_left = left.copy() + expected_right = right.copy() + for key in ["two", "four"]: + del expected_left[key] + del expected_right[key] + result_left, result_right = result + self.assertDictEqual(expected_left, result_left) + self.assertDictEqual(expected_right, result_right) + + result = self.metadata._difference_strict_attributes(right, left) + result_left, result_right = result + self.assertDictEqual(expected_right, result_left) + self.assertDictEqual(expected_left, result_right) + + def test_different_none(self): + left = self.values.copy() + right = self.values.copy() + left["one"] = left["three"] = left["five"] = None + + result = self.metadata._difference_strict_attributes(left, right) + expected_left = left.copy() + expected_right = right.copy() + for key in ["two", "four"]: + del expected_left[key] + del expected_right[key] + result_left, result_right = result + self.assertDictEqual(expected_left, result_left) + self.assertDictEqual(expected_right, result_right) + + result = self.metadata._difference_strict_attributes(right, left) + result_left, result_right = result + self.assertDictEqual(expected_right, result_left) + self.assertDictEqual(expected_left, result_right) + + def test_extra(self): + left = self.values.copy() + right = self.values.copy() + left["extra_left"] = sentinel.extra_left + right["extra_right"] = sentinel.extra_right + + result = self.metadata._difference_strict_attributes(left, right) + expected_left = dict(extra_left=left["extra_left"]) + expected_right = dict(extra_right=right["extra_right"]) + result_left, result_right = result + self.assertDictEqual(expected_left, result_left) + self.assertDictEqual(expected_right, result_right) + + result = self.metadata._difference_strict_attributes(right, left) + result_left, result_right = result + self.assertDictEqual(expected_right, result_left) + self.assertDictEqual(expected_left, result_right) + + +class Test__is_attributes(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.metadata = self.cls(*(None,) * len(self.cls._fields)) + self.field = "attributes" + + def test_field(self): + self.assertTrue(self.metadata._is_attributes(self.field, {}, {})) + + def test_field_not_attributes(self): + self.assertFalse(self.metadata._is_attributes(None, {}, {})) + + def test_left_not_mapping(self): + self.assertFalse(self.metadata._is_attributes(self.field, None, {})) + + def test_right_not_mapping(self): + self.assertFalse(self.metadata._is_attributes(self.field, {}, None)) + + +class Test_combine(tests.IrisTest): + def setUp(self): + kwargs = dict( + standard_name="standard_name", + long_name="long_name", + var_name="var_name", + units="units", + attributes="attributes", + ) + self.cls = BaseMetadata + self.metadata = self.cls(**kwargs) + self.mock_kwargs = OrderedDict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_cannot_combine_non_class(self): + emsg = "Cannot combine" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.combine(None) + + def test_cannot_combine_different_class(self): + other = CubeMetadata(*(None,) * len(CubeMetadata._fields)) + emsg = "Cannot combine" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.combine(other) + + def test_lenient_default(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_combine", return_value=return_value + ) as mocker: + result = self.metadata.combine(self.metadata) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_true(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_combine", return_value=return_value + ) as mcombine: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.combine(self.metadata, lenient=True) + + self.assertEqual(1, mcontext.call_count) + (arg,), kwargs = mcontext.call_args + self.assertEqual(_qualname(self.cls.combine), arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(result._asdict(), self.mock_kwargs) + self.assertEqual(1, mcombine.call_count) + (arg,), kwargs = mcombine.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_false(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_combine", return_value=return_value + ) as mcombine: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.combine(self.metadata, lenient=False) + + self.assertEqual(1, mcontext.call_count) + args, kwargs = mcontext.call_args + self.assertEqual((), args) + self.assertEqual({_qualname(self.cls.combine): False}, kwargs) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mcombine.call_count) + (arg,), kwargs = mcombine.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + +class Test_difference(tests.IrisTest): + def setUp(self): + kwargs = dict( + standard_name="standard_name", + long_name="long_name", + var_name="var_name", + units="units", + attributes="attributes", + ) + self.cls = BaseMetadata + self.metadata = self.cls(**kwargs) + self.mock_kwargs = OrderedDict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_cannot_differ_non_class(self): + emsg = "Cannot differ" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.difference(None) + + def test_cannot_differ_different_class(self): + other = CubeMetadata(*(None,) * len(CubeMetadata._fields)) + emsg = "Cannot differ" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.difference(other) + + def test_lenient_default(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_difference", return_value=return_value + ) as mocker: + result = self.metadata.difference(self.metadata) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_true(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_difference", return_value=return_value + ) as mdifference: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.difference(self.metadata, lenient=True) + + self.assertEqual(1, mcontext.call_count) + (arg,), kwargs = mcontext.call_args + self.assertEqual(_qualname(self.cls.difference), arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mdifference.call_count) + (arg,), kwargs = mdifference.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_false(self): + return_value = self.mock_kwargs.values() + with mock.patch.object( + self.cls, "_difference", return_value=return_value + ) as mdifference: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.difference(self.metadata, lenient=False) + + self.assertEqual(mcontext.call_count, 1) + args, kwargs = mcontext.call_args + self.assertEqual((), args) + self.assertEqual({_qualname(self.cls.difference): False}, kwargs) + + self.assertEqual(self.mock_kwargs, result._asdict()) + self.assertEqual(1, mdifference.call_count) + (arg,), kwargs = mdifference.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + +class Test_equal(tests.IrisTest): + def setUp(self): + kwargs = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + ) + self.cls = BaseMetadata + self.metadata = self.cls(**kwargs) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue((_LENIENT[self.cls.equal])) + + def test_cannot_compare_non_class(self): + emsg = "Cannot compare" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.equal(None) + + def test_cannot_compare_different_class(self): + other = CubeMetadata(*(None,) * len(CubeMetadata._fields)) + emsg = "Cannot compare" + with self.assertRaisesRegex(TypeError, emsg): + self.metadata.equal(other) + + def test_lenient_default(self): + return_value = sentinel.return_value + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as mocker: + result = self.metadata.equal(self.metadata) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_true(self): + return_value = sentinel.return_value + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as m__eq__: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.equal(self.metadata, lenient=True) + + self.assertEqual(return_value, result) + self.assertEqual(1, mcontext.call_count) + (arg,), kwargs = mcontext.call_args + self.assertEqual(_qualname(self.cls.equal), arg) + self.assertEqual(dict(), kwargs) + + self.assertEqual(1, m__eq__.call_count) + (arg,), kwargs = m__eq__.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + def test_lenient_false(self): + return_value = sentinel.return_value + with mock.patch.object( + self.cls, "__eq__", return_value=return_value + ) as m__eq__: + with mock.patch.object(_LENIENT, "context") as mcontext: + result = self.metadata.equal(self.metadata, lenient=False) + + self.assertEqual(1, mcontext.call_count) + args, kwargs = mcontext.call_args + self.assertEqual((), args) + self.assertEqual({_qualname(self.cls.equal): False}, kwargs) + + self.assertEqual(return_value, result) + self.assertEqual(1, m__eq__.call_count) + (arg,), kwargs = m__eq__.call_args + self.assertEqual(id(self.metadata), id(arg)) + self.assertEqual(dict(), kwargs) + + +class Test_name(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + self.default = self.cls.DEFAULT_NAME + + @staticmethod + def _make(standard_name=None, long_name=None, var_name=None): + return BaseMetadata( + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + units=None, + attributes=None, + ) + + def test_standard_name(self): + token = "standard_name" + metadata = self._make(standard_name=token) + + result = metadata.name() + self.assertEqual(token, result) + result = metadata.name(token=True) + self.assertEqual(token, result) + + def test_standard_name__invalid_token(self): + token = "nope nope" + metadata = self._make(standard_name=token) + + result = metadata.name() + self.assertEqual(token, result) + result = metadata.name(token=True) + self.assertEqual(self.default, result) + + def test_long_name(self): + token = "long_name" + metadata = self._make(long_name=token) + + result = metadata.name() + self.assertEqual(token, result) + result = metadata.name(token=True) + self.assertEqual(token, result) + + def test_long_name__invalid_token(self): + token = "nope nope" + metadata = self._make(long_name=token) + + result = metadata.name() + self.assertEqual(token, result) + result = metadata.name(token=True) + self.assertEqual(self.default, result) + + def test_var_name(self): + token = "var_name" + metadata = self._make(var_name=token) + + result = metadata.name() + self.assertEqual(token, result) + result = metadata.name(token=True) + self.assertEqual(token, result) + + def test_var_name__invalid_token(self): + token = "nope nope" + metadata = self._make(var_name=token) + + result = metadata.name() + self.assertEqual(token, result) + result = metadata.name(token=True) + self.assertEqual(self.default, result) + + def test_default(self): + metadata = self._make() + + result = metadata.name() + self.assertEqual(self.default, result) + result = metadata.name(token=True) + self.assertEqual(self.default, result) + + def test_default__invalid_token(self): + token = "nope nope" + metadata = self._make() + + result = metadata.name(default=token) + self.assertEqual(token, result) + + emsg = "Cannot retrieve a valid name token" + with self.assertRaisesRegex(ValueError, emsg): + metadata.name(default=token, token=True) + + +class Test_token(tests.IrisTest): + def setUp(self): + self.cls = BaseMetadata + + def test_passthru_None(self): + result = self.cls.token(None) + self.assertIsNone(result) + + def test_fail_leading_underscore(self): + result = self.cls.token("_nope") + self.assertIsNone(result) + + def test_fail_leading_dot(self): + result = self.cls.token(".nope") + self.assertIsNone(result) + + def test_fail_leading_plus(self): + result = self.cls.token("+nope") + self.assertIsNone(result) + + def test_fail_leading_at(self): + result = self.cls.token("@nope") + self.assertIsNone(result) + + def test_fail_space(self): + result = self.cls.token("nope nope") + self.assertIsNone(result) + + def test_fail_colon(self): + result = self.cls.token("nope:") + self.assertIsNone(result) + + def test_pass_simple(self): + token = "simple" + result = self.cls.token(token) + self.assertEqual(token, result) + + def test_pass_leading_digit(self): + token = "123simple" + result = self.cls.token(token) + self.assertEqual(token, result) + + def test_pass_mixture(self): + token = "S.imple@one+two_3" + result = self.cls.token(token) + self.assertEqual(token, result) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/metadata/test_CellMeasureMetadata.py b/lib/iris/tests/unit/common/metadata/test_CellMeasureMetadata.py new file mode 100644 index 0000000000..6044fbc628 --- /dev/null +++ b/lib/iris/tests/unit/common/metadata/test_CellMeasureMetadata.py @@ -0,0 +1,663 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.metadata.CellMeasureMetadata`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from copy import deepcopy +import unittest.mock as mock +from unittest.mock import sentinel + +from iris.common.lenient import _LENIENT, _qualname +from iris.common.metadata import BaseMetadata, CellMeasureMetadata + + +class Test(tests.IrisTest): + def setUp(self): + self.standard_name = mock.sentinel.standard_name + self.long_name = mock.sentinel.long_name + self.var_name = mock.sentinel.var_name + self.units = mock.sentinel.units + self.attributes = mock.sentinel.attributes + self.measure = mock.sentinel.measure + self.cls = CellMeasureMetadata + + def test_repr(self): + metadata = self.cls( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + units=self.units, + attributes=self.attributes, + measure=self.measure, + ) + fmt = ( + "CellMeasureMetadata(standard_name={!r}, long_name={!r}, " + "var_name={!r}, units={!r}, attributes={!r}, measure={!r})" + ) + expected = fmt.format( + self.standard_name, + self.long_name, + self.var_name, + self.units, + self.attributes, + self.measure, + ) + self.assertEqual(expected, repr(metadata)) + + def test__fields(self): + expected = ( + "standard_name", + "long_name", + "var_name", + "units", + "attributes", + "measure", + ) + self.assertEqual(self.cls._fields, expected) + + def test_bases(self): + self.assertTrue(issubclass(self.cls, BaseMetadata)) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + measure=sentinel.measure, + ) + self.dummy = sentinel.dummy + self.cls = CellMeasureMetadata + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.__eq__.__doc__, self.cls.__eq__.__doc__, + ) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_call(self): + other = sentinel.other + return_value = sentinel.return_value + metadata = self.cls(*(None,) * len(self.cls._fields)) + with mock.patch.object( + BaseMetadata, "__eq__", return_value=return_value + ) as mocker: + result = metadata.__eq__(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different_measure(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_measure(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + +class Test___lt__(tests.IrisTest): + def setUp(self): + self.cls = CellMeasureMetadata + self.one = self.cls(1, 1, 1, 1, 1, 1) + self.two = self.cls(1, 1, 1, 2, 1, 1) + self.none = self.cls(1, 1, 1, None, 1, 1) + self.attributes = self.cls(1, 1, 1, 1, 10, 1) + + def test__ascending_lt(self): + result = self.one < self.two + self.assertTrue(result) + + def test__descending_lt(self): + result = self.two < self.one + self.assertFalse(result) + + def test__none_rhs_operand(self): + result = self.one < self.none + self.assertFalse(result) + + def test__none_lhs_operand(self): + result = self.none < self.one + self.assertTrue(result) + + def test__ignore_attributes(self): + result = self.one < self.attributes + self.assertFalse(result) + result = self.attributes < self.one + self.assertFalse(result) + + +class Test_combine(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + measure=sentinel.measure, + ) + self.dummy = sentinel.dummy + self.cls = CellMeasureMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.combine.__doc__, self.cls.combine.__doc__, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + expected = right.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertTrue(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["units"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different_measure(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["measure"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_measure(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["measure"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["measure"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + +class Test_difference(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + measure=sentinel.measure, + ) + self.dummy = sentinel.dummy + self.cls = CellMeasureMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.difference.__doc__, self.cls.difference.__doc__, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["measure"] = (sentinel.measure, None) + rexpected = deepcopy(self.none)._asdict() + rexpected["measure"] = (None, sentinel.measure) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["units"] = (left["units"], right["units"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["units"] = lexpected["units"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different_measure(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["measure"] = (left["measure"], right["measure"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["measure"] = lexpected["measure"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_strict_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_measure(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["measure"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["measure"] = (left["measure"], right["measure"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["measure"] = lexpected["measure"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_measure_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["measure"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["measure"] = (left["measure"], right["measure"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["measure"] = lexpected["measure"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + +class Test_equal(tests.IrisTest): + def setUp(self): + self.cls = CellMeasureMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual(BaseMetadata.equal.__doc__, self.cls.equal.__doc__) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue(_LENIENT[self.cls.equal]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/metadata/test_CoordMetadata.py b/lib/iris/tests/unit/common/metadata/test_CoordMetadata.py new file mode 100644 index 0000000000..c37d33c62f --- /dev/null +++ b/lib/iris/tests/unit/common/metadata/test_CoordMetadata.py @@ -0,0 +1,724 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.metadata.CoordMetadata`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from copy import deepcopy +import unittest.mock as mock +from unittest.mock import sentinel + +from iris.common.lenient import _LENIENT, _qualname +from iris.common.metadata import BaseMetadata, CoordMetadata + + +class Test(tests.IrisTest): + def setUp(self): + self.standard_name = mock.sentinel.standard_name + self.long_name = mock.sentinel.long_name + self.var_name = mock.sentinel.var_name + self.units = mock.sentinel.units + self.attributes = mock.sentinel.attributes + self.coord_system = mock.sentinel.coord_system + self.climatological = mock.sentinel.climatological + self.cls = CoordMetadata + + def test_repr(self): + metadata = self.cls( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + units=self.units, + attributes=self.attributes, + coord_system=self.coord_system, + climatological=self.climatological, + ) + fmt = ( + "CoordMetadata(standard_name={!r}, long_name={!r}, " + "var_name={!r}, units={!r}, attributes={!r}, coord_system={!r}, " + "climatological={!r})" + ) + expected = fmt.format( + self.standard_name, + self.long_name, + self.var_name, + self.units, + self.attributes, + self.coord_system, + self.climatological, + ) + self.assertEqual(expected, repr(metadata)) + + def test__fields(self): + expected = ( + "standard_name", + "long_name", + "var_name", + "units", + "attributes", + "coord_system", + "climatological", + ) + self.assertEqual(self.cls._fields, expected) + + def test_bases(self): + self.assertTrue(issubclass(self.cls, BaseMetadata)) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + coord_system=sentinel.coord_system, + climatological=sentinel.climatological, + ) + self.dummy = sentinel.dummy + self.cls = CoordMetadata + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.__eq__.__doc__, self.cls.__eq__.__doc__, + ) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_call(self): + other = sentinel.other + return_value = sentinel.return_value + metadata = self.cls(*(None,) * len(self.cls._fields)) + with mock.patch.object( + BaseMetadata, "__eq__", return_value=return_value + ) as mocker: + result = metadata.__eq__(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different_members(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_members(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + +class Test___lt__(tests.IrisTest): + def setUp(self): + self.cls = CoordMetadata + self.one = self.cls(1, 1, 1, 1, 1, 1, 1) + self.two = self.cls(1, 1, 1, 2, 1, 1, 1) + self.none = self.cls(1, 1, 1, None, 1, 1, 1) + self.attributes_cs = self.cls(1, 1, 1, 1, 10, 10, 1) + + def test__ascending_lt(self): + result = self.one < self.two + self.assertTrue(result) + + def test__descending_lt(self): + result = self.two < self.one + self.assertFalse(result) + + def test__none_rhs_operand(self): + result = self.one < self.none + self.assertFalse(result) + + def test__none_lhs_operand(self): + result = self.none < self.one + self.assertTrue(result) + + def test__ignore_attributes_coord_system(self): + result = self.one < self.attributes_cs + self.assertFalse(result) + result = self.attributes_cs < self.one + self.assertFalse(result) + + +class Test_combine(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + coord_system=sentinel.coord_system, + climatological=sentinel.climatological, + ) + self.dummy = sentinel.dummy + self.cls = CoordMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.combine.__doc__, self.cls.combine.__doc__, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + expected = right.copy() + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertTrue( + expected, lmetadata.combine(rmetadata)._asdict() + ) + self.assertTrue( + expected, rmetadata.combine(lmetadata)._asdict() + ) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["units"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different_members(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected[member] = None + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertEqual( + expected, lmetadata.combine(rmetadata)._asdict() + ) + self.assertEqual( + expected, rmetadata.combine(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_members(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected[member] = None + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertEqual( + expected, lmetadata.combine(rmetadata)._asdict() + ) + self.assertEqual( + expected, rmetadata.combine(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected[member] = None + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertEqual( + expected, lmetadata.combine(rmetadata)._asdict() + ) + self.assertEqual( + expected, rmetadata.combine(lmetadata)._asdict() + ) + + +class Test_difference(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + coord_system=sentinel.coord_system, + climatological=sentinel.climatological, + ) + self.dummy = sentinel.dummy + self.cls = CoordMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.difference.__doc__, self.cls.difference.__doc__, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_members_none(self): + for member in self.cls._members: + lmetadata = self.cls(**self.values) + member_value = getattr(lmetadata, member) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected[member] = (member_value, None) + rexpected = deepcopy(self.none)._asdict() + rexpected[member] = (None, member_value) + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["units"] = (left["units"], right["units"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["units"] = lexpected["units"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different_members(self): + for member in self.cls._members: + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected[member] = (left[member], right[member]) + rexpected = deepcopy(self.none)._asdict() + rexpected[member] = lexpected[member][::-1] + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=True + ): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_strict_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_members(self): + for member in self.cls._members: + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right[member] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected[member] = (left[member], right[member]) + rexpected = deepcopy(self.none)._asdict() + rexpected[member] = lexpected[member][::-1] + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_members_none(self): + for member in self.cls._members: + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right[member] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected[member] = (left[member], right[member]) + rexpected = deepcopy(self.none)._asdict() + rexpected[member] = lexpected[member][::-1] + + with mock.patch( + "iris.common.metadata._LENIENT", return_value=False + ): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + +class Test_equal(tests.IrisTest): + def setUp(self): + self.cls = CoordMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual(BaseMetadata.equal.__doc__, self.cls.equal.__doc__) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue(_LENIENT[self.cls.equal]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/metadata/test_CubeMetadata.py b/lib/iris/tests/unit/common/metadata/test_CubeMetadata.py new file mode 100644 index 0000000000..1636f85189 --- /dev/null +++ b/lib/iris/tests/unit/common/metadata/test_CubeMetadata.py @@ -0,0 +1,831 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.metadata.CubeMetadata`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from copy import deepcopy +import unittest.mock as mock +from unittest.mock import sentinel + +from iris.common.lenient import _LENIENT, _qualname +from iris.common.metadata import BaseMetadata, CubeMetadata + + +def _make_metadata( + standard_name=None, + long_name=None, + var_name=None, + attributes=None, + force_mapping=True, +): + if force_mapping: + if attributes is None: + attributes = {} + else: + attributes = dict(STASH=attributes) + + return CubeMetadata( + standard_name=standard_name, + long_name=long_name, + var_name=var_name, + units=None, + attributes=attributes, + cell_methods=None, + ) + + +class Test(tests.IrisTest): + def setUp(self): + self.standard_name = mock.sentinel.standard_name + self.long_name = mock.sentinel.long_name + self.var_name = mock.sentinel.var_name + self.units = mock.sentinel.units + self.attributes = mock.sentinel.attributes + self.cell_methods = mock.sentinel.cell_methods + self.cls = CubeMetadata + + def test_repr(self): + metadata = self.cls( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + units=self.units, + attributes=self.attributes, + cell_methods=self.cell_methods, + ) + fmt = ( + "CubeMetadata(standard_name={!r}, long_name={!r}, var_name={!r}, " + "units={!r}, attributes={!r}, cell_methods={!r})" + ) + expected = fmt.format( + self.standard_name, + self.long_name, + self.var_name, + self.units, + self.attributes, + self.cell_methods, + ) + self.assertEqual(expected, repr(metadata)) + + def test__fields(self): + expected = ( + "standard_name", + "long_name", + "var_name", + "units", + "attributes", + "cell_methods", + ) + self.assertEqual(self.cls._fields, expected) + + def test_bases(self): + self.assertTrue(issubclass(self.cls, BaseMetadata)) + + +class Test___eq__(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + # Must be a mapping. + attributes=dict(), + cell_methods=sentinel.cell_methods, + ) + self.dummy = sentinel.dummy + self.cls = CubeMetadata + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.__eq__.__doc__, self.cls.__eq__.__doc__, + ) + + def test_lenient_service(self): + qualname___eq__ = _qualname(self.cls.__eq__) + self.assertIn(qualname___eq__, _LENIENT) + self.assertTrue(_LENIENT[qualname___eq__]) + self.assertTrue(_LENIENT[self.cls.__eq__]) + + def test_call(self): + other = sentinel.other + return_value = sentinel.return_value + metadata = self.cls(*(None,) * len(self.cls._fields)) + with mock.patch.object( + BaseMetadata, "__eq__", return_value=return_value + ) as mocker: + result = metadata.__eq__(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_same_cell_methods_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_lenient_different_cell_methods(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertTrue(lmetadata.__eq__(rmetadata)) + self.assertTrue(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_cell_methods(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + def test_op_strict_different_measure_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertFalse(lmetadata.__eq__(rmetadata)) + self.assertFalse(rmetadata.__eq__(lmetadata)) + + +class Test___lt__(tests.IrisTest): + def setUp(self): + self.cls = CubeMetadata + self.one = self.cls(1, 1, 1, 1, 1, 1) + self.two = self.cls(1, 1, 1, 2, 1, 1) + self.none = self.cls(1, 1, 1, None, 1, 1) + self.attributes_cm = self.cls(1, 1, 1, 1, 10, 10) + + def test__ascending_lt(self): + result = self.one < self.two + self.assertTrue(result) + + def test__descending_lt(self): + result = self.two < self.one + self.assertFalse(result) + + def test__none_rhs_operand(self): + result = self.one < self.none + self.assertFalse(result) + + def test__none_lhs_operand(self): + result = self.none < self.one + self.assertTrue(result) + + def test__ignore_attributes_cell_methods(self): + result = self.one < self.attributes_cm + self.assertFalse(result) + result = self.attributes_cm < self.one + self.assertFalse(result) + + +class Test_combine(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + cell_methods=sentinel.cell_methods, + ) + self.dummy = sentinel.dummy + self.cls = CubeMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.combine.__doc__, self.cls.combine.__doc__, + ) + + def test_lenient_service(self): + qualname_combine = _qualname(self.cls.combine) + self.assertIn(qualname_combine, _LENIENT) + self.assertTrue(_LENIENT[qualname_combine]) + self.assertTrue(_LENIENT[self.cls.combine]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "combine", return_value=return_value + ) as mocker: + result = self.none.combine(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + expected = self.values + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_same_cell_methods_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + expected = right.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertTrue(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertTrue(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["units"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_lenient_different_cell_methods(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["cell_methods"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + expected = self.values.copy() + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_cell_methods(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["cell_methods"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["long_name"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + def test_op_strict_different_cell_methods_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + expected = self.values.copy() + expected["cell_methods"] = None + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual(expected, lmetadata.combine(rmetadata)._asdict()) + self.assertEqual(expected, rmetadata.combine(lmetadata)._asdict()) + + +class Test_difference(tests.IrisTest): + def setUp(self): + self.values = dict( + standard_name=sentinel.standard_name, + long_name=sentinel.long_name, + var_name=sentinel.var_name, + units=sentinel.units, + attributes=sentinel.attributes, + cell_methods=sentinel.cell_methods, + ) + self.dummy = sentinel.dummy + self.cls = CubeMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual( + BaseMetadata.difference.__doc__, self.cls.difference.__doc__, + ) + + def test_lenient_service(self): + qualname_difference = _qualname(self.cls.difference) + self.assertIn(qualname_difference, _LENIENT) + self.assertTrue(_LENIENT[qualname_difference]) + self.assertTrue(_LENIENT[self.cls.difference]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "difference", return_value=return_value + ) as mocker: + result = self.none.difference(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + def test_op_lenient_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["var_name"] = None + rmetadata = self.cls(**right) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_lenient_same_cell_methods_none(self): + lmetadata = self.cls(**self.values) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["cell_methods"] = (sentinel.cell_methods, None) + rexpected = deepcopy(self.none)._asdict() + rexpected["cell_methods"] = (None, sentinel.cell_methods) + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["units"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["units"] = (left["units"], right["units"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["units"] = lexpected["units"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_lenient_different_cell_methods(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["cell_methods"] = ( + left["cell_methods"], + right["cell_methods"], + ) + rexpected = deepcopy(self.none)._asdict() + rexpected["cell_methods"] = lexpected["cell_methods"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=True): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_same(self): + lmetadata = self.cls(**self.values) + rmetadata = self.cls(**self.values) + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertIsNone(lmetadata.difference(rmetadata)) + self.assertIsNone(rmetadata.difference(lmetadata)) + + def test_op_strict_different(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_cell_methods(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["cell_methods"] = self.dummy + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["cell_methods"] = ( + left["cell_methods"], + right["cell_methods"], + ) + rexpected = deepcopy(self.none)._asdict() + rexpected["cell_methods"] = lexpected["cell_methods"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["long_name"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["long_name"] = (left["long_name"], right["long_name"]) + rexpected = deepcopy(self.none)._asdict() + rexpected["long_name"] = lexpected["long_name"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + def test_op_strict_different_measure_none(self): + left = self.values.copy() + lmetadata = self.cls(**left) + right = self.values.copy() + right["cell_methods"] = None + rmetadata = self.cls(**right) + lexpected = deepcopy(self.none)._asdict() + lexpected["cell_methods"] = ( + left["cell_methods"], + right["cell_methods"], + ) + rexpected = deepcopy(self.none)._asdict() + rexpected["cell_methods"] = lexpected["cell_methods"][::-1] + + with mock.patch("iris.common.metadata._LENIENT", return_value=False): + self.assertEqual( + lexpected, lmetadata.difference(rmetadata)._asdict() + ) + self.assertEqual( + rexpected, rmetadata.difference(lmetadata)._asdict() + ) + + +class Test_equal(tests.IrisTest): + def setUp(self): + self.cls = CubeMetadata + self.none = self.cls(*(None,) * len(self.cls._fields)) + + def test_wraps_docstring(self): + self.assertEqual(BaseMetadata.equal.__doc__, self.cls.equal.__doc__) + + def test_lenient_service(self): + qualname_equal = _qualname(self.cls.equal) + self.assertIn(qualname_equal, _LENIENT) + self.assertTrue(_LENIENT[qualname_equal]) + self.assertTrue(_LENIENT[self.cls.equal]) + + def test_lenient_default(self): + other = sentinel.other + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=None), kwargs) + + def test_lenient(self): + other = sentinel.other + lenient = sentinel.lenient + return_value = sentinel.return_value + with mock.patch.object( + BaseMetadata, "equal", return_value=return_value + ) as mocker: + result = self.none.equal(other, lenient=lenient) + + self.assertEqual(return_value, result) + self.assertEqual(1, mocker.call_count) + (arg,), kwargs = mocker.call_args + self.assertEqual(other, arg) + self.assertEqual(dict(lenient=lenient), kwargs) + + +class Test_name(tests.IrisTest): + def setUp(self): + self.default = CubeMetadata.DEFAULT_NAME + + def test_standard_name(self): + token = "standard_name" + metadata = _make_metadata(standard_name=token) + result = metadata.name() + self.assertEqual(result, token) + result = metadata.name(token=True) + self.assertEqual(result, token) + + def test_standard_name__invalid_token(self): + token = "nope nope" + metadata = _make_metadata(standard_name=token) + result = metadata.name() + self.assertEqual(result, token) + result = metadata.name(token=True) + self.assertEqual(result, self.default) + + def test_long_name(self): + token = "long_name" + metadata = _make_metadata(long_name=token) + result = metadata.name() + self.assertEqual(result, token) + result = metadata.name(token=True) + self.assertEqual(result, token) + + def test_long_name__invalid_token(self): + token = "nope nope" + metadata = _make_metadata(long_name=token) + result = metadata.name() + self.assertEqual(result, token) + result = metadata.name(token=True) + self.assertEqual(result, self.default) + + def test_var_name(self): + token = "var_name" + metadata = _make_metadata(var_name=token) + result = metadata.name() + self.assertEqual(result, token) + result = metadata.name(token=True) + self.assertEqual(result, token) + + def test_var_name__invalid_token(self): + token = "nope nope" + metadata = _make_metadata(var_name=token) + result = metadata.name() + self.assertEqual(result, token) + result = metadata.name(token=True) + self.assertEqual(result, self.default) + + def test_attributes(self): + token = "stash" + metadata = _make_metadata(attributes=token) + result = metadata.name() + self.assertEqual(result, token) + result = metadata.name(token=True) + self.assertEqual(result, token) + + def test_attributes__invalid_token(self): + token = "nope nope" + metadata = _make_metadata(attributes=token) + result = metadata.name() + self.assertEqual(result, token) + result = metadata.name(token=True) + self.assertEqual(result, self.default) + + def test_attributes__non_mapping(self): + metadata = _make_metadata(force_mapping=False) + self.assertIsNone(metadata.attributes) + emsg = "Invalid 'CubeMetadata.attributes' member, must be a mapping." + with self.assertRaisesRegex(AttributeError, emsg): + _ = metadata.name() + + def test_default(self): + metadata = _make_metadata() + result = metadata.name() + self.assertEqual(result, self.default) + result = metadata.name(token=True) + self.assertEqual(result, self.default) + + def test_default__invalid_token(self): + token = "nope nope" + metadata = _make_metadata() + result = metadata.name(default=token) + self.assertEqual(result, token) + emsg = "Cannot retrieve a valid name token" + with self.assertRaisesRegex(ValueError, emsg): + _ = metadata.name(default=token, token=True) + + +class Test__names(tests.IrisTest): + def test_standard_name(self): + token = "standard_name" + metadata = _make_metadata(standard_name=token) + expected = (token, None, None, None) + result = metadata._names + self.assertEqual(expected, result) + + def test_long_name(self): + token = "long_name" + metadata = _make_metadata(long_name=token) + expected = (None, token, None, None) + result = metadata._names + self.assertEqual(expected, result) + + def test_var_name(self): + token = "var_name" + metadata = _make_metadata(var_name=token) + expected = (None, None, token, None) + result = metadata._names + self.assertEqual(expected, result) + + def test_attributes(self): + token = "stash" + metadata = _make_metadata(attributes=token) + expected = (None, None, None, token) + result = metadata._names + self.assertEqual(expected, result) + + def test_attributes__non_mapping(self): + metadata = _make_metadata(force_mapping=False) + self.assertIsNone(metadata.attributes) + emsg = "Invalid 'CubeMetadata.attributes' member, must be a mapping." + with self.assertRaisesRegex(AttributeError, emsg): + _ = metadata._names + + def test_None(self): + metadata = _make_metadata() + expected = (None, None, None, None) + result = metadata._names + self.assertEqual(expected, result) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/metadata/test__NamedTupleMeta.py b/lib/iris/tests/unit/common/metadata/test__NamedTupleMeta.py new file mode 100644 index 0000000000..72b3c1bc8f --- /dev/null +++ b/lib/iris/tests/unit/common/metadata/test__NamedTupleMeta.py @@ -0,0 +1,148 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.metadata._NamedTupleMeta`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from abc import abstractmethod + +from iris.common.metadata import _NamedTupleMeta + + +class Test(tests.IrisTest): + @staticmethod + def names(classes): + return [cls.__name__ for cls in classes] + + @staticmethod + def emsg_generate(members): + if isinstance(members, str): + members = (members,) + emsg = ".* missing {} required positional argument{}: {}" + args = ", ".join([f"{member!r}" for member in members[:-1]]) + count = len(members) + if count == 1: + args += f"{members[-1]!r}" + elif count == 2: + args += f" and {members[-1]!r}" + else: + args += f", and {members[-1]!r}" + plural = "s" if count > 1 else "" + return emsg.format(len(members), plural, args) + + def test__no_bases_with_abstract_members_property(self): + class Metadata(metaclass=_NamedTupleMeta): + @property + @abstractmethod + def _members(self): + pass + + expected = ["object"] + self.assertEqual(self.names(Metadata.__bases__), expected) + expected = ["Metadata", "object"] + self.assertEqual(self.names(Metadata.__mro__), expected) + emsg = ( + "Can't instantiate abstract class .* with abstract " + "methods _members" + ) + with self.assertRaisesRegex(TypeError, emsg): + _ = Metadata() + + def test__no_bases_single_member(self): + member = "arg_one" + + class Metadata(metaclass=_NamedTupleMeta): + _members = member + + expected = ["MetadataNamedtuple"] + self.assertEqual(self.names(Metadata.__bases__), expected) + expected = ["Metadata", "MetadataNamedtuple", "tuple", "object"] + self.assertEqual(self.names(Metadata.__mro__), expected) + emsg = self.emsg_generate(member) + with self.assertRaisesRegex(TypeError, emsg): + _ = Metadata() + metadata = Metadata(1) + self.assertEqual(metadata._fields, (member,)) + self.assertEqual(metadata.arg_one, 1) + + def test__no_bases_multiple_members(self): + members = ("arg_one", "arg_two") + + class Metadata(metaclass=_NamedTupleMeta): + _members = members + + expected = ["MetadataNamedtuple"] + self.assertEqual(self.names(Metadata.__bases__), expected) + expected = ["Metadata", "MetadataNamedtuple", "tuple", "object"] + self.assertEqual(self.names(Metadata.__mro__), expected) + emsg = self.emsg_generate(members) + with self.assertRaisesRegex(TypeError, emsg): + _ = Metadata() + values = range(len(members)) + metadata = Metadata(*values) + self.assertEqual(metadata._fields, members) + expected = dict(zip(members, values)) + self.assertEqual(metadata._asdict(), expected) + + def test__multiple_bases_multiple_members(self): + members_parent = ("arg_one", "arg_two") + members_child = ("arg_three", "arg_four") + + class MetadataParent(metaclass=_NamedTupleMeta): + _members = members_parent + + class MetadataChild(MetadataParent): + _members = members_child + + # Check the parent class... + expected = ["MetadataParentNamedtuple"] + self.assertEqual(self.names(MetadataParent.__bases__), expected) + expected = [ + "MetadataParent", + "MetadataParentNamedtuple", + "tuple", + "object", + ] + self.assertEqual(self.names(MetadataParent.__mro__), expected) + emsg = self.emsg_generate(members_parent) + with self.assertRaisesRegex(TypeError, emsg): + _ = MetadataParent() + values_parent = range(len(members_parent)) + metadata_parent = MetadataParent(*values_parent) + self.assertEqual(metadata_parent._fields, members_parent) + expected = dict(zip(members_parent, values_parent)) + self.assertEqual(metadata_parent._asdict(), expected) + + # Check the dependant child class... + expected = ["MetadataChildNamedtuple", "MetadataParent"] + self.assertEqual(self.names(MetadataChild.__bases__), expected) + expected = [ + "MetadataChild", + "MetadataChildNamedtuple", + "MetadataParent", + "MetadataParentNamedtuple", + "tuple", + "object", + ] + self.assertEqual(self.names(MetadataChild.__mro__), expected) + emsg = self.emsg_generate((*members_parent, *members_child)) + with self.assertRaisesRegex(TypeError, emsg): + _ = MetadataChild() + fields_child = (*members_parent, *members_child) + values_child = range(len(fields_child)) + metadata_child = MetadataChild(*values_child) + self.assertEqual(metadata_child._fields, fields_child) + expected = dict(zip(fields_child, values_child)) + self.assertEqual(metadata_child._asdict(), expected) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/metadata/test__hexdigest.py b/lib/iris/tests/unit/common/metadata/test__hexdigest.py new file mode 100644 index 0000000000..798f71bcd0 --- /dev/null +++ b/lib/iris/tests/unit/common/metadata/test__hexdigest.py @@ -0,0 +1,179 @@ +# 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 +# licensing details. +""" +Unit tests for the :func:`iris.common.metadata._hexdigest`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from unittest import mock + +import numpy.ma as ma +import numpy as np +from xxhash import xxh64, xxh64_hexdigest + +from iris.common.metadata import _hexdigest as hexdigest + + +class TestBytesLikeObject(tests.IrisTest): + def setUp(self): + self.hasher = xxh64() + self.hasher.reset() + + @staticmethod + def _ndarray(value): + parts = str((value.shape, xxh64_hexdigest(value))) + return xxh64_hexdigest(parts) + + @staticmethod + def _masked(value): + parts = str( + ( + value.shape, + xxh64_hexdigest(value.data), + xxh64_hexdigest(value.mask), + ) + ) + return xxh64_hexdigest(parts) + + def test_string(self): + value = "hello world" + self.hasher.update(value) + expected = self.hasher.hexdigest() + self.assertEqual(expected, hexdigest(value)) + + def test_numpy_array_int(self): + value = np.arange(10, dtype=np.int) + expected = self._ndarray(value) + self.assertEqual(expected, hexdigest(value)) + + def test_numpy_array_float(self): + value = np.arange(10, dtype=np.float) + expected = self._ndarray(value) + self.assertEqual(expected, hexdigest(value)) + + def test_numpy_array_float_not_int(self): + ivalue = np.arange(10, dtype=np.int) + fvalue = np.arange(10, dtype=np.float) + expected = self._ndarray(ivalue) + self.assertNotEqual(expected, hexdigest(fvalue)) + + def test_numpy_array_reshape(self): + value = np.arange(10).reshape(2, 5) + expected = self._ndarray(value) + self.assertEqual(expected, hexdigest(value)) + + def test_numpy_array_reshape_not_flat(self): + value = np.arange(10).reshape(2, 5) + expected = self._ndarray(value) + self.assertNotEqual(expected, hexdigest(value.flatten())) + + def test_masked_array_int(self): + value = ma.arange(10, dtype=np.int) + expected = self._masked(value) + self.assertEqual(expected, hexdigest(value)) + + value[0] = ma.masked + self.assertNotEqual(expected, hexdigest(value)) + expected = self._masked(value) + self.assertEqual(expected, hexdigest(value)) + + def test_masked_array_float(self): + value = ma.arange(10, dtype=np.float) + expected = self._masked(value) + self.assertEqual(expected, hexdigest(value)) + + value[0] = ma.masked + self.assertNotEqual(expected, hexdigest(value)) + expected = self._masked(value) + self.assertEqual(expected, hexdigest(value)) + + def test_masked_array_float_not_int(self): + ivalue = ma.arange(10, dtype=np.int) + fvalue = ma.arange(10, dtype=np.float) + expected = self._masked(ivalue) + self.assertNotEqual(expected, hexdigest(fvalue)) + + def test_masked_array_not_array(self): + value = ma.arange(10) + expected = self._masked(value) + self.assertNotEqual(expected, hexdigest(value.data)) + + def test_masked_array_reshape(self): + value = ma.arange(10).reshape(2, 5) + expected = self._masked(value) + self.assertEqual(expected, hexdigest(value)) + + def test_masked_array_reshape_not_flat(self): + value = ma.arange(10).reshape(2, 5) + expected = self._masked(value) + self.assertNotEqual(expected, hexdigest(value.flatten())) + + +class TestNotBytesLikeObject(tests.IrisTest): + def _expected(self, value): + parts = str((type(value), value)) + return xxh64_hexdigest(parts) + + def test_int(self): + value = 123 + expected = self._expected(value) + self.assertEqual(expected, hexdigest(value)) + + def test_numpy_int(self): + value = np.int(123) + expected = self._expected(value) + self.assertEqual(expected, hexdigest(value)) + + def test_float(self): + value = 123.4 + expected = self._expected(value) + self.assertEqual(expected, hexdigest(value)) + + def test_numpy_float(self): + value = np.float(123.4) + expected = self._expected(value) + self.assertEqual(expected, hexdigest(value)) + + def test_list(self): + value = [1, 2, 3] + expected = self._expected(value) + self.assertEqual(expected, hexdigest(value)) + + def test_tuple(self): + value = (1, 2, 3) + expected = self._expected(value) + self.assertEqual(expected, hexdigest(value)) + + def test_dict(self): + value = dict(one=1, two=2, three=3) + expected = self._expected(value) + self.assertEqual(expected, hexdigest(value)) + + def test_sentinel(self): + value = mock.sentinel.value + expected = self._expected(value) + self.assertEqual(expected, hexdigest(value)) + + def test_instance(self): + class Dummy: + pass + + value = Dummy() + expected = self._expected(value) + self.assertEqual(expected, hexdigest(value)) + + def test_int_not_str(self): + value = 123 + expected = self._expected(value) + self.assertNotEqual(expected, hexdigest(str(value))) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py b/lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py new file mode 100644 index 0000000000..6678aca446 --- /dev/null +++ b/lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py @@ -0,0 +1,210 @@ +# 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 +# licensing details. +""" +Unit tests for the :func:`iris.common.metadata.metadata_manager_factory`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +import pickle +import unittest.mock as mock + +from cf_units import Unit + +from iris.common.metadata import ( + AncillaryVariableMetadata, + BaseMetadata, + CellMeasureMetadata, + CoordMetadata, + CubeMetadata, + metadata_manager_factory, +) + + +BASES = [ + AncillaryVariableMetadata, + BaseMetadata, + CellMeasureMetadata, + CoordMetadata, + CubeMetadata, +] + + +class Test_factory(tests.IrisTest): + def test__subclass_invalid(self): + class Other: + pass + + emsg = "Require a subclass of 'BaseMetadata'" + with self.assertRaisesRegex(TypeError, emsg): + _ = metadata_manager_factory(Other) + + def test__kwargs_invalid(self): + emsg = "Invalid 'BaseMetadata' field parameters, got 'wibble'." + with self.assertRaisesRegex(ValueError, emsg): + metadata_manager_factory(BaseMetadata, wibble="nope") + + +class Test_instance(tests.IrisTest): + def setUp(self): + self.bases = BASES + + def test__namespace(self): + namespace = [ + "DEFAULT_NAME", + "__init__", + "__eq__", + "__getstate__", + "__ne__", + "__reduce__", + "__repr__", + "__setstate__", + "fields", + "name", + "token", + "values", + ] + for base in self.bases: + metadata = metadata_manager_factory(base) + for name in namespace: + self.assertTrue(hasattr(metadata, name)) + if base is CubeMetadata: + self.assertTrue(hasattr(metadata, "_names")) + self.assertIs(metadata.cls, base) + + def test__kwargs_default(self): + for base in self.bases: + kwargs = dict(zip(base._fields, [None] * len(base._fields))) + metadata = metadata_manager_factory(base) + self.assertEqual(metadata.values._asdict(), kwargs) + + def test__kwargs(self): + for base in self.bases: + kwargs = dict(zip(base._fields, range(len(base._fields)))) + metadata = metadata_manager_factory(base, **kwargs) + self.assertEqual(metadata.values._asdict(), kwargs) + + +class Test_instance___eq__(tests.IrisTest): + def setUp(self): + self.metadata = metadata_manager_factory(BaseMetadata) + + def test__not_implemented(self): + self.assertNotEqual(self.metadata, 1) + + def test__not_is_cls(self): + base = BaseMetadata + other = metadata_manager_factory(base) + self.assertIs(other.cls, base) + other.cls = CoordMetadata + self.assertNotEqual(self.metadata, other) + + def test__not_values(self): + standard_name = mock.sentinel.standard_name + other = metadata_manager_factory( + BaseMetadata, standard_name=standard_name + ) + self.assertEqual(other.standard_name, standard_name) + self.assertIsNone(other.long_name) + self.assertIsNone(other.var_name) + self.assertIsNone(other.units) + self.assertIsNone(other.attributes) + self.assertNotEqual(self.metadata, other) + + def test__same_default(self): + other = metadata_manager_factory(BaseMetadata) + self.assertEqual(self.metadata, other) + + def test__same(self): + kwargs = dict( + standard_name=1, long_name=2, var_name=3, units=4, attributes=5 + ) + metadata = metadata_manager_factory(BaseMetadata, **kwargs) + other = metadata_manager_factory(BaseMetadata, **kwargs) + self.assertEqual(metadata.values._asdict(), kwargs) + self.assertEqual(metadata, other) + + +class Test_instance____repr__(tests.IrisTest): + def setUp(self): + self.metadata = metadata_manager_factory(BaseMetadata) + + def test(self): + standard_name = mock.sentinel.standard_name + long_name = mock.sentinel.long_name + var_name = mock.sentinel.var_name + units = mock.sentinel.units + attributes = mock.sentinel.attributes + values = (standard_name, long_name, var_name, units, attributes) + + for field, value in zip(self.metadata.fields, values): + setattr(self.metadata, field, value) + + result = repr(self.metadata) + expected = ( + "MetadataManager(standard_name={!r}, long_name={!r}, var_name={!r}, " + "units={!r}, attributes={!r})" + ) + self.assertEqual(result, expected.format(*values)) + + +class Test_instance__pickle(tests.IrisTest): + def setUp(self): + self.standard_name = "standard_name" + self.long_name = "long_name" + self.var_name = "var_name" + self.units = Unit("1") + self.attributes = dict(hello="world") + values = ( + self.standard_name, + self.long_name, + self.var_name, + self.units, + self.attributes, + ) + self.kwargs = dict(zip(BaseMetadata._fields, values)) + self.metadata = metadata_manager_factory(BaseMetadata, **self.kwargs) + + def test_pickle(self): + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + with self.temp_filename(suffix=".pkl") as fname: + with open(fname, "wb") as fo: + pickle.dump(self.metadata, fo, protocol=protocol) + with open(fname, "rb") as fi: + metadata = pickle.load(fi) + self.assertEqual(metadata, self.metadata) + + +class Test_instance__fields(tests.IrisTest): + def setUp(self): + self.bases = BASES + + def test(self): + for base in self.bases: + fields = base._fields + metadata = metadata_manager_factory(base) + self.assertEqual(metadata.fields, fields) + for field in fields: + hasattr(metadata, field) + + +class Test_instance__values(tests.IrisTest): + def setUp(self): + self.bases = BASES + + def test(self): + for base in self.bases: + metadata = metadata_manager_factory(base) + result = metadata.values + self.assertIsInstance(result, base) + self.assertEqual(result._fields, base._fields) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/mixin/__init__.py b/lib/iris/tests/unit/common/mixin/__init__.py new file mode 100644 index 0000000000..493e140626 --- /dev/null +++ b/lib/iris/tests/unit/common/mixin/__init__.py @@ -0,0 +1,6 @@ +# 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 +# licensing details. +"""Unit tests for the :mod:`iris.common.mixin` package.""" diff --git a/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py new file mode 100644 index 0000000000..5ac9361e4f --- /dev/null +++ b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py @@ -0,0 +1,364 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.mixin.CFVariableMixin`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from collections import OrderedDict, namedtuple +from unittest import mock + +from cf_units import Unit + +from iris.common.metadata import ( + AncillaryVariableMetadata, + BaseMetadata, + CellMeasureMetadata, + CoordMetadata, + CubeMetadata, +) +from iris.common.mixin import CFVariableMixin, LimitedAttributeDict + + +class Test__getter(tests.IrisTest): + def setUp(self): + self.standard_name = mock.sentinel.standard_name + self.long_name = mock.sentinel.long_name + self.var_name = mock.sentinel.var_name + self.units = mock.sentinel.units + self.attributes = mock.sentinel.attributes + self.metadata = mock.sentinel.metadata + + metadata = mock.MagicMock( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + units=self.units, + attributes=self.attributes, + values=self.metadata, + ) + + self.item = CFVariableMixin() + self.item._metadata_manager = metadata + + def test_standard_name(self): + self.assertEqual(self.item.standard_name, self.standard_name) + + def test_long_name(self): + self.assertEqual(self.item.long_name, self.long_name) + + def test_var_name(self): + self.assertEqual(self.item.var_name, self.var_name) + + def test_units(self): + self.assertEqual(self.item.units, self.units) + + def test_attributes(self): + self.assertEqual(self.item.attributes, self.attributes) + + def test_metadata(self): + self.assertEqual(self.item.metadata, self.metadata) + + +class Test__setter(tests.IrisTest): + def setUp(self): + metadata = mock.MagicMock( + standard_name=mock.sentinel.standard_name, + long_name=mock.sentinel.long_name, + var_name=mock.sentinel.var_name, + units=mock.sentinel.units, + attributes=mock.sentinel.attributes, + token=lambda name: name, + ) + + self.item = CFVariableMixin() + self.item._metadata_manager = metadata + + def test_standard_name__valid(self): + standard_name = "air_temperature" + self.item.standard_name = standard_name + self.assertEqual( + self.item._metadata_manager.standard_name, standard_name + ) + + def test_standard_name__none(self): + self.item.standard_name = None + self.assertIsNone(self.item._metadata_manager.standard_name) + + def test_standard_name__invalid(self): + standard_name = "nope nope" + emsg = f"{standard_name!r} is not a valid standard_name" + with self.assertRaisesRegex(ValueError, emsg): + self.item.standard_name = standard_name + + def test_long_name(self): + long_name = "long_name" + self.item.long_name = long_name + self.assertEqual(self.item._metadata_manager.long_name, long_name) + + def test_long_name__none(self): + self.item.long_name = None + self.assertIsNone(self.item._metadata_manager.long_name) + + def test_var_name(self): + var_name = "var_name" + self.item.var_name = var_name + self.assertEqual(self.item._metadata_manager.var_name, var_name) + + def test_var_name__none(self): + self.item.var_name = None + self.assertIsNone(self.item._metadata_manager.var_name) + + def test_var_name__invalid_token(self): + var_name = "nope nope" + self.item._metadata_manager.token = lambda name: None + emsg = f"{var_name!r} is not a valid NetCDF variable name." + with self.assertRaisesRegex(ValueError, emsg): + self.item.var_name = var_name + + def test_attributes(self): + attributes = dict(hello="world") + self.item.attributes = attributes + self.assertEqual(self.item._metadata_manager.attributes, attributes) + self.assertIsNot(self.item._metadata_manager.attributes, attributes) + self.assertIsInstance( + self.item._metadata_manager.attributes, LimitedAttributeDict + ) + + def test_attributes__none(self): + self.item.attributes = None + self.assertEqual(self.item._metadata_manager.attributes, {}) + + +class Test__metadata_setter(tests.IrisTest): + def setUp(self): + class Metadata: + def __init__(self): + self.cls = BaseMetadata + self.fields = BaseMetadata._fields + self.standard_name = mock.sentinel.standard_name + self.long_name = mock.sentinel.long_name + self.var_name = mock.sentinel.var_name + self.units = mock.sentinel.units + self.attributes = mock.sentinel.attributes + self.token = lambda name: name + + @property + def values(self): + return dict( + standard_name=self.standard_name, + long_name=self.long_name, + var_name=self.var_name, + units=self.units, + attributes=self.attributes, + ) + + metadata = Metadata() + self.item = CFVariableMixin() + self.item._metadata_manager = metadata + self.attributes = dict(one=1, two=2, three=3) + self.args = OrderedDict( + standard_name="air_temperature", + long_name="long_name", + var_name="var_name", + units=Unit("1"), + attributes=self.attributes, + ) + + def test_dict(self): + metadata = dict(**self.args) + self.item.metadata = metadata + self.assertEqual(self.item._metadata_manager.values, metadata) + self.assertIsNot( + self.item._metadata_manager.attributes, self.attributes + ) + + def test_dict__partial(self): + metadata = dict(**self.args) + del metadata["standard_name"] + self.item.metadata = metadata + metadata["standard_name"] = mock.sentinel.standard_name + self.assertEqual(self.item._metadata_manager.values, metadata) + self.assertIsNot( + self.item._metadata_manager.attributes, self.attributes + ) + + def test_ordereddict(self): + metadata = self.args + self.item.metadata = metadata + self.assertEqual(self.item._metadata_manager.values, metadata) + self.assertIsNot( + self.item._metadata_manager.attributes, self.attributes + ) + + def test_ordereddict__partial(self): + metadata = self.args + del metadata["long_name"] + del metadata["units"] + self.item.metadata = metadata + metadata["long_name"] = mock.sentinel.long_name + metadata["units"] = mock.sentinel.units + self.assertEqual(self.item._metadata_manager.values, metadata) + + def test_tuple(self): + metadata = tuple(self.args.values()) + self.item.metadata = metadata + result = tuple( + [ + getattr(self.item._metadata_manager, field) + for field in self.item._metadata_manager.fields + ] + ) + self.assertEqual(result, metadata) + self.assertIsNot( + self.item._metadata_manager.attributes, self.attributes + ) + + def test_tuple__missing(self): + metadata = list(self.args.values()) + del metadata[2] + emsg = "Invalid .* metadata, require .* to be specified." + with self.assertRaisesRegex(TypeError, emsg): + self.item.metadata = tuple(metadata) + + def test_namedtuple(self): + Metadata = namedtuple( + "Metadata", + ("standard_name", "long_name", "var_name", "units", "attributes"), + ) + metadata = Metadata(**self.args) + self.item.metadata = metadata + self.assertEqual( + self.item._metadata_manager.values, metadata._asdict() + ) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + + def test_namedtuple__partial(self): + Metadata = namedtuple( + "Metadata", ("standard_name", "long_name", "var_name", "units") + ) + del self.args["attributes"] + metadata = Metadata(**self.args) + self.item.metadata = metadata + expected = metadata._asdict() + expected.update(dict(attributes=mock.sentinel.attributes)) + self.assertEqual(self.item._metadata_manager.values, expected) + + def test_class_ancillaryvariablemetadata(self): + metadata = AncillaryVariableMetadata(**self.args) + self.item.metadata = metadata + self.assertEqual( + self.item._metadata_manager.values, metadata._asdict() + ) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + + def test_class_basemetadata(self): + metadata = BaseMetadata(**self.args) + self.item.metadata = metadata + self.assertEqual( + self.item._metadata_manager.values, metadata._asdict() + ) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + + def test_class_cellmeasuremetadata(self): + self.args["measure"] = None + metadata = CellMeasureMetadata(**self.args) + self.item.metadata = metadata + expected = metadata._asdict() + del expected["measure"] + self.assertEqual(self.item._metadata_manager.values, expected) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + + def test_class_coordmetadata(self): + self.args.update(dict(coord_system=None, climatological=False)) + metadata = CoordMetadata(**self.args) + self.item.metadata = metadata + expected = metadata._asdict() + del expected["coord_system"] + del expected["climatological"] + self.assertEqual(self.item._metadata_manager.values, expected) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + + def test_class_cubemetadata(self): + self.args["cell_methods"] = None + metadata = CubeMetadata(**self.args) + self.item.metadata = metadata + expected = metadata._asdict() + del expected["cell_methods"] + self.assertEqual(self.item._metadata_manager.values, expected) + self.assertIsNot( + self.item._metadata_manager.attributes, metadata.attributes + ) + + +class Test_rename(tests.IrisTest): + def setUp(self): + metadata = mock.MagicMock( + standard_name=mock.sentinel.standard_name, + long_name=mock.sentinel.long_name, + var_name=mock.sentinel.var_name, + units=mock.sentinel.units, + attributes=mock.sentinel.attributes, + values=mock.sentinel.metadata, + token=lambda name: name, + ) + + self.item = CFVariableMixin() + self.item._metadata_manager = metadata + + def test__valid_standard_name(self): + name = "air_temperature" + self.item.rename(name) + self.assertEqual(self.item._metadata_manager.standard_name, name) + self.assertIsNone(self.item._metadata_manager.long_name) + self.assertIsNone(self.item._metadata_manager.var_name) + + def test__invalid_standard_name(self): + name = "nope nope" + self.item.rename(name) + self.assertIsNone(self.item._metadata_manager.standard_name) + self.assertEqual(self.item._metadata_manager.long_name, name) + self.assertIsNone(self.item._metadata_manager.var_name) + + +class Test_name(tests.IrisTest): + def setUp(self): + class Metadata: + def __init__(self, name): + self.name = mock.MagicMock(return_value=name) + + self.name = mock.sentinel.name + metadata = Metadata(self.name) + + self.item = CFVariableMixin() + self.item._metadata_manager = metadata + + def test(self): + default = mock.sentinel.default + token = mock.sentinel.token + result = self.item.name(default=default, token=token) + self.assertEqual(result, self.name) + self.item._metadata_manager.name.assert_called_with( + default=default, token=token + ) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/common/mixin/test_LimitedAttributeDict.py b/lib/iris/tests/unit/common/mixin/test_LimitedAttributeDict.py new file mode 100644 index 0000000000..bfaeae2daf --- /dev/null +++ b/lib/iris/tests/unit/common/mixin/test_LimitedAttributeDict.py @@ -0,0 +1,69 @@ +# 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 +# licensing details. +""" +Unit tests for the :class:`iris.common.mixin.LimitedAttributeDict`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from unittest import mock +import numpy as np + +from iris.common.mixin import LimitedAttributeDict + + +class Test(tests.IrisTest): + def setUp(self): + self.forbidden_keys = LimitedAttributeDict._forbidden_keys + self.emsg = "{!r} is not a permitted attribute" + + def test__invalid_keys(self): + for key in self.forbidden_keys: + with self.assertRaisesRegex(ValueError, self.emsg.format(key)): + _ = LimitedAttributeDict(**{key: None}) + + def test___eq__(self): + values = dict( + one=mock.sentinel.one, + two=mock.sentinel.two, + three=mock.sentinel.three, + ) + left = LimitedAttributeDict(**values) + right = LimitedAttributeDict(**values) + self.assertEqual(left, right) + self.assertEqual(left, values) + + def test___eq___numpy(self): + values = dict(one=np.arange(1), two=np.arange(2), three=np.arange(3),) + left = LimitedAttributeDict(**values) + right = LimitedAttributeDict(**values) + self.assertEqual(left, right) + self.assertEqual(left, values) + values = dict(one=np.arange(1), two=np.arange(1), three=np.arange(1),) + left = LimitedAttributeDict(dict(one=0, two=0, three=0)) + right = LimitedAttributeDict(**values) + self.assertEqual(left, right) + self.assertEqual(left, values) + + def test___setitem__(self): + for key in self.forbidden_keys: + item = LimitedAttributeDict() + with self.assertRaisesRegex(ValueError, self.emsg.format(key)): + item[key] = None + + def test_update(self): + for key in self.forbidden_keys: + item = LimitedAttributeDict() + with self.assertRaisesRegex(ValueError, self.emsg.format(key)): + other = {key: None} + item.update(other) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/cube_coord_common/test_get_valid_standard_name.py b/lib/iris/tests/unit/common/mixin/test__get_valid_standard_name.py similarity index 70% rename from lib/iris/tests/unit/cube_coord_common/test_get_valid_standard_name.py rename to lib/iris/tests/unit/common/mixin/test__get_valid_standard_name.py index ae084f33e4..6d6dcb182e 100644 --- a/lib/iris/tests/unit/cube_coord_common/test_get_valid_standard_name.py +++ b/lib/iris/tests/unit/common/mixin/test__get_valid_standard_name.py @@ -4,7 +4,7 @@ # See COPYING and COPYING.LESSER in the root of the repository for full # licensing details. """ -Unit tests for the :func:`iris._cube_coord_common.get_valid_standard_name`. +Unit tests for the :func:`iris.common.mixin._get_valid_standard_name`. """ @@ -12,7 +12,7 @@ # importing anything else. import iris.tests as tests -from iris._cube_coord_common import get_valid_standard_name +from iris.common.mixin import _get_valid_standard_name class Test(tests.IrisTest): @@ -21,51 +21,51 @@ def setUp(self): def test_pass_thru_none(self): name = None - self.assertEqual(get_valid_standard_name(name), name) + self.assertEqual(_get_valid_standard_name(name), name) def test_pass_thru_empty(self): name = "" - self.assertEqual(get_valid_standard_name(name), name) + self.assertEqual(_get_valid_standard_name(name), name) def test_pass_thru_whitespace(self): name = " " - self.assertEqual(get_valid_standard_name(name), name) + self.assertEqual(_get_valid_standard_name(name), name) def test_valid_standard_name(self): name = "air_temperature" - self.assertEqual(get_valid_standard_name(name), name) + self.assertEqual(_get_valid_standard_name(name), name) def test_standard_name_alias(self): name = "atmosphere_optical_thickness_due_to_pm1_ambient_aerosol" - self.assertEqual(get_valid_standard_name(name), name) + self.assertEqual(_get_valid_standard_name(name), name) def test_invalid_standard_name(self): name = "not_a_standard_name" with self.assertRaisesRegex(ValueError, self.emsg.format(name)): - get_valid_standard_name(name) + _get_valid_standard_name(name) def test_valid_standard_name_valid_modifier(self): name = "air_temperature standard_error" - self.assertEqual(get_valid_standard_name(name), name) + self.assertEqual(_get_valid_standard_name(name), name) def test_valid_standard_name_valid_modifier_extra_spaces(self): name = "air_temperature standard_error" - self.assertEqual(get_valid_standard_name(name), name) + self.assertEqual(_get_valid_standard_name(name), name) def test_invalid_standard_name_valid_modifier(self): name = "not_a_standard_name standard_error" with self.assertRaisesRegex(ValueError, self.emsg.format(name)): - get_valid_standard_name(name) + _get_valid_standard_name(name) def test_valid_standard_invalid_name_modifier(self): name = "air_temperature extra_names standard_error" with self.assertRaisesRegex(ValueError, self.emsg.format(name)): - get_valid_standard_name(name) + _get_valid_standard_name(name) def test_valid_standard_valid_name_modifier_extra_names(self): name = "air_temperature standard_error extra words" with self.assertRaisesRegex(ValueError, self.emsg.format(name)): - get_valid_standard_name(name) + _get_valid_standard_name(name) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/coords/test_CellMethod.py b/lib/iris/tests/unit/coords/test_CellMethod.py index 3014823f9f..530c39cf6d 100644 --- a/lib/iris/tests/unit/coords/test_CellMethod.py +++ b/lib/iris/tests/unit/coords/test_CellMethod.py @@ -11,7 +11,7 @@ # importing anything else. import iris.tests as tests -from iris._cube_coord_common import CFVariableMixin +from iris.common import BaseMetadata from iris.coords import CellMethod, AuxCoord @@ -21,7 +21,7 @@ def setUp(self): def _check(self, token, coord, default=False): result = CellMethod(self.method, coords=coord) - token = token if not default else CFVariableMixin._DEFAULT_NAME + token = token if not default else BaseMetadata.DEFAULT_NAME expected = "{}: {}".format(self.method, token) self.assertEqual(str(result), expected) @@ -54,7 +54,7 @@ def test_coord_var_name_fail(self): def test_coord_stash(self): token = "stash" coord = AuxCoord(1, attributes=dict(STASH=token)) - self._check(token, coord) + self._check(token, coord, default=True) def test_coord_stash_default(self): token = "_stash" # includes leading underscore diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index b3fdd215d6..b7fa7a5ce7 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -1010,6 +1010,17 @@ def test_remove_bounds(self): coord.bounds = None self.assertFalse(coord.climatological) + def test_change_units(self): + coord = AuxCoord( + points=[0, 1], + bounds=[[0, 1], [1, 2]], + units="days since 1970-01-01", + climatological=True, + ) + self.assertTrue(coord.climatological) + coord.units = "K" + self.assertFalse(coord.climatological) + class Test___init____abstractmethod(tests.IrisTest): def test(self): diff --git a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py b/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py deleted file mode 100644 index 0f08d397cb..0000000000 --- a/lib/iris/tests/unit/cube_coord_common/test_CFVariableMixin.py +++ /dev/null @@ -1,199 +0,0 @@ -# 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 -# licensing details. -""" -Unit tests for the :class:`iris._cube_coord_common.CFVariableMixin`. -""" - -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests - -from iris._cube_coord_common import CFVariableMixin - - -class Test_token(tests.IrisTest): - def test_passthru_None(self): - result = CFVariableMixin.token(None) - self.assertIsNone(result) - - def test_fail_leading_underscore(self): - result = CFVariableMixin.token("_nope") - self.assertIsNone(result) - - def test_fail_leading_dot(self): - result = CFVariableMixin.token(".nope") - self.assertIsNone(result) - - def test_fail_leading_plus(self): - result = CFVariableMixin.token("+nope") - self.assertIsNone(result) - - def test_fail_leading_at(self): - result = CFVariableMixin.token("@nope") - self.assertIsNone(result) - - def test_fail_space(self): - result = CFVariableMixin.token("nope nope") - self.assertIsNone(result) - - def test_fail_colon(self): - result = CFVariableMixin.token("nope:") - self.assertIsNone(result) - - def test_pass_simple(self): - token = "simple" - result = CFVariableMixin.token(token) - self.assertEqual(result, token) - - def test_pass_leading_digit(self): - token = "123simple" - result = CFVariableMixin.token(token) - self.assertEqual(result, token) - - def test_pass_mixture(self): - token = "S.imple@one+two_3" - result = CFVariableMixin.token(token) - self.assertEqual(result, token) - - -class Test_name(tests.IrisTest): - def setUp(self): - # None token CFVariableMixin - self.cf_var = CFVariableMixin() - self.cf_var.standard_name = None - self.cf_var.long_name = None - self.cf_var.var_name = None - self.cf_var.attributes = {} - self.default = CFVariableMixin._DEFAULT_NAME - # bad token CFVariableMixin - self.cf_bad = CFVariableMixin() - self.cf_bad.standard_name = None - self.cf_bad.long_name = "nope nope" - self.cf_bad.var_name = None - self.cf_bad.attributes = {"STASH": "nope nope"} - - def test_standard_name(self): - token = "air_temperature" - self.cf_var.standard_name = token - result = self.cf_var.name() - self.assertEqual(result, token) - - def test_long_name(self): - token = "long_name" - self.cf_var.long_name = token - result = self.cf_var.name() - self.assertEqual(result, token) - - def test_var_name(self): - token = "var_name" - self.cf_var.var_name = token - result = self.cf_var.name() - self.assertEqual(result, token) - - def test_stash(self): - token = "stash" - self.cf_var.attributes["STASH"] = token - result = self.cf_var.name() - self.assertEqual(result, token) - - def test_default(self): - result = self.cf_var.name() - self.assertEqual(result, self.default) - - def test_token_long_name(self): - token = "long_name" - self.cf_bad.long_name = token - result = self.cf_bad.name(token=True) - self.assertEqual(result, token) - - def test_token_var_name(self): - token = "var_name" - self.cf_bad.var_name = token - result = self.cf_bad.name(token=True) - self.assertEqual(result, token) - - def test_token_stash(self): - token = "stash" - self.cf_bad.attributes["STASH"] = token - result = self.cf_bad.name(token=True) - self.assertEqual(result, token) - - def test_token_default(self): - result = self.cf_var.name(token=True) - self.assertEqual(result, self.default) - - def test_fail_token_default(self): - emsg = "Cannot retrieve a valid name token" - with self.assertRaisesRegex(ValueError, emsg): - self.cf_var.name(default="_nope", token=True) - - -class Test_names(tests.IrisTest): - def setUp(self): - self.cf_var = CFVariableMixin() - self.cf_var.standard_name = None - self.cf_var.long_name = None - self.cf_var.var_name = None - self.cf_var.attributes = dict() - - def test_standard_name(self): - standard_name = "air_temperature" - self.cf_var.standard_name = standard_name - expected = (standard_name, None, None, None) - result = self.cf_var.names - self.assertEqual(expected, result) - self.assertEqual(result.standard_name, standard_name) - - def test_long_name(self): - long_name = "air temperature" - self.cf_var.long_name = long_name - expected = (None, long_name, None, None) - result = self.cf_var.names - self.assertEqual(expected, result) - self.assertEqual(result.long_name, long_name) - - def test_var_name(self): - var_name = "atemp" - self.cf_var.var_name = var_name - expected = (None, None, var_name, None) - result = self.cf_var.names - self.assertEqual(expected, result) - self.assertEqual(result.var_name, var_name) - - def test_STASH(self): - stash = "m01s16i203" - self.cf_var.attributes = dict(STASH=stash) - expected = (None, None, None, stash) - result = self.cf_var.names - self.assertEqual(expected, result) - self.assertEqual(result.STASH, stash) - - def test_None(self): - expected = (None, None, None, None) - result = self.cf_var.names - self.assertEqual(expected, result) - - -class Test_standard_name__setter(tests.IrisTest): - def test_valid_standard_name(self): - cf_var = CFVariableMixin() - cf_var.standard_name = "air_temperature" - self.assertEqual(cf_var.standard_name, "air_temperature") - - def test_invalid_standard_name(self): - cf_var = CFVariableMixin() - emsg = "'not_a_standard_name' is not a valid standard_name" - with self.assertRaisesRegex(ValueError, emsg): - cf_var.standard_name = "not_a_standard_name" - - def test_none_standard_name(self): - cf_var = CFVariableMixin() - cf_var.standard_name = None - self.assertIsNone(cf_var.standard_name) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/experimental/stratify/test_relevel.py b/lib/iris/tests/unit/experimental/stratify/test_relevel.py index 8746625f7e..aa8a363895 100644 --- a/lib/iris/tests/unit/experimental/stratify/test_relevel.py +++ b/lib/iris/tests/unit/experimental/stratify/test_relevel.py @@ -79,7 +79,10 @@ def test_static_level(self): def test_coord_input(self): source = AuxCoord(self.src_levels.data) - source.metadata = self.src_levels.metadata + metadata = self.src_levels.metadata._asdict() + metadata["coord_system"] = None + metadata["climatological"] = None + source.metadata = metadata for axis in self.axes: result = relevel(self.cube, source, [0, 12, 13], axis=axis) diff --git a/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py b/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py index 3bbac6b309..609f7d097a 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py @@ -23,7 +23,9 @@ class TestAtmosphereHybridSigmaPressureCoordinate(tests.IrisTest): def setUp(self): standard_name = "atmosphere_hybrid_sigma_pressure_coordinate" self.requires = dict(formula_type=standard_name) - coordinates = [(mock.sentinel.b, "b"), (mock.sentinel.ps, "ps")] + self.ap = mock.MagicMock(units="units") + self.ps = mock.MagicMock(units="units") + coordinates = [(mock.sentinel.b, "b"), (self.ps, "ps")] self.provides = dict(coordinates=coordinates) self.engine = mock.Mock(requires=self.requires, provides=self.provides) self.cube = mock.create_autospec(Cube, spec_set=True, instance=True) @@ -34,7 +36,7 @@ def setUp(self): self.addCleanup(patcher.stop) def test_formula_terms_ap(self): - self.provides["coordinates"].append((mock.sentinel.ap, "ap")) + self.provides["coordinates"].append((self.ap, "ap")) self.requires["formula_terms"] = dict(ap="ap", b="b", ps="ps") _load_aux_factory(self.engine, self.cube) # Check cube.add_aux_coord method. @@ -44,9 +46,9 @@ def test_formula_terms_ap(self): args, _ = self.cube.add_aux_factory.call_args self.assertEqual(len(args), 1) factory = args[0] - self.assertEqual(factory.delta, mock.sentinel.ap) + self.assertEqual(factory.delta, self.ap) self.assertEqual(factory.sigma, mock.sentinel.b) - self.assertEqual(factory.surface_air_pressure, mock.sentinel.ps) + self.assertEqual(factory.surface_air_pressure, self.ps) def test_formula_terms_a_p0(self): coord_a = DimCoord(np.arange(5), units="Pa") @@ -78,7 +80,7 @@ def test_formula_terms_a_p0(self): factory = args[0] self.assertEqual(factory.delta, coord_expected) self.assertEqual(factory.sigma, mock.sentinel.b) - self.assertEqual(factory.surface_air_pressure, mock.sentinel.ps) + self.assertEqual(factory.surface_air_pressure, self.ps) def test_formula_terms_p0_non_scalar(self): coord_p0 = DimCoord(np.arange(5)) @@ -113,7 +115,7 @@ def _check_no_delta(self): # Check that the factory has no delta term self.assertEqual(factory.delta, None) self.assertEqual(factory.sigma, mock.sentinel.b) - self.assertEqual(factory.surface_air_pressure, mock.sentinel.ps) + self.assertEqual(factory.surface_air_pressure, self.ps) def test_formula_terms_ap_missing_coords(self): self.requires["formula_terms"] = dict(ap="ap", b="b", ps="ps") diff --git a/lib/iris/util.py b/lib/iris/util.py index 3212eba4a5..95afb251a5 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -1218,7 +1218,7 @@ def as_compatible_shape(src_cube, target_cube): dimension coordinates where necessary. It operates by matching coordinate metadata to infer the dimensions that need modifying, so the provided cubes must have coordinates with the same metadata - (see :class:`iris.coords.CoordDefn`). + (see :class:`iris.common.CoordMetadata`). .. note:: This function will load and copy the data payload of `src_cube`. diff --git a/requirements/core.txt b/requirements/core.txt index dbc0333d7c..56544d1926 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -12,3 +12,4 @@ matplotlib<3.3 netcdf4 numpy>=1.14 scipy +xxhash #conda: python-xxhash From f49a8132cafaab51b53ffbceb5b68fb1f1c379ab Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Fri, 14 Aug 2020 15:00:44 +0100 Subject: [PATCH 27/32] Correct links in docs (#3781) * Used ref syntax for links * Tweaked image link --- docs/iris/src/whatsnew/2.3.rst | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/iris/src/whatsnew/2.3.rst b/docs/iris/src/whatsnew/2.3.rst index a515f6daad..7c5a5c27b2 100644 --- a/docs/iris/src/whatsnew/2.3.rst +++ b/docs/iris/src/whatsnew/2.3.rst @@ -243,21 +243,19 @@ Documentation ============= * Adopted a - `new colour logo for Iris <../_static/Iris7_1_trim_full.png>`_ + `new colour logo for Iris `_ * Added a gallery example showing how to concatenate NEMO ocean model data, see :ref:`sphx_glr_generated_gallery_oceanography_plot_load_nemo.py`. -* Added an example in the - `Loading Iris Cubes: Constraining on Time <../userguide/loading_iris_cubes - .html#constraining-on-time>`_ - Userguide section, demonstrating how to load data within a specified date +* Added an example for loading Iris cubes for :ref:`using-time-constraints` + in the user guide, demonstrating how to load data within a specified date range. * Added notes to the :func:`iris.load` documentation, and the userguide - `Loading Iris Cubes <../userguide/loading_iris_cubes.html>`_ - chapter, emphasizing that the *order* of the cubes returned by an iris load - operation is effectively random and unstable, and should not be relied on. + :ref:`loading_iris_cubes` chapter, emphasizing that the *order* of the cubes + returned by an iris load operation is effectively random and unstable, and + should not be relied on. * Fixed references in the documentation of :func:`iris.util.find_discontiguities` to a nonexistent From a935ab0f9cf545f04ee2f1b62c92542c3fe46357 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Sat, 15 Aug 2020 17:43:11 +0100 Subject: [PATCH 28/32] tidy/fix latest whatsnew (#3786) --- docs/iris/src/whatsnew/latest.rst | 69 ++++++++++++++++--------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index a8057cf870..a32aca6d5f 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -13,8 +13,8 @@ This document explains the changes made to Iris for this release Features ======== -* The :class:`~iris.fileformats.nimrod` provides richer meta-data translation - when loading Nimrod-format data into cubes. This covers most known +* The :mod:`~iris.fileformats.nimrod` module provides richer meta-data translation + when loading ``Nimrod`` data into cubes. This covers most known operational use-cases. * Statistical operations :meth:`iris.cube.Cube.collapsed`, @@ -23,11 +23,11 @@ Features cube. Now, a :class:`iris.coord.CellMeasure` will only be removed if it is associated with an axis over which the statistic is being run. -* Supporting Iris for both Python2 and Python3 resulted in pinning our - dependency on matplotlib at v2.x. Now that Python2 support has been dropped, - Iris is free to use the latest version of matplotlib. +* Supporting ``Iris`` for both ``Python2`` and ``Python3`` resulted in pinning our + dependency on `matplotlib`_ at ``v2.x``. Now that ``Python2`` support has + been dropped, ``Iris`` is free to use the latest version of `matplotlib`_. -* CF Ancillary Variables are now supported in cubes. +* `CF Ancillary Data`_ variables are now supported. Bugs Fixed @@ -36,49 +36,50 @@ Bugs Fixed * The method :meth:`~iris.Cube.cube.remove_coord` would fail to remove derived coordinates, will now remove derived coordinates by removing aux_factories. -* The `__iter__()` method in class:`iris.cube.Cube` was set to `None`. - `TypeError` is still raised if a `Cube` is iterated over but - `isinstance(cube, collections.Iterable)` now behaves as expected. +* The ``__iter__()`` method in :class:`~iris.cube.Cube` was set to ``None``. + ``TypeError`` is still raised if a :class:`~iris.cube.Cube` is iterated over + but ``isinstance(cube, collections.Iterable)`` now behaves as expected. * Concatenating cubes along an axis shared by cell measures would cause concatenation to inappropriately fail. These cell measures are now concatenated together in the resulting cube. * Copying a cube would previously ignore any attached - class:`iris.coords.CellMeasure`. These are now copied over. + :class:`~iris.coords.CellMeasure`. These are now copied over. -* A :class:`iris.coords.CellMeasure` requires a string ``measure`` attribute +* A :class:`~iris.coords.CellMeasure` requires a string ``measure`` attribute to be defined, which can only have a value of ``area`` or ``volume``. Previously, the ``measure`` was provided as a keyword argument to :class:`~iris.coords.CellMeasure` with an default value of ``None``, which - caused a ``TypeError`` when no ``measure`` was provided. The default value + caused a ``TypeError`` when no ``measure`` was provided. The default value of ``area`` is now used. Incompatible Changes ==================== -* The method :meth:`~iris.cube.CubeList.extract_strict`, and the 'strict' +* The method :meth:`~iris.cube.CubeList.extract_strict`, and the ``strict`` keyword to :meth:`~iris.cube.CubeList.extract` method have been removed, and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` and :meth:`~iris.cube.CubeList.extract_cubes`. The new routines perform the same operation, but in a style more like other - Iris functions such as :meth:`iris.load_cube` and :meth:`iris.load_cubes`. - Unlike 'strict extraction', the type of return value is now completely - consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a cube, - and :meth:`~iris.cube.CubeList.extract_cubes` always returns a CubeList of a - length equal to the number of constraints. - -* The former function "iris.analysis.coord_comparison" has been removed. - -* The :func:`iris.experimental.equalise_cubes.equalise_attributes` function - has been moved from the :mod:`iris.experimental` module into the - :mod:`iris.util` module. Please use the :func:`iris.util.equalise_attributes` + ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. + Unlike ``strict`` extraction, the type of return value is now completely + consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a + :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` + always returns an :class:`iris.cube.CubeList` of a length equal to the + number of constraints. + +* The former function ``iris.analysis.coord_comparison`` has been removed. + +* The :func:`iris.experimental.equalise_cubes.equalise_attributes` function + has been moved from the :mod:`iris.experimental` module into the + :mod:`iris.util` module. Please use the :func:`iris.util.equalise_attributes` function instead. * The :mod:`iris.experimental.concatenate` module has now been removed. In - ``v1.6.0`` the experimental `concatenate` functionality was moved to the - :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the + ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the + :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the :func:`iris.experimental.concatenate.concatenate` function raised an exception. @@ -86,12 +87,12 @@ Incompatible Changes Deprecations ============ -* The deprecated :class:`iris.Future` flags `cell_date_time_objects`, - `netcdf_promote`, `netcdf_no_unlimited` and `clip_latitudes` have +* The deprecated :class:`iris.Future` flags ``cell_date_time_objects``, + ``netcdf_promote``, ``netcdf_no_unlimited`` and ``clip_latitudes`` have been removed. -* :attr:`iris.fileformats.pp.PPField.lbproc` is now an `int`. The - deprecated attributes `flag1`, `flag2` etc. have been removed from it. +* :attr:`iris.fileformats.pp.PPField.lbproc` is now an ``int``. The + deprecated attributes ``flag1``, ``flag2`` etc. have been removed from it. Documentation @@ -116,8 +117,10 @@ Documentation :issue:`2104` and :issue:`3451`. Also updated the :ref:`iris_development_releases_steps` to follow when making a release. -* Enabled the pdf creation of the documentation on the `Read the Docs`_ service. - The pdf may be accessed by clicking on the version at the bottom of the side - bar, then selecting **pdf** from the downloads section. +* Enabled the PDF creation of the documentation on the `Read the Docs`_ service. + The PDF may be accessed by clicking on the version at the bottom of the side + bar, then selecting ``PDF`` from the ``Downloads`` section. .. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ +.. _matplotlib: https://matplotlib.org/ +.. _CF Ancillary Data: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#ancillary-data \ No newline at end of file From 8c171e9fb923c3533a4ea44a62cd91e8d37c2f46 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Sat, 15 Aug 2020 22:06:25 +0100 Subject: [PATCH 29/32] Spell checker for the sphinx documentation (not enabled for now) (#3779) * Put in place but not enabled yet, a spell checker for the sphinx documentation (sphinxcontrib-spelling). Includes all the spelling corrections. * Reverted to US spelling --- .travis.yml | 17 + docs/iris/Makefile | 5 + docs/iris/gallery_code/README.rst | 2 +- docs/iris/src/Makefile | 5 + docs/iris/src/conf.py | 26 +- .../contributing_documentation.rst | 48 ++- .../documenting/docstrings.rst | 14 +- .../documenting/rest_guide.rst | 6 +- .../documenting/whats_new_contributions.rst | 2 +- .../gitwash/configure_git.rst | 2 +- .../gitwash/development_workflow.rst | 2 +- .../src/developers_guide/graphics_tests.rst | 2 +- docs/iris/src/developers_guide/pulls.rst | 11 +- docs/iris/src/developers_guide/release.rst | 2 +- docs/iris/src/spelling_allow.txt | 356 ++++++++++++++++++ .../iris/src/techpapers/change_management.rst | 10 +- docs/iris/src/techpapers/um_files_loading.rst | 4 +- docs/iris/src/userguide/code_maintenance.rst | 8 +- docs/iris/src/userguide/cube_statistics.rst | 2 +- .../interpolation_and_regridding.rst | 3 +- .../iris/src/userguide/loading_iris_cubes.rst | 2 +- docs/iris/src/whatsnew/1.10.rst | 6 +- docs/iris/src/whatsnew/1.3.rst | 2 +- docs/iris/src/whatsnew/1.4.rst | 2 +- docs/iris/src/whatsnew/1.5.rst | 2 +- docs/iris/src/whatsnew/1.7.rst | 4 +- docs/iris/src/whatsnew/1.9.rst | 2 +- docs/iris/src/whatsnew/2.3.rst | 4 +- docs/iris/src/whatsnew/2.4.rst | 4 +- lib/iris/analysis/__init__.py | 4 +- lib/iris/analysis/_grid_angles.py | 4 +- lib/iris/aux_factory.py | 2 +- lib/iris/coord_systems.py | 5 +- lib/iris/coords.py | 2 +- lib/iris/cube.py | 2 +- lib/iris/fileformats/rules.py | 2 +- lib/iris/io/__init__.py | 4 +- lib/iris/util.py | 2 +- 38 files changed, 501 insertions(+), 81 deletions(-) create mode 100644 docs/iris/src/spelling_allow.txt diff --git a/.travis.yml b/.travis.yml index 604dbbb353..30490c5101 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,9 @@ env: - PYTHON_VERSION=3.7 TEST_TARGET=gallery - PYTHON_VERSION=3.7 TEST_TARGET=doctest PUSH_BUILT_DOCS=true - PYTHON_VERSION=3.7 TEST_TARGET=linkcheck + # TODO: Dependencies for sphinxcontrib-spelling to be in place before this + # spelling code block is enabled + #- PYTHON_VERSION=3.7 TEST_TARGET=spelling git: # We need a deep clone so that we can compute the age of the files using their git history. @@ -144,6 +147,20 @@ script: make clean && make linkcheck; fi + # TODO: Dependencies for sphinxcontrib-spelling to be in place before this + # spelling code block is enabled + + # check the spelling in the docs + # - > + # if [[ "${TEST_TARGET}" == 'spelling' ]]; then + # MPL_RC_DIR="${HOME}/.config/matplotlib"; + # mkdir -p ${MPL_RC_DIR}; + # echo 'backend : agg' > ${MPL_RC_DIR}/matplotlibrc; + # echo 'image.cmap : viridis' >> ${MPL_RC_DIR}/matplotlibrc; + # cd ${INSTALL_DIR}/docs/iris; + # make clean && make spelling; + # fi + # Split the organisation out of the slug. See https://stackoverflow.com/a/5257398/741316 for description. # NOTE: a *separate* "export" command appears to be necessary here : A command of the # form "export ORG=.." failed to define ORG for the following command (?!) diff --git a/docs/iris/Makefile b/docs/iris/Makefile index 4ab54b291f..e9632ddb9f 100644 --- a/docs/iris/Makefile +++ b/docs/iris/Makefile @@ -10,6 +10,11 @@ html-noplot: echo "make html-noplot in $$i..."; \ (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) html-noplot); done +spelling: + @for i in $(SUBDIRS); do \ + echo "make spelling in $$i..."; \ + (cd $$i; $(MAKE) $(MFLAGS) $(MYMAKEFLAGS) spelling); done + all: @for i in $(SUBDIRS); do \ echo "make all in $$i..."; \ diff --git a/docs/iris/gallery_code/README.rst b/docs/iris/gallery_code/README.rst index 7d8fb60e81..02263dc5e5 100644 --- a/docs/iris/gallery_code/README.rst +++ b/docs/iris/gallery_code/README.rst @@ -7,7 +7,7 @@ to download the code directly as source or as part of a `jupyter notebook `_, these links are at the bottom of the page. -In order to successfuly view the jupyter notebook locally so you may +In order to successfully view the jupyter notebook locally so you may experiment with the code you will need an environment setup with the appropriate dependencies, see :ref:`installing_iris` for instructions. Ensure that ``iris-sample-data`` is installed as it is used in the gallery. diff --git a/docs/iris/src/Makefile b/docs/iris/src/Makefile index 5589cce730..0aa921fd2a 100644 --- a/docs/iris/src/Makefile +++ b/docs/iris/src/Makefile @@ -48,6 +48,11 @@ html-noplot: @echo @echo "Build finished. The HTML (no gallery) pages are in $(BUILDDIR)/html" +spelling: + $(SPHINXBUILD) -b spelling $(SRCDIR) $(BUILDDIR) + @echo + @echo "Build finished. The HTML (no gallery) pages are in $(BUILDDIR)/html" + dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index b28f5fbb7d..9b061f5ec6 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -114,6 +114,8 @@ def autolog(message): "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx_copybutton", + # TODO: Spelling extension disabled until the dependencies can be included + # "sphinxcontrib.spelling", "sphinx_gallery.gen_gallery", "matplotlib.sphinxext.mathmpl", "matplotlib.sphinxext.plot_directive", @@ -123,10 +125,22 @@ def autolog(message): "generate_package_rst", ] -# sphinx_copybutton config +# -- spellingextension -------------------------------------------------------- +# See https://sphinxcontrib-spelling.readthedocs.io/en/latest/customize.html +spelling_lang = "en_GB" +# The lines in this file must only use line feeds (no carriage returns). +spelling_word_list_filename = ["spelling_allow.txt"] +spelling_show_suggestions = False +spelling_show_whole_line = False +spelling_ignore_importable_modules = True +spelling_ignore_python_builtins = True + +# -- copybutton extension ----------------------------------------------------- +# See https://sphinx-copybutton.readthedocs.io/en/latest/ copybutton_prompt_text = ">>> " # sphinx.ext.todo configuration +# See https://www.sphinx-doc.org/en/master/usage/extensions/todo.html todo_include_todos = True # api generation configuration @@ -141,6 +155,8 @@ def autolog(message): # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] +# -- intersphinx extension ---------------------------------------------------- +# See https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html intersphinx_mapping = { "cartopy": ("http://scitools.org.uk/cartopy/docs/latest/", None), "matplotlib": ("http://matplotlib.org/", None), @@ -152,12 +168,14 @@ def autolog(message): # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" -# plot directive options (extension: matplotlib.sphinxext.plot_directive --- +# -- plot_directive extension ------------------------------------------------- +# See https://matplotlib.org/3.1.3/devel/plot_directive.html#options plot_formats = [ ("png", 100), ] # -- Extlinks extension ------------------------------------------------------- +# See https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html extlinks = { "issue": ("https://github.com/SciTools/iris/issues/%s", "Issue #"), @@ -220,6 +238,7 @@ def autolog(message): html_style = "theme_override.css" # url link checker. Some links work but report as broken, lets ignore them. +# See https://www.sphinx-doc.org/en/1.2/config.html#options-for-the-linkcheck-builder linkcheck_ignore = [ "https://github.com/SciTools/iris/commit/69597eb3d8501ff16ee3d56aef1f7b8f1c2bb316#diff-1680206bdc5cfaa83e14428f5ba0f848", "http://www.wmo.int/pages/prog/www/DPFS/documents/485_Vol_I_en_colour.pdf", @@ -230,6 +249,7 @@ def autolog(message): exclude_patterns = [] # -- sphinx-gallery config ---------------------------------------------------- +# See https://sphinx-gallery.github.io/stable/configuration.html sphinx_gallery_conf = { # path to your example scripts @@ -242,6 +262,8 @@ def autolog(message): "ignore_pattern": r"__init__\.py", } + +# ----------------------------------------------------------------------------- # Remove matplotlib agg warnings from generated doc when using plt.show warnings.filterwarnings( "ignore", diff --git a/docs/iris/src/developers_guide/contributing_documentation.rst b/docs/iris/src/developers_guide/contributing_documentation.rst index b7bc99b647..618e5fbd08 100644 --- a/docs/iris/src/developers_guide/contributing_documentation.rst +++ b/docs/iris/src/developers_guide/contributing_documentation.rst @@ -53,7 +53,7 @@ If you wish to run a clean build you can run:: make clean make html -This is useful for a final test before commiting your changes. +This is useful for a final test before committing your changes. .. note:: In addition to the automated `travis-ci`_ build of the documentation, the https://readthedocs.org/ service is also used. The configuration @@ -68,41 +68,55 @@ This is useful for a final test before commiting your changes. Testing ------- -There are three ways to test various aspects of the documentation. +There are a ways to test various aspects of the documentation. The +``make`` commands shown below can be run in the ``iris/docs/iris`` or +``iris/docs/iris/src`` directory. Each :ref:`contributing.documentation.gallery` entry has a corresponding test. -The below command must be run in the ``iris/docs/iris`` directory:: +To run the tests:: make gallerytest -Many documentation pages includes python code itself that can be run to ensure it -is still valid. The below command can be run in the ``iris/docs/iris`` or -``iris/docs/iris/src`` directory:: +Many documentation pages includes python code itself that can be run to ensure +it is still valid:: make doctest -Finally, all the hyperlinks in the documentation can be checked automatically. +The hyperlinks in the documentation can be checked automatically. If there is a link that is known to work it can be excluded from the checks by adding it to the ``linkcheck_ignore`` array that is defined in the -`conf.py `_. -The hyperlink check can be run via:: +`conf.py`_. The hyperlink check can be run via:: make linkcheck If this fails check the output for the text **broken** and then correct or ignore the url. +.. comment + Finally, the spelling in the documentation can be checked automatically via the + command:: + + make spelling + + The spelling check may pull up many technical abbreviations and acronyms. This + can be managed by using an **allow** list in the form of a file. This file, + or list of files is set in the `conf.py`_ using the string list + ``spelling_word_list_filename``. + + .. note:: All of the above tests are automatically run as part of the `travis-ci`_ automated build. +.. _conf.py: https://github.com/SciTools/iris/blob/master/docs/iris/src/conf.py + .. _contributing.documentation.api: Generating API documentation ---------------------------- -In order to auto generate the API documentation based upon the docstrings a custom -set of python scripts are used, these are located in the directory +In order to auto generate the API documentation based upon the docstrings a +custom set of python scripts are used, these are located in the directory ``iris/docs/iris/src/sphinxext``. Once the ``make html`` command has been run, the output of these scripts can be found in ``iris/docs/iris/src/_build/generated/api``. @@ -130,17 +144,17 @@ respective ``README.rst`` in each folder is included in the gallery output. For each gallery entry there must be a corresponding test script located in ``iris/docs/iris/gallery_tests``. -To add an entry to the gallery simple place your python code into the appropriate -sub directory and name it with a prefix of ``plot_``. If your gallery entry does not -fit into any existing sub directories then create a new directoy and place it in -there. +To add an entry to the gallery simple place your python code into the +appropriate sub directory and name it with a prefix of ``plot_``. If your +gallery entry does not fit into any existing sub directories then create a new +directory and place it in there. The reStructuredText (rst) output of the gallery is located in ``iris/docs/iris/src/_build/generated/gallery``. For more information on the directory structure and options please see the -`sphinx-gallery getting started `_ -documentation. +`sphinx-gallery getting started +`_ documentation. diff --git a/docs/iris/src/developers_guide/documenting/docstrings.rst b/docs/iris/src/developers_guide/documenting/docstrings.rst index afc56014ea..641bf7717e 100644 --- a/docs/iris/src/developers_guide/documenting/docstrings.rst +++ b/docs/iris/src/developers_guide/documenting/docstrings.rst @@ -1,9 +1,9 @@ -=========== - Docstrings -=========== +========== +Docstrings +========== - -Guiding principle: Every public object in the Iris package should have an appropriate docstring. +Guiding principle: Every public object in the Iris package should have an +appropriate docstring. This document has been influenced by the following PEP's, @@ -35,7 +35,7 @@ The multi-line docstring *description section* should expand on what was stated Sample multi-line docstring --------------------------- -Here is a simple example of a standard dosctring: +Here is a simple example of a standard docstring: .. literalinclude:: docstrings_sample_routine.py @@ -58,7 +58,7 @@ Documenting classes =================== The class constructor should be documented in the docstring for its ``__init__`` or ``__new__`` method. Methods should be documented by their own docstring, not in the class header itself. -If a class subclasses another class and its behavior is mostly inherited from that class, its docstring should mention this and summarise the differences. Use the verb "override" to indicate that a subclass method replaces a superclass method and does not call the superclass method; use the verb "extend" to indicate that a subclass method calls the superclass method (in addition to its own behavior). +If a class subclasses another class and its behaviour is mostly inherited from that class, its docstring should mention this and summarise the differences. Use the verb "override" to indicate that a subclass method replaces a superclass method and does not call the superclass method; use the verb "extend" to indicate that a subclass method calls the superclass method (in addition to its own behaviour). Attribute and property docstrings diff --git a/docs/iris/src/developers_guide/documenting/rest_guide.rst b/docs/iris/src/developers_guide/documenting/rest_guide.rst index e42e27c18e..aadb5ffea4 100644 --- a/docs/iris/src/developers_guide/documenting/rest_guide.rst +++ b/docs/iris/src/developers_guide/documenting/rest_guide.rst @@ -1,6 +1,6 @@ -=============== -reST quickstart -=============== +================ +reST quick start +================ reST (http://en.wikipedia.org/wiki/ReStructuredText) is a lightweight markup diff --git a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst index 811aeb59cc..b4ca483075 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst @@ -42,7 +42,7 @@ what's :ref:`iris_whatsnew` entries. .. note:: The reStructuredText syntax will be checked as part of building the documentation. Any warnings should be corrected. - `travis-ci`_ will automatically build the documention when + `travis-ci`_ will automatically build the documentation when creating a pull request, however you can also manually :ref:`build ` the documentation. diff --git a/docs/iris/src/developers_guide/gitwash/configure_git.rst b/docs/iris/src/developers_guide/gitwash/configure_git.rst index 0e18b666d0..fd3cf0db20 100644 --- a/docs/iris/src/developers_guide/gitwash/configure_git.rst +++ b/docs/iris/src/developers_guide/gitwash/configure_git.rst @@ -55,7 +55,7 @@ In detail user.name and user.email ------------------------ -It is good practice to tell git_ who you are, for labeling any changes +It is good practice to tell git_ who you are, for labelling any changes you make to the code. The simplest way to do this is from the command line:: diff --git a/docs/iris/src/developers_guide/gitwash/development_workflow.rst b/docs/iris/src/developers_guide/gitwash/development_workflow.rst index 312e114188..dee06454a1 100644 --- a/docs/iris/src/developers_guide/gitwash/development_workflow.rst +++ b/docs/iris/src/developers_guide/gitwash/development_workflow.rst @@ -148,7 +148,7 @@ Testing your changes ==================== Once you are happy with your changes, work thorough the :ref:`pr_check` and make sure -your branch passess all the relevant tests. +your branch passes all the relevant tests. Ask for your changes to be reviewed or merged ============================================= diff --git a/docs/iris/src/developers_guide/graphics_tests.rst b/docs/iris/src/developers_guide/graphics_tests.rst index e84a59d48d..8f63bd3381 100644 --- a/docs/iris/src/developers_guide/graphics_tests.rst +++ b/docs/iris/src/developers_guide/graphics_tests.rst @@ -112,7 +112,7 @@ you should follow: #. Now re-run the tests. The 'new' result should now be recognised and the relevant test should pass. However, some tests can perform *multiple* graphics - checks within a single testcase function : In those cases, any failing + checks within a single test case function : In those cases, any failing check will prevent the following ones from being run, so a test re-run may encounter further (new) graphical test failures. If that happens, simply repeat the check-and-accept process until all tests pass. diff --git a/docs/iris/src/developers_guide/pulls.rst b/docs/iris/src/developers_guide/pulls.rst index 62535d27c6..366cedd159 100644 --- a/docs/iris/src/developers_guide/pulls.rst +++ b/docs/iris/src/developers_guide/pulls.rst @@ -1,5 +1,6 @@ .. _pr_check: + Pull request check List *********************** @@ -33,17 +34,17 @@ The Iris check list * Have new tests been provided for all additional functionality? -* Do all modified and new sourcefiles pass PEP8? +* Do all modified and new source files pass PEP8? * PEP8_ is the Python source code style guide. * There is a python module for checking pep8 compliance: python-pep8_ - * a standard Iris test checks that all sourcefiles meet PEP8 compliance + * a standard Iris test checks that all source files meet PEP8 compliance (see "iris.tests.test_coding_standards.TestCodeFormat"). -* Do all modified and new sourcefiles have a correct, up-to-date copyright +* Do all modified and new source files have a correct, up-to-date copyright header? - * a standard Iris test checks that all sourcefiles include a copyright + * a standard Iris test checks that all source files include a copyright message, including the correct year of the latest change (see "iris.tests.test_coding_standards.TestLicenseHeaders"). @@ -64,7 +65,7 @@ The Iris check list * The documentation tests may be run with ``make doctest``, from within the ``./docs/iris`` subdirectory. -* Have you provided a 'whats new' contribution? +* Have you provided a "what's new" contribution? * this should be done for all changes that affect API or behaviour. See :ref:`whats_new_contributions` diff --git a/docs/iris/src/developers_guide/release.rst b/docs/iris/src/developers_guide/release.rst index c44a248352..d71f149186 100644 --- a/docs/iris/src/developers_guide/release.rst +++ b/docs/iris/src/developers_guide/release.rst @@ -150,7 +150,7 @@ Post release steps triggered by any commit to master. Additionally check that the versions available in the pop out menu in the bottom left corner include the new release version. If it is not present you will need to configure the - versions avaiable in the **admin** dashboard in Read The Docs + versions available in the **admin** dashboard in Read The Docs #. Copy ``docs/iris/src/whatsnew/latest.rst.template`` to ``docs/iris/src/whatsnew/latest.rst``. This will reset the file with the ``unreleased`` heading and placeholders for the what's diff --git a/docs/iris/src/spelling_allow.txt b/docs/iris/src/spelling_allow.txt new file mode 100644 index 0000000000..6ef4134699 --- /dev/null +++ b/docs/iris/src/spelling_allow.txt @@ -0,0 +1,356 @@ +Admin +Albers +Arakawa +Arg +Args +Autoscale +Biggus +CF +Cartopy +Checklist +Color +Conda +Constraining +DAP +Dask +Debian +Duchon +EO +Eos +Exner +Fieldsfile +Fieldsfiles +FillValue +Gb +GeogCS +Hovmoller +Jul +Jun +Jupyter +Lanczos +Mappables +Matplotlib +Mb +Modeling +Mollweide +NetCDF +Nino +PPfield +PPfields +Perez +Proj +Quickplot +Regrids +Royer +Scitools +Scitools +Sep +Stehfest +Steroegraphic +Subsetting +TestCodeFormat +TestLicenseHeaders +Torvalds +Trans +Trenberth +Tri +URIs +URLs +Ubuntu +Ugrid +Unidata +Vol +Vuuren +Workflow +Yury +Zaytsev +Zorder +abf +abl +advection +aggregator +aggregators +alphap +ancils +antimeridian +ap +arg +args +arithmetic +arraylike +atol +auditable +aux +basemap +behaviour +betap +bhulev +biggus +blev +boolean +boundpoints +branchname +broadcastable +bugfix +bugfixes +builtin +bulev +carrée +cartesian +celsius +center +centrepoints +cf +cftime +chunksizes +clabel +cmap +cmpt +codebase +color +colorbar +colorbars +complevel +conda +config +constraining +convertor +coord +coords +cs +datafiles +datatype +datetime +datetimes +ddof +deepcopy +deprecations +der +dewpoint +dict +dicts +diff +discontiguities +discontiguous +djf +docstring +docstrings +doi +dom +dropdown +dtype +dtypes +dx +dy +edgecolor +endian +endianness +equirectangular +eta +etc +fh +fieldsfile +fieldsfiles +fileformat +fileformats +filename +filenames +filepath +filespec +fullname +func +geolocations +github +gregorian +grib +gribapi +gridcell +griddata +gridlines +hPa +hashable +hindcast +hyperlink +hyperlinks +idiff +ieee +ifunc +imagehash +init +inline +inplace +int +interable +interpolator +ints +io +isosurfaces +iterable +jja +kwarg +kwargs +landsea +lat +latlon +latlons +lats +lbcode +lbegin +lbext +lbfc +lbft +lblrec +lbmon +lbmond +lbnrec +lbrsvd +lbtim +lbuser +lbvc +lbyr +lbyrd +lh +lhs +linewidth +linted +linting +lon +lons +lt +mam +markup +matplotlib +matplotlibrc +max +mdtol +meaned +mercator +metadata +min +mpl +nanmask +nc +ndarray +neighbor +ness +netCDF +netcdf +netcdftime +nimrod +np +nsigma +numpy +nx +ny +online +orog +paramId +params +parsable +pcolormesh +pdf +placeholders +plugin +png +proj +ps +pseudocolor +pseudocolour +pseudocoloured +py +pyplot +quickplot +rST +rc +rd +reST +reStructuredText +rebase +rebases +rebasing +regrid +regridded +regridder +regridders +regridding +regrids +rel +repo +repos +reprojecting +rh +rhs +rst +rtol +scipy +scitools +seekable +setup +sines +sinh +spec +specs +src +ssh +st +stashcode +stashcodes +stats +std +stdout +str +subcube +subcubes +submodule +submodules +subsetting +sys +tanh +tb +testcases +tgt +th +timepoint +timestamp +timesteps +tol +tos +traceback +travis +tripolar +tuple +tuples +txt +udunits +ufunc +ugrid +ukmo +un +unhandled +unicode +unittest +unrotate +unrotated +uris +url +urls +util +var +versioning +vmax +vmin +waypoint +waypoints +whitespace +wildcard +wildcards +windspeeds +withnans +workflow +workflows +xN +xx +xxx +zeroth +zlev +zonal \ No newline at end of file diff --git a/docs/iris/src/techpapers/change_management.rst b/docs/iris/src/techpapers/change_management.rst index 2218eb4212..d09237a4bf 100644 --- a/docs/iris/src/techpapers/change_management.rst +++ b/docs/iris/src/techpapers/change_management.rst @@ -51,7 +51,7 @@ Checklist : * when a new **minor version is released** - * review the 'Whats New' documentation to see if it introduces any + * review the 'What's New' documentation to see if it introduces any deprecations that may affect you. * run your working legacy code and check for any deprecation warnings, indicating that modifications may be necessary at some point @@ -227,7 +227,7 @@ are : * A non-zero "" denotes a bugfix version, thus a release "X.Y.0" may be followed by "X.Y.1", "X.Y.2" etc, which *only* differ by containing - bugfixes. Any bugfix release supercedes its predecessors, and does not + bugfixes. Any bugfix release supersedes its predecessors, and does not change any (valid) APIs or behaviour : hence, it is always advised to replace a given version with its latest bugfix successor, and there should be no reason not to. @@ -261,7 +261,7 @@ behaviour of existing code can only be made at a **major** release, i.e. when "X.0" is released following the last previous "(X-1).Y.Z". *Minor* releases, by contrast, consist of bugfixes, new features, and -deprecations : Any valid exisiting code should be unaffected by these, so it +deprecations : Any valid existing code should be unaffected by these, so it will still run with the same results. At a major release, only **deprecated** behaviours and APIs can be changed or @@ -361,7 +361,7 @@ with the new release, which we obviously need to avoid. * the user code usage is simply by calls to "iris.load" * the change is not a bugfix, as the old way isn't actually "wrong" * we don't want to add an extra keyword into all the relevant calls - * we don't see a longterm future for the existing behaviour : we + * we don't see a long term future for the existing behaviour : we expect everyone to adopt the new interpretation, eventually For changes of this sort, the release will define a new boolean property of the @@ -427,7 +427,7 @@ At (major) release ".0...": * If your code is explicitly turning the option off, it will continue to work in the same way at this point, but obviously time is - runnning out. + running out. * If your code is still using the old behaviour and *not* setting the control option at all, its behaviour might now have changed diff --git a/docs/iris/src/techpapers/um_files_loading.rst b/docs/iris/src/techpapers/um_files_loading.rst index fd2d2a2341..d8c796b31f 100644 --- a/docs/iris/src/techpapers/um_files_loading.rst +++ b/docs/iris/src/techpapers/um_files_loading.rst @@ -30,7 +30,7 @@ Notes: #. Iris treats Fieldsfile data almost exactly as if it were PP -- i.e. it treats each field's lookup table entry like a PP header. -#. The Iris datamodel is based on +#. The Iris data model is based on `NetCDF CF conventions `_, so most of this can also be seen as a metadata translation between PP and CF terms, but it is easier to discuss in terms of Iris elements. @@ -132,7 +132,7 @@ For an ordinary latitude-longitude grid, the cubes have coordinates called ``ZDX/Y + BDX/Y * (1 .. LBNPT/LBROW)`` (*except*, if BDX/BDY is zero, the values are taken from the extra data vector X/Y, if present). * If X/Y_LOWER_BOUNDS extra data is available, this appears as bounds values - of the horizontal cooordinates. + of the horizontal coordinates. For **rotated** latitude-longitude coordinates (as for LBCODE=101), the horizontal coordinates differ only slightly -- diff --git a/docs/iris/src/userguide/code_maintenance.rst b/docs/iris/src/userguide/code_maintenance.rst index f5914da471..d03808e18f 100644 --- a/docs/iris/src/userguide/code_maintenance.rst +++ b/docs/iris/src/userguide/code_maintenance.rst @@ -9,18 +9,18 @@ Stability and change --------------------- In practice, as Iris develops, most users will want to periodically upgrade -their installed version to access new features or at least bugfixes. +their installed version to access new features or at least bug fixes. This is obvious if you are still developing other code that uses Iris, or using code from other sources. However, even if you have only legacy code that remains untouched, some code -maintenance effort is probably still necessary : +maintenance effort is probably still necessary: * On the one hand, *in principle*, working code will go on working, as long as you don't change anything else. - * However, such "version statis" can easily become a growing burden, if you - are simply waiting until an update becomes unavoidable : Often, that will + * However, such "version stasis" can easily become a growing burden, if you + are simply waiting until an update becomes unavoidable, often that will eventually occur when you need to update some other software component, for some completely unconnected reason. diff --git a/docs/iris/src/userguide/cube_statistics.rst b/docs/iris/src/userguide/cube_statistics.rst index 31e165d35c..310551c76f 100644 --- a/docs/iris/src/userguide/cube_statistics.rst +++ b/docs/iris/src/userguide/cube_statistics.rst @@ -245,7 +245,7 @@ These two coordinates can now be used to aggregate by season and climate-year: The primary change in the cube is that the cube's data has been reduced in the 'time' dimension by aggregation (taking means, in this case). -This has collected together all datapoints with the same values of season and +This has collected together all data points with the same values of season and season-year. The results are now indexed by the 19 different possible values of season and season-year in a new, reduced 'time' dimension. diff --git a/docs/iris/src/userguide/interpolation_and_regridding.rst b/docs/iris/src/userguide/interpolation_and_regridding.rst index 565f9b61eb..65ac36eada 100644 --- a/docs/iris/src/userguide/interpolation_and_regridding.rst +++ b/docs/iris/src/userguide/interpolation_and_regridding.rst @@ -1,6 +1,5 @@ .. _interpolation_and_regridding: - .. testsetup:: * import numpy as np @@ -16,7 +15,7 @@ Iris provides powerful cube-aware interpolation and regridding functionality, exposed through Iris cube methods. This functionality is provided by building upon existing interpolation schemes implemented by SciPy. -In Iris we refer to the avaliable types of interpolation and regridding as +In Iris we refer to the available types of interpolation and regridding as `schemes`. The following are the interpolation schemes that are currently available in Iris: diff --git a/docs/iris/src/userguide/loading_iris_cubes.rst b/docs/iris/src/userguide/loading_iris_cubes.rst index bbb83194db..006a919408 100644 --- a/docs/iris/src/userguide/loading_iris_cubes.rst +++ b/docs/iris/src/userguide/loading_iris_cubes.rst @@ -269,7 +269,7 @@ boundary of a circular coordinate (this is often the meridian or the dateline / antimeridian). An example use-case of this is to extract the entire Pacific Ocean from a cube whose longitudes are bounded by the dateline. -This functionality cannot be provided reliably using contraints. Instead you should use the +This functionality cannot be provided reliably using constraints. Instead you should use the functionality provided by :meth:`cube.intersection ` to extract this region. diff --git a/docs/iris/src/whatsnew/1.10.rst b/docs/iris/src/whatsnew/1.10.rst index 6413323203..b5dfc1974b 100644 --- a/docs/iris/src/whatsnew/1.10.rst +++ b/docs/iris/src/whatsnew/1.10.rst @@ -207,12 +207,12 @@ Bugs fixed * A bug that prevented cube printing in some cases has been fixed. * Fixed a bug where a deepcopy of a :class:`~iris.coords.DimCoord` would have - writable ``points`` and ``bounds`` arrays. These arrays can now no longer be + writeable ``points`` and ``bounds`` arrays. These arrays can now no longer be modified in-place. * Concatenation no longer occurs when the auxiliary coordinates of the cubes do not match. This check is not applied to AuxCoords that span the dimension the - concatenation is occuring along. This behaviour can be switched off by + concatenation is occurring along. This behaviour can be switched off by setting the ``check_aux_coords`` kwarg in :meth:`iris.cube.CubeList.concatenate` to False. @@ -240,7 +240,7 @@ Deprecations * Deprecated the module :mod:`iris.fileformats.grib`. The new package `iris_grib `_ replaces this - fuctionality, which will shortly be removed. + functionality, which will shortly be removed. Please see :ref:`iris_grib package `, above. * The use of :data:`iris.config.SAMPLE_DATA_DIR` has been deprecated and diff --git a/docs/iris/src/whatsnew/1.3.rst b/docs/iris/src/whatsnew/1.3.rst index 9e898a2b23..fd6f2cfef9 100644 --- a/docs/iris/src/whatsnew/1.3.rst +++ b/docs/iris/src/whatsnew/1.3.rst @@ -66,7 +66,7 @@ netCDF file. The following keys within the ``iris.site_configuration`` dictionary have been **reserved** as hooks to *external* user-defined CF profile functions: - * ``cf_profile`` injests a :class:`iris.cube.Cube` for analysis and returns a + * ``cf_profile`` ingests a :class:`iris.cube.Cube` for analysis and returns a profile result * ``cf_patch`` modifies the CF-netCDF file associated with export of the :class:`iris.cube.Cube` diff --git a/docs/iris/src/whatsnew/1.4.rst b/docs/iris/src/whatsnew/1.4.rst index 23b70b10c9..7f96643f5f 100644 --- a/docs/iris/src/whatsnew/1.4.rst +++ b/docs/iris/src/whatsnew/1.4.rst @@ -118,7 +118,7 @@ For example:: .. _iris-pandas: -Iris-Pandas interoperablilty +Iris-Pandas interoperability ---------------------------- Conversion to and from Pandas Series_ and DataFrames_ is now available. diff --git a/docs/iris/src/whatsnew/1.5.rst b/docs/iris/src/whatsnew/1.5.rst index c891e6c7ef..07f54e15cf 100644 --- a/docs/iris/src/whatsnew/1.5.rst +++ b/docs/iris/src/whatsnew/1.5.rst @@ -175,7 +175,7 @@ Bugs fixed * Coding of maximum and minimum time-stats in GRIB2 saving has been fixed. -* Example code in section 4.1 of the userguide updated so it uses a sample +* Example code in section 4.1 of the user guide updated so it uses a sample data file that exists. * Zorder of contour lines drawn by :func:`~iris.plot.contourf` has been changed diff --git a/docs/iris/src/whatsnew/1.7.rst b/docs/iris/src/whatsnew/1.7.rst index 22d9b05257..e60c1083d9 100644 --- a/docs/iris/src/whatsnew/1.7.rst +++ b/docs/iris/src/whatsnew/1.7.rst @@ -218,13 +218,13 @@ Bugs fixed vertical coordinates. * Auxiliary coordinate factory loading now correctly interprets formula term - varibles for "atmosphere hybrid sigma pressure" coordinate data. + variables for "atmosphere hybrid sigma pressure" coordinate data. * Corrected comparison of NumPy NaN values in cube merge process. * Fixes for :meth:`iris.cube.Cube.intersection` to correct calculating the intersection of a cube with split bounds, handling of circular coordinates, - handling of monotonically descending bounded coordinats and for finding a + handling of monotonically descending bounded coordinates and for finding a wrapped two-point result and longitude tolerances. * A bug affecting :meth:`iris.cube.Cube.extract` and diff --git a/docs/iris/src/whatsnew/1.9.rst b/docs/iris/src/whatsnew/1.9.rst index da3fabe613..77d03b5de3 100644 --- a/docs/iris/src/whatsnew/1.9.rst +++ b/docs/iris/src/whatsnew/1.9.rst @@ -26,7 +26,7 @@ Features whitespace before the colon. This is not strictly in the CF spec, but is a common occurrence. -* Basic cube arithemetic (plus, minus, times, divide) now supports lazy +* Basic cube arithmetic (plus, minus, times, divide) now supports lazy evaluation. * :meth:`iris.analysis.cartography.rotate_winds` can now operate much faster diff --git a/docs/iris/src/whatsnew/2.3.rst b/docs/iris/src/whatsnew/2.3.rst index 7c5a5c27b2..914d86fda2 100644 --- a/docs/iris/src/whatsnew/2.3.rst +++ b/docs/iris/src/whatsnew/2.3.rst @@ -85,7 +85,7 @@ Features previously could produce a large number of small chunks. This had an adverse effect on performance. - In addition, Iris now takes its default chunksize from the default configured + In addition, Iris now takes its default chunk size from the default configured in Dask itself, i.e. ``dask.config.get('array.chunk-size')``. .. admonition:: Lazy Statistics @@ -258,7 +258,7 @@ Documentation should not be relied on. * Fixed references in the documentation of - :func:`iris.util.find_discontiguities` to a nonexistent + :func:`iris.util.find_discontiguities` to a non existent "mask_discontiguities" routine : these now refer to :func:`~iris.util.mask_cube`. diff --git a/docs/iris/src/whatsnew/2.4.rst b/docs/iris/src/whatsnew/2.4.rst index 0df2a909ff..ca7be20cd8 100644 --- a/docs/iris/src/whatsnew/2.4.rst +++ b/docs/iris/src/whatsnew/2.4.rst @@ -33,7 +33,7 @@ Features * The area weights used when performing area weighted regridding with :class:`iris.analysis.AreaWeighted` are now cached. This allows a - significant speedup when regridding multiple similar cubes, by repeatedly + significant speed up when regridding multiple similar cubes, by repeatedly using a :func:`iris.analysis.AreaWeighted.regridder` objects which you created first. @@ -57,7 +57,7 @@ Bugs fixed * Fixed a problem which was causing file loads to fetch *all* field data whenever UM files (PP or Fieldsfiles) were loaded. - With large sourcefiles, initial file loads are slow, with large memory usage + With large source files, initial file loads are slow, with large memory usage before any cube data is even fetched. Large enough files will cause a crash. The problem occurs only with Dask versions >= 2.0. diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 12560cefda..c94baf0a92 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -605,7 +605,7 @@ def update_metadata(self, cube, coords, **kwargs): Kwargs: - * This function is intended to be used in conjuction with aggregate() + * This function is intended to be used in conjunction with aggregate() and should be passed the same keywords (for example, the "ddof" keyword for a standard deviation aggregator). @@ -981,7 +981,7 @@ def update_metadata(self, cube, coords, **kwargs): Kwargs: - * This function is intended to be used in conjuction with aggregate() + * This function is intended to be used in conjunction with aggregate() and should be passed the same keywords (for example, the "ddof" keyword for a standard deviation aggregator). diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py index c7f084bc1b..261c93e8ef 100644 --- a/lib/iris/analysis/_grid_angles.py +++ b/lib/iris/analysis/_grid_angles.py @@ -147,11 +147,11 @@ def gridcell_angles(x, y=None, cell_angle_boundpoints="mid-lhs, mid-rhs"): connected by wraparound. Input can be either two arrays, two coordinates, or a single cube - containing two suitable coordinates identified with the 'x' and'y' axes. + containing two suitable coordinates identified with the 'x' and 'y' axes. Args: - The inputs (x [,y]) can be any of the folliwing : + The inputs (x [,y]) can be any of the following : * x (:class:`~iris.cube.Cube`): a grid cube with 2D X and Y coordinates, identified by 'axis'. diff --git a/lib/iris/aux_factory.py b/lib/iris/aux_factory.py index 0cc6bf068f..5b63ff53ed 100644 --- a/lib/iris/aux_factory.py +++ b/lib/iris/aux_factory.py @@ -972,7 +972,7 @@ def make_coord(self, coord_dims_func): Args: * coord_dims_func: - A callable which can return the list of dimesions relevant + A callable which can return the list of dimensions relevant to a given coordinate. See :meth:`iris.cube.Cube.coord_dims()`. """ diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index cc41b27b34..812dfae23e 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -126,12 +126,13 @@ def __init__( radius. If just two of semi_major_axis, semi_minor_axis, and - inverse_flattening are given the missing element is calulated from the + inverse_flattening are given the missing element is calculated from the formula: :math:`flattening = (major - minor) / major` Currently, Iris will not allow over-specification (all three ellipsoid - paramaters). + parameters). + Examples:: cs = GeogCS(6371229) diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 8fbe1abf56..09569f4ba5 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -170,7 +170,7 @@ def copy(self, values=None): * values An array of values for the new dimensional metadata object. - This may be a different shape to the orginal values array being + This may be a different shape to the original values array being copied. """ diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 7c28018512..cc833f8848 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2835,7 +2835,7 @@ def intersection(self, *args, **kwargs): For ranges defined over "circular" coordinates (i.e. those where the `units` attribute has a modulus defined) the cube - will be "rolled" to fit where neccesary. + will be "rolled" to fit where necessary. .. warning:: diff --git a/lib/iris/fileformats/rules.py b/lib/iris/fileformats/rules.py index 1e6cac691e..07ed5eb8ce 100644 --- a/lib/iris/fileformats/rules.py +++ b/lib/iris/fileformats/rules.py @@ -28,7 +28,7 @@ class ConcreteReferenceTarget: """Everything you need to make a real Cube for a named reference.""" def __init__(self, name, transform=None): - #: The name used to connect references with referencees. + #: The name used to connect references with references. self.name = name #: An optional transformation to apply to the cubes. self.transform = transform diff --git a/lib/iris/io/__init__.py b/lib/iris/io/__init__.py index 36f79d32d3..31cd862d85 100644 --- a/lib/iris/io/__init__.py +++ b/lib/iris/io/__init__.py @@ -334,7 +334,7 @@ def find_saver(filespec): def save(source, target, saver=None, **kwargs): """ - Save one or more Cubes to file (or other writable). + Save one or more Cubes to file (or other writeable). Iris currently supports three file formats for saving, which it can recognise by filename extension: @@ -353,7 +353,7 @@ def save(source, target, saver=None, **kwargs): * source - A :class:`iris.cube.Cube`, :class:`iris.cube.CubeList` or sequence of cubes. - * target - A filename (or writable, depending on file format). + * target - A filename (or writeable, depending on file format). When given a filename or file, Iris can determine the file format. diff --git a/lib/iris/util.py b/lib/iris/util.py index 95afb251a5..2e69ca6f97 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -1061,7 +1061,7 @@ def clip_string(the_str, clip_length=70, rider="..."): Returns: The string clipped to the required length with a rider appended. - If the clip length was greater than the orignal string, the + If the clip length was greater than the original string, the original string is returned unaltered. """ From 4bb2e4ddedcaffe7358b6e1995efd600ea8c61c2 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Sat, 15 Aug 2020 22:07:49 +0100 Subject: [PATCH 30/32] Fixed typo (#3783) --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 30490c5101..5015ac153e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -121,7 +121,8 @@ script: python -m iris.tests.runner --default-tests --system-tests; fi - - if [[ "${TEST_TARGET}" == 'gallery' ]]; then + - > + if [[ "${TEST_TARGET}" == 'gallery' ]]; then python -m iris.tests.runner --gallery-tests; fi From 108faf1a22cf50afc68630676d08c854eecbe604 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Sat, 15 Aug 2020 22:09:55 +0100 Subject: [PATCH 31/32] Removed redundant whatsnew test. (#3784) --- lib/iris/tests/test_whatsnew_contributions.py | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 lib/iris/tests/test_whatsnew_contributions.py diff --git a/lib/iris/tests/test_whatsnew_contributions.py b/lib/iris/tests/test_whatsnew_contributions.py deleted file mode 100644 index 42e816acba..0000000000 --- a/lib/iris/tests/test_whatsnew_contributions.py +++ /dev/null @@ -1,63 +0,0 @@ -# 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 -# licensing details. -""" -Check the the latest "whatsnew" contributions files have usable names. - -The files in "./docs...whatsnew/contributions_/" should have filenames -with a particular structure, which encodes a summary name. -These names are interpreted by "./docs...whatsnew/aggregate_directory.py". -This test just ensures that all those files have names which that process -can accept. - -.. note: - This only works within a developer installation: In a 'normal' install the - location of the docs sources is not known. - In a Travis installation, this test silently passes and the .travis.yml - invokes the checking command directly. - -""" - -# import iris tests first. -import iris.tests as tests - -import os -import os.path -import subprocess -import sys - -import iris - - -class TestWhatsnewContribs(tests.IrisTest): - def test_check_contributions(self): - # Get dirpath of overall iris installation. - # Note: assume iris at "/lib/iris". - iris_module_dirpath = os.path.dirname(iris.__file__) - iris_dirs = iris_module_dirpath.split(os.sep) - install_dirpath = os.sep.join(iris_dirs[:-2]) - - # Construct path to docs 'whatsnew' directory. - # Note: assume docs at "/docs". - whatsnew_dirpath = os.path.join( - install_dirpath, "docs", "iris", "src", "whatsnew" - ) - - # Quietly ignore if the directory does not exist: It is only there in - # in a developer installation, not a normal install. - # Travis bypasses this problem by running the test directly. - if os.path.exists(whatsnew_dirpath): - # Run a 'check contributions' command in that directory. - cmd = [ - sys.executable, - "aggregate_directory.py", - "--checkonly", - "--quiet", - ] - subprocess.check_call(cmd, cwd=whatsnew_dirpath) - - -if __name__ == "__main__": - tests.main() From 1873ce1471247904a167d9504523f70637436134 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 17 Aug 2020 17:03:07 +0100 Subject: [PATCH 32/32] set third party default logging level (#3787) --- lib/iris/etc/logging.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/iris/etc/logging.yaml b/lib/iris/etc/logging.yaml index 5671916ff9..a73906e7db 100644 --- a/lib/iris/etc/logging.yaml +++ b/lib/iris/etc/logging.yaml @@ -33,6 +33,12 @@ loggers: level: INFO handlers: [console-func] propagate: no + matplotlib: + level: INFO + PIL: + level: INFO + urllib3: + level: INFO root: level: INFO