From 71e6863ad7c0f8fae0719e836a480429129277a8 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Fri, 2 Oct 2020 10:36:59 +0100 Subject: [PATCH 01/23] Add release highlights and pin rc version (#3898) * Add release highlights and pin rc version * review actions --- docs/iris/src/whatsnew/3.0.rst | 43 +++++++++++++++++++++++- docs/iris/src/whatsnew/index.rst | 1 - docs/iris/src/whatsnew/latest.rst | 55 ------------------------------- lib/iris/__init__.py | 2 +- 4 files changed, 43 insertions(+), 58 deletions(-) delete mode 100644 docs/iris/src/whatsnew/latest.rst diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 0caba69de8..41ea0f5a0b 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -1,12 +1,44 @@ .. include:: ../common_links.inc -v3.0 (01 Oct 2020) +v3.0 (02 Oct 2020) ****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) +.. dropdown:: :opticon:`report` Release Highlights + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + + The highlights for this major release of Iris include: + + * We've finally dropped support for ``Python 2``, so welcome to ``Iris 3`` + and ``Python 3``! + * Our :ref:`documentation ` has been refreshed, + restructured, revitalised and rehosted on `readthedocs`_, + * It's now easier than ever to :ref:`install Iris ` + as a user or a developer, and the newly revamped developers guide walks + you though how you can :ref:`get involved ` + and contribute to Iris, + * We've extended our coverage of the `CF Conventions and Metadata`_ by + introducting support for `CF Ancillary Data`_ and `Quality Flags`_, + * Lazy regridding is now available for several regridding schemes, + * We've introduced a common metadata API to simplify and unify the + management of metadata across Iris, + * Cube arithmetic has been significantly improved with regards to extended + broadcasting, auto-transposition and a more lenient behaviour towards + handling metadata and coordinates, + * Also, this is a major release of Iris, so please be aware of the + :ref:`incompatible changes ` and + :ref:`deprecations `. + + And finally, get in touch with us on `GitHub`_ if you have any issues or + feature requests for improving Iris. Enjoy! + + 📢 Announcements ================ @@ -133,6 +165,8 @@ This document explains the changes made to Iris for this release removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) +.. _whatsnew 3.0 changes: + 💣 Incompatible Changes ======================= @@ -191,6 +225,8 @@ This document explains the changes made to Iris for this release exception was raised. (:pull:`3785`) +.. _whatsnew 3.0 deprecations: + 🔥 Deprecations =============== @@ -238,6 +274,8 @@ This document explains the changes made to Iris for this release dependency group. We no longer consider it to be an extension. (:pull:`3762`) +.. _whatsnew 3.0 docs: + 📚 Documentation ================ @@ -417,3 +455,6 @@ This document explains the changes made to Iris for this release .. _PyKE: https://pypi.org/project/scitools-pyke/ .. _matplotlib.rcdefaults: https://matplotlib.org/3.1.1/api/matplotlib_configuration_api.html?highlight=rcdefaults#matplotlib.rcdefaults .. _@owena11: https://github.com/owena11 +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose +.. _readthedocs: https://readthedocs.org/ +.. _CF Conventions and Metadata: https://cfconventions.org/ diff --git a/docs/iris/src/whatsnew/index.rst b/docs/iris/src/whatsnew/index.rst index 3fd5fe6070..19860791c8 100644 --- a/docs/iris/src/whatsnew/index.rst +++ b/docs/iris/src/whatsnew/index.rst @@ -10,7 +10,6 @@ Iris versions. .. toctree:: :maxdepth: 1 - latest.rst 3.0.rst 2.4.rst 2.3.rst diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst deleted file mode 100644 index 67518e539a..0000000000 --- a/docs/iris/src/whatsnew/latest.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. include:: ../common_links.inc - - -************ - -This document explains the changes made to Iris for this release -(:doc:`View all changes `.) - - -📢 Announcements -================ - -* N/A - - -✨ Features -=========== - -* N/A - - -🐛 Bugs Fixed -============= - -* N/A - - -💣 Incompatible Changes -======================= - -* N/A - - -🔥 Deprecations -=============== - -* N/A - - -🔗 Dependencies -=============== - -* N/A - - -📚 Documentation -================ - -* N/A - - -💼 Internal -=========== - -* N/A diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index e31c7b58d7..ad07426e51 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -106,7 +106,7 @@ def callback(cube, field, filename): # Iris revision. -__version__ = "3.1.dev0" +__version__ = "3.0.0rc0" # Restrict the names imported when using "from iris import *" __all__ = [ From 3b9a0bd546f1e938a4ae998765932a4fbcc449ea Mon Sep 17 00:00:00 2001 From: Bill Little Date: Fri, 2 Oct 2020 12:38:53 +0100 Subject: [PATCH 02/23] reorder release highlights (#3899) Tweak release highlights --- docs/iris/src/whatsnew/3.0.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 41ea0f5a0b..f0098ee0b4 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -12,25 +12,26 @@ This document explains the changes made to Iris for this release :title: text-primary text-center font-weight-bold :body: bg-light :animate: fade-in + :open: The highlights for this major release of Iris include: * We've finally dropped support for ``Python 2``, so welcome to ``Iris 3`` and ``Python 3``! + * We've extended our coverage of the `CF Conventions and Metadata`_ by + introducting support for `CF Ancillary Data`_ and `Quality Flags`_, + * Lazy regridding is now available for several regridding schemes, + * Managing and manipulating metadata within Iris is now easier and more + consistent thanks to the introduction of a new common metadata API, + * :ref:`Cube arithmetic ` has been significantly improved with + regards to extended broadcasting, auto-transposition and a more lenient + behaviour towards handling metadata and coordinates, * Our :ref:`documentation ` has been refreshed, restructured, revitalised and rehosted on `readthedocs`_, * It's now easier than ever to :ref:`install Iris ` as a user or a developer, and the newly revamped developers guide walks you though how you can :ref:`get involved ` and contribute to Iris, - * We've extended our coverage of the `CF Conventions and Metadata`_ by - introducting support for `CF Ancillary Data`_ and `Quality Flags`_, - * Lazy regridding is now available for several regridding schemes, - * We've introduced a common metadata API to simplify and unify the - management of metadata across Iris, - * Cube arithmetic has been significantly improved with regards to extended - broadcasting, auto-transposition and a more lenient behaviour towards - handling metadata and coordinates, * Also, this is a major release of Iris, so please be aware of the :ref:`incompatible changes ` and :ref:`deprecations `. From 1b59ffe05e33d403abe6c4151a14d85880343ef9 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Fri, 2 Oct 2020 13:31:42 +0100 Subject: [PATCH 03/23] Add whatsnew announcement (#3900) --- docs/iris/src/whatsnew/3.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index f0098ee0b4..d5d604fc21 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -49,6 +49,9 @@ This document explains the changes made to Iris for this release and performance metrics tool for routine evaluation of Earth system models in CMIP*". Welcome aboard! 🎉 +* Congratulations also goes to `@jonseddon`_ who recently became an Iris core + developer. We look forward to seeing more of your awesome contributions! 🎉 + ✨ Features =========== From a9c1e797bae26c48d02e50bbd59378393436e7aa Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Mon, 5 Oct 2020 15:51:41 +0100 Subject: [PATCH 04/23] Fix spelling (#3903) --- docs/iris/src/further_topics/lenient_metadata.rst | 4 ++-- docs/iris/src/whatsnew/3.0.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/iris/src/further_topics/lenient_metadata.rst b/docs/iris/src/further_topics/lenient_metadata.rst index 1b31759d9a..ada7049786 100644 --- a/docs/iris/src/further_topics/lenient_metadata.rst +++ b/docs/iris/src/further_topics/lenient_metadata.rst @@ -335,10 +335,10 @@ Lenient combination The behaviour of the lenient ``combine`` metadata class method is outlined in :numref:`lenient combine table`, and as with :ref:`lenient equality` and -:ref:`lenient difference` is enabled throught the ``lenient`` keyword argument. +:ref:`lenient difference` is enabled through the ``lenient`` keyword argument. The difference in behaviour between **lenient** and -:ref:`strict combination ` is centered around the lenient +:ref:`strict combination ` is centred around the lenient handling of combining **something** with **nothing** (``None``) to return **something**. Whereas strict combination will only return a result from combining identical objects. diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index d5d604fc21..f0df167cce 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -19,7 +19,7 @@ This document explains the changes made to Iris for this release * We've finally dropped support for ``Python 2``, so welcome to ``Iris 3`` and ``Python 3``! * We've extended our coverage of the `CF Conventions and Metadata`_ by - introducting support for `CF Ancillary Data`_ and `Quality Flags`_, + introducing support for `CF Ancillary Data`_ and `Quality Flags`_, * Lazy regridding is now available for several regridding schemes, * Managing and manipulating metadata within Iris is now easier and more consistent thanks to the introduction of a new common metadata API, From 6d36021e5f74cf74e1f07c1075694848ea7ebaf6 Mon Sep 17 00:00:00 2001 From: Zeb Nicholls Date: Wed, 7 Oct 2020 19:44:01 +1100 Subject: [PATCH 05/23] Fix unit label handling (#3902) * Add failing test of plotting * Implement fix to pass test * Update idiff to ignore irrelevant hyphens in path * Update imagerepo (following docs) * Update after review by @trexfeathers * Add whatsnew entries * Move whatsnew entries into correct file --- docs/iris/src/whatsnew/3.0.rst | 5 +++++ lib/iris/quickplot.py | 2 +- lib/iris/tests/idiff.py | 4 +++- lib/iris/tests/results/imagerepo.json | 3 +++ lib/iris/tests/test_quickplot.py | 7 +++++++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index f0df167cce..a6d1c97036 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -168,6 +168,7 @@ This document explains the changes made to Iris for this release Previously, the first tick label would occasionally be duplicated. This also removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) +* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) .. _whatsnew 3.0 changes: @@ -417,6 +418,9 @@ This document explains the changes made to Iris for this release * `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) +* `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). + +* `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). .. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ .. _Matplotlib: https://matplotlib.org/ @@ -450,6 +454,7 @@ This document explains the changes made to Iris for this release .. _@rcomer: https://github.com/rcomer .. _@jvegasbsc: https://github.com/jvegasbsc .. _@zklaus: https://github.com/zklaus +.. _@znicholls: https://github.com/znicholls .. _ESMValTool: https://github.com/ESMValGroup/ESMValTool .. _v75: https://cfconventions.org/Data/cf-standard-names/75/build/cf-standard-name-table.html .. _sphinx-panels: https://sphinx-panels.readthedocs.io/en/latest/ diff --git a/lib/iris/quickplot.py b/lib/iris/quickplot.py index 42c0dba46a..350d61b537 100644 --- a/lib/iris/quickplot.py +++ b/lib/iris/quickplot.py @@ -49,7 +49,7 @@ def _title(cube_or_coord, with_units): if _use_symbol(units): units = units.symbol - if units.is_time_reference(): + elif units.is_time_reference(): # iris.plot uses matplotlib.dates.date2num, which is fixed to the below unit. if version.parse(_mpl_version) >= version.parse("3.3"): days_since = "1970-01-01" diff --git a/lib/iris/tests/idiff.py b/lib/iris/tests/idiff.py index e45d8a709e..84a966624f 100755 --- a/lib/iris/tests/idiff.py +++ b/lib/iris/tests/idiff.py @@ -220,7 +220,9 @@ def step_over_diffs(result_dir, action, display=True): count = len(results) for count_index, result_fname in enumerate(results): - key = os.path.splitext("-".join(result_fname.split("-")[1:]))[0] + key = os.path.splitext( + "-".join(result_fname.split("result-")[1:]) + )[0] try: # Calculate the test result perceptual image hash. phash = imagehash.phash( diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json index f9430ae9f5..a7fd9e1faf 100644 --- a/lib/iris/tests/results/imagerepo.json +++ b/lib/iris/tests/results/imagerepo.json @@ -908,6 +908,9 @@ "https://scitools.github.io/test-iris-imagehash/images/v4/bb433d4e94a4c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eea16affc05ab500956e974ac53f3d80925ac03f3f81c07e3fa12da1c27e3f80.png" ], + "iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/eea16affc05ab500956e974ac53f3d80925ac03f3f80c07e3fa12da1c27f3f80.png" + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_non_cube_coordinate.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85857a955ae17e957ec57e7a81855fc17e3a81c57e1a813a85c57a1a05.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe816a85857a957ac07f957ac07f3e80956ac07f3e80c07f3e813e85c07e3f80.png" diff --git a/lib/iris/tests/test_quickplot.py b/lib/iris/tests/test_quickplot.py index cf25324ea7..8abbf48a94 100644 --- a/lib/iris/tests/test_quickplot.py +++ b/lib/iris/tests/test_quickplot.py @@ -201,6 +201,13 @@ def test_pcolormesh(self): self.check_graphic() + def test_pcolormesh_str_symbol(self): + pcube = self._small().copy() + pcube.coords("level_height")[0].units = "centimeters" + qplt.pcolormesh(pcube) + + self.check_graphic() + def test_map(self): cube = self._slice(["grid_latitude", "grid_longitude"]) qplt.contour(cube) From c797fc88a0da727badf523136599bed7215bd716 Mon Sep 17 00:00:00 2001 From: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Date: Mon, 12 Oct 2020 09:22:09 +0100 Subject: [PATCH 06/23] Release Docs Improvements (#3895) * Minor phrasing change in 'Release candidate'. * Before release deprecations. * Whatsnew highlights section. --- docs/iris/src/developers_guide/release.rst | 37 +++++++++++++++------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/docs/iris/src/developers_guide/release.rst b/docs/iris/src/developers_guide/release.rst index d71f149186..2ec787a780 100644 --- a/docs/iris/src/developers_guide/release.rst +++ b/docs/iris/src/developers_guide/release.rst @@ -3,17 +3,28 @@ Releases ======== -A release of Iris is a `tag on the SciTools/Iris`_ +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. +Before release +-------------- + +Deprecations +~~~~~~~~~~~~ + +Ensure that any behaviour which has been deprecated for the correct number of +previous releases is now finally changed. More detail, including the correct +number of releases, is in :ref:`iris_development_deprecations`. + + Release branch -------------- -Once the features intended for the release are on master, a 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: :literal:`v{major release number}.{minor release number}.x` @@ -35,12 +46,12 @@ number, e.g.: :literal:`v1.9.0rc1` -If created, the pre-release shall be available for a minimum of two weeks +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 will not for a +A pre-release is expected for a major or minor release, but not for a point release. If new features are required for a release after a release candidate has been @@ -59,7 +70,7 @@ Steps to achieve this can be found in the :ref:`iris_development_releases_steps` The release ----------- -The final steps are to change the version string in the source of +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. @@ -72,7 +83,7 @@ Conda recipe 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`_. +`conda forge`_. .. _Iris feedstock: https://github.com/conda-forge/iris-feedstock/tree/master/recipe .. _conda forge: https://anaconda.org/conda-forge/iris @@ -102,7 +113,7 @@ 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`. +:literal:`master`. .. _iris_development_releases_steps: @@ -118,7 +129,7 @@ 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: +#. Update the what's new for the release: * Copy ``docs/iris/src/whatsnew/latest.rst`` to a file named ``v1.9.rst`` @@ -128,6 +139,8 @@ Release steps 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 + * Work with the development team to create a 'highlights' section at the + top of the file, providing extra detail on notable changes * Add ``v1.9.rst`` to git and commit all changes, including removal of ``latest.rst`` @@ -138,7 +151,7 @@ Release steps #. 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 +#. Once all the above steps are complete, the release is cut, using the :guilabel:`Draft a new release` button on the `Iris release page `_ @@ -146,16 +159,16 @@ Release steps Post release steps ~~~~~~~~~~~~~~~~~~ -#. Check the documentation has built on `Read The Docs`_. The build is +#. 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 available in the **admin** dashboard in Read The Docs -#. Copy ``docs/iris/src/whatsnew/latest.rst.template`` to +#. 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 +#. 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 From 2e6aa01cb29b933e2a54a81bf9cf226c18c06aad Mon Sep 17 00:00:00 2001 From: Bill Little Date: Thu, 15 Oct 2020 14:02:00 +0100 Subject: [PATCH 07/23] Relax setup.py setup requirements (#3909) --- requirements/setup.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements/setup.txt b/requirements/setup.txt index 2e14da4905..9232946a6a 100644 --- a/requirements/setup.txt +++ b/requirements/setup.txt @@ -1,6 +1,4 @@ # Dependencies necessary to run setup.py of iris # ---------------------------------------------- -scitools-pyke setuptools>=40.8.0 -wheel From 5da592ff9fdb7e51534171d1c27ce65fe40b5551 Mon Sep 17 00:00:00 2001 From: Jon Seddon <17068361+jonseddon@users.noreply.github.com> Date: Tue, 24 Nov 2020 19:32:00 +0000 Subject: [PATCH 08/23] Updated CF saver version in User Guide and docstring (#3925) * Updated CF saver version in User Guide and docstring * Remove references to CF version of the loader in docstrings * Added whatsnew * Pin cftime<1.3.0 --- docs/iris/src/userguide/saving_iris_cubes.rst | 2 +- docs/iris/src/whatsnew/3.0.rst | 2 ++ lib/iris/fileformats/cf.py | 2 +- lib/iris/fileformats/netcdf.py | 5 ++--- requirements/ci/py36.yml | 2 +- requirements/ci/py37.yml | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/iris/src/userguide/saving_iris_cubes.rst b/docs/iris/src/userguide/saving_iris_cubes.rst index cca8b44bd1..3a30321979 100644 --- a/docs/iris/src/userguide/saving_iris_cubes.rst +++ b/docs/iris/src/userguide/saving_iris_cubes.rst @@ -6,7 +6,7 @@ Saving Iris cubes Iris supports the saving of cubes and cube lists to: -* CF netCDF (version 1.6) +* CF netCDF (version 1.7) * GRIB edition 2 (if `iris-grib `_ is installed) * Met Office PP diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index a6d1c97036..79de8b3edf 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -351,6 +351,8 @@ This document explains the changes made to Iris for this release included documentation for :ref:`metadata`, :ref:`lenient metadata`, and :ref:`lenient maths`. (:pull:`3890`) +* `@jonseddon`_ updated the CF version of the netCDF saver in the + :ref:`saving_iris_cubes` section and in the equivalent function docstring. 💼 Internal =========== diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 5c6e11f3ac..47ff6291b0 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -9,7 +9,7 @@ References: -[CF] NetCDF Climate and Forecast (CF) Metadata conventions, Version 1.5, October, 2010. +[CF] NetCDF Climate and Forecast (CF) Metadata conventions. [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 d0c3a3c534..98f712a970 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -8,8 +8,7 @@ See also: `netCDF4 python `_. -Also refer to document 'NetCDF Climate and Forecast (CF) Metadata Conventions', -Version 1.4, 27 February 2009. +Also refer to document 'NetCDF Climate and Forecast (CF) Metadata Conventions'. """ @@ -2490,7 +2489,7 @@ def save( """ Save cube(s) to a netCDF file, given the cube and the filename. - * Iris will write CF 1.5 compliant NetCDF files. + * Iris will write CF 1.7 compliant NetCDF files. * The attributes dictionaries on each cube in the saved cube list will be compared and common attributes saved as NetCDF global attributes where appropriate. diff --git a/requirements/ci/py36.yml b/requirements/ci/py36.yml index 9e6d76c931..3b3328cf1f 100644 --- a/requirements/ci/py36.yml +++ b/requirements/ci/py36.yml @@ -13,7 +13,7 @@ dependencies: # Core dependencies. - cartopy>=0.18 - cf-units>=2 - - cftime + - cftime<1.3.0 - dask>=2 - matplotlib - netcdf4 diff --git a/requirements/ci/py37.yml b/requirements/ci/py37.yml index 4c9825a97d..8817f575b7 100644 --- a/requirements/ci/py37.yml +++ b/requirements/ci/py37.yml @@ -13,7 +13,7 @@ dependencies: # Core dependencies. - cartopy>=0.18 - cf-units>=2 - - cftime + - cftime<1.3.0 - dask>=2 - matplotlib - netcdf4 From 0ff0dca3ce5b4de38a4ae02438bb00206e664460 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 25 Nov 2020 10:02:24 +0000 Subject: [PATCH 09/23] Migrate to cirrus-ci (#3928) * migrate from travis-ci to cirrus-ci * added whatsnew entries --- .cirrus.yml | 227 +++++++++++++++++ .gitignore | 1 + .stickler.yml | 4 - .travis.yml | 169 ------------ README.md | 14 +- docs/iris/src/common_links.inc | 1 + docs/iris/src/conf.py | 10 +- docs/iris/src/whatsnew/3.0.rst | 9 + lib/iris/tests/results/analysis/sqrt.cml | 4 +- lib/iris/tests/test_basic_maths.py | 7 +- lib/iris/tests/test_coding_standards.py | 1 + lib/iris/tests/test_netcdf.py | 25 +- noxfile.py | 312 +++++++++++++++++++++++ requirements/ci/py36.yml | 3 +- requirements/ci/py37.yml | 1 + requirements/core.txt | 2 +- setup.cfg | 44 ---- 17 files changed, 585 insertions(+), 249 deletions(-) create mode 100644 .cirrus.yml delete mode 100644 .stickler.yml delete mode 100644 .travis.yml create mode 100644 noxfile.py delete mode 100644 setup.cfg diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 0000000000..d4aedd3955 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,227 @@ +# Reference: +# - https://cirrus-ci.org/guide/writing-tasks/ +# - https://cirrus-ci.org/guide/tips-and-tricks/#sharing-configuration-between-tasks +# - https://cirrus-ci.org/guide/linux/ +# - https://cirrus-ci.org/guide/macOS/ +# - https://cirrus-ci.org/guide/windows/ +# - https://hub.docker.com/_/gcc/ +# - https://hub.docker.com/_/python/ + +# +# Global defaults. +# +container: + image: python:3.8 + cpu: 2 + memory: 4G + + +env: + # Maximum cache period (in weeks) before forcing a new cache upload. + CACHE_PERIOD: "2" + # Increment the build number to force new cartopy cache upload. + CARTOPY_CACHE_BUILD: "0" + # Increment the build number to force new conda cache upload. + CONDA_CACHE_BUILD: "0" + # Increment the build number to force new nox cache upload. + NOX_CACHE_BUILD: "0" + # Increment the build number to force new pip cache upload. + PIP_CACHE_BUILD: "0" + # Pip package to be upgraded/installed. + PIP_CACHE_PACKAGES: "pip setuptools wheel nox" + # Git commit hash for iris test data. + IRIS_TEST_DATA_REF: "fffb9b14b9cb472c5eb2ebb7fd19acb7f6414a30" + # Base directory for the iris-test-data. + IRIS_TEST_DATA_DIR: ${HOME}/iris-test-data + + +# +# Linting +# +lint_task: + auto_cancellation: true + name: "${CIRRUS_OS}: flake8 and black" + pip_cache: + folder: ~/.cache/pip + fingerprint_script: + - echo "${CIRRUS_TASK_NAME}" + - echo "$(date +%Y).$(($(date +%U) / ${CACHE_PERIOD})):${PIP_CACHE_BUILD} ${PIP_CACHE_PACKAGES}" + lint_script: + - pip list + - python -m pip install --retries 3 --upgrade ${PIP_CACHE_PACKAGES} + - pip list + - nox --session flake8 + - nox --session black + + +# +# YAML alias for common linux test infra-structure. +# +linux_task_template: &LINUX_TASK_TEMPLATE + auto_cancellation: true + env: + IRIS_REPO_DIR: ${CIRRUS_WORKING_DIR} + PATH: ${HOME}/miniconda/bin:${PATH} + SITE_CFG: ${CIRRUS_WORKING_DIR}/lib/iris/etc/site.cfg + conda_cache: + folder: ${HOME}/miniconda + fingerprint_script: + - wget --quiet https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh + - echo "${CIRRUS_OS} $(sha256sum miniconda.sh)" + - echo "$(date +%Y).$(($(date +%U) / ${CACHE_PERIOD})):${CONDA_CACHE_BUILD}" + populate_script: + - bash miniconda.sh -b -p ${HOME}/miniconda + - conda config --set always_yes yes --set changeps1 no + - conda config --set show_channel_urls True + - conda config --add channels conda-forge + - conda update --quiet --name base conda + - conda install --quiet --name base nox pip + cartopy_cache: + folder: ${HOME}/.local/share/cartopy + fingerprint_script: + - echo "${CIRRUS_OS}" + - echo "$(date +%Y).$(($(date +%U) / ${CACHE_PERIOD})):${CARTOPY_CACHE_BUILD}" + nox_cache: + folder: ${CIRRUS_WORKING_DIR}/.nox + fingerprint_script: + - echo "${CIRRUS_TASK_NAME}" + - echo "$(date +%Y).$(($(date +%U) / ${CACHE_PERIOD})):${NOX_CACHE_BUILD}" + - sha256sum ${CIRRUS_WORKING_DIR}/requirements/ci/py$(echo ${PY_VER} | tr -d ".").yml + + +# +# Testing Minimal (Linux) +# +linux_minimal_task: + matrix: + env: + PY_VER: 3.6 + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} tests (minimal)" + container: + image: gcc:latest + cpu: 2 + memory: 4G + << : *LINUX_TASK_TEMPLATE + tests_script: + - echo "[Resources]" > ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - nox --session tests + + +# +# Testing Full (Linux) +# +linux_task: + matrix: + env: + PY_VER: 3.6 + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} tests (full)" + container: + image: gcc:latest + cpu: 6 + memory: 8G + data_cache: + folder: ${IRIS_TEST_DATA_DIR} + fingerprint_script: + - echo "${IRIS_TEST_DATA_REF}" + populate_script: + - wget --quiet https://github.com/SciTools/iris-test-data/archive/${IRIS_TEST_DATA_REF}.zip -O iris-test-data.zip + - unzip -q iris-test-data.zip + - mv iris-test-data-$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//") ${IRIS_TEST_DATA_DIR} + << : *LINUX_TASK_TEMPLATE + tests_script: + - echo "[Resources]" > ${SITE_CFG} + - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - nox --session tests + + +# +# Testing Documentation Gallery (Linux) +# +gallery_task: + matrix: + env: + PY_VER: 3.6 + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} doc tests (gallery)" + container: + image: gcc:latest + cpu: 2 + memory: 4G + data_cache: + folder: ${IRIS_TEST_DATA_DIR} + fingerprint_script: + - echo "${IRIS_TEST_DATA_REF}" + populate_script: + - wget --quiet https://github.com/SciTools/iris-test-data/archive/${IRIS_TEST_DATA_REF}.zip -O iris-test-data.zip + - unzip -q iris-test-data.zip + - mv iris-test-data-$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//") ${IRIS_TEST_DATA_DIR} + << : *LINUX_TASK_TEMPLATE + tests_script: + - echo "[Resources]" > ${SITE_CFG} + - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - nox --session gallery + + +# +# Testing Documentation (Linux) +# +doctest_task: + matrix: + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} doc tests" + container: + image: gcc:latest + cpu: 2 + memory: 4G + env: + MPL_RC_DIR: ${HOME}/.config/matplotlib + MPL_RC_FILE: ${HOME}/.config/matplotlib/matplotlibrc + data_cache: + folder: ${IRIS_TEST_DATA_DIR} + fingerprint_script: + - echo "${IRIS_TEST_DATA_REF}" + populate_script: + - wget --quiet https://github.com/SciTools/iris-test-data/archive/${IRIS_TEST_DATA_REF}.zip -O iris-test-data.zip + - unzip -q iris-test-data.zip + - mv iris-test-data-$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//") ${IRIS_TEST_DATA_DIR} + << : *LINUX_TASK_TEMPLATE + tests_script: + - echo "[Resources]" > ${SITE_CFG} + - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - mkdir -p ${MPL_RC_DIR} + - echo "backend : agg" > ${MPL_RC_FILE} + - echo "image.cmap : viridis" >> ${MPL_RC_FILE} + - nox --session doctest + + +# +# Testing Documentation Link Check (Linux) +# +link_task: + matrix: + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} doc link check" + container: + image: gcc:latest + cpu: 2 + memory: 4G + env: + MPL_RC_DIR: ${HOME}/.config/matplotlib + MPL_RC_FILE: ${HOME}/.config/matplotlib/matplotlibrc + << : *LINUX_TASK_TEMPLATE + tests_script: + - mkdir -p ${MPL_RC_DIR} + - echo "backend : agg" > ${MPL_RC_FILE} + - echo "image.cmap : viridis" >> ${MPL_RC_FILE} + - nox --session linkcheck diff --git a/.gitignore b/.gitignore index d589c306fe..618913e7ec 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ var sdist develop-eggs .installed.cfg +.nox # Installer logs pip-log.txt diff --git a/.stickler.yml b/.stickler.yml deleted file mode 100644 index 6edee0f6a5..0000000000 --- a/.stickler.yml +++ /dev/null @@ -1,4 +0,0 @@ -linters: - flake8: - python: 3 - config: ./.flake8 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ab1accab4a..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,169 +0,0 @@ -# Please update the test data git references below if appropriate. -# -# Note: Contrary to the travis documentation, -# http://about.travis-ci.org/docs/user/languages/python/#Travis-CI-Uses-Isolated-virtualenvs -# we will use conda to give us a much faster setup time. - -language: minimal -dist: xenial - -env: - global: - # The decryption key for the encrypted .github/deploy_key.scitools-docs.enc. - - secure: "N9/qBUT5CqfC7KQBDy5mIWZcGNuUJk3e/qmKJpotWYV+zwOI4GghJsRce6nFnlRiwl65l5oBEcvf3+sBvUfbZqh7U0MdHpw2tHhr2FSCmMB3bkvARZblh9M37f4da9G9VmRkqnyBM5G5TImXtoq4dusvNWKvLW0qETciaipq7ws=" - matrix: - - PYTHON_VERSION='36' TEST_TARGET='default' TEST_MINIMAL=true - - PYTHON_VERSION='36' TEST_TARGET='default' TEST_BLACK=true - - PYTHON_VERSION='36' TEST_TARGET='gallery' - - PYTHON_VERSION='37' TEST_TARGET='default' TEST_MINIMAL=true - - PYTHON_VERSION='37' TEST_TARGET='default' TEST_BLACK=true - - PYTHON_VERSION='37' TEST_TARGET='gallery' - - PYTHON_VERSION='37' TEST_TARGET='doctest' PUSH_BUILT_DOCS=true - - PYTHON_VERSION='37' TEST_TARGET='linkcheck' - # TODO: Dependencies for sphinxcontrib-spelling to be in place before this - # spelling code block is enabled - #- PYTHON_VERSION='37' TEST_TARGET='spelling' - -git: - # We need a deep clone so that we can compute the age of the files using their git history. - depth: 10000 - -install: - - > - export IRIS_TEST_DATA_REF="fffb9b14b9cb472c5eb2ebb7fd19acb7f6414a30"; - export IRIS_TEST_DATA_SUFFIX=$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//"); - - # Install miniconda - # ----------------- - - > - echo 'Installing miniconda'; - export CONDA_BASE="https://repo.continuum.io/miniconda/Miniconda"; - wget --quiet ${CONDA_BASE}3-latest-Linux-x86_64.sh -O miniconda.sh; - bash miniconda.sh -b -p ${HOME}/miniconda; - export PATH="${HOME}/miniconda/bin:${PATH}"; - - # Create the testing environment - # ------------------------------ - # Explicitly add defaults channel, see https://github.com/conda/conda/issues/2675 - - > - echo 'Configure conda and create an environment'; - conda config --set always_yes yes --set changeps1 no; - conda config --set show_channel_urls True; - conda config --add channels conda-forge; - conda update --quiet conda; - export ENV_NAME='iris-dev'; - ENV_FILE="requirements/ci/py${PYTHON_VERSION}.yml"; - cat ${ENV_FILE}; - conda env create --quiet --file=${ENV_FILE}; - source activate ${ENV_NAME}; - export PREFIX="${CONDA_PREFIX}"; - - # Output debug info - - > - conda list -n ${ENV_NAME}; - conda list -n ${ENV_NAME} --explicit; - conda info -a; - -# Pre-load Natural Earth data to avoid multiple, overlapping downloads. -# i.e. There should be no DownloadWarning reports in the log. - - python -c 'import cartopy; cartopy.io.shapereader.natural_earth()' - -# iris test data - - > - if [[ "${TEST_MINIMAL}" != true ]]; then - wget --quiet -O iris-test-data.zip https://github.com/SciTools/iris-test-data/archive/${IRIS_TEST_DATA_REF}.zip; - unzip -q iris-test-data.zip; - mv "iris-test-data-${IRIS_TEST_DATA_SUFFIX}" iris-test-data; - fi - -# set config paths - - > - SITE_CFG="lib/iris/etc/site.cfg"; - echo "[Resources]" > ${SITE_CFG}; - echo "test_data_dir = $(pwd)/iris-test-data/test_data" >> ${SITE_CFG}; - echo "doc_dir = $(pwd)/docs/iris" >> ${SITE_CFG}; - echo "[System]" >> ${SITE_CFG}; - echo "udunits2_path = ${PREFIX}/lib/libudunits2.so" >> ${SITE_CFG}; - - - python setup.py --quiet install - -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. - - export INSTALL_DIR=$(pwd) - - - > - if [[ "${TEST_BLACK}" == 'true' ]]; then - echo $(black --version); - rm ${INSTALL_DIR}/.gitignore; - black --check ${INSTALL_DIR}; - fi - - - > - if [[ "${TEST_TARGET}" == 'default' ]]; then - export IRIS_REPO_DIR=${INSTALL_DIR}; - python -m iris.tests.runner --default-tests --system-tests; - fi - - - > - if [[ "${TEST_TARGET}" == 'gallery' ]]; then - python -m iris.tests.runner --gallery-tests; - fi - - # Build the docs. - - > - if [[ "${TEST_TARGET}" == 'doctest' ]]; 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 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 - - # 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 (?!) - - > - ORG=$(echo ${TRAVIS_REPO_SLUG} | cut -d/ -f1); - export ORG - - - echo "Travis job context ORG=${ORG}; TRAVIS_EVENT_TYPE=${TRAVIS_EVENT_TYPE}; PUSH_BUILT_DOCS=${PUSH_BUILT_DOCS}" - - # When we merge a change to SciTools/iris, we can push docs to github pages. - # At present, only the Python 3.7 "doctest" job does this. - # Results appear at https://scitools-docs.github.io/iris/<>/index.html - - if [[ "${ORG}" == 'SciTools' && "${TRAVIS_EVENT_TYPE}" == 'push' && "${PUSH_BUILT_DOCS}" == 'true' ]]; then - cd ${INSTALL_DIR}; - conda install --quiet -n ${ENV_NAME} pip; - pip install doctr; - 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}}; - fi diff --git a/README.md b/README.md index aeadb52d93..6339491955 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,12 @@

- - -Travis-CI + +Cirrus-CI - Documentation Status +Documentation Status conda-forge downloads @@ -26,9 +25,6 @@ Latest version - -Stable docs Commits since last release diff --git a/docs/iris/src/common_links.inc b/docs/iris/src/common_links.inc index 94c2f3c92b..0bc8ca60e6 100644 --- a/docs/iris/src/common_links.inc +++ b/docs/iris/src/common_links.inc @@ -25,3 +25,4 @@ .. _sphinx: https://www.sphinx-doc.org/en/master/ .. _napolean: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/sphinxcontrib.napoleon.html .. _legacy documentation: https://scitools.org.uk/iris/docs/v2.4.0/ +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 9e6276f544..73b60fb982 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -261,13 +261,13 @@ def autolog(message): # 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", + "http://cfconventions.org", "http://code.google.com/p/msysgit/downloads/list", + "https://github.com", + "http://www.personal.psu.edu/cab38/ColorBrewer/ColorBrewer_updates.html", "http://schacon.github.com/git", - "https://github.com/SciTools/iris/pull", - "https://github.com/SciTools/iris/issue", - "http://cfconventions.org", + "http://scitools.github.com/cartopy", + "http://www.wmo.int/pages/prog/www/DPFS/documents/485_Vol_I_en_colour.pdf", ] # list of sources to exclude from the build. diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 79de8b3edf..59f7ec8735 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -424,6 +424,13 @@ This document explains the changes made to Iris for this release * `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). +* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_. (:pull:`3928`) + +* `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. + It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to + run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris + with `flake8`_ and `black`_. (:pull:`3928`) + .. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ .. _Matplotlib: https://matplotlib.org/ .. _CF units rules: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#units @@ -469,3 +476,5 @@ This document explains the changes made to Iris for this release .. _GitHub: https://github.com/SciTools/iris/issues/new/choose .. _readthedocs: https://readthedocs.org/ .. _CF Conventions and Metadata: https://cfconventions.org/ +.. _flake8: https://flake8.pycqa.org/en/stable/ +.. _nox: https://nox.thea.codes/en/stable/ diff --git a/lib/iris/tests/results/analysis/sqrt.cml b/lib/iris/tests/results/analysis/sqrt.cml index 0dd0fe20b3..c6b9b88e9a 100644 --- a/lib/iris/tests/results/analysis/sqrt.cml +++ b/lib/iris/tests/results/analysis/sqrt.cml @@ -1,6 +1,6 @@ - + @@ -39,6 +39,6 @@ - + diff --git a/lib/iris/tests/test_basic_maths.py b/lib/iris/tests/test_basic_maths.py index a559ee0e8a..4b3cde95e4 100644 --- a/lib/iris/tests/test_basic_maths.py +++ b/lib/iris/tests/test_basic_maths.py @@ -537,11 +537,12 @@ def test_multiplication_not_in_place(self): class TestExponentiate(tests.IrisTest): def setUp(self): self.cube = iris.tests.stock.global_pp() - self.cube.data = self.cube.data - 260 + # Increase dtype from float32 to float64 in order + # to avoid dtype quantization errors during maths. + self.cube.data = self.cube.data.astype(np.float64) - 260.0 def test_exponentiate(self): a = self.cube - a.data = a.data.astype(np.float64) e = pow(a, 4) self.assertCMLApproxData(e, ("analysis", "exponentiate.cml")) @@ -553,8 +554,8 @@ def test_square_root(self): e = a ** 0.5 - self.assertCML(e, ("analysis", "sqrt.cml")) self.assertArrayEqual(e.data, a.data ** 0.5) + self.assertCML(e, ("analysis", "sqrt.cml")) self.assertRaises(ValueError, iris.analysis.maths.exponentiate, a, 0.3) def test_type_error(self): diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py index 00ce7b7d44..79dff535eb 100644 --- a/lib/iris/tests/test_coding_standards.py +++ b/lib/iris/tests/test_coding_standards.py @@ -102,6 +102,7 @@ def last_change_by_fname(): def test_license_headers(self): exclude_patterns = ( "setup.py", + "noxfile.py", "build/*", "dist/*", "docs/iris/gallery_code/*/*.py", diff --git a/lib/iris/tests/test_netcdf.py b/lib/iris/tests/test_netcdf.py index 75266ff3fe..2d1b4a53d5 100644 --- a/lib/iris/tests/test_netcdf.py +++ b/lib/iris/tests/test_netcdf.py @@ -543,17 +543,20 @@ def test_noexist_directory(self): pass def test_bad_permissions(self): - # Non-exhaustive check that wrong permissions results in a suitable - # exception being raised. - dir_name = tempfile.mkdtemp() - fnme = os.path.join(dir_name, "tmp.nc") - try: - os.chmod(dir_name, stat.S_IREAD) - with self.assertRaises(IOError): - iris.fileformats.netcdf.Saver(fnme, "NETCDF4") - self.assertFalse(os.path.exists(fnme)) - finally: - os.rmdir(dir_name) + # Skip this test for the root user. This is applicable to + # running within a Docker container and/or CIaaS hosted testing. + if os.getuid(): + # Non-exhaustive check that wrong permissions results in a suitable + # exception being raised. + dir_name = tempfile.mkdtemp() + fname = os.path.join(dir_name, "tmp.nc") + try: + os.chmod(dir_name, stat.S_IREAD) + with self.assertRaises(PermissionError): + iris.fileformats.netcdf.Saver(fname, "NETCDF4") + self.assertFalse(os.path.exists(fname)) + finally: + shutil.rmtree(dir_name) @tests.skip_data diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000000..cd97e8ef8b --- /dev/null +++ b/noxfile.py @@ -0,0 +1,312 @@ +""" +Perform test automation with nox. + +For further details, see https://nox.thea.codes/en/stable/# + +""" + +import hashlib +import os +from pathlib import Path + +import nox + + +#: Default to reusing any pre-existing nox environments. +nox.options.reuse_existing_virtualenvs = True + +#: Name of the package to test. +PACKAGE = str("lib" / Path("iris")) + +#: Cirrus-CI environment variable hook. +PY_VER = os.environ.get("PY_VER", "3.7") + +#: Default cartopy cache directory. +CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy") + + +def venv_cached(session): + """ + Determine whether the nox session environment has been cached. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Returns + ------- + bool + Whether the session has been cached. + + """ + result = False + yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + tmp_dir = Path(session.create_tmp()) + cache = tmp_dir / yml.name + if cache.is_file(): + with open(yml, "rb") as fi: + expected = hashlib.sha256(fi.read()).hexdigest() + with open(cache, "r") as fi: + actual = fi.read() + result = actual == expected + return result + + +def cache_venv(session): + """ + Cache the nox session environment. + + This consists of saving a hexdigest (sha256) of the associated + conda requirements YAML file. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + """ + yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + with open(yml, "rb") as fi: + hexdigest = hashlib.sha256(fi.read()).hexdigest() + tmp_dir = Path(session.create_tmp()) + cache = tmp_dir / yml.name + with open(cache, "w") as fo: + fo.write(hexdigest) + + +def cache_cartopy(session): + """ + Determine whether to cache the cartopy natural earth shapefiles. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + """ + if not CARTOPY_CACHE_DIR.is_dir(): + session.run( + "python", + "-c", + "import cartopy; cartopy.io.shapereader.natural_earth()", + ) + + +@nox.session +def flake8(session): + """ + Perform flake8 linting of iris. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + """ + # Pip install the session requirements. + session.install("flake8") + # Execute the flake8 linter on the package. + session.run("flake8", PACKAGE) + # Execute the flake8 linter on this file. + session.run("flake8", __file__) + + +@nox.session +def black(session): + """ + Perform black format checking of iris. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + """ + # Pip install the session requirements. + session.install("black==20.8b1") + # Execute the black format checker on the package. + session.run("black", "--check", PACKAGE) + # Execute the black format checker on this file. + session.run("black", "--check", __file__) + + +@nox.session(python=[PY_VER], venv_backend="conda") +def tests(session): + """ + Perform iris system, integration and unit tests. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Notes + ----- + See + - https://github.com/theacodes/nox/issues/346 + - https://github.com/theacodes/nox/issues/260 + + """ + if not venv_cached(session): + # Determine the conda requirements yaml file. + fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + # Back-door approach to force nox to use "conda env update". + command = ( + "conda", + "env", + "update", + f"--prefix={session.virtualenv.location}", + f"--file={fname}", + "--prune", + ) + session._run(*command, silent=True, external="error") + cache_venv(session) + + cache_cartopy(session) + session.run("python", "setup.py", "develop") + session.run( + "python", + "-m", + "iris.tests.runner", + "--default-tests", + "--system-tests", + ) + + +@nox.session(python=[PY_VER], venv_backend="conda") +def gallery(session): + """ + Perform iris gallery doc-tests. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Notes + ----- + See + - https://github.com/theacodes/nox/issues/346 + - https://github.com/theacodes/nox/issues/260 + + """ + if not venv_cached(session): + # Determine the conda requirements yaml file. + fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + # Back-door approach to force nox to use "conda env update". + command = ( + "conda", + "env", + "update", + f"--prefix={session.virtualenv.location}", + f"--file={fname}", + "--prune", + ) + session._run(*command, silent=True, external="error") + cache_venv(session) + + cache_cartopy(session) + session.run("python", "setup.py", "develop") + session.run( + "python", + "-m", + "iris.tests.runner", + "--gallery-tests", + ) + + +@nox.session(python=[PY_VER], venv_backend="conda") +def doctest(session): + """ + Perform iris doc-tests. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Notes + ----- + See + - https://github.com/theacodes/nox/issues/346 + - https://github.com/theacodes/nox/issues/260 + + """ + if not venv_cached(session): + # Determine the conda requirements yaml file. + fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + # Back-door approach to force nox to use "conda env update". + command = ( + "conda", + "env", + "update", + f"--prefix={session.virtualenv.location}", + f"--file={fname}", + "--prune", + ) + session._run(*command, silent=True, external="error") + cache_venv(session) + + cache_cartopy(session) + session.run("python", "setup.py", "develop") + session.cd("docs/iris") + session.run( + "make", + "clean", + "html", + external=True, + ) + session.run( + "make", + "doctest", + external=True, + ) + + +@nox.session(python=[PY_VER], venv_backend="conda") +def linkcheck(session): + """ + Perform iris doc link check. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Notes + ----- + See + - https://github.com/theacodes/nox/issues/346 + - https://github.com/theacodes/nox/issues/260 + + """ + if not venv_cached(session): + # Determine the conda requirements yaml file. + fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + # Back-door approach to force nox to use "conda env update". + command = ( + "conda", + "env", + "update", + f"--prefix={session.virtualenv.location}", + f"--file={fname}", + "--prune", + ) + session._run(*command, silent=True, external="error") + cache_venv(session) + + cache_cartopy(session) + session.run("python", "setup.py", "develop") + session.cd("docs/iris") + session.run( + "make", + "clean", + "html", + external=True, + ) + session.run( + "make", + "linkcheck", + external=True, + ) diff --git a/requirements/ci/py36.yml b/requirements/ci/py36.yml index 3b3328cf1f..2b40fbad4e 100644 --- a/requirements/ci/py36.yml +++ b/requirements/ci/py36.yml @@ -13,7 +13,7 @@ dependencies: # Core dependencies. - cartopy>=0.18 - cf-units>=2 - - cftime<1.3.0 + - cftime<1.3.0 - dask>=2 - matplotlib - netcdf4 @@ -35,6 +35,7 @@ dependencies: - asv - black=20.8b1 - filelock + - flake8 - imagehash>=4.0 - nose - pillow<7 diff --git a/requirements/ci/py37.yml b/requirements/ci/py37.yml index 8817f575b7..0f01f0ef75 100644 --- a/requirements/ci/py37.yml +++ b/requirements/ci/py37.yml @@ -35,6 +35,7 @@ dependencies: - asv - black=20.8b1 - filelock + - flake8 - imagehash>=4.0 - nose - pillow<7 diff --git a/requirements/core.txt b/requirements/core.txt index 0b59c573ec..9e0c4fb1bb 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -2,7 +2,7 @@ cartopy>=0.18 cf-units>=2 -cftime +cftime<1.3.0 dask[array]>=2 matplotlib netcdf4 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a87902cbfd..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,44 +0,0 @@ -[flake8] -ignore = E402,\ # Due to conditional imports - E226 # Due to whitespace around operators (e.g. 2*x + 3) -exclude = */iris/std_names.py,\ - */iris/fileformats/cf.py,\ - */iris/fileformats/dot.py,\ - */iris/fileformats/pp_load_rules.py,\ - */iris/fileformats/rules.py,\ - */iris/fileformats/um_cf_map.py,\ - */iris/fileformats/_pyke_rules/compiled_krb/*,\ - */iris/io/__init__.py,\ - */iris/io/format_picker.py,\ - */iris/tests/__init__.py,\ - */iris/tests/pp.py,\ - */iris/tests/system_test.py,\ - */iris/tests/test_analysis.py,\ - */iris/tests/test_analysis_calculus.py,\ - */iris/tests/test_basic_maths.py,\ - */iris/tests/test_cartography.py,\ - */iris/tests/test_cdm.py,\ - */iris/tests/test_cell.py,\ - */iris/tests/test_cf.py,\ - */iris/tests/test_constraints.py,\ - */iris/tests/test_coord_api.py,\ - */iris/tests/test_coord_categorisation.py,\ - */iris/tests/test_coordsystem.py,\ - */iris/tests/test_cube_to_pp.py,\ - */iris/tests/test_file_load.py,\ - */iris/tests/test_file_save.py,\ - */iris/tests/test_hybrid.py,\ - */iris/tests/test_intersect.py,\ - */iris/tests/test_io_init.py,\ - */iris/tests/test_iterate.py,\ - */iris/tests/test_load.py,\ - */iris/tests/test_merge.py,\ - */iris/tests/test_pp_cf.py,\ - */iris/tests/test_pp_module.py,\ - */iris/tests/test_pp_stash.py,\ - */iris/tests/test_pp_to_cube.py,\ - */iris/tests/test_quickplot.py,\ - */iris/tests/test_std_names.py,\ - */iris/tests/test_uri_callback.py,\ - */iris/tests/test_util.py - From 28572d76652eb75b24bf3c5b62eefdaa03b1cd90 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 25 Nov 2020 11:39:35 +0000 Subject: [PATCH 10/23] ignore url for doc link check (#3929) --- docs/iris/src/conf.py | 1 + docs/iris/src/userguide/plotting_a_cube.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 73b60fb982..ab7689479a 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -263,6 +263,7 @@ def autolog(message): linkcheck_ignore = [ "http://cfconventions.org", "http://code.google.com/p/msysgit/downloads/list", + "http://effbot.org", "https://github.com", "http://www.personal.psu.edu/cab38/ColorBrewer/ColorBrewer_updates.html", "http://schacon.github.com/git", diff --git a/docs/iris/src/userguide/plotting_a_cube.rst b/docs/iris/src/userguide/plotting_a_cube.rst index f646aa4b3e..9de20dc6c9 100644 --- a/docs/iris/src/userguide/plotting_a_cube.rst +++ b/docs/iris/src/userguide/plotting_a_cube.rst @@ -209,7 +209,7 @@ the temperature at some latitude cross-sections. ``_. In order to run this example, you will need to copy the code into a file - and run it using ``python2.7 my_file.py``. + and run it using ``python my_file.py``. Plotting 2-dimensional cubes From 8d5dee0ed1854bcccbc57c9fabc5301df1b9a017 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Wed, 25 Nov 2020 12:05:52 +0000 Subject: [PATCH 11/23] whatsnew for coord default units (#3924) --- docs/iris/src/whatsnew/3.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 59f7ec8735..0a9dcd89b0 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -205,6 +205,9 @@ This document explains the changes made to Iris for this release :func:`iris.experimental.concatenate.concatenate` function raised an exception. (:pull:`3523`) +* `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` + and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) + * `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` property. Previously these cases defaulted to ``units='1'``. From c9d0dadc3d31f353584d7827af9762a05b60e0d3 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Wed, 25 Nov 2020 16:00:25 +0000 Subject: [PATCH 12/23] Cube._summary_coord_extra: efficiency and bugfix (#3922) --- docs/iris/src/whatsnew/3.0.rst | 6 +++++- lib/iris/cube.py | 17 +++++++---------- lib/iris/tests/unit/cube/test_Cube.py | 10 ++++++++++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 0a9dcd89b0..4c0bd285fe 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -168,7 +168,11 @@ This document explains the changes made to Iris for this release Previously, the first tick label would occasionally be duplicated. This also removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) -* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) +* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check + ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +* `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's + coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) .. _whatsnew 3.0 changes: diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 3d0854355c..daffe11835 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2182,23 +2182,20 @@ def _summary_coord_extra(self, coord, indent): extra = "" similar_coords = self.coords(coord.name()) if len(similar_coords) > 1: - # Find all the attribute keys - keys = set() - for similar_coord in similar_coords: - keys.update(similar_coord.attributes.keys()) - # Look for any attributes that vary + similar_coords.remove(coord) + # Look for any attributes that vary. vary = set() - attributes = {} - for key in keys: + for key, value in coord.attributes.items(): for similar_coord in similar_coords: if key not in similar_coord.attributes: vary.add(key) break - value = similar_coord.attributes[key] - if attributes.setdefault(key, value) != value: + if not np.array_equal( + similar_coord.attributes[key], value + ): vary.add(key) break - keys = sorted(vary & set(coord.attributes.keys())) + keys = sorted(vary) bits = [ "{}={!r}".format(key, coord.attributes[key]) for key in keys ] diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 01dfe365b4..72bb761cb4 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -484,6 +484,16 @@ def test_ancillary_variable(self): ) self.assertEqual(cube.summary(), expected_summary) + def test_similar_coords(self): + coord1 = AuxCoord( + 42, long_name="foo", attributes=dict(bar=np.array([2, 5])) + ) + coord2 = coord1.copy() + coord2.attributes = dict(bar="baz") + for coord in [coord1, coord2]: + self.cube.add_aux_coord(coord) + self.assertIn("baz", self.cube.summary()) + class Test_is_compatible(tests.IrisTest): def setUp(self): From 723a2f4094217c0193269bae17755cbc8e1e4611 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 22 Dec 2020 11:52:10 +0000 Subject: [PATCH 13/23] Add Documentation Title Case Capitalization (#3940) * Use Title Case Capitalisation for Documentation * add whatsnew enter --- .../general/plot_SOI_filtering.py | 2 +- .../general/plot_anomaly_log_colouring.py | 2 +- .../gallery_code/general/plot_coriolis.py | 2 +- .../general/plot_cross_section.py | 2 +- .../general/plot_custom_aggregation.py | 2 +- .../general/plot_custom_file_loading.py | 2 +- .../gallery_code/general/plot_global_map.py | 2 +- .../general/plot_lineplot_with_legend.py | 2 +- .../gallery_code/general/plot_polar_stereo.py | 2 +- .../general/plot_polynomial_fit.py | 2 +- .../plot_projections_and_annotations.py | 2 +- .../general/plot_rotated_pole_mapping.py | 2 +- .../gallery_code/meteorology/plot_COP_1d.py | 2 +- .../gallery_code/meteorology/plot_COP_maps.py | 2 +- .../iris/gallery_code/meteorology/plot_TEC.py | 2 +- .../meteorology/plot_hovmoller.py | 2 +- .../meteorology/plot_lagged_ensemble.py | 2 +- .../meteorology/plot_wind_speed.py | 4 +- .../oceanography/plot_atlantic_profiles.py | 2 +- .../oceanography/plot_load_nemo.py | 2 +- docs/iris/src/conf.py | 6 +-- docs/iris/src/copyright.rst | 6 +-- .../developers_guide/contributing_changes.rst | 2 +- .../contributing_code_formatting.rst | 2 +- .../contributing_codebase_index.rst | 2 +- .../contributing_deprecations.rst | 12 +++--- .../contributing_documentation.rst | 4 +- .../contributing_getting_involved.rst | 2 +- .../contributing_graphics_tests.rst | 8 ++-- .../contributing_pull_request_checklist.rst | 4 +- .../contributing_running_tests.rst | 4 +- .../developers_guide/contributing_testing.rst | 8 ++-- .../documenting/docstrings.rst | 10 ++--- .../documenting/rest_guide.rst | 4 +- .../documenting/whats_new_contributions.rst | 6 +-- .../gitwash/configure_git.rst | 6 +-- .../gitwash/development_workflow.rst | 28 ++++++------- .../src/developers_guide/gitwash/forking.rst | 6 +-- .../src/developers_guide/gitwash/index.rst | 2 +- .../developers_guide/gitwash/set_up_fork.rst | 8 ++-- docs/iris/src/developers_guide/release.rst | 20 +++++----- docs/iris/src/further_topics/index.rst | 2 +- .../iris/src/further_topics/lenient_maths.rst | 12 +++--- .../src/further_topics/lenient_metadata.rst | 16 ++++---- docs/iris/src/further_topics/metadata.rst | 40 +++++++++---------- docs/iris/src/index.rst | 4 +- docs/iris/src/installing.rst | 10 ++--- .../iris/src/techpapers/change_management.rst | 14 +++---- docs/iris/src/techpapers/index.rst | 2 +- .../src/techpapers/missing_data_handling.rst | 4 +- docs/iris/src/techpapers/um_files_loading.rst | 14 +++---- docs/iris/src/userguide/citation.rst | 6 +-- docs/iris/src/userguide/code_maintenance.rst | 6 +-- docs/iris/src/userguide/cube_maths.rst | 10 ++--- docs/iris/src/userguide/cube_statistics.rst | 10 ++--- .../interpolation_and_regridding.rst | 12 +++--- docs/iris/src/userguide/iris_cubes.rst | 8 ++-- .../iris/src/userguide/loading_iris_cubes.rst | 12 +++--- docs/iris/src/userguide/merge_and_concat.rst | 8 ++-- docs/iris/src/userguide/navigating_a_cube.rst | 12 +++--- docs/iris/src/userguide/plotting_a_cube.rst | 34 ++++++++-------- .../iris/src/userguide/real_and_lazy_data.rst | 10 ++--- docs/iris/src/userguide/saving_iris_cubes.rst | 18 ++++----- docs/iris/src/userguide/subsetting_a_cube.rst | 8 ++-- docs/iris/src/whatsnew/1.0.rst | 20 +++++----- docs/iris/src/whatsnew/1.1.rst | 6 +-- docs/iris/src/whatsnew/1.10.rst | 6 +-- docs/iris/src/whatsnew/1.11.rst | 2 +- docs/iris/src/whatsnew/1.13.rst | 4 +- docs/iris/src/whatsnew/1.2.rst | 4 +- docs/iris/src/whatsnew/1.3.rst | 10 ++--- docs/iris/src/whatsnew/1.4.rst | 28 ++++++------- docs/iris/src/whatsnew/1.5.rst | 2 +- docs/iris/src/whatsnew/1.6.rst | 20 +++++----- docs/iris/src/whatsnew/1.7.rst | 6 +-- docs/iris/src/whatsnew/1.8.rst | 4 +- docs/iris/src/whatsnew/1.9.rst | 6 +-- docs/iris/src/whatsnew/2.0.rst | 4 +- docs/iris/src/whatsnew/2.1.rst | 6 +-- docs/iris/src/whatsnew/2.2.rst | 2 +- docs/iris/src/whatsnew/2.3.rst | 2 +- docs/iris/src/whatsnew/2.4.rst | 2 +- docs/iris/src/whatsnew/3.0.rst | 6 +++ docs/iris/src/whatsnew/index.rst | 2 +- 84 files changed, 306 insertions(+), 300 deletions(-) diff --git a/docs/iris/gallery_code/general/plot_SOI_filtering.py b/docs/iris/gallery_code/general/plot_SOI_filtering.py index 116e819af7..d7948ac965 100644 --- a/docs/iris/gallery_code/general/plot_SOI_filtering.py +++ b/docs/iris/gallery_code/general/plot_SOI_filtering.py @@ -1,5 +1,5 @@ """ -Applying a filter to a time-series +Applying a Filter to a Time-Series ================================== This example demonstrates low pass filtering a time-series by applying a 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 b0cee818de..778f92db1b 100644 --- a/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py +++ b/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py @@ -1,5 +1,5 @@ """ -Colouring anomaly data with logarithmic scaling +Colouring Anomaly Data With Logarithmic Scaling =============================================== In this example, we need to plot anomaly data where the values have a diff --git a/docs/iris/gallery_code/general/plot_coriolis.py b/docs/iris/gallery_code/general/plot_coriolis.py index cc67d1267c..77066d362a 100644 --- a/docs/iris/gallery_code/general/plot_coriolis.py +++ b/docs/iris/gallery_code/general/plot_coriolis.py @@ -1,5 +1,5 @@ """ -Deriving the Coriolis frequency over the globe +Deriving the Coriolis Frequency Over the Globe ============================================== This code computes the Coriolis frequency and stores it in a cube with diff --git a/docs/iris/gallery_code/general/plot_cross_section.py b/docs/iris/gallery_code/general/plot_cross_section.py index a4bc918fc7..12f4bdb0dc 100644 --- a/docs/iris/gallery_code/general/plot_cross_section.py +++ b/docs/iris/gallery_code/general/plot_cross_section.py @@ -1,5 +1,5 @@ """ -Cross section plots +Cross Section Plots =================== This example demonstrates contour plots of a cross-sectioned multi-dimensional diff --git a/docs/iris/gallery_code/general/plot_custom_aggregation.py b/docs/iris/gallery_code/general/plot_custom_aggregation.py index 9c847be779..5fba3669b6 100644 --- a/docs/iris/gallery_code/general/plot_custom_aggregation.py +++ b/docs/iris/gallery_code/general/plot_custom_aggregation.py @@ -1,5 +1,5 @@ """ -Calculating a custom statistic +Calculating a Custom Statistic ============================== This example shows how to define and use a custom 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 b96e152bf8..6890650704 100644 --- a/docs/iris/gallery_code/general/plot_custom_file_loading.py +++ b/docs/iris/gallery_code/general/plot_custom_file_loading.py @@ -1,5 +1,5 @@ """ -Loading a cube from a custom file format +Loading a Cube From a Custom File Format ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This example shows how a custom text file can be loaded using the standard Iris diff --git a/docs/iris/gallery_code/general/plot_global_map.py b/docs/iris/gallery_code/general/plot_global_map.py index 41fd226921..8d2bdee174 100644 --- a/docs/iris/gallery_code/general/plot_global_map.py +++ b/docs/iris/gallery_code/general/plot_global_map.py @@ -1,5 +1,5 @@ """ -Quickplot of a 2d cube on a map +Quickplot of a 2D Cube on a Map =============================== This example demonstrates a contour plot of global air temperature. The plot 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 5641b9c4d0..78401817ba 100644 --- a/docs/iris/gallery_code/general/plot_lineplot_with_legend.py +++ b/docs/iris/gallery_code/general/plot_lineplot_with_legend.py @@ -1,5 +1,5 @@ """ -Multi-line temperature profile plot +Multi-Line Temperature Profile Plot ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ """ diff --git a/docs/iris/gallery_code/general/plot_polar_stereo.py b/docs/iris/gallery_code/general/plot_polar_stereo.py index bd4a11923d..71c0f3b00e 100644 --- a/docs/iris/gallery_code/general/plot_polar_stereo.py +++ b/docs/iris/gallery_code/general/plot_polar_stereo.py @@ -1,5 +1,5 @@ """ -Example of a polar stereographic plot +Example of a Polar Stereographic Plot ===================================== Demonstrates plotting data that are defined on a polar stereographic diff --git a/docs/iris/gallery_code/general/plot_polynomial_fit.py b/docs/iris/gallery_code/general/plot_polynomial_fit.py index 237f4044b6..5da5d50571 100644 --- a/docs/iris/gallery_code/general/plot_polynomial_fit.py +++ b/docs/iris/gallery_code/general/plot_polynomial_fit.py @@ -1,5 +1,5 @@ """ -Fitting a polynomial +Fitting a Polynomial ==================== This example demonstrates computing a polynomial fit to 1D data from an 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 e59bb236d7..f93ac3714f 100644 --- a/docs/iris/gallery_code/general/plot_projections_and_annotations.py +++ b/docs/iris/gallery_code/general/plot_projections_and_annotations.py @@ -1,5 +1,5 @@ """ -Plotting in different projections +Plotting in Different Projections ================================= This example shows how to overlay data and graphics in different projections, 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 063fe93674..8a0c80c707 100644 --- a/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py +++ b/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py @@ -1,5 +1,5 @@ """ -Rotated pole mapping +Rotated Pole Mapping ===================== This example uses several visualisation methods to achieve an array of diff --git a/docs/iris/gallery_code/meteorology/plot_COP_1d.py b/docs/iris/gallery_code/meteorology/plot_COP_1d.py index 2f93627b77..bebbad4224 100644 --- a/docs/iris/gallery_code/meteorology/plot_COP_1d.py +++ b/docs/iris/gallery_code/meteorology/plot_COP_1d.py @@ -1,5 +1,5 @@ """ -Global average annual temperature plot +Global Average Annual Temperature Plot ====================================== Produces a time-series plot of North American temperature forecasts for 2 diff --git a/docs/iris/gallery_code/meteorology/plot_COP_maps.py b/docs/iris/gallery_code/meteorology/plot_COP_maps.py index a8e6055a77..5555a0b85c 100644 --- a/docs/iris/gallery_code/meteorology/plot_COP_maps.py +++ b/docs/iris/gallery_code/meteorology/plot_COP_maps.py @@ -1,5 +1,5 @@ """ -Global average annual temperature maps +Global Average Annual Temperature Maps ====================================== Produces maps of global temperature forecasts from the A1B and E1 scenarios. diff --git a/docs/iris/gallery_code/meteorology/plot_TEC.py b/docs/iris/gallery_code/meteorology/plot_TEC.py index df2e29ef19..71a743a161 100644 --- a/docs/iris/gallery_code/meteorology/plot_TEC.py +++ b/docs/iris/gallery_code/meteorology/plot_TEC.py @@ -1,5 +1,5 @@ """ -Ionosphere space weather +Ionosphere Space Weather ======================== This space weather example plots a filled contour of rotated pole point diff --git a/docs/iris/gallery_code/meteorology/plot_hovmoller.py b/docs/iris/gallery_code/meteorology/plot_hovmoller.py index 9f18b8021e..e9f8207a94 100644 --- a/docs/iris/gallery_code/meteorology/plot_hovmoller.py +++ b/docs/iris/gallery_code/meteorology/plot_hovmoller.py @@ -1,5 +1,5 @@ """ -Hovmoller diagram of monthly surface temperature +Hovmoller Diagram of Monthly Surface Temperature ================================================ This example demonstrates the creation of a Hovmoller diagram with fine control diff --git a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py index cb82a663d4..6a5d1bb157 100644 --- a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py +++ b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py @@ -1,5 +1,5 @@ """ -Seasonal ensemble model plots +Seasonal Ensemble Model Plots ============================= This example demonstrates the loading of a lagged ensemble dataset from the diff --git a/docs/iris/gallery_code/meteorology/plot_wind_speed.py b/docs/iris/gallery_code/meteorology/plot_wind_speed.py index 6844d3874c..79be64ddd7 100644 --- a/docs/iris/gallery_code/meteorology/plot_wind_speed.py +++ b/docs/iris/gallery_code/meteorology/plot_wind_speed.py @@ -1,6 +1,6 @@ """ -Plotting wind direction using quiver -=========================================================== +Plotting Wind Direction Using Quiver +==================================== This example demonstrates using quiver to plot wind speed contours and wind direction arrows from wind vector component input data. The vector components diff --git a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py index a7e82c34f5..f41f900cac 100644 --- a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py +++ b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py @@ -1,5 +1,5 @@ """ -Oceanographic profiles and T-S diagrams +Oceanographic Profiles and T-S Diagrams ======================================= This example demonstrates how to plot vertical profiles of different diff --git a/docs/iris/gallery_code/oceanography/plot_load_nemo.py b/docs/iris/gallery_code/oceanography/plot_load_nemo.py index 5f2b72c956..c7ad5aaee4 100644 --- a/docs/iris/gallery_code/oceanography/plot_load_nemo.py +++ b/docs/iris/gallery_code/oceanography/plot_load_nemo.py @@ -1,5 +1,5 @@ """ -Load a time series of data from the NEMO model +Load a Time Series of Data From the NEMO Model ============================================== This example demonstrates how to load multiple files containing data output by diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index ab7689479a..e3d9266b0b 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -230,7 +230,7 @@ def autolog(message): "menu_links_name": "Support", "menu_links": [ ( - ' Source code', + ' Source Code', "https://github.com/SciTools/iris", ), ( @@ -242,11 +242,11 @@ def autolog(message): "https://groups.google.com/forum/#!forum/scitools-iris-dev", ), ( - ' StackOverflow for "How do I?"', + ' StackOverflow for "How Do I?"', "https://stackoverflow.com/questions/tagged/python-iris", ), ( - ' Legacy documentation', + ' Legacy Documentation', "https://scitools.org.uk/iris/docs/v2.4.0/index.html", ), ], diff --git a/docs/iris/src/copyright.rst b/docs/iris/src/copyright.rst index 08a40e5a1e..16ac07acb3 100644 --- a/docs/iris/src/copyright.rst +++ b/docs/iris/src/copyright.rst @@ -1,8 +1,8 @@ -Iris copyright, licensing and contributors +Iris Copyright, Licensing and Contributors ========================================== -Iris code +Iris Code --------- All Iris source code, unless explicitly stated, is ``Copyright Iris @@ -20,7 +20,7 @@ You should find all source files with the following header: licensing details. -Iris documentation and examples +Iris Documentation and Examples ------------------------------- All documentation, examples and sample data found on this website and in source repository diff --git a/docs/iris/src/developers_guide/contributing_changes.rst b/docs/iris/src/developers_guide/contributing_changes.rst index a752986ec4..48357874a7 100644 --- a/docs/iris/src/developers_guide/contributing_changes.rst +++ b/docs/iris/src/developers_guide/contributing_changes.rst @@ -1,7 +1,7 @@ .. _contributing.changes: -Contributing your changes +Contributing Your Changes ========================= .. toctree:: diff --git a/docs/iris/src/developers_guide/contributing_code_formatting.rst b/docs/iris/src/developers_guide/contributing_code_formatting.rst index b3f23f655a..6bf8dca717 100644 --- a/docs/iris/src/developers_guide/contributing_code_formatting.rst +++ b/docs/iris/src/developers_guide/contributing_code_formatting.rst @@ -2,7 +2,7 @@ .. _code_formatting: -Code formatting +Code Formatting =============== To ensure a consistent code format throughout Iris, we recommend using diff --git a/docs/iris/src/developers_guide/contributing_codebase_index.rst b/docs/iris/src/developers_guide/contributing_codebase_index.rst index 8d7eed8c84..88986c0c7a 100644 --- a/docs/iris/src/developers_guide/contributing_codebase_index.rst +++ b/docs/iris/src/developers_guide/contributing_codebase_index.rst @@ -1,6 +1,6 @@ .. _contributing.documentation.codebase: -Contributing to the code base +Contributing to the Code Base ============================= .. toctree:: diff --git a/docs/iris/src/developers_guide/contributing_deprecations.rst b/docs/iris/src/developers_guide/contributing_deprecations.rst index c7a6888984..1ecafdca9f 100644 --- a/docs/iris/src/developers_guide/contributing_deprecations.rst +++ b/docs/iris/src/developers_guide/contributing_deprecations.rst @@ -10,12 +10,12 @@ one release, before removing/updating it in the next `major release `_. -Adding a deprecation +Adding a Deprecation ==================== .. _removing-a-public-api: -Removing a public API +Removing a Public API --------------------- The simplest form of deprecation occurs when you need to remove a public @@ -49,7 +49,7 @@ Under these circumstances the following points apply: - You should check the documentation for references to the deprecated API and update them as appropriate. -Changing a default +Changing a Default ------------------ When you need to change the default behaviour of a public API the @@ -74,7 +74,7 @@ API: deprecation warning and corresponding Sphinx deprecation directive. -Removing a deprecation +Removing a Deprecation ====================== When the time comes to make a new major release you should locate any @@ -83,7 +83,7 @@ minimum period described previously. Locating deprecated APIs can easily be done by searching for the Sphinx deprecation directives and/or deprecation warnings. -Removing a public API +Removing a Public API --------------------- The deprecated API should be removed and any corresponding documentation @@ -91,7 +91,7 @@ and/or example code should be removed/updated as appropriate. .. _iris_developer_future: -Changing a default +Changing a Default ------------------ - You should update the initial state of the relevant boolean attribute diff --git a/docs/iris/src/developers_guide/contributing_documentation.rst b/docs/iris/src/developers_guide/contributing_documentation.rst index 9674289568..966c44b859 100644 --- a/docs/iris/src/developers_guide/contributing_documentation.rst +++ b/docs/iris/src/developers_guide/contributing_documentation.rst @@ -1,7 +1,7 @@ .. _contributing.documentation: -Contributing to the documentation +Contributing to the Documentation --------------------------------- Documentation is important and we encourage any improvements that can be made. @@ -112,7 +112,7 @@ or ignore the url. .. _contributing.documentation.api: -Generating API documentation +Generating API Documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to auto generate the API documentation based upon the docstrings a diff --git a/docs/iris/src/developers_guide/contributing_getting_involved.rst b/docs/iris/src/developers_guide/contributing_getting_involved.rst index edcbbaf726..0fd873517f 100644 --- a/docs/iris/src/developers_guide/contributing_getting_involved.rst +++ b/docs/iris/src/developers_guide/contributing_getting_involved.rst @@ -2,7 +2,7 @@ .. _development_where_to_start: -Getting involved +Getting Involved ---------------- Iris_ is an Open Source project hosted on Github and as such anyone with a diff --git a/docs/iris/src/developers_guide/contributing_graphics_tests.rst b/docs/iris/src/developers_guide/contributing_graphics_tests.rst index a276f520d6..b6d352cacc 100644 --- a/docs/iris/src/developers_guide/contributing_graphics_tests.rst +++ b/docs/iris/src/developers_guide/contributing_graphics_tests.rst @@ -2,7 +2,7 @@ .. _testing.graphics: -Graphics tests +Graphics Tests ************** Iris may be used to create various forms of graphical output; to ensure @@ -31,7 +31,7 @@ known acceptable output may fail. The failure may also not be visually perceived as it may be a simple pixel shift. -Testing strategy +Testing Strategy ================ The `Iris Travis matrix`_ defines multiple test runs that use @@ -64,7 +64,7 @@ This consists of: against the existing accepted reference images, for each failing test. -Reviewing failing tests +Reviewing Failing Tests ======================= When you find that a graphics test in the Iris testing suite has failed, @@ -122,7 +122,7 @@ you should follow: happens, simply repeat the check-and-accept process until all tests pass. -Add your changes to Iris +Add Your Changes to Iris ======================== To add your changes to Iris, you need to make two pull requests (PR). diff --git a/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst b/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst index b01f370ea2..65d8516a15 100644 --- a/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst +++ b/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst @@ -2,8 +2,8 @@ .. _pr_check: -Pull request check list -======================= +Pull Request Checklist +====================== All pull request will be reviewed by a core developer who will manage the process of merging. It is the responsibility of a developer submitting a diff --git a/docs/iris/src/developers_guide/contributing_running_tests.rst b/docs/iris/src/developers_guide/contributing_running_tests.rst index cadf3710db..3ac0ed905e 100644 --- a/docs/iris/src/developers_guide/contributing_running_tests.rst +++ b/docs/iris/src/developers_guide/contributing_running_tests.rst @@ -2,7 +2,7 @@ .. _developer_running_tests: -Running the tests +Running the Tests ***************** A prerequisite of running the tests is to have the Python environment @@ -90,4 +90,4 @@ due to an experimental dependency not being present. All Python decorators that skip tests will be defined in ``lib/iris/tests/__init__.py`` with a function name with a prefix of - ``skip_``. \ No newline at end of file + ``skip_``. diff --git a/docs/iris/src/developers_guide/contributing_testing.rst b/docs/iris/src/developers_guide/contributing_testing.rst index 375ad57003..486af706d3 100644 --- a/docs/iris/src/developers_guide/contributing_testing.rst +++ b/docs/iris/src/developers_guide/contributing_testing.rst @@ -3,7 +3,7 @@ .. _developer_test_categories: -Test categories +Test Categories *************** There are two main categories of tests within Iris: @@ -20,7 +20,7 @@ feel free to submit a pull-request in any state and ask for assistance. .. _testing.unit_test: -Unit tests +Unit Tests ========== Code changes should be accompanied by enough unit tests to give a @@ -128,7 +128,7 @@ Within that file the tests might look something like: .. _testing.integration: -Integration tests +Integration Tests ================= Some code changes may require tests which exercise several units in @@ -141,4 +141,4 @@ tests. But folders and files must be created as required to help developers locate relevant tests. It is recommended they are named according to the capabilities under test, e.g. ``metadata/test_pp_preservation.py``, and not named according to the -module(s) under test. \ No newline at end of file +module(s) under test. diff --git a/docs/iris/src/developers_guide/documenting/docstrings.rst b/docs/iris/src/developers_guide/documenting/docstrings.rst index 34ec790d03..8a06024ee2 100644 --- a/docs/iris/src/developers_guide/documenting/docstrings.rst +++ b/docs/iris/src/developers_guide/documenting/docstrings.rst @@ -27,7 +27,7 @@ There are two forms of docstrings: **single-line** and **multi-line** docstrings. -Single-line docstrings +Single-Line Docstrings ====================== The single line docstring of an object must state the **purpose** of that @@ -35,7 +35,7 @@ object, known as the **purpose section**. This terse overview must be on one line and ideally no longer than 80 characters. -Multi-line docstrings +Multi-Line Docstrings ===================== Multi-line docstrings must consist of at least a purpose section akin to the @@ -53,7 +53,7 @@ not to document *argument* and *keyword argument* details. Such information should be documented in the following *arguments and keywords section*. -Sample multi-line docstring +Sample Multi-Line Docstring --------------------------- Here is a simple example of a standard docstring: @@ -75,7 +75,7 @@ Additionally, a summary can be extracted automatically, which would result in: documenting.docstrings_sample_routine.sample_routine -Documenting classes +Documenting Classes =================== The class constructor should be documented in the docstring for its @@ -90,7 +90,7 @@ superclass method and does not call the superclass method; use the verb (in addition to its own behaviour). -Attribute and property docstrings +Attribute and Property Docstrings --------------------------------- Here is a simple example of a class containing an attribute docstring and a diff --git a/docs/iris/src/developers_guide/documenting/rest_guide.rst b/docs/iris/src/developers_guide/documenting/rest_guide.rst index bc34d16cd8..4845132b15 100644 --- a/docs/iris/src/developers_guide/documenting/rest_guide.rst +++ b/docs/iris/src/developers_guide/documenting/rest_guide.rst @@ -3,7 +3,7 @@ .. _reST_quick_start: ================ -reST quick start +reST Quick Start ================ `reST`_ is used to create the documentation for Iris_. It is used to author @@ -19,7 +19,7 @@ reST markup syntaxes, for the basics of reST the following links may be useful: Reference documentation for reST can be found at http://docutils.sourceforge.net/rst.html. -Creating links +Creating Links -------------- Basic links can be created with ```Text of the link `_`` which will look like `Text of the link `_ 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 856d9af0a9..7f79657b0d 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst @@ -1,7 +1,7 @@ .. _whats_new_contributions: ================================= -Contributing a "What's New" entry +Contributing a "What's New" Entry ================================= Iris uses a file named ``latest.rst`` to keep a draft of upcoming changes @@ -48,7 +48,7 @@ for the minimum time, minimising conflicts and minimising the need to rebase or merge from trunk. -Writing a contribution +Writing a Contribution ====================== As introduced above, a contribution is the description of a change to Iris @@ -103,7 +103,7 @@ examine past what's :ref:`iris_whatsnew` entries. .. _travis-ci: https://travis-ci.org/github/SciTools/iris -Contribution categories +Contribution Categories ======================= The structure of the what's new release note should be easy to read by diff --git a/docs/iris/src/developers_guide/gitwash/configure_git.rst b/docs/iris/src/developers_guide/gitwash/configure_git.rst index b958a683ee..6fc288daf9 100644 --- a/docs/iris/src/developers_guide/gitwash/configure_git.rst +++ b/docs/iris/src/developers_guide/gitwash/configure_git.rst @@ -3,7 +3,7 @@ .. _configure-git: ============= -Configure git +Configure Git ============= .. _git-config-basic: @@ -51,7 +51,7 @@ command:: To set up on another computer, you can copy your ``~/.gitconfig`` file, or run the commands above. -In detail +In Detail ========= user.name and user.email @@ -124,7 +124,7 @@ Or from the command line:: .. _fancy-log: -Fancy log output +Fancy Log Output ---------------- This is a very nice alias to get a fancy log output; it should go in the diff --git a/docs/iris/src/developers_guide/gitwash/development_workflow.rst b/docs/iris/src/developers_guide/gitwash/development_workflow.rst index b67885e6bd..f6144a05e9 100644 --- a/docs/iris/src/developers_guide/gitwash/development_workflow.rst +++ b/docs/iris/src/developers_guide/gitwash/development_workflow.rst @@ -1,14 +1,14 @@ .. _development-workflow: #################### -Development workflow +Development Workflow #################### You already have your own forked copy of the `iris`_ repository, by following :ref:`forking`. You have :ref:`set-up-fork`. You have configured git by following :ref:`configure-git`. Now you are ready for some real work. -Workflow summary +Workflow Summary ================ In what follows we'll refer to the upstream iris ``master`` branch, as @@ -34,7 +34,7 @@ what you've done, and why you did it. See `linux git workflow`_ for some explanation. -Consider deleting your master branch +Consider Deleting Your Master Branch ==================================== It may sound strange, but deleting your own ``master`` branch can help reduce @@ -43,7 +43,7 @@ details. .. _update-mirror-trunk: -Update the mirror of trunk +Update the Mirror of Trunk ========================== First make sure you have done :ref:`linking-to-upstream`. @@ -59,7 +59,7 @@ you last checked, ``upstream/master`` will change after you do the fetch. .. _make-feature-branch: -Make a new feature branch +Make a New Feature Branch ========================= When you are ready to make some changes to the code, you should start a new @@ -99,7 +99,7 @@ From now on git will know that ``my-new-feature`` is related to the .. _edit-flow: -The editing workflow +The Editing Workflow ==================== Overview @@ -112,7 +112,7 @@ Overview git commit -am 'NF - some message' git push -In more detail +In More Detail -------------- #. Make some changes @@ -144,14 +144,14 @@ In more detail push`` (see `git push`_). -Testing your changes +Testing Your Changes ==================== Once you are happy with your changes, work thorough the :ref:`pr_check` and make sure your branch passes all the relevant tests. -Ask for your changes to be reviewed or merged +Ask for Your Changes to be Reviewed or Merged ============================================= When you are ready to ask for someone to review your code and consider a merge: @@ -175,10 +175,10 @@ When you are ready to ask for someone to review your code and consider a merge: pull request message. This is still a good way of getting some preliminary code review. -Some other things you might want to do +Some Other Things you Might Want to do ====================================== -Delete a branch on github +Delete a Branch on Github ------------------------- :: @@ -193,7 +193,7 @@ Note the colon ``:`` before ``test-branch``. See also: http://github.com/guides/remove-a-remote-branch -Several people sharing a single repository +Several People Sharing a Single Repository ------------------------------------------ If you want to work on some stuff with other people, where you are all @@ -225,7 +225,7 @@ usual:: git commit -am 'ENH - much better code' git push origin master # pushes directly into your repo -Explore your repository +Explore Your Repository ----------------------- To see a graphical representation of the repository branches and @@ -243,7 +243,7 @@ graph of the repository. .. _rebase-on-trunk: -Rebasing on trunk +Rebasing on Trunk ----------------- For more information please see the diff --git a/docs/iris/src/developers_guide/gitwash/forking.rst b/docs/iris/src/developers_guide/gitwash/forking.rst index e10b8f84ca..161847ed79 100644 --- a/docs/iris/src/developers_guide/gitwash/forking.rst +++ b/docs/iris/src/developers_guide/gitwash/forking.rst @@ -3,7 +3,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 @@ -12,7 +12,7 @@ that page for more detail. We're repeating some of it here just to give the specifics for the `Iris`_ project, and to suggest some default names. -Set up and configure a github account +Set up and Configure a Github Account ===================================== If you don't have a github account, go to the github page, and make one. @@ -21,7 +21,7 @@ You then need to configure your account to allow write access, see the `generating sss keys for GitHub`_ 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/index.rst b/docs/iris/src/developers_guide/gitwash/index.rst index d0e70597f1..3cde622583 100644 --- a/docs/iris/src/developers_guide/gitwash/index.rst +++ b/docs/iris/src/developers_guide/gitwash/index.rst @@ -1,6 +1,6 @@ .. _using-git: -Working with Iris source code +Working With Iris Source Code ============================= .. toctree:: diff --git a/docs/iris/src/developers_guide/gitwash/set_up_fork.rst b/docs/iris/src/developers_guide/gitwash/set_up_fork.rst index 9dc6618c64..70d602c97c 100644 --- a/docs/iris/src/developers_guide/gitwash/set_up_fork.rst +++ b/docs/iris/src/developers_guide/gitwash/set_up_fork.rst @@ -3,7 +3,7 @@ .. _set-up-fork: ================ -Set up your fork +Set up Your Fork ================ First you follow the instructions for :ref:`forking`. @@ -17,10 +17,10 @@ Overview cd iris git remote add upstream git://github.com/SciTools/iris.git -In detail +In Detail ========= -Clone your fork +Clone Your Fork --------------- #. Clone your fork to the local computer with ``git clone @@ -42,7 +42,7 @@ Clone your fork .. _linking-to-upstream: -Linking your repository to the upstream repo +Linking Your Repository to the Upstream Repo -------------------------------------------- :: diff --git a/docs/iris/src/developers_guide/release.rst b/docs/iris/src/developers_guide/release.rst index 2ec787a780..6ac3af5c75 100644 --- a/docs/iris/src/developers_guide/release.rst +++ b/docs/iris/src/developers_guide/release.rst @@ -10,7 +10,7 @@ The summary below is of the main areas that constitute the release. The final section details the :ref:`iris_development_releases_steps` to take. -Before release +Before Release -------------- Deprecations @@ -21,7 +21,7 @@ previous releases is now finally changed. More detail, including the correct number of releases, is in :ref:`iris_development_deprecations`. -Release branch +Release Branch -------------- Once the features intended for the release are on master, a release branch @@ -37,7 +37,7 @@ 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 @@ -67,7 +67,7 @@ This content should be reviewed and adapted as required. Steps to achieve this can be found in the :ref:`iris_development_releases_steps`. -The release +The Release ----------- The final steps are to change the version string in the source of @@ -78,7 +78,7 @@ 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 `Iris feedstock`_ for the conda recipe must be @@ -88,7 +88,7 @@ updated to build the latest release of Iris and push this artefact to .. _Iris feedstock: https://github.com/conda-forge/iris-feedstock/tree/master/recipe .. _conda forge: https://anaconda.org/conda-forge/iris -Merge back +Merge Back ---------- After the release is cut, the changes shall be merged back onto the @@ -101,7 +101,7 @@ 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 @@ -118,12 +118,12 @@ release process is to be followed, including the merge back of changes into .. _iris_development_releases_steps: -Maintainer steps +Maintainer Steps ---------------- These steps assume a release for ``v1.9`` is to be created -Release steps +Release Steps ~~~~~~~~~~~~~ #. Create the branch ``1.9.x`` on the main repo, not in a forked repo, for the @@ -156,7 +156,7 @@ Release steps `Iris release page `_ -Post release steps +Post Release Steps ~~~~~~~~~~~~~~~~~~ #. Check the documentation has built on `Read The Docs`_. The build is diff --git a/docs/iris/src/further_topics/index.rst b/docs/iris/src/further_topics/index.rst index 8a4d95b6cd..dc162d6a1e 100644 --- a/docs/iris/src/further_topics/index.rst +++ b/docs/iris/src/further_topics/index.rst @@ -5,7 +5,7 @@ Introduction Some specific areas of Iris may require further explanation or a deep dive into additional detail above and beyond that offered by the -:ref:`User guide `. +:ref:`User Guide `. This section provides a collection of additional material on focused topics that may be of interest to the more advanced or curious user. diff --git a/docs/iris/src/further_topics/lenient_maths.rst b/docs/iris/src/further_topics/lenient_maths.rst index 6f139fd9bf..4aad721780 100644 --- a/docs/iris/src/further_topics/lenient_maths.rst +++ b/docs/iris/src/further_topics/lenient_maths.rst @@ -1,6 +1,6 @@ .. _lenient maths: -Lenient cube maths +Lenient Cube Maths ****************** This section provides an overview of lenient cube maths. In particular, it explains @@ -46,7 +46,7 @@ a practical worked example, which we'll explore together next. .. _lenient example: -Lenient example +Lenient Example =============== .. testsetup:: lenient-example @@ -154,7 +154,7 @@ Now let's compare and contrast this lenient result with the strict alternative. But before we do so, let's first clarify how to control the behaviour of cube maths. -Control the behaviour +Control the Behaviour ===================== As stated earlier, lenient cube maths is the default behaviour from Iris ``3.0.0``. @@ -191,7 +191,7 @@ scope of the ``LENIENT`` `context manager`_, Lenient(maths=True) -Strict example +Strict Example ============== Now that we know how to control the underlying behaviour of cube maths, @@ -229,7 +229,7 @@ This is because strict cube maths, in general, will only return common metadata and common coordinates that are :ref:`strictly equivalent `. -Finer detail +Finer Detail ============ In general, if you want to preserve as much metadata and coordinate information as @@ -278,4 +278,4 @@ resultant :class:`~iris.cube.Cube`, .. _atmosphere hybrid height parametric vertical coordinate: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#atmosphere-hybrid-height-coordinate -.. _context manager: https://docs.python.org/3/library/contextlib.html \ No newline at end of file +.. _context manager: https://docs.python.org/3/library/contextlib.html diff --git a/docs/iris/src/further_topics/lenient_metadata.rst b/docs/iris/src/further_topics/lenient_metadata.rst index ada7049786..b68ed501ba 100644 --- a/docs/iris/src/further_topics/lenient_metadata.rst +++ b/docs/iris/src/further_topics/lenient_metadata.rst @@ -1,6 +1,6 @@ .. _lenient metadata: -Lenient metadata +Lenient Metadata **************** This section discusses lenient metadata; what it is, what it means, and how you @@ -27,7 +27,7 @@ methods that provide this rich metadata behaviour, all of which are explored more fully in :ref:`metadata`. -Strict behaviour +Strict Behaviour ================ .. testsetup:: strict-behaviour @@ -137,7 +137,7 @@ practical behaviour is available. .. _lenient behaviour: -Lenient behaviour +Lenient Behaviour ================= .. testsetup:: lenient-behaviour @@ -210,7 +210,7 @@ lenient behaviour for each of the metadata classes. .. _lenient equality: -Lenient equality +Lenient Equality ---------------- Lenient equality is enabled using the ``lenient`` keyword argument, therefore @@ -273,7 +273,7 @@ forgiving and practical alternative to strict behaviour. .. _lenient difference: -Lenient difference +Lenient Difference ------------------ Similar to :ref:`lenient equality`, the lenient ``difference`` method @@ -330,7 +330,7 @@ highlights the change in how such dissimilar metadata is treated gracefully, .. _lenient combination: -Lenient combination +Lenient Combination ------------------- The behaviour of the lenient ``combine`` metadata class method is outlined @@ -380,7 +380,7 @@ for more inclusive, richer metadata, .. _lenient members: -Lenient members +Lenient Members --------------- :ref:`lenient behaviour` is not applied regardlessly across all metadata members @@ -429,7 +429,7 @@ strict behaviour, regardlessly. .. _special lenient name: -Special lenient name behaviour +Special Lenient Name Behaviour ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``standard_name``, ``long_name`` and ``var_name`` have a closer association diff --git a/docs/iris/src/further_topics/metadata.rst b/docs/iris/src/further_topics/metadata.rst index 3536c87a2b..e6d6ebc57a 100644 --- a/docs/iris/src/further_topics/metadata.rst +++ b/docs/iris/src/further_topics/metadata.rst @@ -42,7 +42,7 @@ Collectively, the aforementioned classes will be known here as the Iris `SciTools/iris`_ -Common metadata +Common Metadata =============== Each of the Iris `CF Conventions`_ classes use **metadata** to define them and @@ -69,7 +69,7 @@ actual `data attribute`_ names of the metadata members on the Iris class. :align: center =================== ======================================= ============================== ========================================== ================================= ======================== ============================== =================== - Metadata members :class:`~iris.coords.AncillaryVariable` :class:`~iris.coords.AuxCoord` :class:`~iris.aux_factory.AuxCoordFactory` :class:`~iris.coords.CellMeasure` :class:`~iris.cube.Cube` :class:`~iris.coords.DimCoord` Metadata members + Metadata Members :class:`~iris.coords.AncillaryVariable` :class:`~iris.coords.AuxCoord` :class:`~iris.aux_factory.AuxCoordFactory` :class:`~iris.coords.CellMeasure` :class:`~iris.cube.Cube` :class:`~iris.coords.DimCoord` Metadata Members =================== ======================================= ============================== ========================================== ================================= ======================== ============================== =================== ``standard_name`` ✔ ✔ ✔ ✔ ✔ ✔ ``standard_name`` ``long_name`` ✔ ✔ ✔ ✔ ✔ ✔ ``long_name`` @@ -90,7 +90,7 @@ actual `data attribute`_ names of the metadata members on the Iris class. terms. -Common metadata API +Common Metadata API =================== .. testsetup:: @@ -149,7 +149,7 @@ a **common** and **consistent** approach to managing your metadata, which we'll now explore a little more fully. -Metadata classes +Metadata Classes ---------------- The ``metadata`` property will return an appropriate `namedtuple`_ metadata class @@ -162,7 +162,7 @@ each container class is shown in :numref:`metadata classes table` below, :align: center ========================================== ======================================================== - Container class Metadata class + Container Class Metadata Class ========================================== ======================================================== :class:`~iris.coords.AncillaryVariable` :class:`~iris.common.metadata.AncillaryVariableMetadata` :class:`~iris.coords.AuxCoord` :class:`~iris.common.metadata.CoordMetadata` @@ -232,7 +232,7 @@ discussion on options how to **set** and **get** metadata on the instance of an Iris `CF Conventions`_ container class (:numref:`metadata classes table`). -Metadata class behaviour +Metadata Class Behaviour ------------------------ As mentioned previously, the metadata classes in :numref:`metadata classes table` @@ -301,7 +301,7 @@ which we explore next. .. _richer metadata: -Richer metadata behaviour +Richer Metadata Behaviour ------------------------- .. testsetup:: richer-metadata @@ -320,7 +320,7 @@ allows you to easily **compare**, **combine**, **convert** and understand the .. _metadata equality: -Metadata equality +Metadata Equality ^^^^^^^^^^^^^^^^^ The metadata classes support both **equality** (``__eq__``) and **inequality** @@ -357,7 +357,7 @@ a means to enable **lenient** equality, as discussed in :ref:`lenient equality`. .. _strict equality: -Strict equality +Strict Equality """"""""""""""" By default, metadata class equality will perform a **strict** comparison between @@ -426,7 +426,7 @@ However, metadata class equality is rich enough to handle this eventuality, .. _compare like: -Comparing like with like +Comparing Like With Like """""""""""""""""""""""" So far in our journey through metadata class equality, we have only considered @@ -446,7 +446,7 @@ metadata class contains **different** members, as shown in .. _exception rule: -Exception to the rule +Exception to the Rule ~~~~~~~~~~~~~~~~~~~~~ In general, **different** metadata classes cannot be compared, however support @@ -502,7 +502,7 @@ methods of metadata classes. .. _metadata difference: -Metadata difference +Metadata Difference ^^^^^^^^^^^^^^^^^^^ Being able to compare metadata is valuable, especially when we have the @@ -605,7 +605,7 @@ Now, let's compare the two above instances and see what ``attributes`` member di .. _diff like: -Diffing like with like +Diffing Like With Like """""""""""""""""""""" As discussed in :ref:`compare like`, it only makes sense to determine the @@ -655,7 +655,7 @@ In general, however, comparing **different** metadata classes will result in a .. _metadata combine: -Metadata combination +Metadata Combination ^^^^^^^^^^^^^^^^^^^^ .. testsetup:: metadata-combine @@ -740,7 +740,7 @@ metadata class. This is explored in a little further detail next. .. _combine like: -Combine like with like +Combine Like With Like """""""""""""""""""""" Akin to the :ref:`equal ` and @@ -788,7 +788,7 @@ However, note that commutativity in this case cannot be honoured, for obvious re .. _metadata conversion: -Metadata conversion +Metadata Conversion ^^^^^^^^^^^^^^^^^^^ .. testsetup:: metadata-convert @@ -853,7 +853,7 @@ class instance, .. _metadata assignment: -Metadata assignment +Metadata Assignment ^^^^^^^^^^^^^^^^^^^ .. testsetup:: metadata-assign @@ -888,7 +888,7 @@ coordinate, DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False) -Assign by iterable +Assign by Iterable """""""""""""""""" It is also possible to assign to the ``metadata`` property of an Iris @@ -903,7 +903,7 @@ number** of associated member values, e.g., DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False) -Assign by namedtuple +Assign by Namedtuple """""""""""""""""""" A `namedtuple`_ may also be used to assign to the ``metadata`` property of an @@ -933,7 +933,7 @@ of the ``longitude`` coordinate, DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False) -Assign by mapping +Assign by Mapping """"""""""""""""" It is also possible to assign to the ``metadata`` property using a `mapping`_, diff --git a/docs/iris/src/index.rst b/docs/iris/src/index.rst index f230e36f75..80aa696ba1 100644 --- a/docs/iris/src/index.rst +++ b/docs/iris/src/index.rst @@ -46,7 +46,7 @@ For **Iris 2.4** and earlier documentation please see the :container: container-lg pb-3 :column: col-lg-4 col-md-4 col-sm-6 col-xs-12 p-2 - Install Iris to use or for development. + Install Iris as a user or developer. +++ .. link-button:: installing_iris :type: ref @@ -91,7 +91,7 @@ For **Iris 2.4** and earlier documentation please see the .. toctree:: :maxdepth: 1 - :caption: Getting started + :caption: Getting Started :hidden: installing diff --git a/docs/iris/src/installing.rst b/docs/iris/src/installing.rst index 762fe60e4d..fbe59858a4 100644 --- a/docs/iris/src/installing.rst +++ b/docs/iris/src/installing.rst @@ -22,7 +22,7 @@ any WSL_ distributions. .. _installing_using_conda: -Installing using conda (users) +Installing Using Conda (Users) ------------------------------ To install Iris using conda, you must first download and install conda, @@ -44,8 +44,8 @@ at https://conda.io/en/latest/index.html. .. _installing_from_source: -Installing from source (devs) ------------------------------ +Installing From Source (Developers) +----------------------------------- The latest Iris source release is available from https://github.com/SciTools/iris. @@ -81,7 +81,7 @@ to find your local Iris code:: python setup.py develop -Running the tests +Running the Tests ----------------- To ensure your setup is configured correctly you can run the test suite using @@ -92,7 +92,7 @@ the command:: For more information see :ref:`developer_running_tests`. -Custom site configuration +Custom Site Configuration ------------------------- The default site configuration values can be overridden by creating the file diff --git a/docs/iris/src/techpapers/change_management.rst b/docs/iris/src/techpapers/change_management.rst index ab45fe7926..f39d64f430 100644 --- a/docs/iris/src/techpapers/change_management.rst +++ b/docs/iris/src/techpapers/change_management.rst @@ -4,7 +4,7 @@ .. _change_management: -Change Management in Iris from the User's perspective +Change Management in Iris From the User's Perspective ***************************************************** As Iris changes, user code will need revising from time to time to keep it @@ -16,7 +16,7 @@ Here, we define ways to make this as easy as possible. .. include:: ../userguide/change_management_goals.txt -Key principles you can rely on +Key Principles you can Rely on ============================== Iris code editions are published as defined version releases, with a given @@ -42,7 +42,7 @@ If your code produces :ref:`deprecation warnings `, then it -User Actions : How you should respond to changes and releases +User Actions : How you Should Respond to Changes and Releases ============================================================= Checklist : @@ -96,7 +96,7 @@ Key concepts covered here: .. _iris_backward_compatibility: -Backwards compatibility +Backwards Compatibility ----------------------- "Backwards-compatible" changes are those that leave any existing valid API @@ -135,7 +135,7 @@ See :ref:`Usage of iris.FUTURE `, below. .. _iris_api: -Terminology : API, features, usages and behaviours +Terminology : API, Features, Usages and Behaviours ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The API is the components of the iris module and its submodules which are @@ -320,7 +320,7 @@ This is to warn users : * eventually to rewrite old code to use the newer or better alternatives -Deprecated features support through the Release cycle +Deprecated Features Support Through the Release Cycle ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The whole point of a deprecation is that the feature continues to work, but @@ -341,7 +341,7 @@ follows: .. _iris_future_usage: -Future options, `iris.FUTURE` +Future Options, `iris.FUTURE` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A special approach is needed where the replacement behaviour is not controlled diff --git a/docs/iris/src/techpapers/index.rst b/docs/iris/src/techpapers/index.rst index 3074569eae..773c8f7059 100644 --- a/docs/iris/src/techpapers/index.rst +++ b/docs/iris/src/techpapers/index.rst @@ -1,7 +1,7 @@ .. _techpapers_index: -Iris technical papers +Iris Technical Papers ===================== Extra information on specific technical issues. diff --git a/docs/iris/src/techpapers/missing_data_handling.rst b/docs/iris/src/techpapers/missing_data_handling.rst index 46279bc566..13b00d3424 100644 --- a/docs/iris/src/techpapers/missing_data_handling.rst +++ b/docs/iris/src/techpapers/missing_data_handling.rst @@ -1,5 +1,5 @@ ============================= -Missing data handling in Iris +Missing Data Handling in Iris ============================= This document provides a brief overview of how Iris handles missing data values @@ -73,7 +73,7 @@ all have the same fill-value. If the components have differing fill-values, a default fill-value will be used instead. -Other operations +Other Operations ---------------- Other operations, such as :class:`~iris.cube.Cube` arithmetic operations, diff --git a/docs/iris/src/techpapers/um_files_loading.rst b/docs/iris/src/techpapers/um_files_loading.rst index d8c796b31f..72d34962ce 100644 --- a/docs/iris/src/techpapers/um_files_loading.rst +++ b/docs/iris/src/techpapers/um_files_loading.rst @@ -14,7 +14,7 @@ =================================== -Iris handling of PP and Fieldsfiles +Iris Handling of PP and Fieldsfiles =================================== This document provides a basic account of how PP and Fieldsfiles data is @@ -40,7 +40,7 @@ For details of Iris terms (cubes, coordinates, attributes), refer to For details of CF conventions, see http://cfconventions.org/. -Overview of loading process +Overview of Loading Process --------------------------- The basics of Iris loading are explained at :ref:`loading_iris_cubes`. @@ -165,7 +165,7 @@ For example: sections are written only if the actual values are unevenly spaced. -Phenomenon identification +Phenomenon Identification ------------------------- **UM Field elements** @@ -218,7 +218,7 @@ For example: LBUSER4 and LBUSER7 elements. -Vertical coordinates +Vertical Coordinates -------------------- **UM Field elements** @@ -319,7 +319,7 @@ See an example printout of a hybrid height cube, .. _um_time_metadata: -Time information +Time Information ---------------- **UM Field elements** @@ -391,7 +391,7 @@ See an example printout of a forecast data cube, 'forecast_reference_time' is a constant. -Statistical measures +Statistical Measures -------------------- **UM Field elements** @@ -438,7 +438,7 @@ For example: (CellMethod(method='mean', coord_names=('time',), intervals=('6 hour',), comments=()),) -Other metadata +Other Metadata -------------- LBRSVD4 diff --git a/docs/iris/src/userguide/citation.rst b/docs/iris/src/userguide/citation.rst index 56eab0a4eb..9938f9e22c 100644 --- a/docs/iris/src/userguide/citation.rst +++ b/docs/iris/src/userguide/citation.rst @@ -8,7 +8,7 @@ If Iris played an important part in your research then please add us to your reference list by using one of the recommendations below. ************ -BibTeX entry +BibTeX Entry ************ For example:: @@ -24,7 +24,7 @@ For example:: ******************* -Downloaded software +Downloaded Software ******************* Suggested format:: @@ -37,7 +37,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 d03808e18f..b2b498bc80 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 eebff53e62..77bbd1c9a3 100644 --- a/docs/iris/src/userguide/cube_maths.rst +++ b/docs/iris/src/userguide/cube_maths.rst @@ -1,7 +1,7 @@ .. _cube maths: ========== -Cube maths +Cube Maths ========== @@ -29,7 +29,7 @@ In order to reduce the amount of metadata which becomes inconsistent, fundamental arithmetic operations such as addition, subtraction, division and multiplication can be applied directly to any cube. -Calculating the difference between two cubes +Calculating the Difference Between Two Cubes -------------------------------------------- Let's load some air temperature which runs from 1860 to 2100:: @@ -77,7 +77,7 @@ but with the data representing their difference: .. _cube-maths_anomaly: -Calculating a cube anomaly +Calculating a Cube Anomaly -------------------------- In section :doc:`cube_statistics` we discussed how the dimensionality of a cube @@ -165,7 +165,7 @@ broadcasting behaviour:: >>> print(result.summary(True)) unknown / (K) (time: 240; latitude: 37; longitude: 49) -Combining multiple phenomena to form a new one +Combining Multiple Phenomena to Form a New One ---------------------------------------------- Combining cubes of potential-temperature and pressure we can calculate @@ -223,7 +223,7 @@ The result could now be plotted using the guidance provided in the .. _cube_maths_combining_units: -Combining units +Combining Units --------------- It should be noted that when combining cubes by multiplication, division or diff --git a/docs/iris/src/userguide/cube_statistics.rst b/docs/iris/src/userguide/cube_statistics.rst index 310551c76f..4eb016078e 100644 --- a/docs/iris/src/userguide/cube_statistics.rst +++ b/docs/iris/src/userguide/cube_statistics.rst @@ -1,12 +1,12 @@ .. _cube-statistics: =============== -Cube statistics +Cube Statistics =============== .. _cube-statistics-collapsing: -Collapsing entire data dimensions +Collapsing Entire Data Dimensions --------------------------------- .. testsetup:: @@ -100,7 +100,7 @@ in the gallery takes a zonal mean of an ``XYT`` cube by using the .. _cube-statistics-collapsing-average: -Area averaging +Area Averaging ^^^^^^^^^^^^^^ Some operators support additional keywords to the ``cube.collapsed`` method. @@ -152,14 +152,14 @@ including an example on taking a :ref:`global area-weighted mean .. _cube-statistics-aggregated-by: -Partially reducing data dimensions +Partially Reducing Data Dimensions ---------------------------------- Instead of completely collapsing a dimension, other methods can be applied to reduce or filter the number of data points of a particular dimension. -Aggregation of grouped data +Aggregation of Grouped Data ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :meth:`Cube.aggregated_by ` operation diff --git a/docs/iris/src/userguide/interpolation_and_regridding.rst b/docs/iris/src/userguide/interpolation_and_regridding.rst index ffed21a7f5..5a5a985ccb 100644 --- a/docs/iris/src/userguide/interpolation_and_regridding.rst +++ b/docs/iris/src/userguide/interpolation_and_regridding.rst @@ -8,7 +8,7 @@ warnings.simplefilter('ignore') ================================= -Cube interpolation and regridding +Cube Interpolation and Regridding ================================= Iris provides powerful cube-aware interpolation and regridding functionality, @@ -123,7 +123,7 @@ will be orthogonal: air_temperature / (K) (latitude: 13; longitude: 14) -Interpolating non-horizontal coordinates +Interpolating Non-Horizontal Coordinates ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Interpolation in Iris is not limited to horizontal-spatial coordinates - any @@ -195,7 +195,7 @@ For example, to mask values that lie beyond the range of the original data: .. _caching_an_interpolator: -Caching an interpolator +Caching an Interpolator ^^^^^^^^^^^^^^^^^^^^^^^ If you need to interpolate a cube on multiple sets of sample points you can @@ -305,7 +305,7 @@ cells have now become rectangular in a plate carrée (equirectangular) projectio The spatial grid of the resulting cube is really global, with a large proportion of the data being masked. -Area-weighted regridding +Area-Weighted Regridding ^^^^^^^^^^^^^^^^^^^^^^^^ It is often the case that a point-based regridding scheme (such as @@ -384,7 +384,7 @@ To visualise the above regrid, let's plot the original data, along with 3 distin .. _caching_a_regridder: -Caching a regridder +Caching a Regridder ^^^^^^^^^^^^^^^^^^^ If you need to regrid multiple cubes with a common source grid onto a common @@ -415,7 +415,7 @@ In each case ``result`` will be the input cube regridded to the grid defined by the target grid cube (in this case ``rotated_psl``) that we used to define the cached regridder. -Regridding lazy data +Regridding Lazy Data ^^^^^^^^^^^^^^^^^^^^ If you are working with large cubes, especially when you are regridding to a diff --git a/docs/iris/src/userguide/iris_cubes.rst b/docs/iris/src/userguide/iris_cubes.rst index 5929c402f2..de206486d3 100644 --- a/docs/iris/src/userguide/iris_cubes.rst +++ b/docs/iris/src/userguide/iris_cubes.rst @@ -1,7 +1,7 @@ .. _iris_data_structures: ==================== -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. @@ -71,11 +71,11 @@ A cube consists of: * a list of coordinate "factories" used for deriving coordinates from the values of other coordinates in the cube -Cubes in practice +Cubes in Practice ----------------- -A simple cube example +A Simple Cube Example ===================== Suppose we have some gridded data which has 24 air temperature readings (in Kelvin) which is located at @@ -137,7 +137,7 @@ For example, it is possible to attach any of the following: a collection of "ensembles" (i.e. multiple model runs). -Printing a cube +Printing a Cube =============== Every Iris cube can be printed to screen as you will see later in the user guide. It is worth familiarising yourself with the diff --git a/docs/iris/src/userguide/loading_iris_cubes.rst b/docs/iris/src/userguide/loading_iris_cubes.rst index 006a919408..659c28420a 100644 --- a/docs/iris/src/userguide/loading_iris_cubes.rst +++ b/docs/iris/src/userguide/loading_iris_cubes.rst @@ -1,7 +1,7 @@ .. _loading_iris_cubes: =================== -Loading Iris cubes +Loading Iris Cubes =================== To load a single file into a **list** of Iris cubes @@ -116,7 +116,7 @@ This was the output discussed at the end of the :doc:`iris_cubes` section. appropriate column for each cube data dimension that they describe. -Loading multiple files +Loading Multiple Files ----------------------- To load more than one file into a list of cubes, a list of filenames can be @@ -142,7 +142,7 @@ star wildcards can be used:: The cubes returned will not necessarily be in the same order as the order of the filenames. -Lazy loading +Lazy Loading ------------ In fact when Iris loads data from most file types, it normally only reads the @@ -155,7 +155,7 @@ For more on the benefits, handling and uses of lazy data, see :doc:`Real and Laz .. _constrained-loading: -Constrained loading +Constrained Loading ----------------------- Given a large dataset, it is possible to restrict or constrain the load to match specific Iris cube metadata. @@ -261,7 +261,7 @@ then specific STASH codes can be filtered:: :class:`iris.Constraint` reference documentation. -Constraining a circular coordinate across its boundary +Constraining a Circular Coordinate Across its Boundary ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Occasionally you may need to constrain your cube with a region that crosses the @@ -403,7 +403,7 @@ Notice how the dates printed are between the range specified in the ``st_swithun and that they span multiple years. -Strict loading +Strict Loading -------------- The :py:func:`iris.load_cube` and :py:func:`iris.load_cubes` functions are diff --git a/docs/iris/src/userguide/merge_and_concat.rst b/docs/iris/src/userguide/merge_and_concat.rst index 0d844ac403..ffa36ccdeb 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 @@ -203,7 +203,7 @@ single cube. An example of fixing an issue like this can be found in the :ref:`merge_concat_common_issues` section. -Merge in Iris load +Merge in Iris Load ================== The CubeList's :meth:`~iris.cube.CubeList.merge` method is used internally @@ -365,7 +365,7 @@ single cube. An example of fixing an issue like this can be found in the .. _merge_concat_common_issues: -Common issues with merge and concatenate +Common Issues With Merge and Concatenate ---------------------------------------- The Iris algorithms that drive :meth:`~iris.cube.CubeList.merge` and @@ -529,7 +529,7 @@ Trying to merge the input cubes with duplicate cubes not allowed raises an error highlighting the presence of the duplicate cube. -**Single value coordinates** +**Single Value Coordinates** Coordinates containing only a single value can cause confusion when combining input cubes. Remember: diff --git a/docs/iris/src/userguide/navigating_a_cube.rst b/docs/iris/src/userguide/navigating_a_cube.rst index a7b7717ae3..df18c032c1 100644 --- a/docs/iris/src/userguide/navigating_a_cube.rst +++ b/docs/iris/src/userguide/navigating_a_cube.rst @@ -1,5 +1,5 @@ ================= -Navigating a cube +Navigating a Cube ================= .. testsetup:: @@ -15,7 +15,7 @@ Navigating a cube After loading any cube, you will want to investigate precisely what it contains. This section is all about accessing and manipulating the metadata contained within a cube. -Cube string representations +Cube String Representations --------------------------- We have already seen a basic string representation of a cube when printing: @@ -52,7 +52,7 @@ variable. In most cases it is reasonable to ignore anything starting with a "``_ dir(cube) help(cube) -Working with cubes +Working With Cubes ------------------ Every cube has a standard name, long name and units which are accessed with @@ -111,7 +111,7 @@ cube with the :attr:`Cube.cell_methods ` attribute: print(cube.cell_methods) -Accessing coordinates on the cube +Accessing Coordinates on the Cube --------------------------------- A cube's coordinates can be retrieved via :meth:`Cube.coords `. @@ -148,7 +148,7 @@ numpy array. If the coordinate has no bounds ``None`` will be returned:: print(type(coord.bounds)) -Adding metadata to a cube +Adding Metadata to a Cube ------------------------- We can add and remove coordinates via :func:`Cube.add_dim_coord`, @@ -177,7 +177,7 @@ We can add and remove coordinates via :func:`Cube.add_dim_coord`_ package in order to generate @@ -13,7 +13,7 @@ been extended within Iris to facilitate easy visualisation of a cube's data. *************************** -Matplotlib's pyplot basics +Matplotlib's Pyplot Basics *************************** A simple line plot can be created using the @@ -35,7 +35,7 @@ There are two modes of rendering within Matplotlib; **interactive** and **non-interactive**. -Interactive plot rendering +Interactive Plot Rendering ========================== The previous example was *non-interactive* as the figure is only rendered *after* the call to :py:func:`plt.show() `. @@ -84,7 +84,7 @@ so ensure that interactive mode is turned off with:: plt.interactive(False) -Saving a plot +Saving a Plot ============= The :py:func:`matplotlib.pyplot.savefig` function is similar to **plt.show()** @@ -113,7 +113,7 @@ Some of the formats which are supported by **plt.savefig**: ====== ====== ====================================================================== ****************** -Iris cube plotting +Iris Cube Plotting ****************** The Iris modules :py:mod:`iris.quickplot` and :py:mod:`iris.plot` extend the @@ -149,7 +149,7 @@ where appropriate. import iris.quickplot as qplt -Plotting 1-dimensional cubes +Plotting 1-Dimensional Cubes ============================ The simplest 1D plot is achieved with the :py:func:`iris.plot.plot` function. @@ -181,7 +181,7 @@ For example, the previous plot can be improved quickly by replacing -Multi-line plot +Multi-Line Plot --------------- A multi-lined (or over-plotted) plot, with a legend, can be achieved easily by @@ -212,10 +212,10 @@ the temperature at some latitude cross-sections. and run it using ``python my_file.py``. -Plotting 2-dimensional cubes +Plotting 2-Dimensional Cubes ============================ -Creating maps +Creating Maps ------------- Whenever a 2D plot is created using an :class:`iris.coord_systems.CoordSystem`, a cartopy :class:`~cartopy.mpl.GeoAxes` instance is created, which can be @@ -230,7 +230,7 @@ things. :meth:`cartopy's coastlines() `. -Cube contour +Cube Contour ------------ A simple contour plot of a cube can be created with either the :func:`iris.plot.contour` or :func:`iris.quickplot.contour` functions: @@ -239,7 +239,7 @@ A simple contour plot of a cube can be created with either the :include-source: -Cube filled contour +Cube Filled Contour ------------------- Similarly a filled contour plot of a cube can be created with the :func:`iris.plot.contourf` or :func:`iris.quickplot.contourf` functions: @@ -248,7 +248,7 @@ Similarly a filled contour plot of a cube can be created with the :include-source: -Cube block plot +Cube Block Plot --------------- In some situations the underlying coordinates are better represented with a continuous bounded coordinate, in which case a "block" plot may be more @@ -268,7 +268,7 @@ or :func:`iris.quickplot.pcolormesh`. .. _brewer-info: *********************** -Brewer colour palettes +Brewer Colour Palettes *********************** Iris includes colour specifications and designs developed by @@ -303,7 +303,7 @@ The following subset of Brewer palettes found at .. plot:: userguide/plotting_examples/brewer.py -Plotting with Brewer +Plotting With Brewer ==================== To plot a cube using a Brewer colour palette, simply select one of the Iris @@ -316,7 +316,7 @@ become available once :mod:`iris.plot` or :mod:`iris.quickplot` are imported. .. _brewer-cite: -Adding a citation +Adding a Citation ================= Citations can be easily added to a plot using the diff --git a/docs/iris/src/userguide/real_and_lazy_data.rst b/docs/iris/src/userguide/real_and_lazy_data.rst index 574ca4e1a0..0bc1846457 100644 --- a/docs/iris/src/userguide/real_and_lazy_data.rst +++ b/docs/iris/src/userguide/real_and_lazy_data.rst @@ -10,7 +10,7 @@ ================== -Real and lazy data +Real and Lazy Data ================== We have seen in the :doc:`iris_cubes` section of the user guide that @@ -21,7 +21,7 @@ In this section of the user guide we will look specifically at the concepts of real and lazy data as they apply to the cube and other data structures in Iris. -What is real and lazy data? +What is Real and Lazy Data? --------------------------- In Iris, we use the term **real data** to describe data arrays that are loaded @@ -97,7 +97,7 @@ In such cases, a required portion can be extracted and realised without calculat .. _when_real_data: -When does my data become real? +When Does My Data Become Real? ------------------------------ Certain operations, such as cube indexing and statistics, can be @@ -134,7 +134,7 @@ You can also realise (and so load into memory) your cube's lazy data if you 'tou To 'touch' the data means directly accessing the data by calling ``cube.data``, as in the previous example. -Core data +Core Data ^^^^^^^^^ Cubes have the concept of "core data". This returns the cube's data in its @@ -225,7 +225,7 @@ coordinates' lazy points and bounds: Printing a lazy :class:`~iris.coords.AuxCoord` will realise its points and bounds arrays! -Dask processing options +Dask Processing Options ----------------------- Iris uses dask to provide lazy data arrays for both Iris cubes and coordinates, diff --git a/docs/iris/src/userguide/saving_iris_cubes.rst b/docs/iris/src/userguide/saving_iris_cubes.rst index 3a30321979..237ceb18b6 100644 --- a/docs/iris/src/userguide/saving_iris_cubes.rst +++ b/docs/iris/src/userguide/saving_iris_cubes.rst @@ -1,7 +1,7 @@ .. _saving_iris_cubes: ================== -Saving Iris cubes +Saving Iris Cubes ================== Iris supports the saving of cubes and cube lists to: @@ -39,8 +39,8 @@ and the keyword argument `saver` is not required. attempting to overwrite an existing file. -Controlling the save process ------------------------------ +Controlling the Save Process +---------------------------- The :py:func:`iris.save` function passes all other keywords through to the saver function defined, or automatically set from the file extension. This enables saver specific functionality to be called. @@ -73,8 +73,8 @@ See for more details on supported arguments for the individual savers. -Customising the save process ------------------------------ +Customising the Save Process +---------------------------- When saving to GRIB or PP, the save process may be intercepted between the translation step and the file writing. This enables customisation of the output messages, based on Cube metadata if required, over and above the translations supplied by Iris. @@ -103,14 +103,14 @@ Similarly a PP field may need to be written out with a specific value for LBEXP. iris.fileformats.pp.save_fields(tweaked_fields(cubes[0]), '/tmp/app.pp') -netCDF -^^^^^^^ +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. Such a custom saver will need be written to meet the needs of the file format and to handle the metadata translation from cube metadata effectively. diff --git a/docs/iris/src/userguide/subsetting_a_cube.rst b/docs/iris/src/userguide/subsetting_a_cube.rst index 5d9a560be9..02cf1645a1 100644 --- a/docs/iris/src/userguide/subsetting_a_cube.rst +++ b/docs/iris/src/userguide/subsetting_a_cube.rst @@ -1,7 +1,7 @@ .. _subsetting_a_cube: ================= -Subsetting a cube +Subsetting a Cube ================= The :doc:`loading_iris_cubes` section of the user guide showed how to load data into multidimensional Iris cubes. @@ -11,7 +11,7 @@ Iris provides several ways of reducing both the amount of data and/or the number In all cases **the subset of a valid cube is itself a valid cube**. -Cube extraction +Cube Extraction ^^^^^^^^^^^^^^^^ A subset of a cube can be "extracted" from a multi-dimensional cube in order to reduce its dimensionality: @@ -101,7 +101,7 @@ same way as loading with constraints: um_version: 7.3 -Cube iteration +Cube Iteration ^^^^^^^^^^^^^^^ It is not possible to directly iterate over an Iris cube. That is, you cannot use code such as ``for x in cube:``. However, you can iterate over cube slices, as this section details. @@ -152,7 +152,7 @@ slicing the 3 dimensional cube (15, 100, 100) by longitude (i starts at 0 and 15 cube using the slices method. -Cube indexing +Cube Indexing ^^^^^^^^^^^^^ In the same way that you would expect a numeric multidimensional array to be **indexed** to take a subset of your original array, you can **index** a Cube for the same purpose. diff --git a/docs/iris/src/whatsnew/1.0.rst b/docs/iris/src/whatsnew/1.0.rst index 11d29320b6..b226dc609b 100644 --- a/docs/iris/src/whatsnew/1.0.rst +++ b/docs/iris/src/whatsnew/1.0.rst @@ -10,7 +10,7 @@ work. Following this release we plan to deliver significant performance improvements and additional features. -The role of 1.x +The Role of 1.x =============== The 1.x series of releases is intended to provide a relatively stable, @@ -58,7 +58,7 @@ A summary of the main features added with version 1.0: contain bounds. -CF-netCDF coordinate systems +CF-NetCDF Coordinate Systems ---------------------------- The coordinate systems in Iris are now defined by the CF-netCDF @@ -73,7 +73,7 @@ The coordinate systems available in Iris 1.0 and their corresponding Iris classes are: ================================================================================================================= ========================================= -CF name Iris class +CF Name Iris Class ================================================================================================================= ========================================= `Latitude-longitude `_ :class:`~iris.coord_systems.GeogCS` `Rotated pole `_ :class:`~iris.coord_systems.RotatedGeogCS` @@ -88,7 +88,7 @@ coordinate system used by the British .. _whats-new-cartopy: -Using Cartopy for mapping in matplotlib +Using Cartopy for Mapping in Matplotlib --------------------------------------- The underlying map drawing package has now been updated to use @@ -135,7 +135,7 @@ For more examples of what can be done with Cartopy, see the Iris gallery and `Cartopy's documentation `_. -Hybrid-pressure +Hybrid-Pressure --------------- With the introduction of the :class:`~iris.aux_factory.HybridPressureFactory` @@ -181,7 +181,7 @@ dealing with large numbers of netCDF files, or in long running processes. -Brewer colour palettes +Brewer Colour Palettes ---------------------- Iris includes a selection of carefully designed colour palettes produced @@ -207,7 +207,7 @@ To include a reference in a journal article or report please refer to in the citation guidance provided by Cynthia Brewer. -Metadata attributes +Metadata Attributes ------------------- Iris now stores "source" and "history" metadata in Cube attributes. @@ -241,7 +241,7 @@ Where previously it would have appeared as:: cube.add_aux_coord(src_coord) -New loading functions +New Loading Functions --------------------- The main functions for loading cubes are now: @@ -264,7 +264,7 @@ now use the :func:`iris.load_cube()` and :func:`iris.load_cubes()` functions instead. -Cube projection +Cube Projection --------------- Iris now has the ability to project a cube into a number of map projections. @@ -302,7 +302,7 @@ preserved. This function currently assumes global data and will if necessary extrapolate beyond the geographical extent of the source cube. -Incompatible changes +Incompatible Changes ==================== * The "source" and "history" metadata are now represented as Cube diff --git a/docs/iris/src/whatsnew/1.1.rst b/docs/iris/src/whatsnew/1.1.rst index f2b0995fa0..86f0bb16fa 100644 --- a/docs/iris/src/whatsnew/1.1.rst +++ b/docs/iris/src/whatsnew/1.1.rst @@ -44,7 +44,7 @@ some notable improvements to netCDF/PP import. with product template 4.9. -Coordinate categorisation +Coordinate Categorisation ------------------------- An :func:`~iris.coord_categorisation.add_day_of_year` categorisation @@ -52,7 +52,7 @@ function has been added to the existing suite in :mod:`iris.coord_categorisation`. -Custom seasons +Custom Seasons ~~~~~~~~~~~~~~ The conventional seasonal categorisation functions have been @@ -87,7 +87,7 @@ This function adds a coordinate containing True/False values determined by membership of a single custom season. -Bugs fixed +Bugs Fixed ========== * PP export no longer attempts to set/overwrite the STASH code based on diff --git a/docs/iris/src/whatsnew/1.10.rst b/docs/iris/src/whatsnew/1.10.rst index 3f51287fa1..92822087dd 100644 --- a/docs/iris/src/whatsnew/1.10.rst +++ b/docs/iris/src/whatsnew/1.10.rst @@ -1,5 +1,5 @@ v1.10 (05 Sep 2016) -********************* +******************* This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -137,7 +137,7 @@ Features attributes is now allowed. -Bugs fixed +Bugs Fixed ========== * Altered Cell Methods to display coordinate's standard_name rather than @@ -215,7 +215,7 @@ Bugs fixed thrown while trying to subset over a non-dimensional scalar coordinate. -Incompatible changes +Incompatible Changes ==================== * The source and target for diff --git a/docs/iris/src/whatsnew/1.11.rst b/docs/iris/src/whatsnew/1.11.rst index e0d46d0f09..356e6ec85b 100644 --- a/docs/iris/src/whatsnew/1.11.rst +++ b/docs/iris/src/whatsnew/1.11.rst @@ -16,7 +16,7 @@ Features * 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 diff --git a/docs/iris/src/whatsnew/1.13.rst b/docs/iris/src/whatsnew/1.13.rst index 2d3b3ffce5..028c298505 100644 --- a/docs/iris/src/whatsnew/1.13.rst +++ b/docs/iris/src/whatsnew/1.13.rst @@ -1,5 +1,5 @@ v1.13 (17 May 2017) -************************* +******************* This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -17,7 +17,7 @@ Features :meth:`iris.cube.share_data` flag. -Bug fixes +Bug Fixes ========= * The bounds are now set correctly on the longitude coordinate if a zonal mean diff --git a/docs/iris/src/whatsnew/1.2.rst b/docs/iris/src/whatsnew/1.2.rst index d4bb863a3b..dce0b6dc04 100644 --- a/docs/iris/src/whatsnew/1.2.rst +++ b/docs/iris/src/whatsnew/1.2.rst @@ -44,7 +44,7 @@ Features :class:`~iris.cube.Cube`. -Bugs fixed +Bugs Fixed ========== * The GRIB hindcast interpretation of negative forecast times can be enabled @@ -54,7 +54,7 @@ Bugs fixed coordinates. -Incompatible changes +Incompatible Changes ==================== * The deprecated :attr:`iris.cube.Cube.unit` and :attr:`iris.coords.Coord.unit` diff --git a/docs/iris/src/whatsnew/1.3.rst b/docs/iris/src/whatsnew/1.3.rst index 9a2ac2eba1..beaa594ab5 100644 --- a/docs/iris/src/whatsnew/1.3.rst +++ b/docs/iris/src/whatsnew/1.3.rst @@ -30,7 +30,7 @@ Features .. _whats-new-abf: -Loading ABF/ABL files +Loading ABF/ABL Files --------------------- Support for the ABF and ABL file formats (as @@ -51,7 +51,7 @@ For example:: .. _whats-new-cf-profile: -Customised CF profiles +Customised CF Profiles ---------------------- Iris now provides hooks in the CF-netCDF export process to allow @@ -74,7 +74,7 @@ For further implementation details see ``iris/fileformats/netcdf.py``. .. _whats-new-concat: -Cube concatenation +Cube Concatenation ------------------ Iris now provides initial support for concatenating Cubes along one or @@ -101,7 +101,7 @@ combine these into a single Cube as follows:: As this is an experimental feature, your feedback is especially welcome. -Bugs fixed +Bugs Fixed ========== * Printing a Cube now supports Unicode attribute values. @@ -123,7 +123,7 @@ Deprecations naming conventions. ====================================== =========================================== - Deprecated property/method New method + 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()` diff --git a/docs/iris/src/whatsnew/1.4.rst b/docs/iris/src/whatsnew/1.4.rst index 29f2079af8..858f985ec6 100644 --- a/docs/iris/src/whatsnew/1.4.rst +++ b/docs/iris/src/whatsnew/1.4.rst @@ -61,7 +61,7 @@ Features .. _OPeNDAP: http://www.opendap.org/about .. _exp-regrid: -Experimental regridding enhancements +Experimental Regridding Enhancements ------------------------------------ Bilinear, area-weighted and area-conservative regridding functions are now @@ -72,7 +72,7 @@ development. In the meantime: -Bilinear rectilinear regridding +Bilinear Rectilinear Regridding ------------------------------- :func:`~iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid` @@ -85,7 +85,7 @@ For example:: regridded_cube = regrid_bilinear_rectilinear_src_and_grid(source_cube, target_grid_cube) -Area-weighted regridding +Area-Weighted Regridding ------------------------ :func:`~iris.experimental.regrid.regrid_area_weighted_rectilinear_src_and_grid` @@ -98,7 +98,7 @@ For example:: regridded_cube = regrid_area_weighted(source_cube, target_grid_cube) -Area-conservative regridding +Area-Conservative Regridding ---------------------------- :func:`~iris.experimental.regrid_conservative.regrid_conservative_via_esmpy` @@ -113,7 +113,7 @@ For example:: .. _iris-pandas: -Iris-Pandas interoperability +Iris-Pandas Interoperability ---------------------------- Conversion to and from Pandas Series_ and DataFrames_ is now available. @@ -125,7 +125,7 @@ See :mod:`iris.pandas` for more details. .. _load-opendap: -Load cubes from the internet via OPeNDAP +Load Cubes From the Internet via OPeNDAP ---------------------------------------- Cubes can now be loaded directly from the internet, via OPeNDAP_. @@ -137,7 +137,7 @@ For example:: .. _geotiff_export: -GeoTiff export +GeoTiff Export -------------- With this experimental feature, two dimensional cubes can now be exported to @@ -155,7 +155,7 @@ For example:: .. _cube-merge-update: -Cube merge update +Cube Merge Update ----------------- Cube merging now favours numerical coordinates over string coordinates @@ -167,7 +167,7 @@ dimensions"*. .. _season-year-name: -Unambiguous season year naming +Unambiguous Season Year Naming ------------------------------ The default names of categorisation coordinates are now less ambiguous. @@ -178,7 +178,7 @@ For example, :func:`~iris.coord_categorisation.add_month_number` and .. _grib-novert: -Cubes with no vertical coord can now be exported to GRIB +Cubes With no Vertical Coord can now be Exported to GRIB -------------------------------------------------------- Iris can now export cubes with no vertical coord to GRIB. @@ -188,7 +188,7 @@ https://github.com/SciTools/iris/issues/519. .. _simple_cfg: -Simplified resource configuration +Simplified Resource Configuration --------------------------------- A new configuration variable called :data:`iris.config.TEST_DATA_DIR` @@ -202,7 +202,7 @@ be set by adding a ``test_data_dir`` entry to the ``Resources`` section of .. _grib_params: -Extended GRIB parameter translation +Extended GRIB Parameter Translation ----------------------------------- - More GRIB2 params are recognised on input. @@ -213,7 +213,7 @@ Extended GRIB parameter translation .. _one-d-linear: -One dimensional linear interpolation fix +One dimensional Linear Interpolation Fix ---------------------------------------- :func:`~iris.analysis.interpolate.linear` can now extrapolate from a single @@ -232,7 +232,7 @@ to cause the loss of coordinate metadata when calculating the curl or the derivative of a cube has been fixed. -Incompatible changes +Incompatible Changes ==================== * As part of simplifying the mechanism for accessing test data, diff --git a/docs/iris/src/whatsnew/1.5.rst b/docs/iris/src/whatsnew/1.5.rst index ea7965fe15..72bdbac480 100644 --- a/docs/iris/src/whatsnew/1.5.rst +++ b/docs/iris/src/whatsnew/1.5.rst @@ -125,7 +125,7 @@ Features systems and mapping 0 to 360 longitudes to the -180 to 180 range. -Bugs fixed +Bugs Fixed ========== * NetCDF error handling on save has been extended to capture file path and diff --git a/docs/iris/src/whatsnew/1.6.rst b/docs/iris/src/whatsnew/1.6.rst index 3855d71479..8b0205b86f 100644 --- a/docs/iris/src/whatsnew/1.6.rst +++ b/docs/iris/src/whatsnew/1.6.rst @@ -146,7 +146,7 @@ Features .. _caching: -A new utility function to assist with caching +A New Utility Function to Assist With Caching --------------------------------------------- To assist with management of caching results to file, the new utility function :func:`iris.util.file_is_newer_than` may be used to easily determine whether @@ -173,7 +173,7 @@ consuming processing, or to reap the benefit of fast-loading a pickled cube. .. _rms: -The RMS aggregator supports weights +The RMS Aggregator Supports Weights ----------------------------------- The :data:`iris.analysis.RMS` aggregator has been extended to allow the use of @@ -189,7 +189,7 @@ For example, an RMS weighted cube collapse is performed as follows: .. _equalise: -Equalise cube attributes +Equalise Cube Attributes ------------------------ To assist with :class:`iris.cube.Cube` merging, the new experimental in-place @@ -202,7 +202,7 @@ have the same attributes. .. _tolerance: -Masking a collapsed result by missing-data tolerance +Masking a Collapsed Result by Missing-Data Tolerance ---------------------------------------------------- The result from collapsing masked cube data may now be completely @@ -216,7 +216,7 @@ less than or equal to the provided tolerance. .. _promote: -Promote a scalar coordinate +Promote a Scalar Coordinate --------------------------- The new utility function :func:`iris.util.new_axis` creates a new cube with @@ -229,7 +229,7 @@ Note that, this function will load the data payload of the cube. .. _peak: -A new PEAK aggregator providing spline interpolation +A New PEAK Aggregator Providing Spline Interpolation ---------------------------------------------------- The new :data:`iris.analysis.PEAK` aggregator calculates the global peak @@ -244,7 +244,7 @@ For example, to calculate the peak time: collapsed_cube = cube.collapsed('time', PEAK) -Bugs fixed +Bugs Fixed ========== * :meth:`iris.cube.Cube.rolling_window` has been extended to support masked @@ -283,7 +283,7 @@ Bugs fixed * Exception no longer raised for any ellipsoid definition in nimrod loading. -Incompatible changes +Incompatible Changes ==================== * The experimental 'concatenate' function is now a method of a @@ -312,7 +312,7 @@ Incompatible changes been removed. ====================================== =========================================== - Removed property/method New method + 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()` @@ -335,7 +335,7 @@ Incompatible changes removed. =============================================================== ======================================================= - Removed function New function + 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` diff --git a/docs/iris/src/whatsnew/1.7.rst b/docs/iris/src/whatsnew/1.7.rst index f6e818fedf..44ebe9ec60 100644 --- a/docs/iris/src/whatsnew/1.7.rst +++ b/docs/iris/src/whatsnew/1.7.rst @@ -1,5 +1,5 @@ v1.7 (04 Jul 2014) -******************** +****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -196,7 +196,7 @@ Features * A speed improvement when loading PP or FF data and constraining on STASH code. -Bugs fixed +Bugs Fixed ========== * Data containing more than one reference cube for constructing hybrid height @@ -282,7 +282,7 @@ v1.7.4 (15 Apr 2015) create LambertConformal coordinate systems with Cartopy >= 0.12. -Incompatible changes +Incompatible Changes ==================== * Saving a cube with a STASH attribute to NetCDF now produces a variable diff --git a/docs/iris/src/whatsnew/1.8.rst b/docs/iris/src/whatsnew/1.8.rst index 579d4d20c5..0e327b4f5a 100644 --- a/docs/iris/src/whatsnew/1.8.rst +++ b/docs/iris/src/whatsnew/1.8.rst @@ -1,5 +1,5 @@ v1.8 (14 Apr 2015) -******************** +****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -151,7 +151,7 @@ Features "iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid". -Bugs fixed +Bugs Fixed ========== * Fix in netCDF loader to correctly determine whether the longitude coordinate diff --git a/docs/iris/src/whatsnew/1.9.rst b/docs/iris/src/whatsnew/1.9.rst index c9d91bf33c..9829d8ff3b 100644 --- a/docs/iris/src/whatsnew/1.9.rst +++ b/docs/iris/src/whatsnew/1.9.rst @@ -1,5 +1,5 @@ v1.9 (10 Dec 2015) -******************** +****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -93,7 +93,7 @@ Features 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` @@ -170,7 +170,7 @@ v1.9.2 (28 Jan 2016) * 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 diff --git a/docs/iris/src/whatsnew/2.0.rst b/docs/iris/src/whatsnew/2.0.rst index fbd012dd1f..400a395e90 100644 --- a/docs/iris/src/whatsnew/2.0.rst +++ b/docs/iris/src/whatsnew/2.0.rst @@ -60,7 +60,7 @@ Features respectively. -The :data:`iris.FUTURE` has arrived! +The :data:`iris.FUTURE` has Arrived! ------------------------------------ Throughout version 1 of Iris a set of toggles in @@ -111,7 +111,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 diff --git a/docs/iris/src/whatsnew/2.1.rst b/docs/iris/src/whatsnew/2.1.rst index ef03f023b2..18c562d3da 100644 --- a/docs/iris/src/whatsnew/2.1.rst +++ b/docs/iris/src/whatsnew/2.1.rst @@ -43,7 +43,7 @@ Features the ``standard_parallel`` keyword argument (:pull:`3041`). -Bugs fixed +Bugs Fixed ========== * All var names being written to NetCDF are now CF compliant. @@ -59,7 +59,7 @@ Bugs fixed ``axes`` keyword (:pull:`3010`). -Incompatible changes +Incompatible Changes ==================== * The deprecated :mod:`iris.experimental.um` was removed. @@ -94,4 +94,4 @@ Internal * 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 + directory of the Iris' the source. diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst index 48280895fe..a1f48f962b 100644 --- a/docs/iris/src/whatsnew/2.2.rst +++ b/docs/iris/src/whatsnew/2.2.rst @@ -66,7 +66,7 @@ Features a NaN-tolerant array comparison. -Bugs fixed +Bugs Fixed ========== * The bug has been fixed that prevented printing time coordinates with bounds diff --git a/docs/iris/src/whatsnew/2.3.rst b/docs/iris/src/whatsnew/2.3.rst index 5997a7f4dc..2509242c05 100644 --- a/docs/iris/src/whatsnew/2.3.rst +++ b/docs/iris/src/whatsnew/2.3.rst @@ -147,7 +147,7 @@ Features `metarelate/metOcean commit 448f2ef, 2019-11-29 `_ -Bugs fixed +Bugs Fixed ========== * Cube equality of boolean data is now handled correctly. diff --git a/docs/iris/src/whatsnew/2.4.rst b/docs/iris/src/whatsnew/2.4.rst index c62e84c129..0e271389b5 100644 --- a/docs/iris/src/whatsnew/2.4.rst +++ b/docs/iris/src/whatsnew/2.4.rst @@ -47,7 +47,7 @@ Features ``STASH`` from the attributes dictionary of a :class:`~iris.cube.Cube`. -Bugs fixed +Bugs Fixed ========== * Fixed a problem which was causing file loads to fetch *all* field data diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 4c0bd285fe..7fe83c8bce 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -360,6 +360,11 @@ This document explains the changes made to Iris for this release * `@jonseddon`_ updated the CF version of the netCDF saver in the :ref:`saving_iris_cubes` section and in the equivalent function docstring. + (:pull:`3925`) + +* `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. + (:pull:`3940`) + 💼 Internal =========== @@ -485,3 +490,4 @@ This document explains the changes made to Iris for this release .. _CF Conventions and Metadata: https://cfconventions.org/ .. _flake8: https://flake8.pycqa.org/en/stable/ .. _nox: https://nox.thea.codes/en/stable/ +.. _Title Case Capitalization: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case diff --git a/docs/iris/src/whatsnew/index.rst b/docs/iris/src/whatsnew/index.rst index 19860791c8..f10e73554a 100644 --- a/docs/iris/src/whatsnew/index.rst +++ b/docs/iris/src/whatsnew/index.rst @@ -1,6 +1,6 @@ .. _iris_whatsnew: -What's new in Iris +What's New in Iris ****************** These "What's new" pages describe the important changes between major From 6e6fe03e327e205bc865cc1914235b645238f838 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 23 Dec 2020 16:52:18 +0000 Subject: [PATCH 14/23] CI requirements drop pip packages (#3939) * requirements pip to conda * use pip install over develop * default PY_VER to python versions --- noxfile.py | 30 +++++++++++++++--------------- requirements/ci/py36.yml | 7 ++----- requirements/ci/py37.yml | 6 ++---- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/noxfile.py b/noxfile.py index cd97e8ef8b..7bfcc73dd7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -19,7 +19,7 @@ PACKAGE = str("lib" / Path("iris")) #: Cirrus-CI environment variable hook. -PY_VER = os.environ.get("PY_VER", "3.7") +PY_VER = os.environ.get("PY_VER", ["3.6", "3.7"]) #: Default cartopy cache directory. CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy") @@ -41,7 +41,7 @@ def venv_cached(session): """ result = False - yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + yml = Path(f"requirements/ci/py{session.python.replace('.', '')}.yml") tmp_dir = Path(session.create_tmp()) cache = tmp_dir / yml.name if cache.is_file(): @@ -66,7 +66,7 @@ def cache_venv(session): A `nox.sessions.Session` object. """ - yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + yml = Path(f"requirements/ci/py{session.python.replace('.', '')}.yml") with open(yml, "rb") as fi: hexdigest = hashlib.sha256(fi.read()).hexdigest() tmp_dir = Path(session.create_tmp()) @@ -131,7 +131,7 @@ def black(session): session.run("black", "--check", __file__) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def tests(session): """ Perform iris system, integration and unit tests. @@ -150,7 +150,7 @@ def tests(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -164,7 +164,7 @@ def tests(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.run( "python", "-m", @@ -174,7 +174,7 @@ def tests(session): ) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def gallery(session): """ Perform iris gallery doc-tests. @@ -193,7 +193,7 @@ def gallery(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -207,7 +207,7 @@ def gallery(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.run( "python", "-m", @@ -216,7 +216,7 @@ def gallery(session): ) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def doctest(session): """ Perform iris doc-tests. @@ -235,7 +235,7 @@ def doctest(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -249,7 +249,7 @@ def doctest(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.cd("docs/iris") session.run( "make", @@ -264,7 +264,7 @@ def doctest(session): ) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def linkcheck(session): """ Perform iris doc link check. @@ -283,7 +283,7 @@ def linkcheck(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -297,7 +297,7 @@ def linkcheck(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.cd("docs/iris") session.run( "make", diff --git a/requirements/ci/py36.yml b/requirements/ci/py36.yml index 2b40fbad4e..4d9d25d7c6 100644 --- a/requirements/ci/py36.yml +++ b/requirements/ci/py36.yml @@ -44,11 +44,8 @@ dependencies: # Documentation dependencies. - sphinx + - sphinxcontrib-napoleon - sphinx-copybutton - sphinx-gallery + - sphinx-panels - sphinx_rtd_theme - - pip - - pip: - - sphinxcontrib-napoleon - - sphinx-panels - diff --git a/requirements/ci/py37.yml b/requirements/ci/py37.yml index 0f01f0ef75..bdb097796a 100644 --- a/requirements/ci/py37.yml +++ b/requirements/ci/py37.yml @@ -44,10 +44,8 @@ dependencies: # Documentation dependencies. - sphinx + - sphinxcontrib-napoleon - sphinx-copybutton - sphinx-gallery + - sphinx-panels - sphinx_rtd_theme - - pip - - pip: - - sphinxcontrib-napoleon - - sphinx-panels From 83d1b72442a22b5ad54a9cab8daa315e54061284 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Wed, 30 Dec 2020 16:05:53 +0000 Subject: [PATCH 15/23] update links (#3942) * update links * added s to http --- docs/iris/src/userguide/citation.rst | 2 +- docs/iris/src/userguide/cube_maths.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/iris/src/userguide/citation.rst b/docs/iris/src/userguide/citation.rst index 9938f9e22c..0a3a85fb89 100644 --- a/docs/iris/src/userguide/citation.rst +++ b/docs/iris/src/userguide/citation.rst @@ -48,7 +48,7 @@ For example:: Iris. Met Office. git@github.com:SciTools/iris.git 06-03-2013 -.. _How to cite and describe software: http://software.ac.uk/so-exactly-what-software-did-you-use +.. _How to cite and describe software: https://software.ac.uk/how-cite-software Reference: [Jackson]_. diff --git a/docs/iris/src/userguide/cube_maths.rst b/docs/iris/src/userguide/cube_maths.rst index 77bbd1c9a3..d2d4d84b68 100644 --- a/docs/iris/src/userguide/cube_maths.rst +++ b/docs/iris/src/userguide/cube_maths.rst @@ -243,7 +243,7 @@ unit (if ``a`` had units ``'m2'`` then ``a ** 0.5`` would result in a cube with units ``'m'``). Iris inherits units from `cf_units `_ -which in turn inherits from `UDUNITS `_. +which in turn inherits from `UDUNITS `_. As well as the units UDUNITS provides, cf units also provides the units ``'no-unit'`` and ``'unknown'``. A unit of ``'no-unit'`` means that the associated data is not suitable for describing with a unit, cf units From 5c8edc1bdf9189870c70888e244f2deae912c5a2 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 8 Jan 2021 11:48:31 +0000 Subject: [PATCH 16/23] Add support for 1-d weights in collapse. (#3943) --- docs/iris/src/whatsnew/3.0.rst | 7 ++ lib/iris/cube.py | 15 +++- lib/iris/tests/unit/cube/test_Cube.py | 102 ++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 7fe83c8bce..745cf717f5 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -113,6 +113,12 @@ This document explains the changes made to Iris for this release and preservation of common metadata and coordinates during cube math operations. Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) +* `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when + collapsing over a single dimension. + Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. + The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works + with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) + 🐛 Bugs Fixed ============= @@ -472,6 +478,7 @@ This document explains the changes made to Iris for this release .. _@tkknight: https://github.com/tkknight .. _@lbdreyer: https://github.com/lbdreyer .. _@SimonPeatman: https://github.com/SimonPeatman +.. _@TomekTrzeciak: https://github.com/TomekTrzeciak .. _@rcomer: https://github.com/rcomer .. _@jvegasbsc: https://github.com/jvegasbsc .. _@zklaus: https://github.com/zklaus diff --git a/lib/iris/cube.py b/lib/iris/cube.py index daffe11835..40f5fbcef3 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -3916,10 +3916,15 @@ def collapsed(self, coords, aggregator, **kwargs): # on the cube lazy array. # NOTE: do not reform the data in this case, as 'lazy_aggregate' # accepts multiple axes (unlike 'aggregate'). - collapse_axis = list(dims_to_collapse) + collapse_axes = list(dims_to_collapse) + if len(collapse_axes) == 1: + # Replace a "list of 1 axes" with just a number : This single-axis form is *required* by functions + # like da.average (and np.average), if a 1d weights array is specified. + collapse_axes = collapse_axes[0] + try: data_result = aggregator.lazy_aggregate( - self.lazy_data(), axis=collapse_axis, **kwargs + self.lazy_data(), axis=collapse_axes, **kwargs ) except TypeError: # TypeError - when unexpected keywords passed through (such as @@ -3943,8 +3948,10 @@ def collapsed(self, coords, aggregator, **kwargs): unrolled_data = np.transpose(self.data, dims).reshape(new_shape) # Perform the same operation on the weights if applicable - if kwargs.get("weights") is not None: - weights = kwargs["weights"].view() + weights = kwargs.get("weights") + if weights is not None and weights.ndim > 1: + # Note: *don't* adjust 1d weights arrays, these have a special meaning for statistics functions. + weights = weights.view() kwargs["weights"] = np.transpose(weights, dims).reshape( new_shape ) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 72bb761cb4..9fe90f5a4e 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -336,6 +336,108 @@ def test_non_lazy_aggregator(self): self.assertArrayEqual(result.data, np.mean(self.data, axis=1)) +class Test_collapsed__multidim_weighted(tests.IrisTest): + def setUp(self): + self.data = np.arange(6.0).reshape((2, 3)) + self.lazydata = as_lazy_data(self.data) + # Test cubes wth (same-valued) real and lazy data + cube_real = Cube(self.data) + for i_dim, name in enumerate(("y", "x")): + npts = cube_real.shape[i_dim] + coord = DimCoord(np.arange(npts), long_name=name) + cube_real.add_dim_coord(coord, i_dim) + self.cube_real = cube_real + self.cube_lazy = cube_real.copy(data=self.lazydata) + # Test weights and expected result for a y-collapse + self.y_weights = np.array([0.3, 0.5]) + self.full_weights_y = np.broadcast_to( + self.y_weights.reshape((2, 1)), cube_real.shape + ) + self.expected_result_y = np.array([1.875, 2.875, 3.875]) + # Test weights and expected result for an x-collapse + self.x_weights = np.array([0.7, 0.4, 0.6]) + self.full_weights_x = np.broadcast_to( + self.x_weights.reshape((1, 3)), cube_real.shape + ) + self.expected_result_x = np.array([0.941176, 3.941176]) + + def test_weighted_fullweights_real_y(self): + # Supplying full-shape weights for collapsing over a single dimension. + cube_collapsed = self.cube_real.collapsed( + "y", MEAN, weights=self.full_weights_y + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_fullweights_lazy_y(self): + # Full-shape weights, lazy data : Check lazy result, same values as real calc. + cube_collapsed = self.cube_lazy.collapsed( + "y", MEAN, weights=self.full_weights_y + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_1dweights_real_y(self): + # 1-D weights, real data : Check same results as full-shape. + cube_collapsed = self.cube_real.collapsed( + "y", MEAN, weights=self.y_weights + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_1dweights_lazy_y(self): + # 1-D weights, lazy data : Check lazy result, same values as real calc. + cube_collapsed = self.cube_lazy.collapsed( + "y", MEAN, weights=self.y_weights + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_fullweights_real_x(self): + # Full weights, real data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_real.collapsed( + "x", MEAN, weights=self.full_weights_x + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + def test_weighted_fullweights_lazy_x(self): + # Full weights, lazy data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_lazy.collapsed( + "x", MEAN, weights=self.full_weights_x + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + def test_weighted_1dweights_real_x(self): + # 1-D weights, real data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_real.collapsed( + "x", MEAN, weights=self.x_weights + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + def test_weighted_1dweights_lazy_x(self): + # 1-D weights, lazy data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_lazy.collapsed( + "x", MEAN, weights=self.x_weights + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + class Test_collapsed__cellmeasure_ancils(tests.IrisTest): def setUp(self): cube = Cube(np.arange(6.0).reshape((2, 3))) From 805a4febf8cc0f189031f6fe687896d2952603b5 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Wed, 20 Jan 2021 18:31:14 +0000 Subject: [PATCH 17/23] Remove warning for convert_units on lazy data (#3951) --- lib/iris/cube.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 40f5fbcef3..c3f77c9288 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -981,9 +981,7 @@ def convert_units(self, unit): celsius and subtract 273.15 from each value in :attr:`~iris.cube.Cube.data`. - .. warning:: - Calling this method will trigger any deferred loading, causing - the cube's data array to be loaded into memory. + This operation preserves lazy data. """ # If the cube has units convert the data. From e689ef41affa664c4d9912660a7b114246d5680b Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 25 Jan 2021 10:28:25 +0000 Subject: [PATCH 18/23] drop stickler references in docs (#3953) * drop stickler references in docs * remove sticker from common links --- docs/iris/src/common_links.inc | 1 - .../src/developers_guide/contributing_ci_tests.rst | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/docs/iris/src/common_links.inc b/docs/iris/src/common_links.inc index 0bc8ca60e6..79adc2a50d 100644 --- a/docs/iris/src/common_links.inc +++ b/docs/iris/src/common_links.inc @@ -14,7 +14,6 @@ .. _readthedocs.yml: https://github.com/SciTools/iris/blob/master/requirements/ci/readthedocs.yml .. _travis-ci: https://travis-ci.org/github/SciTools/iris .. _.travis.yml: https://github.com/SciTools/iris/blob/master/.travis.yml -.. _.stickler.yml: https://github.com/SciTools/iris/blob/master/.stickler.yml .. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8 .. _GitHub Help Documentation: https://docs.github.com/en/github .. _using git: https://docs.github.com/en/github/using-git diff --git a/docs/iris/src/developers_guide/contributing_ci_tests.rst b/docs/iris/src/developers_guide/contributing_ci_tests.rst index c7a041bcb2..492996f8b8 100644 --- a/docs/iris/src/developers_guide/contributing_ci_tests.rst +++ b/docs/iris/src/developers_guide/contributing_ci_tests.rst @@ -11,7 +11,6 @@ Iris **master**. The checks performed are: * :ref:`testing_cla` * :ref:`testing_travis` -* :ref:`testing_stickler` .. _testing_cla: @@ -24,16 +23,6 @@ A bot that checks the user who created the pull request has signed the please see https://scitools.org.uk/organisation.html#governance -.. _testing_stickler: - -Stickler CI -*********** - -Automatically enforces coding standards. The configuration file named -`.stickler.yml`_ is in the Iris_ root directory. For more information see -https://stickler-ci.com/. - - .. _testing_travis: Travis-CI From 62dcb17538dbf24c4bad0c7667b80957db20a540 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 25 Jan 2021 11:20:33 +0000 Subject: [PATCH 19/23] update docs for travis-ci to cirrus-ci (#3954) * update docs for travis-ci to cirrus-ci * add 'travis-ci' reference locally to whatsnew * update whatsnew comment --- docs/iris/src/common_links.inc | 4 ++-- .../developers_guide/contributing_ci_tests.rst | 16 ++++++++-------- .../contributing_documentation.rst | 6 +++--- .../contributing_graphics_tests.rst | 6 +++--- .../contributing_pull_request_checklist.rst | 2 +- .../documenting/whats_new_contributions.rst | 6 +++--- docs/iris/src/whatsnew/3.0.rst | 4 +++- 7 files changed, 23 insertions(+), 21 deletions(-) diff --git a/docs/iris/src/common_links.inc b/docs/iris/src/common_links.inc index 79adc2a50d..3941bfaff2 100644 --- a/docs/iris/src/common_links.inc +++ b/docs/iris/src/common_links.inc @@ -12,8 +12,8 @@ .. _iris-sample-data: https://github.com/SciTools/iris-sample-data .. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash .. _readthedocs.yml: https://github.com/SciTools/iris/blob/master/requirements/ci/readthedocs.yml -.. _travis-ci: https://travis-ci.org/github/SciTools/iris -.. _.travis.yml: https://github.com/SciTools/iris/blob/master/.travis.yml +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris +.. _.cirrus.yml: https://github.com/SciTools/iris/blob/master/.cirrus.yml .. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8 .. _GitHub Help Documentation: https://docs.github.com/en/github .. _using git: https://docs.github.com/en/github/using-git diff --git a/docs/iris/src/developers_guide/contributing_ci_tests.rst b/docs/iris/src/developers_guide/contributing_ci_tests.rst index 492996f8b8..a6bdac4ae0 100644 --- a/docs/iris/src/developers_guide/contributing_ci_tests.rst +++ b/docs/iris/src/developers_guide/contributing_ci_tests.rst @@ -10,7 +10,7 @@ automatically when a pull request is created, updated or merged against Iris **master**. The checks performed are: * :ref:`testing_cla` -* :ref:`testing_travis` +* :ref:`testing_cirrus` .. _testing_cla: @@ -23,23 +23,23 @@ A bot that checks the user who created the pull request has signed the please see https://scitools.org.uk/organisation.html#governance -.. _testing_travis: +.. _testing_cirrus: -Travis-CI +Cirrus-CI ********* The unit and integration tests in Iris are an essential mechanism to ensure that the Iris code base is working as expected. :ref:`developer_running_tests` may be run manually but to ensure the checks are performed a -continuous integration testing tool named `travis-ci`_ is used. +continuous integration testing tool named `cirrus-ci`_ is used. -A `travis-ci`_ configuration file named `.travis.yml`_ -is in the Iris repository which tells travis-ci what commands to run. The +A `cirrus-ci`_ configuration file named `.cirrus.yml`_ +is in the Iris repository which tells Cirrus-CI what commands to run. The commands include retrieving the Iris code base and associated test files using -conda and then running the tests. `travis-ci`_ allows for a matrix of tests to +conda and then running the tests. `cirrus-ci`_ allows for a matrix of tests to be performed to ensure that all expected variations test successfully. -The `travis-ci`_ tests are run automatically against the `Iris`_ master +The `cirrus-ci`_ tests are run automatically against the `Iris`_ master repository when a pull request is submitted, updated or merged. GitHub Checklist diff --git a/docs/iris/src/developers_guide/contributing_documentation.rst b/docs/iris/src/developers_guide/contributing_documentation.rst index 966c44b859..56d2257a55 100644 --- a/docs/iris/src/developers_guide/contributing_documentation.rst +++ b/docs/iris/src/developers_guide/contributing_documentation.rst @@ -28,7 +28,7 @@ The build can be run from the documentation directory ``iris/docs/iris/src``. 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. +*warnings* otherwise it may fail the automated `cirrus-ci`_ build. Once the build is complete, if it is rerun it will only rebuild the impacted build artefacts so should take less time. @@ -50,7 +50,7 @@ This is useful for a final test before committing your changes. have been promoted to be **errors** to ensure they are addressed. This **only** applies when ``make html`` is run. -.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris .. _contributing.documentation.testing: @@ -99,7 +99,7 @@ or ignore the url. ``spelling_word_list_filename``. -.. note:: In addition to the automated `travis-ci`_ build of all the +.. note:: In addition to the automated `cirrus-ci`_ build of all the documentation build options above, the https://readthedocs.org/ service is also used. The configuration of this held in a file in the root of the diff --git a/docs/iris/src/developers_guide/contributing_graphics_tests.rst b/docs/iris/src/developers_guide/contributing_graphics_tests.rst index b6d352cacc..8d8189c69b 100644 --- a/docs/iris/src/developers_guide/contributing_graphics_tests.rst +++ b/docs/iris/src/developers_guide/contributing_graphics_tests.rst @@ -34,7 +34,7 @@ perceived as it may be a simple pixel shift. Testing Strategy ================ -The `Iris Travis matrix`_ defines multiple test runs that use +The `Iris Cirrus-CI matrix`_ defines multiple test runs that use different versions of Python to ensure Iris is working as expected. To make this manageable, the ``iris.tests.IrisTest_nometa.check_graphic`` test @@ -155,7 +155,7 @@ To add your changes to Iris, you need to make two pull requests (PR). .. important:: - The Iris pull-request will not test successfully in Travis until the + The Iris pull-request will not test successfully in Cirrus-CI until the ``test-iris-imagehash`` pull request has been merged. This is because there is an Iris_ test which ensures the existence of the reference images (uris) for all the targets in the image results database. It will also fail @@ -163,4 +163,4 @@ To add your changes to Iris, you need to make two pull requests (PR). image-listing file in ``test-iris-imagehash``. -.. _Iris travis matrix: https://github.com/scitools/iris/blob/master/.travis.yml#L15 +.. _Iris Cirrus-CI matrix: https://github.com/scitools/iris/blob/master/.cirrus.yml diff --git a/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst b/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst index 65d8516a15..3e7a9f1ae3 100644 --- a/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst +++ b/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst @@ -38,7 +38,7 @@ is merged. Before submitting a pull request please consider this list. #. **Check the documentation builds without warnings or errors**. See :ref:`contributing.documentation.building` -#. **Check for any new dependencies in the** `.travis.yml`_ **config file.** +#. **Check for any new dependencies in the** `.cirrus.yml`_ **config file.** #. **Check for any new dependencies in the** `readthedocs.yml`_ **file**. This file is used to build the documentation that is served from 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 7f79657b0d..d6f805c511 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst @@ -38,7 +38,7 @@ situation is thought likely (large PR, high repo activity etc.): * PR author: create the "What's New" pull request * PR reviewer: once the "What's New" PR is created, **merge the main PR**. - (this will fix any `travis-ci`_ linkcheck errors where the links in the + (this will fix any `cirrus-ci`_ linkcheck errors where the links in the "What's New" PR reference new features introduced in the main PR) * PR reviewer: review the "What's New" PR, merge once acceptable @@ -96,11 +96,11 @@ examine past 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 documentation when + `cirrus-ci`_ will automatically build the documentation when creating a pull request, however you can also manually :ref:`build ` the documentation. -.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris Contribution Categories diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 745cf717f5..824aa8170f 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -442,7 +442,7 @@ This document explains the changes made to Iris for this release * `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). -* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_. (:pull:`3928`) +* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ support. (:pull:`3928`) * `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to @@ -498,3 +498,5 @@ This document explains the changes made to Iris for this release .. _flake8: https://flake8.pycqa.org/en/stable/ .. _nox: https://nox.thea.codes/en/stable/ .. _Title Case Capitalization: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case +.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _stickler-ci: https://stickler-ci.com/ From 9c6d7782395f751f5d3a8116c57520ee0dbb4d6e Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 25 Jan 2021 11:21:08 +0000 Subject: [PATCH 20/23] docs for nox (#3955) * docs for nox * add titles, notices and additional detail * review actions --- .../contributing_running_tests.rst | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/iris/src/developers_guide/contributing_running_tests.rst b/docs/iris/src/developers_guide/contributing_running_tests.rst index 3ac0ed905e..99ea4e831c 100644 --- a/docs/iris/src/developers_guide/contributing_running_tests.rst +++ b/docs/iris/src/developers_guide/contributing_running_tests.rst @@ -5,6 +5,11 @@ Running the Tests ***************** +Using setuptools for Testing Iris +================================= + +.. warning:: The `setuptools`_ ``test`` command was deprecated in `v41.5.0`_. See :ref:`using nox`. + A prerequisite of running the tests is to have the Python environment setup. For more information on this see :ref:`installing_from_source`. @@ -91,3 +96,92 @@ due to an experimental dependency not being present. All Python decorators that skip tests will be defined in ``lib/iris/tests/__init__.py`` with a function name with a prefix of ``skip_``. + + +.. _using nox: + +Using Nox for Testing Iris +========================== + +Iris has adopted the use of the `nox`_ tool for automated testing on `cirrus-ci`_ +and also locally on the command-line for developers. + +`nox`_ is similar to `tox`_, but instead leverages the expressiveness and power of a Python +configuration file rather than an `.ini` style file. As with `tox`_, `nox`_ can use `virtualenv`_ +to create isolated Python environments, but in addition also supports `conda`_ as a testing +environment backend. + + +Where is Nox Used? +------------------ + +Iris uses `nox`_ as a convenience to fully automate the process of executing the Iris tests, but also +automates the process of: + +* building the documentation and executing the doc-tests +* building the documentation gallery +* running the documentation URL link check +* linting the code-base +* ensuring the code-base style conforms to the `black`_ standard + + +You can perform all of these tasks manually yourself, however the onus is on you to first ensure +that all of the required package dependencies are installed and available in the testing environment. + +`Nox`_ has been configured to automatically do this for you, and provides a means to easily replicate +the remote testing behaviour of `cirrus-ci`_ locally for the developer. + + +Installing Nox +-------------- + +We recommend installing `nox`_ using `conda`_. To install `nox`_ in a separate `conda`_ environment:: + + conda create -n nox -c conda-forge nox + conda activate nox + +To install `nox`_ in an existing active `conda`_ environment:: + + conda install -c conda-forge nox + +The `nox`_ package is also available on PyPI, however `nox`_ has been configured to use the `conda`_ +backend for Iris, so an installation of `conda`_ must always be available. + + +Testing with Nox +---------------- + +The `nox`_ configuration file `noxfile.py` is available in the root ``iris`` project directory, and +defines all the `nox`_ sessions (i.e., tasks) that may be performed. `nox`_ must always be executed +from the ``iris`` root directory. + +To list the configured `nox`_ sessions for Iris:: + + nox --list + +To run the Iris tests for all configured versions of Python:: + + nox --session tests + +To build the Iris documentation specifically for Python 3.7:: + + nox --session doctest-3.7 + +To run all the Iris `nox`_ sessions:: + + nox + +For further `nox`_ command-line options:: + + nox --help + +.. note:: `nox`_ will cache its testing environments in the `.nox` root ``iris`` project directory. + + +.. _black: https://black.readthedocs.io/en/stable/ +.. _nox: https://nox.thea.codes/en/latest/ +.. _setuptools: https://setuptools.readthedocs.io/en/latest/ +.. _tox: https://tox.readthedocs.io/en/latest/ +.. _virtualenv: https://virtualenv.pypa.io/en/latest/ +.. _PyPI: https://pypi.org/project/nox/ +.. _v41.5.0: https://setuptools.readthedocs.io/en/latest/history.html#v41-5-0 From 78de3c666d9f69ddbad16765dd6bd1ea85384217 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 25 Jan 2021 11:21:54 +0000 Subject: [PATCH 21/23] Resolve test coverage (#3947) * test coverage for __init__ and __call__ * test coverage for metadata resolve and coverage * partial test coverage for metadata mapping * python 3.6 workaround for deepcopy of mock.sentinel * test coverage for Resolve._free_mapping * test coverage for Resolve convenience methods * add test stub for Resolve._metadata_mapping * fix Test__tgt_cube_position * test coverage for shape * test coverage for _as_compatible_cubes * test coverage for Resolve._metadata_mapping * test coverage for Resolve._prepare_common_dim_payload * test coverage for Resolve._prepare_common_aux_payload * test coverage for Resolve._prepare_points_and_bounds * test coverage for Resolve._create_prepared_item * test coverage for Resolve._prepare_local_payload_dim * test coverage for Resolve._prepare_local_payload_aux * test coverage for Resolve._prepare_local_payload_scalar + docs URL skip * test coverage for Resolve._prepare_local_payload * test coverage for Resolve._metadata_prepare * added docs URL linkcheck skip * test coverage for Resolve._prepare_factory_payload * test coverage for Resolve._get_prepared_item * review actions * test coverage for Resolve.cube --- docs/iris/src/conf.py | 2 + lib/iris/common/resolve.py | 714 ++- .../tests/unit/common/resolve/__init__.py | 6 + .../tests/unit/common/resolve/test_Resolve.py | 4795 +++++++++++++++++ 4 files changed, 5443 insertions(+), 74 deletions(-) create mode 100644 lib/iris/tests/unit/common/resolve/__init__.py create mode 100644 lib/iris/tests/unit/common/resolve/test_Resolve.py diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index e3d9266b0b..7232d7c40e 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -269,6 +269,8 @@ def autolog(message): "http://schacon.github.com/git", "http://scitools.github.com/cartopy", "http://www.wmo.int/pages/prog/www/DPFS/documents/485_Vol_I_en_colour.pdf", + "https://software.ac.uk/how-cite-software", + "http://www.esrl.noaa.gov/psd/data/gridded/conventions/cdc_netcdf_standard.shtml", ] # list of sources to exclude from the build. diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py index ad37247809..e772eeefce 100644 --- a/lib/iris/common/resolve.py +++ b/lib/iris/common/resolve.py @@ -230,7 +230,7 @@ def __init__(self, lhs=None, rhs=None): """ #: The ``lhs`` operand to be resolved into the resultant :class:`~iris.cube.Cube`. - self.lhs_cube = None # set in _call__ + self.lhs_cube = None # set in __call__ #: The ``rhs`` operand to be resolved into the resultant :class:`~iris.cube.Cube`. self.rhs_cube = None # set in __call__ @@ -294,6 +294,25 @@ def __init__(self, lhs=None, rhs=None): self(lhs, rhs) def __call__(self, lhs, rhs): + """ + Resolve the ``lhs`` :class:`~iris.cube.Cube` operand and ``rhs`` + :class:`~iris.cube.Cube` operand metadata. + + Involves determining all the common coordinate metadata shared between + the operands, and the metadata that is local to each operand. Given + the common metadata, the broadcast shape of the resultant resolved + :class:`~iris.cube.Cube`, which may be auto-transposed, can be + determined. + + Args: + + * lhs: + The left-hand-side :class:`~iris.cube.Cube` operand. + + * rhs: + The right-hand-side :class:`~iris.cube.Cube` operand. + + """ from iris.cube import Cube emsg = ( @@ -338,11 +357,31 @@ def __call__(self, lhs, rhs): return self def _as_compatible_cubes(self): + """ + Determine whether the ``src`` and ``tgt`` :class:`~iris.cube.Cube` can + be transposed and/or broadcast successfully together. + + If compatible, the ``_broadcast_shape`` of the resultant resolved cube is + calculated, and the ``_src_cube_resolved`` (transposed/broadcast ``src`` + cube) and ``_tgt_cube_resolved`` (same as the ``tgt`` cube) are + calculated. + + An exception will be raised if the ``src`` and ``tgt`` cannot be + broadcast, even after a suitable transpose has been performed. + + .. note:: + + Requires that **all** ``src`` cube dimensions have been mapped + successfully to an appropriate ``tgt`` cube dimension. + + """ from iris.cube import Cube src_cube = self._src_cube tgt_cube = self._tgt_cube + assert src_cube.ndim == len(self.mapping) + # 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(): @@ -430,6 +469,40 @@ def _aux_coverage( common_aux_metadata, common_scalar_metadata, ): + """ + Determine the dimensions covered by each of the local and common + auxiliary coordinates of the provided :class:`~iris.cube.Cube`. + + The cube dimensions not covered by any of the auxiliary coordinates is + also determined; these are known as `free` dimensions. + + The scalar coordinates local to the cube are also determined. + + Args: + + * cube: + The :class:`~iris.cube.Cube` to be analysed for coverage. + + * cube_items_aux: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each auxiliary coordinate owned by the cube. + + * cube_items_scalar: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each scalar coordinate owned by the cube. + + * common_aux_metadata: + The list of common auxiliary coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + * common_scalar_metadata: + The list of common scalar coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + Returns: + :class:`~iris.common.resolve._AuxCoverage` + + """ common_items_aux = [] common_items_scalar = [] local_items_aux = [] @@ -465,7 +538,33 @@ def _aux_coverage( dims_free=sorted(dims_free), ) - def _aux_mapping(self, src_coverage, tgt_coverage): + @staticmethod + def _aux_mapping(src_coverage, tgt_coverage): + """ + Establish the mapping of dimensions from the ``src`` to ``tgt`` + :class:`~iris.cube.Cube` using the auxiliary coordinate metadata + common between each of the operands. + + The ``src`` to ``tgt`` common auxiliary coordinate mapping is held by + the :attr:`~iris.common.resolve.Resolve.mapping`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube` i.e., map from the common ``src`` + dimensions. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube` i.e., map to the common ``tgt`` + dimensions. + + Returns: + Dictionary of ``src`` to ``tgt`` dimension mapping. + + """ + mapping = {} for tgt_item in tgt_coverage.common_items_aux: # Search for a src aux metadata match. tgt_metadata = tgt_item.metadata @@ -484,7 +583,7 @@ def _aux_mapping(self, src_coverage, tgt_coverage): 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 + mapping[src_dim] = tgt_dim logger.debug(f"{src_dim}->{tgt_dim}") else: # This situation can only occur due to a systemic internal @@ -504,9 +603,26 @@ def _aux_mapping(self, src_coverage, tgt_coverage): tgt_item.dims, ) ) + return mapping @staticmethod def _categorise_items(cube): + """ + Inspect the provided :class:`~iris.cube.Cube` and group its + coordinates and associated metadata into dimension, auxiliary and + scalar categories. + + Args: + + * cube: + The :class:`~iris.cube.Cube` that will have its coordinates and + metadata grouped into their associated dimension, auxiliary and + scalar categories. + + Returns: + :class:`~iris.common.resolve._CategoryItems` + + """ category = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) # Categorise the dim coordinates of the cube. @@ -530,15 +646,40 @@ def _categorise_items(cube): 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) + def _create_prepared_item( + coord, dims, src_metadata=None, tgt_metadata=None + ): + """ + Convenience method that creates a :class:`~iris.common.resolve._PreparedItem` + containing the data and metadata required to construct and attach a coordinate + to the resultant resolved cube. + + Args: + + * coord: + The coordinate with the ``points`` and ``bounds`` to be extracted. + + * dims: + The dimensions that the ``coord`` spans on the resulting resolved :class:`~iris.cube.Cube`. + + * src_metadata: + The coordinate metadata from the ``src`` :class:`~iris.cube.Cube`. + + * tgt_metadata: + The coordinate metadata from the ``tgt`` :class:`~iris.cube.Cube`. + + Returns: + The :class:`~iris.common.resolve._PreparedItem`. + + """ + if src_metadata is not None and tgt_metadata is not None: + combined = src_metadata.combine(tgt_metadata) else: - combined = src or tgt + combined = src_metadata or tgt_metadata if not isinstance(dims, Iterable): dims = (dims,) prepared_metadata = _PreparedMetadata( - combined=combined, src=src, tgt=tgt + combined=combined, src=src_metadata, tgt=tgt_metadata ) bounds = coord.bounds result = _PreparedItem( @@ -573,6 +714,30 @@ def _show(items, heading): @staticmethod def _dim_coverage(cube, cube_items_dim, common_dim_metadata): + """ + Determine the dimensions covered by each of the local and common + dimension coordinates of the provided :class:`~iris.cube.Cube`. + + The cube dimensions not covered by any of the dimension coordinates is + also determined; these are known as `free` dimensions. + + Args: + + * cube: + The :class:`~iris.cube.Cube` to be analysed for coverage. + + * cube_items_dim: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each dimension coordinate owned by the cube. + + * common_dim_metadata: + The list of common dimension coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + Returns: + :class:`~iris.common.resolve._DimCoverage` + + """ ndim = cube.ndim metadata = [None] * ndim coords = [None] * ndim @@ -599,13 +764,39 @@ def _dim_coverage(cube, cube_items_dim, common_dim_metadata): dims_free=sorted(dims_free), ) - def _dim_mapping(self, src_coverage, tgt_coverage): + @staticmethod + def _dim_mapping(src_coverage, tgt_coverage): + """ + Establish the mapping of dimensions from the ``src`` to ``tgt`` + :class:`~iris.cube.Cube` using the dimension coordinate metadata + common between each of the operands. + + The ``src`` to ``tgt`` common dimension coordinate mapping is held by + the :attr:`~iris.common.resolve.Resolve.mapping`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube` i.e., map from the common ``src`` + dimensions. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube` i.e., map to the common ``tgt`` + dimensions. + + Returns: + Dictionary of ``src`` to ``tgt`` dimension mapping. + + """ + mapping = {} 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 + mapping[src_dim] = tgt_dim logger.debug(f"{src_dim}->{tgt_dim}") except ValueError: # This exception can only occur due to a systemic internal @@ -621,9 +812,10 @@ def _dim_mapping(self, src_coverage, tgt_coverage): src_coverage.cube.name(), tgt_coverage.cube.name(), tgt_metadata, - tuple([tgt_dim]), + (tgt_dim,), ) ) + return mapping def _free_mapping( self, @@ -632,6 +824,57 @@ def _free_mapping( src_aux_coverage, tgt_aux_coverage, ): + """ + Attempt to update the :attr:`~iris.common.resolve.Resolve.mapping` with + ``src`` to ``tgt`` :class:`~iris.cube.Cube` mappings from unmapped ``src`` + dimensions that are free from coordinate metadata coverage to ``tgt`` + dimensions that have local metadata coverage (i.e., is not common between + the ``src`` and ``tgt``) or dimensions that are free from coordinate + metadata coverage. + + If the ``src`` :class:`~iris.cube.Cube` does not have any free dimensions, + the attempt to map unmapped ``tgt`` dimensions that have local metadata + coverage to ``src`` dimensions that are free from coordinate metadata + coverage. + + An exception will be raised if there are any ``src`` :class:`~iris.cube.Cube` + dimensions not mapped to an associated ``tgt`` dimension. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube`. + + * src_aux_coverage: + The :class:`~iris.common.resolve._AuxCoverage` of the ``src`` + :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:`~iris.common.resolve._AuxCoverage` of the ``tgt`` + :class:`~iris.cube.Cube`. + + .. note:: + + All unmapped dimensions with an extend >1 are mapped before those + with an extent of 1, as such dimensions cannot be broadcast. It + is important to map specific non-broadcastable dimensions before + generic broadcastable dimensions otherwise we are open to failing to + map all the src dimensions as a generic src broadcast dimension has + been mapped to the only tgt dimension that a specific non-broadcastable + dimension can be mapped to. + + .. note:: + + A local dimension cannot be mapped to another local dimension, + by definition, otherwise this dimension would be classed as a + common dimension. + + """ src_cube = src_dim_coverage.cube tgt_cube = tgt_dim_coverage.cube src_ndim = src_cube.ndim @@ -663,11 +906,16 @@ def _free_mapping( 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): + def _assign_mapping(extent, unmapped_local_items, free_items=None): result = None if free_items is None: free_items = [] if extent == 1: + # Map to the first available unmapped local dimension or + # the first available free dimension. + # Dimension shape doesn't matter here as the extent is 1, + # therefore broadcasting will take care of any discrepency + # between src and tgt dimension extent. if unmapped_local_items: result, _ = unmapped_local_items.pop(0) elif free_items: @@ -680,10 +928,10 @@ def _filter(items): ) def _pop(item, items): - result, _ = item + dim, _ = item index = items.index(item) items.pop(index) - return result + return dim items = _filter(unmapped_local_items) if items: @@ -700,11 +948,12 @@ def _pop(item, items): (dim, tgt_shape[dim]) for dim in tgt_unmapped_local ] tgt_free_items = [(dim, tgt_shape[dim]) for dim in tgt_free] + # Sort by decreasing src dimension extent and increasing src dimension + # as we want broadcast src dimensions to be mapped last. + src_key_func = lambda dim: (src_max - src_shape[dim], dim) - for src_dim in sorted( - src_free, key=lambda dim: (src_max - src_shape[dim], dim) - ): - tgt_dim = assign_mapping( + for src_dim in sorted(src_free, key=src_key_func): + tgt_dim = _assign_mapping( src_shape[src_dim], tgt_unmapped_local_items, tgt_free_items, @@ -725,11 +974,12 @@ def _pop(item, items): src_unmapped_local_items = [ (dim, src_shape[dim]) for dim in src_unmapped_local ] + # Sort by decreasing tgt dimension extent and increasing tgt dimension + # as we want broadcast tgt dimensions to be mapped last. + tgt_key_func = lambda dim: (tgt_max - tgt_shape[dim], dim) - for tgt_dim in sorted( - tgt_free, key=lambda dim: (tgt_max - tgt_shape[dim], dim) - ): - src_dim = assign_mapping( + for tgt_dim in sorted(tgt_free, key=tgt_key_func): + src_dim = _assign_mapping( tgt_shape[tgt_dim], src_unmapped_local_items ) if src_dim is not None: @@ -758,6 +1008,17 @@ def _pop(item, items): logger.debug(f"mapping free dimensions gives, mapping={self.mapping}") def _metadata_coverage(self): + """ + Using the pre-categorised metadata of the cubes, determine the dimensions + covered by their associated dimension and auxiliary coordinates, and which + dimensions are free of metadata coverage. + + This coverage analysis clarifies how the dimensions covered by common + metadata are related, thus establishing a dimensional mapping between + the cubes. It also identifies the dimensions covered by metadata that + is local to each cube, and indeed which dimensions are free of metadata. + + """ # Determine the common dim coordinate metadata coverage. common_dim_metadata = [ item.metadata for item in self.category_common.items_dim @@ -798,6 +1059,37 @@ def _metadata_coverage(self): ) def _metadata_mapping(self): + """ + Ensure that each ``src`` :class:`~iris.cube.Cube` dimension is mapped to an associated + ``tgt`` :class:`~iris.cube.Cube` dimension using the common dim and aux coordinate metadata. + + If the common metadata does not result in a full mapping of ``src`` to ``tgt`` dimensions + then free dimensions are analysed to determine whether the mapping can be completed. + + Once the ``src`` has been mapped to the ``tgt``, the cubes are checked to ensure that they + will successfully broadcast, and the ``src`` :class:`~iris.cube.Cube` is transposed appropriately, + if necessary. + + The :attr:`~iris.common.resolve.Resolve._broadcast_shape` is set, along with the + :attr:`~iris.common.resolve.Resolve._src_cube_resolved` and :attr:`~iris.common.resolve.Resolve._tgt_cube_resolved`, + which are the broadcast/transposed ``src`` and ``tgt``. + + .. note:: + + An exception will be raised if a ``src`` dimension cannot be mapped to a ``tgt`` dimension. + + .. note:: + + An exception will be raised if the full mapped ``src`` :class:`~iris.cube.Cube` cannot be + broadcast or transposed with the ``tgt`` :class:`~iris.cube.Cube`. + + .. note:: + + The ``src`` and ``tgt`` may be swapped in the case where they both have equal dimensionality and + the ``tgt`` does have the same shape as the resolved broadcast shape (and the ``src`` does) or + the ``tgt`` has more free dimensions than the ``src``. + + """ # Initialise the state. self.mapping = {} @@ -819,7 +1111,9 @@ def _metadata_mapping(self): # 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) + self.mapping.update( + self._dim_mapping(src_dim_coverage, tgt_dim_coverage) + ) logger.debug( f"mapping common dim coordinates gives, mapping={self.mapping}" ) @@ -827,7 +1121,9 @@ def _metadata_mapping(self): # 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) + self.mapping.update( + self._aux_mapping(src_aux_coverage, tgt_aux_coverage) + ) logger.debug( f"mapping common aux coordinates, mapping={self.mapping}" ) @@ -886,6 +1182,12 @@ def _metadata_mapping(self): self._as_compatible_cubes() def _metadata_prepare(self): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_category` and + :attr:`~iris.common.resolve.Resolve.prepared_factories` with the necessary metadata to be constructed + and attached to the resulting resolved :class:`~iris.cube.Cube`. + + """ # Initialise the state. self.prepared_category = _CategoryItems( items_dim=[], items_aux=[], items_scalar=[] @@ -1053,6 +1355,41 @@ def _prepare_common_aux_payload( prepared_items, ignore_mismatch=None, ): + """ + Populate the ``prepared_items`` with a :class:`~iris.common.resolve._PreparedItem` containing + the necessary metadata for each auxiliary coordinate to be constructed and attached to the + resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + For mixed ``src`` and ``tgt`` coordinate types with matching metadata, an + :class:`~iris.coords.AuxCoord` will be nominated for construction. + + Args: + + * src_common_items: + The list of :attr:`~iris.common.resolve._AuxCoverage.common_items_aux` metadata + for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_common_items: + The list of :attr:`~iris.common.resolve._AuxCoverage.common_items_aux` metadata + for the ``tgt`` :class:`~iris.cube.Cube`. + + * prepared_items: + The list of :class:`~iris.common.resolve._PreparedItem` metadata that will be used + to construct the auxiliary coordinates that will be attached to the resulting + resolved :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + When ``False``, an exception will be raised if a difference is detected between corresponding + ``src`` and ``tgt`` coordinate ``points`` and/or ``bounds``. + When ``True``, the coverage metadata is ignored i.e., a coordinate will not be constructed and + added to the resulting resolved :class:`~iris.cube.Cube`. + Defaults to ``False``. + + """ from iris.coords import AuxCoord if ignore_mismatch is None: @@ -1115,6 +1452,30 @@ def _prepare_common_aux_payload( def _prepare_common_dim_payload( self, src_coverage, tgt_coverage, ignore_mismatch=None ): + """ + Populate the ``items_dim`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for + each :class:`~iris.coords.DimCoord` to be constructed and attached to the resulting resolved + :class:`~iris.cube.Cube`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` metadata for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` metadata for the ``tgt`` :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + When ``False``, an exception will be raised if a difference is detected between corresponding + ``src`` and ``tgt`` :class:`~iris.coords.DimCoord` ``points`` and/or ``bounds``. + When ``True``, the coverage metadata is ignored i.e., a :class:`~iris.coords.DimCoord` will not + be constructed and added to the resulting resolved :class:`~iris.cube.Cube`. + Defaults to ``False``. + + """ from iris.coords import DimCoord if ignore_mismatch is None: @@ -1153,55 +1514,123 @@ def _prepare_common_dim_payload( ) 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 + def _get_prepared_item( + self, metadata, category_local, from_src=True, from_local=False + ): + """ + Find the :attr:`~iris.common.resolve._PreparedItem` from the + :attr:`~iris.common.resolve.Resolve.prepared_category` that matches the provided ``metadata``. + + Alternatively, the ``category_local`` is searched to find a :class:`~iris.common.resolve._Item` + with matching ``metadata`` from either the local ``src`` or ``tgt`` :class:`~iris.cube.Cube`. + If a match is found, then a new `~iris.common.resolve._PreparedItem` is created and added to + :attr:`~iris.common.resolve.Resolve.prepared_category` and returned. See ``from_local``. + + Args: + + * metadata: + The target metadata of the prepared (or local) item to retrieve. + + * category_local: + The :class:`~iris.common.resolve._CategoryItems` containing the + local metadata of either the ``src`` or ``tgt`` :class:`~iris.cube.Cube`. + See ``from_local``. + + Kwargs: + + * from_src: + Boolean stating whether the ``metadata`` is from the ``src`` (``True``) + or ``tgt`` :class:`~iris.cube.Cube`. + Defaults to ``True``. + + * from_local: + Boolean controlling whether the ``metadata`` is used to search the + ``category_local`` (``True``) or the :attr:`~iris.common.resolve.Resolve.prepared_category`. + Defaults to ``False``. + + Returns: + The :class:`~iris.common.resolve._PreparedItem` matching the provided ``metadata``. + + """ + 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: - category = self.prepared_category - if from_src: - match = lambda item: item.metadata.src == metadata + 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: - 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 + (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: - result = item - break - return result + tgt = item.metadata + dims = item.dims + result = self._create_prepared_item( + item.coord, + dims, + src_metadata=src, + tgt_metadata=tgt, + ) + getattr(self.prepared_category, member).append(result) + else: + result = item + break + return result + + def _prepare_factory_payload(self, cube, category_local, from_src=True): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_factories` with a :class:`~iris.common.resolve._PreparedFactory` + containing the necessary metadata for each ``src`` and/or ``tgt`` auxiliary factory to be constructed and + attached to the resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + The required dependencies of an auxiliary factory may not all be available in the + :attr:`~iris.common.resolve.Resolve.prepared_category` and therefore this is a legitimate + reason to add the associated metadata of the local dependency to the ``prepared_category``. + + Args: + + * cube: + The :class:`~iris.cube.Cube` that may contain an auxiliary factory to be prepared. + + * category_local: + The :class:`~iris.common.resolve._CategoryItems` of all metadata local to the provided ``cube``. + Kwargs: + + * from_src: + Boolean stating whether the provided ``cube`` is either a ``src`` or ``tgt`` + :class:`~iris.cube.Cube` - used to retrieve the appropriate metadata from a + :class:`~iris.common.resolve._PreparedMetadata`. + + """ for factory in cube.aux_factories: container = type(factory) dependencies = {} prepared_item = None + found = True if tuple( filter( @@ -1222,18 +1651,24 @@ def _get_prepared_item(metadata, from_src=True, from_local=False): dependency_coord, ) in factory.dependencies.items(): metadata = dependency_coord.metadata - prepared_item = _get_prepared_item(metadata, from_src=from_src) + prepared_item = self._get_prepared_item( + metadata, category_local, from_src=from_src + ) if prepared_item is None: - prepared_item = _get_prepared_item( - metadata, from_src=from_src, from_local=True + prepared_item = self._get_prepared_item( + metadata, + category_local, + 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) + found = False break dependencies[dependency_name] = prepared_item.metadata - if prepared_item is not None: + if found and prepared_item is not None: prepared_factory = _PreparedFactory( container=container, dependencies=dependencies ) @@ -1243,6 +1678,29 @@ def _get_prepared_item(metadata, from_src=True, from_local=False): logger.debug(dmsg) def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): + """ + Populate the ``items_aux`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local auxiliary coordinate to be constructed and attached to the resulting + resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, lenient behaviour subscribes to the philosophy that it is easier to remove + metadata than it is to find then add metadata. To those ends, lenient behaviour supports + metadata richness by adding both local ``src`` and ``tgt`` auxiliary coordinates. + Alternatively, strict behaviour will only add a ``tgt`` local auxiliary coordinate that + spans dimensions not mapped to by the ``src`` e.g., extra ``tgt`` dimensions. + + Args: + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # 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. @@ -1259,7 +1717,7 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): 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 + item.coord, tgt_dims, src_metadata=item.metadata ) self.prepared_category.items_aux.append(prepared_item) else: @@ -1281,7 +1739,7 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): [dim in extra_tgt_dims for dim in tgt_dims] ): prepared_item = self._create_prepared_item( - item.coord, tgt_dims, tgt=item.metadata + item.coord, tgt_dims, tgt_metadata=item.metadata ) self.prepared_category.items_aux.append(prepared_item) else: @@ -1293,6 +1751,28 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): logger.debug(dmsg) def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): + """ + Populate the ``items_dim`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local :class:`~iris.coords.DimCoord` to be constructed and attached to the + resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, a local coordinate will only be added if there is no other metadata competing + to describe the same dimension/s on the ``tgt`` :class:`~iris.cube.Cube`. Lenient behaviour + is more liberal, whereas strict behaviour will only add a local ``tgt`` coordinate covering + an unmapped "extra" ``tgt`` dimension/s. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ mapped_tgt_dims = self.mapping.values() # Determine whether there are tgt dimensions not mapped to by an @@ -1314,7 +1794,7 @@ def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): 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 + coord, tgt_dim, src_metadata=metadata ) self.prepared_category.items_dim.append(prepared_item) else: @@ -1347,13 +1827,36 @@ def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): if metadata is not None: coord = tgt_dim_coverage.coords[tgt_dim] prepared_item = self._create_prepared_item( - coord, tgt_dim, tgt=metadata + coord, tgt_dim, tgt_metadata=metadata ) self.prepared_category.items_dim.append(prepared_item) def _prepare_local_payload_scalar( self, src_aux_coverage, tgt_aux_coverage ): + """ + Populate the ``items_scalar`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local scalar coordinate to be constructed and attached to the resulting + resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, lenient behaviour subscribes to the philosophy that it is easier to remove + metadata than it is to find then add metadata. To those ends, lenient behaviour supports + metadata richness by adding both local ``src`` and ``tgt`` scalar coordinates. + Alternatively, strict behaviour will only add a ``tgt`` local scalar coordinate when the + ``src`` is a scalar :class:`~iris.cube.Cube` with no local scalar coordinates. + + Args: + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Add all local tgt scalar coordinates iff the src cube is a # scalar cube with no local src scalar coordinates. # Only for strict maths. @@ -1367,14 +1870,14 @@ def _prepare_local_payload_scalar( # 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 + item.coord, item.dims, src_metadata=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 + item.coord, item.dims, tgt_metadata=item.metadata ) self.prepared_category.items_scalar.append(prepared_item) @@ -1385,6 +1888,27 @@ def _prepare_local_payload( tgt_dim_coverage, tgt_aux_coverage, ): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_category_items` with a + :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata from the ``src`` + and/or ``tgt`` :class:`~iris.cube.Cube` for each coordinate to be constructed and attached + to the resulting resolved :class:`~iris.cube.Cube`. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Add local src/tgt dim coordinates. self._prepare_local_payload_dim(src_dim_coverage, tgt_dim_coverage) @@ -1397,6 +1921,47 @@ def _prepare_local_payload( def _prepare_points_and_bounds( self, src_coord, tgt_coord, src_dims, tgt_dims, ignore_mismatch=None ): + """ + Compare the points and bounds of the ``src`` and ``tgt`` coordinates to ensure + that they are equivalent, taking into account broadcasting when appropriate. + + .. note:: + + An exception will be raised if the ``src`` and ``tgt`` coordinates cannot + be broadcast. + + .. note:: + + An exception will be raised if either the points or bounds are different, + however appropriate lenient behaviour concessions are applied. + + Args: + + * src_coord: + The ``src`` :class:`~iris.cube.Cube` coordinate with metadata matching + the ``tgt_coord``. + + * tgt_coord: + The ``tgt`` :class`~iris.cube.Cube` coordinate with metadata matching + the ``src_coord``. + + * src_dims: + The dimension/s of the ``src_coord`` attached to the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dims: + The dimension/s of the ``tgt_coord`` attached to the ``tgt`` :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + For lenient behaviour only, don't raise an exception if there is a difference between + the ``src`` and ``tgt`` coordinate points or bounds. + Defaults to ``False``. + + Returns: + Tuple of equivalent ``points`` and ``bounds``, otherwise ``None``. + + """ from iris.util import array_equal if ignore_mismatch is None: @@ -1443,6 +2008,7 @@ def _prepare_points_and_bounds( tgt_broadcasting = tgt_shape != tgt_shape_broadcast if src_broadcasting and tgt_broadcasting: + # TBD: Extend capability to support attempting to broadcast two-way multi-dimensional coordinates. emsg = ( f"Cannot broadcast the coordinate {src_coord.name()!r} on " f"{self._src_cube_position} cube {self._src_cube.name()!r} and " diff --git a/lib/iris/tests/unit/common/resolve/__init__.py b/lib/iris/tests/unit/common/resolve/__init__.py new file mode 100644 index 0000000000..d0b189e59d --- /dev/null +++ b/lib/iris/tests/unit/common/resolve/__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.resolve` package.""" diff --git a/lib/iris/tests/unit/common/resolve/test_Resolve.py b/lib/iris/tests/unit/common/resolve/test_Resolve.py new file mode 100644 index 0000000000..94ec48de88 --- /dev/null +++ b/lib/iris/tests/unit/common/resolve/test_Resolve.py @@ -0,0 +1,4795 @@ +# 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.resolve.Resolve`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from collections import namedtuple +from copy import deepcopy + +from cf_units import Unit +import numpy as np +import unittest.mock as mock +from unittest.mock import sentinel + +from iris.common.lenient import LENIENT +from iris.common.metadata import CubeMetadata +from iris.common.resolve import ( + Resolve, + _AuxCoverage, + _CategoryItems, + _DimCoverage, + _Item, + _PreparedItem, + _PreparedFactory, + _PreparedMetadata, +) +from iris.coords import DimCoord +from iris.cube import Cube + + +class Test___init__(tests.IrisTest): + def setUp(self): + target = "iris.common.resolve.Resolve.__call__" + self.m_call = mock.MagicMock(return_value=sentinel.return_value) + _ = self.patch(target, new=self.m_call) + + def _assert_members_none(self, resolve): + self.assertIsNone(resolve.lhs_cube_resolved) + self.assertIsNone(resolve.rhs_cube_resolved) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + self.assertIsNone(resolve.lhs_cube_dim_coverage) + self.assertIsNone(resolve.lhs_cube_aux_coverage) + self.assertIsNone(resolve.rhs_cube_dim_coverage) + self.assertIsNone(resolve.rhs_cube_aux_coverage) + self.assertIsNone(resolve.map_rhs_to_lhs) + self.assertIsNone(resolve.mapping) + self.assertIsNone(resolve.prepared_category) + self.assertIsNone(resolve.prepared_factories) + self.assertIsNone(resolve._broadcast_shape) + + def test_lhs_rhs_default(self): + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self._assert_members_none(resolve) + self.assertEqual(0, self.m_call.call_count) + + def test_lhs_rhs_provided(self): + m_lhs = sentinel.lhs + m_rhs = sentinel.rhs + resolve = Resolve(lhs=m_lhs, rhs=m_rhs) + # The lhs_cube and rhs_cube are only None due + # to __call__ being mocked. See Test___call__ + # for appropriate test coverage. + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self._assert_members_none(resolve) + self.assertEqual(1, self.m_call.call_count) + call_args = mock.call(m_lhs, m_rhs) + self.assertEqual(call_args, self.m_call.call_args) + + +class Test___call__(tests.IrisTest): + def setUp(self): + self.m_lhs = mock.MagicMock(spec=Cube) + self.m_rhs = mock.MagicMock(spec=Cube) + target = "iris.common.resolve.Resolve.{method}" + method = target.format(method="_metadata_resolve") + self.m_metadata_resolve = self.patch(method) + method = target.format(method="_metadata_coverage") + self.m_metadata_coverage = self.patch(method) + method = target.format(method="_metadata_mapping") + self.m_metadata_mapping = self.patch(method) + method = target.format(method="_metadata_prepare") + self.m_metadata_prepare = self.patch(method) + + def test_lhs_not_cube(self): + emsg = "'LHS' argument to be a 'Cube'" + with self.assertRaisesRegex(TypeError, emsg): + _ = Resolve(rhs=self.m_rhs) + + def test_rhs_not_cube(self): + emsg = "'RHS' argument to be a 'Cube'" + with self.assertRaisesRegex(TypeError, emsg): + _ = Resolve(lhs=self.m_lhs) + + def _assert_called_metadata_methods(self): + call_args = mock.call() + self.assertEqual(1, self.m_metadata_resolve.call_count) + self.assertEqual(call_args, self.m_metadata_resolve.call_args) + self.assertEqual(1, self.m_metadata_coverage.call_count) + self.assertEqual(call_args, self.m_metadata_coverage.call_args) + self.assertEqual(1, self.m_metadata_mapping.call_count) + self.assertEqual(call_args, self.m_metadata_mapping.call_args) + self.assertEqual(1, self.m_metadata_prepare.call_count) + self.assertEqual(call_args, self.m_metadata_prepare.call_args) + + def test_map_rhs_to_lhs__less_than(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 1 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertTrue(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + def test_map_rhs_to_lhs__equal(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 2 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertTrue(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + def test_map_lhs_to_rhs(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 3 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertFalse(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + +class Test__categorise_items(tests.IrisTest): + def setUp(self): + self.coord_dims = {} + # configure dim coords + coord = mock.Mock(metadata=sentinel.dim_metadata1) + self.dim_coords = [coord] + self.coord_dims[coord] = sentinel.dims1 + # configure aux and scalar coords + self.aux_coords = [] + pairs = [ + (sentinel.aux_metadata2, sentinel.dims2), + (sentinel.aux_metadata3, sentinel.dims3), + (sentinel.scalar_metadata4, None), + (sentinel.scalar_metadata5, None), + (sentinel.scalar_metadata6, None), + ] + for metadata, dims in pairs: + coord = mock.Mock(metadata=metadata) + self.aux_coords.append(coord) + self.coord_dims[coord] = dims + func = lambda coord: self.coord_dims[coord] + self.cube = mock.Mock( + aux_coords=self.aux_coords, + dim_coords=self.dim_coords, + coord_dims=func, + ) + + def test(self): + result = Resolve._categorise_items(self.cube) + self.assertIsInstance(result, _CategoryItems) + self.assertEqual(1, len(result.items_dim)) + # check dim coords + for item in result.items_dim: + self.assertIsInstance(item, _Item) + (coord,) = self.dim_coords + dims = self.coord_dims[coord] + expected = [_Item(metadata=coord.metadata, coord=coord, dims=dims)] + self.assertEqual(expected, result.items_dim) + # check aux coords + self.assertEqual(2, len(result.items_aux)) + for item in result.items_aux: + self.assertIsInstance(item, _Item) + expected_aux, expected_scalar = [], [] + for coord in self.aux_coords: + dims = self.coord_dims[coord] + item = _Item(metadata=coord.metadata, coord=coord, dims=dims) + if dims: + expected_aux.append(item) + else: + expected_scalar.append(item) + self.assertEqual(expected_aux, result.items_aux) + # check scalar coords + self.assertEqual(3, len(result.items_scalar)) + for item in result.items_scalar: + self.assertIsInstance(item, _Item) + self.assertEqual(expected_scalar, result.items_scalar) + + +class Test__metadata_resolve(tests.IrisTest): + def setUp(self): + self.target = "iris.common.resolve.Resolve._categorise_items" + self.m_lhs_cube = sentinel.lhs_cube + self.m_rhs_cube = sentinel.rhs_cube + + @staticmethod + def _create_items(pairs): + # this wrapper (hack) is necessary in order to support mocking + # the "name" method (callable) of the metadata, as "name" is already + # part of the mock API - this is always troublesome in mock-world. + Wrapper = namedtuple("Wrapper", ("name", "value")) + result = [] + for name, dims in pairs: + metadata = Wrapper(name=lambda: str(name), value=name) + coord = mock.Mock(metadata=metadata) + item = _Item(metadata=metadata, coord=coord, dims=dims) + result.append(item) + return result + + def test_metadata_same(self): + category = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + # configure dim coords + pairs = [(sentinel.dim_metadata1, sentinel.dims1)] + category.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims2), + (sentinel.aux_metadata2, sentinel.dims3), + ] + category.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + (sentinel.scalar_metadata3, None), + ] + category.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category, category) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # require to explicitly configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(mocker.call_count, 2) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category, resolve.lhs_cube_category) + self.assertEqual(category, resolve.rhs_cube_category) + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, resolve.lhs_cube_category_local) + self.assertEqual(expected, resolve.rhs_cube_category_local) + self.assertEqual(category, resolve.category_common) + + def test_metadata_overlap(self): + # configure the lhs cube category + category_lhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata1, sentinel.dims1), + (sentinel.dim_metadata2, sentinel.dims2), + ] + category_lhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims3), + (sentinel.aux_metadata2, sentinel.dims4), + ] + category_lhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + ] + category_lhs.items_scalar.extend(self._create_items(pairs)) + + # configure the rhs cube category + category_rhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + category_rhs.items_dim.append(category_lhs.items_dim[0]) + pairs = [(sentinel.dim_metadata200, sentinel.dims2)] + category_rhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + category_rhs.items_aux.append(category_lhs.items_aux[0]) + pairs = [(sentinel.aux_metadata200, sentinel.dims4)] + category_rhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + category_rhs.items_scalar.append(category_lhs.items_scalar[0]) + pairs = [(sentinel.scalar_metadata200, None)] + category_rhs.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category_lhs, category_rhs) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # require to explicitly configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(2, mocker.call_count) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category_lhs, resolve.lhs_cube_category) + self.assertEqual(category_rhs, resolve.rhs_cube_category) + + items_dim = [category_lhs.items_dim[1]] + items_aux = [category_lhs.items_aux[1]] + items_scalar = [category_lhs.items_scalar[1]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.lhs_cube_category_local) + + items_dim = [category_rhs.items_dim[1]] + items_aux = [category_rhs.items_aux[1]] + items_scalar = [category_rhs.items_scalar[1]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.rhs_cube_category_local) + + items_dim = [category_lhs.items_dim[0]] + items_aux = [category_lhs.items_aux[0]] + items_scalar = [category_lhs.items_scalar[0]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.category_common) + + def test_metadata_different(self): + # configure the lhs cube category + category_lhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata1, sentinel.dims1), + (sentinel.dim_metadata2, sentinel.dims2), + ] + category_lhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims3), + (sentinel.aux_metadata2, sentinel.dims4), + ] + category_lhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + ] + category_lhs.items_scalar.extend(self._create_items(pairs)) + + # configure the rhs cube category + category_rhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata100, sentinel.dims1), + (sentinel.dim_metadata200, sentinel.dims2), + ] + category_rhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata100, sentinel.dims3), + (sentinel.aux_metadata200, sentinel.dims4), + ] + category_rhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata100, None), + (sentinel.scalar_metadata200, None), + ] + category_rhs.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category_lhs, category_rhs) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # first require to explicitly lhs/rhs configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(2, mocker.call_count) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category_lhs, resolve.lhs_cube_category) + self.assertEqual(category_rhs, resolve.rhs_cube_category) + self.assertEqual(category_lhs, resolve.lhs_cube_category_local) + self.assertEqual(category_rhs, resolve.rhs_cube_category_local) + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, resolve.category_common) + + +class Test__dim_coverage(tests.IrisTest): + def setUp(self): + self.ndim = 4 + self.cube = mock.Mock(ndim=self.ndim) + self.items = [] + parts = [ + (sentinel.metadata0, sentinel.coord0, (0,)), + (sentinel.metadata1, sentinel.coord1, (1,)), + (sentinel.metadata2, sentinel.coord2, (2,)), + (sentinel.metadata3, sentinel.coord3, (3,)), + ] + column_parts = [x for x in zip(*parts)] + self.metadata, self.coords, self.dims = [list(x) for x in column_parts] + self.dims = [dim for dim, in self.dims] + for metadata, coord, dims in parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items.append(item) + + def test_coverage_no_local_no_common_all_free(self): + items = [] + common = [] + result = Resolve._dim_coverage(self.cube, items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + expected = [None] * self.ndim + self.assertEqual(expected, result.metadata) + self.assertEqual(expected, result.coords) + self.assertEqual([], result.dims_common) + self.assertEqual([], result.dims_local) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_free) + + def test_coverage_all_local_no_common_no_free(self): + common = [] + result = Resolve._dim_coverage(self.cube, self.items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.metadata, result.metadata) + self.assertEqual(self.coords, result.coords) + self.assertEqual([], result.dims_common) + self.assertEqual(self.dims, result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_no_local_all_common_no_free(self): + result = Resolve._dim_coverage(self.cube, self.items, self.metadata) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.metadata, result.metadata) + self.assertEqual(self.coords, result.coords) + self.assertEqual(self.dims, result.dims_common) + self.assertEqual([], result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_mixed(self): + common = [self.items[1].metadata, self.items[2].metadata] + self.items.pop(0) + self.items.pop(-1) + metadata, coord, dims = sentinel.metadata100, sentinel.coord100, (0,) + self.items.append(_Item(metadata=metadata, coord=coord, dims=dims)) + result = Resolve._dim_coverage(self.cube, self.items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + expected = [ + metadata, + self.items[0].metadata, + self.items[1].metadata, + None, + ] + self.assertEqual(expected, result.metadata) + expected = [coord, self.items[0].coord, self.items[1].coord, None] + self.assertEqual(expected, result.coords) + self.assertEqual([1, 2], result.dims_common) + self.assertEqual([0], result.dims_local) + self.assertEqual([3], result.dims_free) + + +class Test__aux_coverage(tests.IrisTest): + def setUp(self): + self.ndim = 4 + self.cube = mock.Mock(ndim=self.ndim) + # configure aux coords + self.items_aux = [] + aux_parts = [ + (sentinel.aux_metadata0, sentinel.aux_coord0, (0,)), + (sentinel.aux_metadata1, sentinel.aux_coord1, (1,)), + (sentinel.aux_metadata23, sentinel.aux_coord23, (2, 3)), + ] + column_aux_parts = [x for x in zip(*aux_parts)] + self.aux_metadata, self.aux_coords, self.aux_dims = [ + list(x) for x in column_aux_parts + ] + for metadata, coord, dims in aux_parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items_aux.append(item) + # configure scalar coords + self.items_scalar = [] + scalar_parts = [ + (sentinel.scalar_metadata0, sentinel.scalar_coord0, ()), + (sentinel.scalar_metadata1, sentinel.scalar_coord1, ()), + (sentinel.scalar_metadata2, sentinel.scalar_coord2, ()), + ] + column_scalar_parts = [x for x in zip(*scalar_parts)] + self.scalar_metadata, self.scalar_coords, self.scalar_dims = [ + list(x) for x in column_scalar_parts + ] + for metadata, coord, dims in scalar_parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items_scalar.append(item) + + def test_coverage_no_local_no_common_all_free(self): + items_aux, items_scalar = [], [] + common_aux, common_scalar = [], [] + result = Resolve._aux_coverage( + self.cube, items_aux, items_scalar, common_aux, common_scalar + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual([], result.common_items_aux) + self.assertEqual([], result.common_items_scalar) + self.assertEqual([], result.local_items_aux) + self.assertEqual([], result.local_items_scalar) + self.assertEqual([], result.dims_common) + self.assertEqual([], result.dims_local) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_free) + + def test_coverage_all_local_no_common_no_free(self): + common_aux, common_scalar = [], [] + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + common_aux, + common_scalar, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + expected = [] + self.assertEqual(expected, result.common_items_aux) + self.assertEqual(expected, result.common_items_scalar) + self.assertEqual(self.items_aux, result.local_items_aux) + self.assertEqual(self.items_scalar, result.local_items_scalar) + self.assertEqual([], result.dims_common) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_no_local_all_common_no_free(self): + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + self.aux_metadata, + self.scalar_metadata, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.items_aux, result.common_items_aux) + self.assertEqual(self.items_scalar, result.common_items_scalar) + self.assertEqual([], result.local_items_aux) + self.assertEqual([], result.local_items_scalar) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_common) + self.assertEqual([], result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_mixed(self): + common_aux = [self.items_aux[-1].metadata] + common_scalar = [self.items_scalar[1].metadata] + self.items_aux.pop(1) + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + common_aux, + common_scalar, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + expected = [self.items_aux[-1]] + self.assertEqual(expected, result.common_items_aux) + expected = [self.items_scalar[1]] + self.assertEqual(expected, result.common_items_scalar) + expected = [self.items_aux[0]] + self.assertEqual(expected, result.local_items_aux) + expected = [self.items_scalar[0], self.items_scalar[2]] + self.assertEqual(expected, result.local_items_scalar) + self.assertEqual([2, 3], result.dims_common) + self.assertEqual([0], result.dims_local) + self.assertEqual([1], result.dims_free) + + +class Test__metadata_coverage(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.m_lhs_cube = sentinel.lhs_cube + self.resolve.lhs_cube = self.m_lhs_cube + self.m_rhs_cube = sentinel.rhs_cube + self.resolve.rhs_cube = self.m_rhs_cube + self.m_items_dim_metadata = sentinel.items_dim_metadata + self.m_items_aux_metadata = sentinel.items_aux_metadata + self.m_items_scalar_metadata = sentinel.items_scalar_metadata + items_dim = [mock.Mock(metadata=self.m_items_dim_metadata)] + items_aux = [mock.Mock(metadata=self.m_items_aux_metadata)] + items_scalar = [mock.Mock(metadata=self.m_items_scalar_metadata)] + category = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.resolve.category_common = category + self.m_items_dim = sentinel.items_dim + self.m_items_aux = sentinel.items_aux + self.m_items_scalar = sentinel.items_scalar + category = _CategoryItems( + items_dim=self.m_items_dim, + items_aux=self.m_items_aux, + items_scalar=self.m_items_scalar, + ) + self.resolve.lhs_cube_category = category + self.resolve.rhs_cube_category = category + target = "iris.common.resolve.Resolve._dim_coverage" + self.m_lhs_cube_dim_coverage = sentinel.lhs_cube_dim_coverage + self.m_rhs_cube_dim_coverage = sentinel.rhs_cube_dim_coverage + side_effect = ( + self.m_lhs_cube_dim_coverage, + self.m_rhs_cube_dim_coverage, + ) + self.mocker_dim_coverage = self.patch(target, side_effect=side_effect) + target = "iris.common.resolve.Resolve._aux_coverage" + self.m_lhs_cube_aux_coverage = sentinel.lhs_cube_aux_coverage + self.m_rhs_cube_aux_coverage = sentinel.rhs_cube_aux_coverage + side_effect = ( + self.m_lhs_cube_aux_coverage, + self.m_rhs_cube_aux_coverage, + ) + self.mocker_aux_coverage = self.patch(target, side_effect=side_effect) + + def test(self): + self.resolve._metadata_coverage() + self.assertEqual(2, self.mocker_dim_coverage.call_count) + calls = [ + mock.call( + self.m_lhs_cube, self.m_items_dim, [self.m_items_dim_metadata] + ), + mock.call( + self.m_rhs_cube, self.m_items_dim, [self.m_items_dim_metadata] + ), + ] + self.assertEqual(calls, self.mocker_dim_coverage.call_args_list) + self.assertEqual(2, self.mocker_aux_coverage.call_count) + calls = [ + mock.call( + self.m_lhs_cube, + self.m_items_aux, + self.m_items_scalar, + [self.m_items_aux_metadata], + [self.m_items_scalar_metadata], + ), + mock.call( + self.m_rhs_cube, + self.m_items_aux, + self.m_items_scalar, + [self.m_items_aux_metadata], + [self.m_items_scalar_metadata], + ), + ] + self.assertEqual(calls, self.mocker_aux_coverage.call_args_list) + self.assertEqual( + self.m_lhs_cube_dim_coverage, self.resolve.lhs_cube_dim_coverage + ) + self.assertEqual( + self.m_rhs_cube_dim_coverage, self.resolve.rhs_cube_dim_coverage + ) + self.assertEqual( + self.m_lhs_cube_aux_coverage, self.resolve.lhs_cube_aux_coverage + ) + self.assertEqual( + self.m_rhs_cube_aux_coverage, self.resolve.rhs_cube_aux_coverage + ) + + +class Test__dim_mapping(tests.IrisTest): + def setUp(self): + self.ndim = 3 + Wrapper = namedtuple("Wrapper", ("name",)) + cube = Wrapper(name=lambda: sentinel.name) + self.src_coverage = _DimCoverage( + cube=cube, + metadata=[], + coords=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.tgt_coverage = _DimCoverage( + cube=cube, + metadata=[], + coords=None, + dims_common=[], + dims_local=None, + dims_free=None, + ) + self.metadata = [ + sentinel.metadata_0, + sentinel.metadata_1, + sentinel.metadata_2, + ] + self.dummy = [sentinel.dummy_0, sentinel.dummy_1, sentinel.dummy_2] + + def test_no_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.tgt_coverage.metadata.extend(self.dummy) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + self.assertEqual(dict(), result) + + def test_full_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = list(range(self.ndim)) + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1, 2: 2} + self.assertEqual(expected, result) + + def test_transpose_mapping(self): + self.src_coverage.metadata.extend(self.metadata[::-1]) + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = list(range(self.ndim)) + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 1: 1, 2: 0} + self.assertEqual(expected, result) + + def test_partial_mapping__transposed(self): + self.src_coverage.metadata.extend(self.metadata) + self.metadata[1] = sentinel.nope + self.tgt_coverage.metadata.extend(self.metadata[::-1]) + dims_common = [0, 2] + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 2: 0} + self.assertEqual(expected, result) + + def test_bad_metadata_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.metadata[0] = sentinel.bad + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = [0] + self.tgt_coverage.dims_common.extend(dims_common) + emsg = "Failed to map common dim coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + _ = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + + +class Test__aux_mapping(tests.IrisTest): + def setUp(self): + self.ndim = 3 + Wrapper = namedtuple("Wrapper", ("name",)) + cube = Wrapper(name=lambda: sentinel.name) + self.src_coverage = _AuxCoverage( + cube=cube, + common_items_aux=[], + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.tgt_coverage = _AuxCoverage( + cube=cube, + common_items_aux=[], + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.items = [ + _Item( + metadata=sentinel.metadata0, coord=sentinel.coord0, dims=[0] + ), + _Item( + metadata=sentinel.metadata1, coord=sentinel.coord1, dims=[1] + ), + _Item( + metadata=sentinel.metadata2, coord=sentinel.coord2, dims=[2] + ), + ] + + def _copy(self, items): + # Due to a bug in python 3.6.x, performing a deepcopy of a mock.sentinel + # will yield an object that is not equivalent to its parent, so this + # is a work-around until we drop support for python 3.6.x. + import sys + + version = sys.version_info + major, minor = version.major, version.minor + result = deepcopy(items) + if major == 3 and minor <= 6: + for i, item in enumerate(items): + result[i] = result[i]._replace(metadata=item.metadata) + return result + + def test_no_mapping(self): + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + self.assertEqual(dict(), result) + + def test_full_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + self.tgt_coverage.common_items_aux.extend(self.items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1, 2: 2} + self.assertEqual(expected, result) + + def test_transpose_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0].dims[0] = 2 + items[2].dims[0] = 0 + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 1: 1, 2: 0} + self.assertEqual(expected, result) + + def test_partial_mapping__transposed(self): + _ = self.items.pop(1) + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0].dims[0] = 2 + items[1].dims[0] = 0 + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 2: 0} + self.assertEqual(expected, result) + + def test_mapping__match_multiple_src_metadata(self): + items = self._copy(self.items) + _ = self.items.pop(1) + self.src_coverage.common_items_aux.extend(self.items) + items[1] = items[0] + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 2: 2} + self.assertEqual(expected, result) + + def test_mapping__skip_match_multiple_src_metadata(self): + items = self._copy(self.items) + _ = self.items.pop(1) + self.tgt_coverage.common_items_aux.extend(self.items) + items[1] = items[0]._replace(dims=[1]) + self.src_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {2: 2} + self.assertEqual(expected, result) + + def test_mapping__skip_different_rank(self): + items = self._copy(self.items) + self.src_coverage.common_items_aux.extend(self.items) + items[2] = items[2]._replace(dims=[1, 2]) + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1} + self.assertEqual(expected, result) + + def test_bad_metadata_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0] = items[0]._replace(metadata=sentinel.bad) + self.tgt_coverage.common_items_aux.extend(items) + emsg = "Failed to map common aux coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + _ = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + + +class Test_mapped(tests.IrisTest): + def test_mapping_none(self): + resolve = Resolve() + self.assertIsNone(resolve.mapping) + self.assertIsNone(resolve.mapped) + + def test_mapped__src_cube_lhs(self): + resolve = Resolve() + lhs = mock.Mock(ndim=2) + rhs = mock.Mock(ndim=3) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = False + resolve.mapping = {0: 0, 1: 1} + self.assertTrue(resolve.mapped) + + def test_mapped__src_cube_rhs(self): + resolve = Resolve() + lhs = mock.Mock(ndim=3) + rhs = mock.Mock(ndim=2) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = True + resolve.mapping = {0: 0, 1: 1} + self.assertTrue(resolve.mapped) + + def test_partial_mapping(self): + resolve = Resolve() + lhs = mock.Mock(ndim=3) + rhs = mock.Mock(ndim=2) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = True + resolve.mapping = {0: 0} + self.assertFalse(resolve.mapped) + + +class Test__free_mapping(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Wrapper", ("name", "ndim", "shape")) + self.src_dim_coverage = dict( + cube=None, + metadata=None, + coords=None, + dims_common=None, + dims_local=None, + dims_free=[], + ) + self.tgt_dim_coverage = deepcopy(self.src_dim_coverage) + self.src_aux_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=[], + ) + self.tgt_aux_coverage = deepcopy(self.src_aux_coverage) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.mapping = {} + + def _make_args(self): + args = dict( + src_dim_coverage=_DimCoverage(**self.src_dim_coverage), + tgt_dim_coverage=_DimCoverage(**self.tgt_dim_coverage), + src_aux_coverage=_AuxCoverage(**self.src_aux_coverage), + tgt_aux_coverage=_AuxCoverage(**self.tgt_aux_coverage), + ) + return args + + def test_mapping_no_dims_free(self): + ndim = 4 + shape = tuple(range(ndim)) + cube = self.Cube(name=lambda: "name", ndim=ndim, shape=shape) + self.src_dim_coverage["cube"] = cube + self.tgt_dim_coverage["cube"] = cube + args = self._make_args() + emsg = "Insufficient matching coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + def _make_coverage(self, name, shape, dims_free): + if name == "src": + dim_coverage = self.src_dim_coverage + aux_coverage = self.src_aux_coverage + else: + dim_coverage = self.tgt_dim_coverage + aux_coverage = self.tgt_aux_coverage + ndim = len(shape) + cube = self.Cube(name=lambda: name, ndim=ndim, shape=shape) + dim_coverage["cube"] = cube + dim_coverage["dims_free"].extend(dims_free) + aux_coverage["cube"] = cube + aux_coverage["dims_free"].extend(dims_free) + + def test_mapping_src_free_to_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state f l c l state f c f + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 4 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (1, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 1 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 1 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->1 1->2 2->3 + src_shape = (1, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 1, 1: 2, 2: 3} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state f f c f state f c f + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 4 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (1, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 1 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (2, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 1 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (1, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt__fail(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 5 + # state f f c f state f c f + # coord d d d a coord a d d + # fail ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->? + src_shape = (2, 3, 5) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + emsg = "Insufficient matching coordinate metadata to resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + def test_mapping_tgt_free_to_src_local(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 1 3 2 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 1, 3, 2) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 1 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 1) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 1 3 1 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->1 1->2 2->3 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 1, 3, 1) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 1, 1: 2, 2: 3} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_no_free__fail(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 5 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # fail ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->? + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 5) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + emsg = "Insufficient matching coordinate metadata to resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + +class Test__src_cube(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._src_cube) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._src_cube) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube + + +class Test__src_cube_position(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.assertEqual("RHS", self.resolve._src_cube_position) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.assertEqual("LHS", self.resolve._src_cube_position) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_position + + +class Test__src_cube_resolved__getter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._src_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._src_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_resolved + + +class Test__src_cube_resolved__setter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve._src_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.rhs_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve._src_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.lhs_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_resolved = self.expected + + +class Test__tgt_cube(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.rhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.lhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube + + +class Test__tgt_cube_position(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.assertEqual("RHS", self.resolve._tgt_cube_position) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.assertEqual("LHS", self.resolve._tgt_cube_position) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_position + + +class Test__tgt_cube_resolved__getter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.rhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.lhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_resolved + + +class Test__tgt_cube_resolved__setter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve._tgt_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.rhs_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve._tgt_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.lhs_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_resolved = self.expected + + +class Test_shape(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_no_shape(self): + self.assertIsNone(self.resolve.shape) + + def test_shape(self): + expected = sentinel.shape + self.resolve._broadcast_shape = expected + self.assertEqual(expected, self.resolve.shape) + + +class Test__as_compatible_cubes(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple( + "Wrapper", + ( + "name", + "ndim", + "shape", + "metadata", + "core_data", + "coord_dims", + "dim_coords", + "aux_coords", + "aux_factories", + ), + ) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.mapping = {} + self.mocker = self.patch("iris.cube.Cube") + self.args = dict( + name=None, + ndim=None, + shape=None, + metadata=None, + core_data=None, + coord_dims=None, + dim_coords=None, + aux_coords=None, + aux_factories=None, + ) + + def _make_cube(self, name, shape, transpose_shape=None): + self.args["name"] = lambda: name + ndim = len(shape) + self.args["ndim"] = ndim + self.args["shape"] = shape + if name == "src": + self.args["metadata"] = sentinel.metadata + self.reshape = sentinel.reshape + m_reshape = mock.Mock(return_value=self.reshape) + self.transpose = mock.Mock( + shape=transpose_shape, reshape=m_reshape + ) + m_transpose = mock.Mock(return_value=self.transpose) + self.data = mock.Mock( + shape=shape, transpose=m_transpose, reshape=m_reshape + ) + m_copy = mock.Mock(return_value=self.data) + m_core_data = mock.Mock(copy=m_copy) + self.args["core_data"] = mock.Mock(return_value=m_core_data) + self.args["coord_dims"] = mock.Mock(side_effect=([0], [ndim - 1])) + self.dim_coord = sentinel.dim_coord + self.aux_coord = sentinel.aux_coord + self.aux_factory = sentinel.aux_factory + self.args["dim_coords"] = [self.dim_coord] + self.args["aux_coords"] = [self.aux_coord] + self.args["aux_factories"] = [self.aux_factory] + cube = self.Cube(**self.args) + self.resolve.rhs_cube = cube + self.cube = mock.Mock() + self.mocker.return_value = self.cube + else: + cube = self.Cube(**self.args) + self.resolve.lhs_cube = cube + + def test_incomplete_src_to_tgt_mapping__fail(self): + src_shape = (1, 2) + self._make_cube("src", src_shape) + tgt_shape = (3, 4) + self._make_cube("tgt", tgt_shape) + with self.assertRaises(AssertionError): + self.resolve._as_compatible_cubes() + + def test_incompatible_shapes__fail(self): + # key: (state) c=common, f=free + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 2 3 4 shape 2 3 5 + # state f c c c state c c c + # fail ^ fail ^ + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + src_shape = (2, 3, 5) + self._make_cube("src", src_shape) + tgt_shape = (2, 2, 3, 4) + self._make_cube("tgt", tgt_shape) + self.resolve.mapping = {0: 1, 1: 2, 2: 3} + emsg = "Cannot resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._as_compatible_cubes() + + def test_incompatible_shapes__fail_broadcast(self): + # key: (state) c=common, f=free + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 5 + # state f c c c state c c c + # fail ^ fail ^ + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 3, 5) + self._make_cube("src", src_shape) + tgt_shape = (2, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + self.resolve.mapping = {0: 3, 1: 2, 2: 1} + emsg = "Cannot resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._as_compatible_cubes() + + def _check_compatible(self, broadcast_shape): + self.assertEqual( + self.resolve.lhs_cube, self.resolve._tgt_cube_resolved + ) + self.assertEqual(self.cube, self.resolve._src_cube_resolved) + self.assertEqual(broadcast_shape, self.resolve._broadcast_shape) + self.assertEqual(1, self.mocker.call_count) + self.assertEqual(self.args["metadata"], self.cube.metadata) + self.assertEqual(2, self.resolve.rhs_cube.coord_dims.call_count) + self.assertEqual( + [mock.call(self.dim_coord), mock.call(self.aux_coord)], + self.resolve.rhs_cube.coord_dims.call_args_list, + ) + self.assertEqual(1, self.cube.add_dim_coord.call_count) + self.assertEqual( + [mock.call(self.dim_coord, [self.resolve.mapping[0]])], + self.cube.add_dim_coord.call_args_list, + ) + self.assertEqual(1, self.cube.add_aux_coord.call_count) + self.assertEqual( + [mock.call(self.aux_coord, [self.resolve.mapping[2]])], + self.cube.add_aux_coord.call_args_list, + ) + self.assertEqual(1, self.cube.add_aux_factory.call_count) + self.assertEqual( + [mock.call(self.aux_factory)], + self.cube.add_aux_factory.call_args_list, + ) + + def test_compatible(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 4 3 2 + # state c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + src_shape = (4, 3, 2) + self._make_cube("src", src_shape) + tgt_shape = (4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 0, 1: 1, 2: 2} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual([mock.call(self.data)], self.mocker.call_args_list) + + def test_compatible__transpose(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 2 3 4 + # state c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->2, 1->1, 2->0 + src_shape = (2, 3, 4) + self._make_cube("src", src_shape, transpose_shape=(4, 3, 2)) + tgt_shape = (4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 2, 1: 1, 2: 0} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual( + [mock.call(self.transpose)], self.mocker.call_args_list + ) + + def test_compatible__reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + src_shape = (4, 3, 2) + self._make_cube("src", src_shape) + tgt_shape = (5, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + src_shape)], self.data.reshape.call_args_list + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + def test_compatible__transpose_reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 2 3 4 + # state f c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 3, 4) + transpose_shape = (4, 3, 2) + self._make_cube("src", src_shape, transpose_shape=transpose_shape) + tgt_shape = (5, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 3, 1: 2, 2: 1} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + transpose_shape)], + self.data.reshape.call_args_list, + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + def test_compatible__broadcast(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 1 3 2 shape 4 1 2 + # state c c c state c c c + # coord d a + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + src_shape = (4, 1, 2) + self._make_cube("src", src_shape) + tgt_shape = (1, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 0, 1: 1, 2: 2} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=(4, 3, 2)) + self.assertEqual([mock.call(self.data)], self.mocker.call_args_list) + + def test_compatible__broadcast_transpose_reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 1 3 2 shape 2 1 4 + # state f c c c state c c c + # coord d a + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 1, 4) + transpose_shape = (4, 1, 2) + self._make_cube("src", src_shape) + tgt_shape = (5, 1, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 3, 1: 2, 2: 1} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=(5, 4, 3, 2)) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + transpose_shape)], + self.data.reshape.call_args_list, + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + +class Test__metadata_mapping(tests.IrisTest): + def setUp(self): + self.ndim = sentinel.ndim + self.src_cube = mock.Mock(ndim=self.ndim) + self.src_dim_coverage = mock.Mock(dims_free=[]) + self.src_aux_coverage = mock.Mock(dims_free=[]) + self.tgt_cube = mock.Mock(ndim=self.ndim) + self.tgt_dim_coverage = mock.Mock(dims_free=[]) + self.tgt_aux_coverage = mock.Mock(dims_free=[]) + self.resolve = Resolve() + self.map_rhs_to_lhs = True + self.resolve.map_rhs_to_lhs = self.map_rhs_to_lhs + self.resolve.rhs_cube = self.src_cube + self.resolve.rhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.lhs_cube = self.tgt_cube + self.resolve.lhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.tgt_aux_coverage + self.resolve.mapping = {} + self.shape = sentinel.shape + self.resolve._broadcast_shape = self.shape + self.resolve._src_cube_resolved = mock.Mock(shape=self.shape) + self.resolve._tgt_cube_resolved = mock.Mock(shape=self.shape) + self.m_dim_mapping = self.patch( + "iris.common.resolve.Resolve._dim_mapping", return_value={} + ) + self.m_aux_mapping = self.patch( + "iris.common.resolve.Resolve._aux_mapping", return_value={} + ) + self.m_free_mapping = self.patch( + "iris.common.resolve.Resolve._free_mapping" + ) + self.m_as_compatible_cubes = self.patch( + "iris.common.resolve.Resolve._as_compatible_cubes" + ) + self.mapping = {0: 1, 1: 2, 2: 3} + + def test_mapped__dim_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d d d coord d d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = self.mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(0, self.m_aux_mapping.call_count) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__aux_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord a a a coord a a a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.src_cube.ndim = 3 + self.m_aux_mapping.return_value = self.mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_and_aux_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d a d coord d a d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + dim_mapping = {0: 1, 2: 3} + aux_mapping = {1: 2} + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + self.m_aux_mapping.return_value = aux_mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_coords_and_free_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l f c c state f c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + dim_mapping = {1: 2, 2: 3} + free_mapping = {0: 1} + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + side_effect = lambda a, b, c, d: self.resolve.mapping.update( + free_mapping + ) + self.m_free_mapping.side_effect = side_effect + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(1, self.m_free_mapping.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.tgt_dim_coverage, + self.src_aux_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_free_mapping.call_args_list) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_coords_with_broadcast_flip(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 4 dims 0 1 2 4 + # shape 1 4 3 2 shape 5 4 3 2 + # state c c c c state c c c c + # coord d d d d coord d d d d + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2, 3->3 + mapping = {0: 0, 1: 1, 2: 2, 3: 3} + self.src_cube.ndim = 4 + self.tgt_cube.ndim = 4 + self.m_dim_mapping.return_value = mapping + broadcast_shape = (5, 4, 3, 2) + self.resolve._broadcast_shape = broadcast_shape + self.resolve._src_cube_resolved.shape = broadcast_shape + self.resolve._tgt_cube_resolved.shape = (1, 4, 3, 2) + self.resolve._metadata_mapping() + self.assertEqual(mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(0, self.m_aux_mapping.call_count) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(2, self.m_as_compatible_cubes.call_count) + self.assertEqual(not self.map_rhs_to_lhs, self.resolve.map_rhs_to_lhs) + + def test_mapped__dim_coords_free_flip_with_free_flip(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 4 3 2 + # state f f c state l l c + # coord d coord d d d + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + dim_mapping = {2: 2} + free_mapping = {0: 0, 1: 1} + mapping = {0: 0, 1: 1, 2: 2} + self.src_cube.ndim = 3 + self.tgt_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + side_effect = lambda a, b, c, d: self.resolve.mapping.update( + free_mapping + ) + self.m_free_mapping.side_effect = side_effect + self.tgt_dim_coverage.dims_free = [0, 1] + self.tgt_aux_coverage.dims_free = [0, 1] + self.resolve._metadata_mapping() + self.assertEqual(mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(1, self.m_free_mapping.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.tgt_dim_coverage, + self.src_aux_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_free_mapping.call_args_list) + self.assertEqual(2, self.m_as_compatible_cubes.call_count) + + +class Test__prepare_common_dim_payload(tests.IrisTest): + def setUp(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l c c c state c c c + # coord d d d coord d d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.points = (sentinel.points_0, sentinel.points_1, sentinel.points_2) + self.bounds = (sentinel.bounds_0, sentinel.bounds_1, sentinel.bounds_2) + self.pb_0 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[0])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[0])), + ) + self.pb_1 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[1])), + None, + ) + self.pb_2 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[2])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[2])), + ) + side_effect = (self.pb_0, self.pb_1, self.pb_2) + self.m_prepare_points_and_bounds = self.patch( + "iris.common.resolve.Resolve._prepare_points_and_bounds", + side_effect=side_effect, + ) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = self.mapping + self.metadata_combined = ( + sentinel.combined_0, + sentinel.combined_1, + sentinel.combined_2, + ) + self.src_metadata = mock.Mock( + combine=mock.Mock(side_effect=self.metadata_combined) + ) + metadata = [self.src_metadata] * len(self.mapping) + self.src_coords = [ + sentinel.src_coord_0, + sentinel.src_coord_1, + sentinel.src_coord_2, + ] + self.src_dims_common = [0, 1, 2] + self.container = DimCoord + self.src_dim_coverage = _DimCoverage( + cube=None, + metadata=metadata, + coords=self.src_coords, + dims_common=self.src_dims_common, + dims_local=[], + dims_free=[], + ) + self.tgt_metadata = [ + sentinel.tgt_metadata_0, + sentinel.tgt_metadata_1, + sentinel.tgt_metadata_2, + sentinel.tgt_metadata_3, + ] + self.tgt_coords = [ + sentinel.tgt_coord_0, + sentinel.tgt_coord_1, + sentinel.tgt_coord_2, + sentinel.tgt_coord_3, + ] + self.tgt_dims_common = [1, 2, 3] + self.tgt_dim_coverage = _DimCoverage( + cube=None, + metadata=self.tgt_metadata, + coords=self.tgt_coords, + dims_common=self.tgt_dims_common, + dims_local=[], + dims_free=[], + ) + + def _check(self, ignore_mismatch=None, bad_points=None): + if bad_points is None: + bad_points = False + self.resolve._prepare_common_dim_payload( + self.src_dim_coverage, + self.tgt_dim_coverage, + ignore_mismatch=ignore_mismatch, + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + if not bad_points: + self.assertEqual(3, len(self.resolve.prepared_category.items_dim)) + expected = [ + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[0], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[0]], + ), + points=self.points[0], + bounds=self.bounds[0], + dims=(self.mapping[0],), + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[1], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[1]], + ), + points=self.points[1], + bounds=None, + dims=(self.mapping[1],), + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[2], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[2]], + ), + points=self.points[2], + bounds=self.bounds[2], + dims=(self.mapping[2],), + container=self.container, + ), + ] + self.assertEqual( + expected, self.resolve.prepared_category.items_dim + ) + else: + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(3, self.m_prepare_points_and_bounds.call_count) + if ignore_mismatch is None: + ignore_mismatch = False + expected = [ + mock.call( + self.src_coords[0], + self.tgt_coords[self.mapping[0]], + 0, + 1, + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[1], + self.tgt_coords[self.mapping[1]], + 1, + 2, + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[2], + self.tgt_coords[self.mapping[2]], + 2, + 3, + ignore_mismatch=ignore_mismatch, + ), + ] + self.assertEqual( + expected, self.m_prepare_points_and_bounds.call_args_list + ) + if not bad_points: + self.assertEqual(3, self.src_metadata.combine.call_count) + expected = [ + mock.call(metadata) for metadata in self.tgt_metadata[1:] + ] + self.assertEqual( + expected, self.src_metadata.combine.call_args_list + ) + + def test__default_ignore_mismatch(self): + self._check() + + def test__not_ignore_mismatch(self): + self._check(ignore_mismatch=False) + + def test__ignore_mismatch(self): + self._check(ignore_mismatch=True) + + def test__bad_points(self): + side_effect = [(None, None)] * len(self.mapping) + self.m_prepare_points_and_bounds.side_effect = side_effect + self._check(bad_points=True) + + +class Test__prepare_common_aux_payload(tests.IrisTest): + def setUp(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l c c c state c c c + # coord a a a coord a a a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.points = (sentinel.points_0, sentinel.points_1, sentinel.points_2) + self.bounds = (sentinel.bounds_0, sentinel.bounds_1, sentinel.bounds_2) + self.pb_0 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[0])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[0])), + ) + self.pb_1 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[1])), + None, + ) + self.pb_2 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[2])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[2])), + ) + side_effect = (self.pb_0, self.pb_1, self.pb_2) + self.m_prepare_points_and_bounds = self.patch( + "iris.common.resolve.Resolve._prepare_points_and_bounds", + side_effect=side_effect, + ) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = self.mapping + self.resolve.map_rhs_to_lhs = True + self.metadata_combined = ( + sentinel.combined_0, + sentinel.combined_1, + sentinel.combined_2, + ) + self.src_metadata = [ + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[0]) + ), + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[1]) + ), + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[2]) + ), + ] + self.src_coords = [ + sentinel.src_coord_0, + sentinel.src_coord_1, + sentinel.src_coord_2, + ] + self.src_dims = [(dim,) for dim in self.mapping.keys()] + self.src_common_items = [ + _Item(*item) + for item in zip(self.src_metadata, self.src_coords, self.src_dims) + ] + self.tgt_metadata = [sentinel.tgt_metadata_0] + self.src_metadata + self.tgt_coords = [ + sentinel.tgt_coord_0, + sentinel.tgt_coord_1, + sentinel.tgt_coord_2, + sentinel.tgt_coord_3, + ] + self.tgt_dims = [None] + [(dim,) for dim in self.mapping.values()] + self.tgt_common_items = [ + _Item(*item) + for item in zip(self.tgt_metadata, self.tgt_coords, self.tgt_dims) + ] + self.container = type(self.src_coords[0]) + + def _check(self, ignore_mismatch=None, bad_points=None): + if bad_points is None: + bad_points = False + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, + self.tgt_common_items, + prepared_items, + ignore_mismatch=ignore_mismatch, + ) + if not bad_points: + self.assertEqual(3, len(prepared_items)) + expected = [ + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[0], + src=self.src_metadata[0], + tgt=self.tgt_metadata[self.mapping[0]], + ), + points=self.points[0], + bounds=self.bounds[0], + dims=self.tgt_dims[self.mapping[0]], + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[1], + src=self.src_metadata[1], + tgt=self.tgt_metadata[self.mapping[1]], + ), + points=self.points[1], + bounds=None, + dims=self.tgt_dims[self.mapping[1]], + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[2], + src=self.src_metadata[2], + tgt=self.tgt_metadata[self.mapping[2]], + ), + points=self.points[2], + bounds=self.bounds[2], + dims=self.tgt_dims[self.mapping[2]], + container=self.container, + ), + ] + self.assertEqual(expected, prepared_items) + else: + self.assertEqual(0, len(prepared_items)) + self.assertEqual(3, self.m_prepare_points_and_bounds.call_count) + if ignore_mismatch is None: + ignore_mismatch = False + expected = [ + mock.call( + self.src_coords[0], + self.tgt_coords[self.mapping[0]], + self.src_dims[0], + self.tgt_dims[self.mapping[0]], + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[1], + self.tgt_coords[self.mapping[1]], + self.src_dims[1], + self.tgt_dims[self.mapping[1]], + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[2], + self.tgt_coords[self.mapping[2]], + self.src_dims[2], + self.tgt_dims[self.mapping[2]], + ignore_mismatch=ignore_mismatch, + ), + ] + self.assertEqual( + expected, self.m_prepare_points_and_bounds.call_args_list + ) + if not bad_points: + for src_metadata, tgt_metadata in zip( + self.src_metadata, self.tgt_metadata[1:] + ): + self.assertEqual(1, src_metadata.combine.call_count) + expected = [mock.call(tgt_metadata)] + self.assertEqual(expected, src_metadata.combine.call_args_list) + + def test__default_ignore_mismatch(self): + self._check() + + def test__not_ignore_mismatch(self): + self._check(ignore_mismatch=False) + + def test__ignore_mismatch(self): + self._check(ignore_mismatch=True) + + def test__bad_points(self): + side_effect = [(None, None)] * len(self.mapping) + self.m_prepare_points_and_bounds.side_effect = side_effect + self._check(bad_points=True) + + def test__no_tgt_metadata_match(self): + item = self.tgt_common_items[0] + tgt_common_items = [item] * len(self.tgt_common_items) + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, tgt_common_items, prepared_items + ) + self.assertEqual(0, len(prepared_items)) + + def test__multi_tgt_metadata_match(self): + item = self.tgt_common_items[1] + tgt_common_items = [item] * len(self.tgt_common_items) + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, tgt_common_items, prepared_items + ) + self.assertEqual(0, len(prepared_items)) + + +class Test__prepare_points_and_bounds(tests.IrisTest): + def setUp(self): + self.Coord = namedtuple( + "Coord", + [ + "name", + "points", + "bounds", + "metadata", + "ndim", + "shape", + "has_bounds", + ], + ) + self.Cube = namedtuple("Cube", ["name", "shape"]) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.src_name = sentinel.src_name + self.src_points = sentinel.src_points + self.src_bounds = sentinel.src_bounds + self.src_metadata = sentinel.src_metadata + self.src_items = dict( + name=lambda: self.src_name, + points=self.src_points, + bounds=self.src_bounds, + metadata=self.src_metadata, + ndim=None, + shape=None, + has_bounds=None, + ) + self.tgt_name = sentinel.tgt_name + self.tgt_points = sentinel.tgt_points + self.tgt_bounds = sentinel.tgt_bounds + self.tgt_metadata = sentinel.tgt_metadata + self.tgt_items = dict( + name=lambda: self.tgt_name, + points=self.tgt_points, + bounds=self.tgt_bounds, + metadata=self.tgt_metadata, + ndim=None, + shape=None, + has_bounds=None, + ) + self.m_array_equal = self.patch( + "iris.util.array_equal", side_effect=(True, True) + ) + + def test_coord_ndim_unequal__tgt_ndim_greater(self): + self.src_items["ndim"] = 1 + src_coord = self.Coord(**self.src_items) + self.tgt_items["ndim"] = 10 + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims=None, tgt_dims=None + ) + self.assertEqual(self.tgt_points, points) + self.assertEqual(self.tgt_bounds, bounds) + + def test_coord_ndim_unequal__src_ndim_greater(self): + self.src_items["ndim"] = 10 + src_coord = self.Coord(**self.src_items) + self.tgt_items["ndim"] = 1 + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims=None, tgt_dims=None + ) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_src_broadcasting(self): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 9 9 shape 1 9 + # state c c state c c + # coord x-x coord x-x + # bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = (1, 9) + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube(name=None, shape=src_shape) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = broadcast_shape + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube(name=None, shape=tgt_shape) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + self.assertEqual(self.tgt_points, points) + self.assertEqual(self.tgt_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_tgt_broadcasting(self): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 1 9 shape 9 9 + # state c c state c c + # coord x-x coord x-x + # bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = broadcast_shape + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube(name=None, shape=src_shape) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = (1, 9) + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube(name=None, shape=tgt_shape) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_unsupported_broadcasting( + self, + ): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 1 9 shape 9 1 + # state c c state c c + # coord x-x coord x-x + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = (9, 1) + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube( + name=lambda: sentinel.src_cube, shape=src_shape + ) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = (1, 9) + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube( + name=lambda: sentinel.tgt_cube, shape=tgt_shape + ) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + emsg = "Cannot broadcast" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + + def _populate( + self, src_points, tgt_points, src_bounds=None, tgt_bounds=None + ): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state f c state f c + # coord x coord x + # + # src-to-tgt mapping: + # 0->0, 1->1 + shape = (2, 3) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.Cube( + name=lambda: sentinel.src_cube, shape=None + ) + self.resolve.lhs_cube = self.Cube( + name=lambda: sentinel.tgt_cube, shape=None + ) + ndim = 1 + src_dims = 1 + self.src_items["ndim"] = ndim + self.src_items["shape"] = (shape[src_dims],) + self.src_items["points"] = src_points + self.src_items["bounds"] = src_bounds + self.src_items["has_bounds"] = lambda: src_bounds is not None + src_coord = self.Coord(**self.src_items) + tgt_dims = 1 + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = (shape[mapping[tgt_dims]],) + self.tgt_items["points"] = tgt_points + self.tgt_items["bounds"] = tgt_bounds + self.tgt_items["has_bounds"] = lambda: tgt_bounds is not None + tgt_coord = self.Coord(**self.tgt_items) + args = dict( + src_coord=src_coord, + tgt_coord=tgt_coord, + src_dims=src_dims, + tgt_dims=tgt_dims, + ) + return args + + def test_coord_ndim_and_shape_equal__points_equal_with_no_bounds(self): + args = self._populate(self.src_points, self.src_points) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertIsNone(bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_src_bounds_only( + self, + ): + args = self._populate( + self.src_points, self.src_points, src_bounds=self.src_bounds + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_tgt_bounds_only( + self, + ): + args = self._populate( + self.src_points, self.src_points, tgt_bounds=self.tgt_bounds + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.tgt_bounds, bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_src_bounds_only_strict( + self, + ): + args = self._populate( + self.src_points, self.src_points, src_bounds=self.src_bounds + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_tgt_bounds_only_strict( + self, + ): + args = self._populate( + self.src_points, self.src_points, tgt_bounds=self.tgt_bounds + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.tgt_name} has bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_equal(self): + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.src_bounds, + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + self.assertEqual(2, self.m_array_equal.call_count) + expected = [ + mock.call(self.src_points, self.src_points, withnans=True), + mock.call(self.src_bounds, self.src_bounds, withnans=True), + ] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + emsg = f"Coordinate {self.src_name} has different bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different_ignore_mismatch( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + points, bounds = self.resolve._prepare_points_and_bounds( + **args, ignore_mismatch=True + ) + self.assertEqual(self.src_points, points) + self.assertIsNone(bounds) + self.assertEqual(2, self.m_array_equal.call_count) + expected = [ + mock.call(self.src_points, self.src_points, withnans=True), + mock.call(self.src_bounds, self.tgt_bounds, withnans=True), + ] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different_strict( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has different bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_different(self): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + emsg = f"Coordinate {self.src_name} has different points" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_different_ignore_mismatch( + self, + ): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + points, bounds = self.resolve._prepare_points_and_bounds( + **args, ignore_mismatch=True + ) + self.assertIsNone(points) + self.assertIsNone(bounds) + + def test_coord_ndim_and_shape_equal__points_different_strict(self): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has different points" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + +class Test__create_prepared_item(tests.IrisTest): + def setUp(self): + Coord = namedtuple("Coord", ["points", "bounds"]) + self.points_value = sentinel.points + self.points = mock.Mock(copy=mock.Mock(return_value=self.points_value)) + self.bounds_value = sentinel.bounds + self.bounds = mock.Mock(copy=mock.Mock(return_value=self.bounds_value)) + self.coord = Coord(points=self.points, bounds=self.bounds) + self.container = type(self.coord) + self.combined = sentinel.combined + self.src = mock.Mock(combine=mock.Mock(return_value=self.combined)) + self.tgt = sentinel.tgt + + def _check(self, src=None, tgt=None): + dims = 0 + if src is not None and tgt is not None: + combined = self.combined + else: + combined = src or tgt + result = Resolve._create_prepared_item( + self.coord, dims, src_metadata=src, tgt_metadata=tgt + ) + self.assertIsInstance(result, _PreparedItem) + self.assertIsInstance(result.metadata, _PreparedMetadata) + expected = _PreparedMetadata(combined=combined, src=src, tgt=tgt) + self.assertEqual(expected, result.metadata) + self.assertEqual(self.points_value, result.points) + self.assertEqual(1, self.points.copy.call_count) + self.assertEqual([mock.call()], self.points.copy.call_args_list) + self.assertEqual(self.bounds_value, result.bounds) + self.assertEqual(1, self.bounds.copy.call_count) + self.assertEqual([mock.call()], self.bounds.copy.call_args_list) + self.assertEqual((dims,), result.dims) + self.assertEqual(self.container, result.container) + + def test__no_metadata(self): + self._check() + + def test__src_metadata_only(self): + self._check(src=self.src) + + def test__tgt_metadata_only(self): + self._check(tgt=self.tgt) + + def test__combine_metadata(self): + self._check(src=self.src, tgt=self.tgt) + + +class Test__prepare_local_payload_dim(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.resolve.map_rhs_to_lhs = True + self.src_coverage = dict( + cube=None, + metadata=[], + coords=[], + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.prepared_item = sentinel.prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + return_value=self.prepared_item, + ) + + def test_src_no_local_with_tgt_no_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_no_local_with_tgt_no_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.src_coverage["dims_local"] = (1,) + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["dims_local"] = (1,) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.src_coverage["dims_local"] = (1,) + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["dims_local"] = (1,) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_dim = 1 + self.src_coverage["dims_local"] = (src_dim,) + src_metadata = sentinel.src_metadata + self.src_coverage["metadata"] = [None, src_metadata] + src_coord = sentinel.src_coord + self.src_coverage["coords"] = [None, src_coord] + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [ + mock.call(src_coord, mapping[src_dim], src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_free__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_dim = 1 + self.src_coverage["dims_local"] = (src_dim,) + src_metadata = sentinel.src_metadata + self.src_coverage["metadata"] = [None, src_metadata] + src_coord = sentinel.src_coord + self.src_coverage["coords"] = [None, src_coord] + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_free_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord d d coord d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_dim = 1 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [None, tgt_metadata] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [None, tgt_coord] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_free_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord d d coord d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_dim = 1 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [None, tgt_metadata] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [None, tgt_coord] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_no_local_with_tgt_local__extra_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2 + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_dim = 0 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [tgt_metadata, None, None] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [tgt_coord, None, None] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__extra_dims_strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2 + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_dim = 0 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [tgt_metadata, None, None] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [tgt_coord, None, None] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test__prepare_local_payload_aux(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.resolve.map_rhs_to_lhs = True + self.src_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=[], + local_items_scalar=None, + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.src_prepared_item = sentinel.src_prepared_item + self.tgt_prepared_item = sentinel.tgt_prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + side_effect=(self.src_prepared_item, self.tgt_prepared_item), + ) + + def test_src_no_local_with_tgt_no_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_no_local_with_tgt_no_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_local_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [ + mock.call(src_coord, tgt_dims, src_metadata=src_metadata), + mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_local_with_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + self.src_coverage["dims_local"].extend(src_dims) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(src_coord, src_dims, src_metadata=src_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_free__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + self.src_coverage["dims_local"].extend(src_dims) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_free_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord a a coord a + # + # src-to-tgt mapping: + # 0->0, 1->1 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_free_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord a a coord a + # + # src-to-tgt mapping: + # 0->0, 1->1 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_no_local_with_tgt_local__extra_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord a a a coord a a + # + # src-to-tgt mapping: + # 0->1, 1->2 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (0,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__extra_dims_strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord a a a coord a a + # + # src-to-tgt mapping: + # 0->1, 1->2 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (0,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=True): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test__prepare_local_payload_scalar(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.src_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=[], + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.src_prepared_item = sentinel.src_prepared_item + self.tgt_prepared_item = sentinel.tgt_prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + side_effect=(self.src_prepared_item, self.tgt_prepared_item), + ) + self.src_dims = () + self.tgt_dims = () + + def test_src_no_local_with_tgt_no_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_no_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_no_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_no_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_no_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_local(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__strict(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_local__src_scalar_cube(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__src_scalar_cube_strict(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata), + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata), + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + +class Test__prepare_local_payload(tests.IrisTest): + def test(self): + src_dim_coverage = sentinel.src_dim_coverage + src_aux_coverage = sentinel.src_aux_coverage + tgt_dim_coverage = sentinel.tgt_dim_coverage + tgt_aux_coverage = sentinel.tgt_aux_coverage + root = "iris.common.resolve.Resolve" + m_prepare_dim = self.patch(f"{root}._prepare_local_payload_dim") + m_prepare_aux = self.patch(f"{root}._prepare_local_payload_aux") + m_prepare_scalar = self.patch(f"{root}._prepare_local_payload_scalar") + resolve = Resolve() + resolve._prepare_local_payload( + src_dim_coverage, + src_aux_coverage, + tgt_dim_coverage, + tgt_aux_coverage, + ) + self.assertEqual(1, m_prepare_dim.call_count) + expected = [mock.call(src_dim_coverage, tgt_dim_coverage)] + self.assertEqual(expected, m_prepare_dim.call_args_list) + self.assertEqual(1, m_prepare_aux.call_count) + expected = [mock.call(src_aux_coverage, tgt_aux_coverage)] + self.assertEqual(expected, m_prepare_aux.call_args_list) + self.assertEqual(1, m_prepare_scalar.call_count) + expected = [mock.call(src_aux_coverage, tgt_aux_coverage)] + self.assertEqual(expected, m_prepare_scalar.call_args_list) + + +class Test__metadata_prepare(tests.IrisTest): + def setUp(self): + self.src_cube = sentinel.src_cube + self.src_category_local = sentinel.src_category_local + self.src_dim_coverage = sentinel.src_dim_coverage + self.src_aux_coverage = mock.Mock( + common_items_aux=sentinel.src_aux_coverage_common_items_aux, + common_items_scalar=sentinel.src_aux_coverage_common_items_scalar, + ) + self.tgt_cube = sentinel.tgt_cube + self.tgt_category_local = sentinel.tgt_category_local + self.tgt_dim_coverage = sentinel.tgt_dim_coverage + self.tgt_aux_coverage = mock.Mock( + common_items_aux=sentinel.tgt_aux_coverage_common_items_aux, + common_items_scalar=sentinel.tgt_aux_coverage_common_items_scalar, + ) + self.resolve = Resolve() + root = "iris.common.resolve.Resolve" + self.m_prepare_common_dim_payload = self.patch( + f"{root}._prepare_common_dim_payload" + ) + self.m_prepare_common_aux_payload = self.patch( + f"{root}._prepare_common_aux_payload" + ) + self.m_prepare_local_payload = self.patch( + f"{root}._prepare_local_payload" + ) + self.m_prepare_factory_payload = self.patch( + f"{root}._prepare_factory_payload" + ) + + def _check(self): + self.assertIsNone(self.resolve.prepared_category) + self.assertIsNone(self.resolve.prepared_factories) + self.resolve._metadata_prepare() + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, self.resolve.prepared_category) + self.assertEqual([], self.resolve.prepared_factories) + self.assertEqual(1, self.m_prepare_common_dim_payload.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual( + expected, self.m_prepare_common_dim_payload.call_args_list + ) + self.assertEqual(2, self.m_prepare_common_aux_payload.call_count) + expected = [ + mock.call( + self.src_aux_coverage.common_items_aux, + self.tgt_aux_coverage.common_items_aux, + [], + ), + mock.call( + self.src_aux_coverage.common_items_scalar, + self.tgt_aux_coverage.common_items_scalar, + [], + ignore_mismatch=True, + ), + ] + self.assertEqual( + expected, self.m_prepare_common_aux_payload.call_args_list + ) + self.assertEqual(1, self.m_prepare_local_payload.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.src_aux_coverage, + self.tgt_dim_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_prepare_local_payload.call_args_list) + self.assertEqual(2, self.m_prepare_factory_payload.call_count) + expected = [ + mock.call(self.tgt_cube, self.tgt_category_local, from_src=False), + mock.call(self.src_cube, self.src_category_local), + ] + self.assertEqual( + expected, self.m_prepare_factory_payload.call_args_list + ) + + def test_map_rhs_to_lhs__true(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.src_cube + self.resolve.rhs_cube_category_local = self.src_category_local + self.resolve.rhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.lhs_cube = self.tgt_cube + self.resolve.lhs_cube_category_local = self.tgt_category_local + self.resolve.lhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.tgt_aux_coverage + self._check() + + def test_map_rhs_to_lhs__false(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube = self.src_cube + self.resolve.lhs_cube_category_local = self.src_category_local + self.resolve.lhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.rhs_cube = self.tgt_cube + self.resolve.rhs_cube_category_local = self.tgt_category_local + self.resolve.rhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.tgt_aux_coverage + self._check() + + +class Test__prepare_factory_payload(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["aux_factories"]) + self.Coord = namedtuple("Coord", ["metadata"]) + self.Factory_T1 = namedtuple( + "Factory_T1", ["dependencies"] + ) # dummy factory type + self.container_T1 = type(self.Factory_T1(None)) + self.Factory_T2 = namedtuple( + "Factory_T2", ["dependencies"] + ) # dummy factory type + self.container_T2 = type(self.Factory_T2(None)) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.prepared_factories = [] + self.m_get_prepared_item = self.patch( + "iris.common.resolve.Resolve._get_prepared_item" + ) + self.category_local = sentinel.category_local + self.from_src = sentinel.from_src + + def test_no_factory(self): + cube = self.Cube(aux_factories=[]) + self.resolve._prepare_factory_payload(cube, self.category_local) + self.assertEqual(0, len(self.resolve.prepared_factories)) + + def test_skip_factory__already_prepared(self): + aux_factory = self.Factory_T1(dependencies=None) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + prepared_factories = [ + _PreparedFactory(container=self.container_T1, dependencies=None), + _PreparedFactory(container=self.container_T2, dependencies=None), + ] + self.resolve.prepared_factories.extend(prepared_factories) + self.resolve._prepare_factory_payload(cube, self.category_local) + self.assertEqual(prepared_factories, self.resolve.prepared_factories) + + def test_factory__dependency_already_prepared(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (coord_a, coord_b, coord_c) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(1, len(self.resolve.prepared_factories)) + prepared_dependencies = { + name: coord.metadata for name, coord in dependencies.items() + } + expected = [ + _PreparedFactory( + container=self.container_T1, dependencies=prepared_dependencies + ) + ] + self.assertEqual(expected, self.resolve.prepared_factories) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in expected: + self.assertIn(call, actual) + + def test_factory__dependency_local_not_prepared(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (None, coord_a, None, coord_b, None, coord_c) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(1, len(self.resolve.prepared_factories)) + prepared_dependencies = { + name: coord.metadata for name, coord in dependencies.items() + } + expected = [ + _PreparedFactory( + container=self.container_T1, dependencies=prepared_dependencies + ) + ] + self.assertEqual(expected, self.resolve.prepared_factories) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_a.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_b.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_c.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in expected: + self.assertIn(call, actual) + + def test_factory__dependency_not_found(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (None, None) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(0, len(self.resolve.prepared_factories)) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_a.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_b.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_c.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in actual: + self.assertIn(call, expected) + + +class Test__get_prepared_item(tests.IrisTest): + def setUp(self): + PreparedItem = namedtuple("PreparedItem", ["metadata"]) + self.resolve = Resolve() + self.prepared_dim_metadata_src = sentinel.prepared_dim_metadata_src + self.prepared_dim_metadata_tgt = sentinel.prepared_dim_metadata_tgt + self.prepared_items_dim = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_dim_metadata_src, + tgt=self.prepared_dim_metadata_tgt, + ) + ) + self.prepared_aux_metadata_src = sentinel.prepared_aux_metadata_src + self.prepared_aux_metadata_tgt = sentinel.prepared_aux_metadata_tgt + self.prepared_items_aux = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_aux_metadata_src, + tgt=self.prepared_aux_metadata_tgt, + ) + ) + self.prepared_scalar_metadata_src = ( + sentinel.prepared_scalar_metadata_src + ) + self.prepared_scalar_metadata_tgt = ( + sentinel.prepared_scalar_metadata_tgt + ) + self.prepared_items_scalar = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_scalar_metadata_src, + tgt=self.prepared_scalar_metadata_tgt, + ) + ) + self.resolve.prepared_category = _CategoryItems( + items_dim=[self.prepared_items_dim], + items_aux=[self.prepared_items_aux], + items_scalar=[self.prepared_items_scalar], + ) + self.resolve.mapping = {0: 10} + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item" + ) + self.local_dim_metadata = sentinel.local_dim_metadata + self.local_aux_metadata = sentinel.local_aux_metadata + self.local_scalar_metadata = sentinel.local_scalar_metadata + self.local_coord = sentinel.local_coord + self.local_coord_dims = (0,) + self.local_items_dim = _Item( + metadata=self.local_dim_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.local_items_aux = _Item( + metadata=self.local_aux_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.local_items_scalar = _Item( + metadata=self.local_scalar_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.category_local = _CategoryItems( + items_dim=[self.local_items_dim], + items_aux=[self.local_items_aux], + items_scalar=[self.local_items_scalar], + ) + + def test_missing_prepared_coord__from_src(self): + metadata = sentinel.missing + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertIsNone(result) + + def test_missing_prepared_coord__from_tgt(self): + metadata = sentinel.missing + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertIsNone(result) + + def test_get_prepared_dim_coord__from_src(self): + metadata = self.prepared_dim_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_dim, result) + + def test_get_prepared_dim_coord__from_tgt(self): + metadata = self.prepared_dim_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_dim, result) + + def test_get_prepared_aux_coord__from_src(self): + metadata = self.prepared_aux_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_aux, result) + + def test_get_prepared_aux_coord__from_tgt(self): + metadata = self.prepared_aux_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_aux, result) + + def test_get_prepared_scalar_coord__from_src(self): + metadata = self.prepared_scalar_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_scalar, result) + + def test_get_prepared_scalar_coord__from_tgt(self): + metadata = self.prepared_scalar_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_scalar, result) + + def test_missing_local_coord__from_src(self): + metadata = sentinel.missing + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + self.assertIsNone(result) + + def test_missing_local_coord__from_tgt(self): + metadata = sentinel.missing + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + self.assertIsNone(result) + + def test_get_local_dim_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_dim_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(expected, self.resolve.prepared_category.items_dim[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_dim_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_dim_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(expected, self.resolve.prepared_category.items_dim[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_aux_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_aux_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(expected, self.resolve.prepared_category.items_aux[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_aux_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_aux_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(expected, self.resolve.prepared_category.items_aux[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_scalar_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_scalar_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + self.assertEqual( + expected, self.resolve.prepared_category.items_scalar[1] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_scalar_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_scalar_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + self.assertEqual( + expected, self.resolve.prepared_category.items_scalar[1] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test_cube(tests.IrisTest): + def setUp(self): + self.shape = (2, 3) + self.data = np.zeros(np.multiply(*self.shape), dtype=np.int8).reshape( + self.shape + ) + self.bad_data = np.zeros(np.multiply(*self.shape), dtype=np.int8) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve._broadcast_shape = self.shape + self.cube_metadata = CubeMetadata( + standard_name="air_temperature", + long_name="air temp", + var_name="airT", + units=Unit("K"), + attributes={}, + cell_methods=(), + ) + lhs_cube = Cube(self.data) + lhs_cube.metadata = self.cube_metadata + self.resolve.lhs_cube = lhs_cube + rhs_cube = Cube(self.data) + rhs_cube.metadata = self.cube_metadata + self.resolve.rhs_cube = rhs_cube + self.m_add_dim_coord = self.patch("iris.cube.Cube.add_dim_coord") + self.m_add_aux_coord = self.patch("iris.cube.Cube.add_aux_coord") + self.m_add_aux_factory = self.patch("iris.cube.Cube.add_aux_factory") + self.m_coord = self.patch("iris.cube.Cube.coord") + # + # prepared coordinates + # + prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # prepared dim coordinates + self.prepared_dim_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_dim_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_dim_0_points = sentinel.prepared_dim_0_points + self.prepared_dim_0_bounds = sentinel.prepared_dim_0_bounds + self.prepared_dim_0_dims = (0,) + self.prepared_dim_0_coord = mock.Mock(metadata=None) + self.prepared_dim_0_container = mock.Mock( + return_value=self.prepared_dim_0_coord + ) + self.prepared_dim_0 = _PreparedItem( + metadata=self.prepared_dim_0_metadata, + points=self.prepared_dim_0_points, + bounds=self.prepared_dim_0_bounds, + dims=self.prepared_dim_0_dims, + container=self.prepared_dim_0_container, + ) + prepared_category.items_dim.append(self.prepared_dim_0) + self.prepared_dim_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_dim_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_dim_1_points = sentinel.prepared_dim_1_points + self.prepared_dim_1_bounds = sentinel.prepared_dim_1_bounds + self.prepared_dim_1_dims = (1,) + self.prepared_dim_1_coord = mock.Mock(metadata=None) + self.prepared_dim_1_container = mock.Mock( + return_value=self.prepared_dim_1_coord + ) + self.prepared_dim_1 = _PreparedItem( + metadata=self.prepared_dim_1_metadata, + points=self.prepared_dim_1_points, + bounds=self.prepared_dim_1_bounds, + dims=self.prepared_dim_1_dims, + container=self.prepared_dim_1_container, + ) + prepared_category.items_dim.append(self.prepared_dim_1) + + # prepared auxiliary coordinates + self.prepared_aux_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_aux_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_aux_0_points = sentinel.prepared_aux_0_points + self.prepared_aux_0_bounds = sentinel.prepared_aux_0_bounds + self.prepared_aux_0_dims = (0,) + self.prepared_aux_0_coord = mock.Mock(metadata=None) + self.prepared_aux_0_container = mock.Mock( + return_value=self.prepared_aux_0_coord + ) + self.prepared_aux_0 = _PreparedItem( + metadata=self.prepared_aux_0_metadata, + points=self.prepared_aux_0_points, + bounds=self.prepared_aux_0_bounds, + dims=self.prepared_aux_0_dims, + container=self.prepared_aux_0_container, + ) + prepared_category.items_aux.append(self.prepared_aux_0) + self.prepared_aux_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_aux_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_aux_1_points = sentinel.prepared_aux_1_points + self.prepared_aux_1_bounds = sentinel.prepared_aux_1_bounds + self.prepared_aux_1_dims = (1,) + self.prepared_aux_1_coord = mock.Mock(metadata=None) + self.prepared_aux_1_container = mock.Mock( + return_value=self.prepared_aux_1_coord + ) + self.prepared_aux_1 = _PreparedItem( + metadata=self.prepared_aux_1_metadata, + points=self.prepared_aux_1_points, + bounds=self.prepared_aux_1_bounds, + dims=self.prepared_aux_1_dims, + container=self.prepared_aux_1_container, + ) + prepared_category.items_aux.append(self.prepared_aux_1) + + # prepare scalar coordinates + self.prepared_scalar_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_scalar_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_scalar_0_points = sentinel.prepared_scalar_0_points + self.prepared_scalar_0_bounds = sentinel.prepared_scalar_0_bounds + self.prepared_scalar_0_dims = () + self.prepared_scalar_0_coord = mock.Mock(metadata=None) + self.prepared_scalar_0_container = mock.Mock( + return_value=self.prepared_scalar_0_coord + ) + self.prepared_scalar_0 = _PreparedItem( + metadata=self.prepared_scalar_0_metadata, + points=self.prepared_scalar_0_points, + bounds=self.prepared_scalar_0_bounds, + dims=self.prepared_scalar_0_dims, + container=self.prepared_scalar_0_container, + ) + prepared_category.items_scalar.append(self.prepared_scalar_0) + self.prepared_scalar_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_scalar_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_scalar_1_points = sentinel.prepared_scalar_1_points + self.prepared_scalar_1_bounds = sentinel.prepared_scalar_1_bounds + self.prepared_scalar_1_dims = () + self.prepared_scalar_1_coord = mock.Mock(metadata=None) + self.prepared_scalar_1_container = mock.Mock( + return_value=self.prepared_scalar_1_coord + ) + self.prepared_scalar_1 = _PreparedItem( + metadata=self.prepared_scalar_1_metadata, + points=self.prepared_scalar_1_points, + bounds=self.prepared_scalar_1_bounds, + dims=self.prepared_scalar_1_dims, + container=self.prepared_scalar_1_container, + ) + prepared_category.items_scalar.append(self.prepared_scalar_1) + # + # prepared factories + # + prepared_factories = [] + self.aux_factory = sentinel.aux_factory + self.prepared_factory_container = mock.Mock( + return_value=self.aux_factory + ) + self.prepared_factory_metadata_a = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_a_combined, + src=None, + tgt=None, + ) + self.prepared_factory_metadata_b = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_b_combined, + src=None, + tgt=None, + ) + self.prepared_factory_metadata_c = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_c_combined, + src=None, + tgt=None, + ) + self.prepared_factory_dependencies = dict( + name_a=self.prepared_factory_metadata_a, + name_b=self.prepared_factory_metadata_b, + name_c=self.prepared_factory_metadata_c, + ) + self.prepared_factory = _PreparedFactory( + container=self.prepared_factory_container, + dependencies=self.prepared_factory_dependencies, + ) + prepared_factories.append(self.prepared_factory) + self.prepared_factory_side_effect = ( + sentinel.prepared_factory_coord_a, + sentinel.prepared_factory_coord_b, + sentinel.prepared_factory_coord_c, + ) + self.m_coord.side_effect = self.prepared_factory_side_effect + self.resolve.prepared_category = prepared_category + self.resolve.prepared_factories = prepared_factories + + def test_no_resolved_shape(self): + self.resolve._broadcast_shape = None + data = None + emsg = "Cannot resolve resultant cube, as no candidate cubes have been provided" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(data) + + def test_bad_data_shape(self): + emsg = "Cannot resolve resultant cube, as the provided data must have shape" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(self.bad_data) + + def test_bad_data_shape__inplace(self): + self.resolve.lhs_cube = Cube(self.bad_data) + emsg = "Cannot resolve resultant cube in-place" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(self.data, in_place=True) + + def _check(self): + # check dim coordinate 0 + self.assertEqual(1, self.prepared_dim_0.container.call_count) + expected = [ + mock.call( + self.prepared_dim_0_points, bounds=self.prepared_dim_0_bounds + ) + ] + self.assertEqual( + expected, self.prepared_dim_0.container.call_args_list + ) + self.assertEqual( + self.prepared_dim_0_coord.metadata, + self.prepared_dim_0_metadata.combined, + ) + # check dim coordinate 1 + self.assertEqual(1, self.prepared_dim_1.container.call_count) + expected = [ + mock.call( + self.prepared_dim_1_points, bounds=self.prepared_dim_1_bounds + ) + ] + self.assertEqual( + expected, self.prepared_dim_1.container.call_args_list + ) + self.assertEqual( + self.prepared_dim_1_coord.metadata, + self.prepared_dim_1_metadata.combined, + ) + # check add_dim_coord + self.assertEqual(2, self.m_add_dim_coord.call_count) + expected = [ + mock.call(self.prepared_dim_0_coord, self.prepared_dim_0_dims), + mock.call(self.prepared_dim_1_coord, self.prepared_dim_1_dims), + ] + self.assertEqual(expected, self.m_add_dim_coord.call_args_list) + + # check aux coordinate 0 + self.assertEqual(1, self.prepared_aux_0.container.call_count) + expected = [ + mock.call( + self.prepared_aux_0_points, bounds=self.prepared_aux_0_bounds + ) + ] + self.assertEqual( + expected, self.prepared_aux_0.container.call_args_list + ) + self.assertEqual( + self.prepared_aux_0_coord.metadata, + self.prepared_aux_0_metadata.combined, + ) + # check aux coordinate 1 + self.assertEqual(1, self.prepared_aux_1.container.call_count) + expected = [ + mock.call( + self.prepared_aux_1_points, bounds=self.prepared_aux_1_bounds + ) + ] + self.assertEqual( + expected, self.prepared_aux_1.container.call_args_list + ) + self.assertEqual( + self.prepared_aux_1_coord.metadata, + self.prepared_aux_1_metadata.combined, + ) + # check scalar coordinate 0 + self.assertEqual(1, self.prepared_scalar_0.container.call_count) + expected = [ + mock.call( + self.prepared_scalar_0_points, + bounds=self.prepared_scalar_0_bounds, + ) + ] + self.assertEqual( + expected, self.prepared_scalar_0.container.call_args_list + ) + self.assertEqual( + self.prepared_scalar_0_coord.metadata, + self.prepared_scalar_0_metadata.combined, + ) + # check scalar coordinate 1 + self.assertEqual(1, self.prepared_scalar_1.container.call_count) + expected = [ + mock.call( + self.prepared_scalar_1_points, + bounds=self.prepared_scalar_1_bounds, + ) + ] + self.assertEqual( + expected, self.prepared_scalar_1.container.call_args_list + ) + self.assertEqual( + self.prepared_scalar_1_coord.metadata, + self.prepared_scalar_1_metadata.combined, + ) + # check add_aux_coord + self.assertEqual(4, self.m_add_aux_coord.call_count) + expected = [ + mock.call(self.prepared_aux_0_coord, self.prepared_aux_0_dims), + mock.call(self.prepared_aux_1_coord, self.prepared_aux_1_dims), + mock.call( + self.prepared_scalar_0_coord, self.prepared_scalar_0_dims + ), + mock.call( + self.prepared_scalar_1_coord, self.prepared_scalar_1_dims + ), + ] + self.assertEqual(expected, self.m_add_aux_coord.call_args_list) + + # check auxiliary factories + self.assertEqual(1, self.m_add_aux_factory.call_count) + expected = [mock.call(self.aux_factory)] + self.assertEqual(expected, self.m_add_aux_factory.call_args_list) + self.assertEqual(1, self.prepared_factory_container.call_count) + expected = [ + mock.call( + **{ + name: value + for name, value in zip( + sorted(self.prepared_factory_dependencies.keys()), + self.prepared_factory_side_effect, + ) + } + ) + ] + self.assertEqual( + expected, self.prepared_factory_container.call_args_list + ) + self.assertEqual(3, self.m_coord.call_count) + expected = [ + mock.call(self.prepared_factory_metadata_a.combined), + mock.call(self.prepared_factory_metadata_b.combined), + mock.call(self.prepared_factory_metadata_c.combined), + ] + self.assertEqual(expected, self.m_coord.call_args_list) + + def test_resolve(self): + result = self.resolve.cube(self.data) + self.assertEqual(self.cube_metadata, result.metadata) + self._check() + self.assertIsNot(self.resolve.lhs_cube, result) + + def test_resolve__inplace(self): + result = self.resolve.cube(self.data, in_place=True) + self.assertEqual(self.cube_metadata, result.metadata) + self._check() + self.assertIs(self.resolve.lhs_cube, result) + + +if __name__ == "__main__": + tests.main() From ca642eea6152754e851f138cf98e2a154005646d Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 25 Jan 2021 12:13:20 +0000 Subject: [PATCH 22/23] pin v3.0.0 version and whatnew date (#3956) --- docs/iris/src/whatsnew/3.0.rst | 2 +- lib/iris/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 824aa8170f..399325add5 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -1,6 +1,6 @@ .. include:: ../common_links.inc -v3.0 (02 Oct 2020) +v3.0 (25 Jan 2021) ****************** This document explains the changes made to Iris for this release diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index ad07426e51..e858a10566 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -106,7 +106,7 @@ def callback(cube, field, filename): # Iris revision. -__version__ = "3.0.0rc0" +__version__ = "3.0.0" # Restrict the names imported when using "from iris import *" __all__ = [ From 8222f65be81f536f78d4900f474f36bd7988d00a Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 25 Jan 2021 13:14:50 +0000 Subject: [PATCH 23/23] update github ci checks image (#3957) --- docs/iris/src/developers_guide/ci_checks.png | Bin 24457 -> 203990 bytes 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docs/iris/src/developers_guide/ci_checks.png diff --git a/docs/iris/src/developers_guide/ci_checks.png b/docs/iris/src/developers_guide/ci_checks.png old mode 100644 new mode 100755 index cf93239dea4ef9ee6c265d5f74359993ca822e15..e088e03a6657cf5719468a850e4279c162723c58 GIT binary patch literal 203990 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

-F3$~3Eu?g0z()hSyh`PNw(fJxbD5Rcm-jaN2 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 literal 24457 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