From 7e541ec8f24646cae8dfa1a423346f7f67935afe Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 15 Feb 2021 12:07:50 +0000 Subject: [PATCH] Update mesh-data-model branch (#4009) * Add abstract cube summary (#3987) Co-authored-by: stephen.worsley * add nox session conda list (#3990) * Added text to state the Python version used to build the docs. (#3989) * Added text to state the Python version used to build the docs. * Added footer template that includes the Python version used to build. * added new line * Review actions * added whatsnew * Iris py38 (#3976) * support for py38 * update CI and noxfile * enforce alphabetical xml element attribute order * full tests for py38 + fix docs-tests * add whatsnew entry * update doc-strings + review actions * Alternate xml handling routine (#29) * all xml tests pass for nox tests-3.8 * restored docstrings * move sort_xml_attrs * make sort_xml_attrs a classmethod * update sort_xml_attr doc-string Co-authored-by: Bill Little * add jamesp to whatsnew + minor tweak Co-authored-by: James Penn * normalise version to implicit development release number (#3991) * Gallery: update COP maps example (#3934) * update cop maps example * comment tweaks * minor comment tweak + whatsnew * reinstate whatsnew addition * remove duplicate whatsnew * don't support mpl v1.2 (#3941) * Cubesummary tidy (#3988) * Extra tests; fix for array attributes. * Docstring for CubeSummary, and remove some unused parts. * Fix section name capitalisation, in line with existing cube summary. * Handle array differences; quote strings in extras and if 'awkward'-printing. * Ensure scalar string coord 'content' prints on one line. * update intersphinx mapping and matplotlib urls (#4003) * update intersphinx mapping and matplotlib urls * use matplotlib intersphinx where possible * review actions * review actions * update readme badges (#4004) * update readme badges * pimp twitter badge * update readme logo img src and href (#4006) * update setuptools description (#4008) Co-authored-by: Patrick Peglar Co-authored-by: stephen.worsley Co-authored-by: tkknight <2108488+tkknight@users.noreply.github.com> Co-authored-by: James Penn Co-authored-by: Ruth Comer --- README.md | 24 +-- .../general/plot_anomaly_log_colouring.py | 13 +- .../gallery_code/meteorology/plot_COP_maps.py | 134 +++++++--------- .../meteorology/plot_deriving_phenomena.py | 9 +- docs/src/common_links.inc | 2 +- docs/src/conf.py | 12 +- docs/src/whatsnew/3.0.1.rst | 21 ++- docs/src/whatsnew/3.0.rst | 21 ++- docs/src/whatsnew/latest.rst | 12 +- lib/iris/_representation.py | 72 +++++++-- .../representation/test_representation.py | 149 ++++++++++++++++-- setup.py | 2 +- 12 files changed, 307 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index 0ceac7e089..e460f4a01a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- - Iris
+ + Iris

@@ -13,18 +13,24 @@ Cirrus-CI - -Documentation Status + +Documentation Status conda-forge downloads # contributors + +conda-forge + +pypi -Latest version +latest release Commits since last release @@ -35,8 +41,8 @@ black -twitter +twitter scitools_iris

diff --git a/docs/gallery_code/general/plot_anomaly_log_colouring.py b/docs/gallery_code/general/plot_anomaly_log_colouring.py index 778f92db1b..846816aff7 100644 --- a/docs/gallery_code/general/plot_anomaly_log_colouring.py +++ b/docs/gallery_code/general/plot_anomaly_log_colouring.py @@ -12,18 +12,15 @@ "zero band" which is plotted in white. To do this, we create a custom value mapping function (normalization) using -the matplotlib Norm class `matplotlib.colours.SymLogNorm -`_. -We use this to make a cell-filled pseudocolour plot with a colorbar. +the matplotlib Norm class :obj:`matplotlib.colors.SymLogNorm`. +We use this to make a cell-filled pseudocolor plot with a colorbar. NOTE: By "pseudocolour", we mean that each data point is drawn as a "cell" region on the plot, coloured according to its data value. This is provided in Iris by the functions :meth:`iris.plot.pcolor` and :meth:`iris.plot.pcolormesh`, which call the underlying matplotlib -functions of the same names (i.e. `matplotlib.pyplot.pcolor -`_ -and `matplotlib.pyplot.pcolormesh -`_). +functions of the same names (i.e., :obj:`matplotlib.pyplot.pcolor` +and :obj:`matplotlib.pyplot.pcolormesh`). See also: http://en.wikipedia.org/wiki/False_color#Pseudocolor. """ @@ -65,7 +62,7 @@ def main(): # Use a standard colour map which varies blue-white-red. # For suitable options, see the 'Diverging colormaps' section in: - # http://matplotlib.org/examples/color/colormaps_reference.html + # http://matplotlib.org/stable/gallery/color/colormap_reference.html anom_cmap = "bwr" # Create a 'logarithmic' data normalization. diff --git a/docs/gallery_code/meteorology/plot_COP_maps.py b/docs/gallery_code/meteorology/plot_COP_maps.py index 5555a0b85c..5e158346a9 100644 --- a/docs/gallery_code/meteorology/plot_COP_maps.py +++ b/docs/gallery_code/meteorology/plot_COP_maps.py @@ -38,34 +38,32 @@ def cop_metadata_callback(cube, field, filename): filename. """ - # Extract the experiment name (such as a1b or e1) from the filename (in - # this case it is just the parent folder's name) - containing_folder = os.path.dirname(filename) - experiment_label = os.path.basename(containing_folder) + # Extract the experiment name (such as A1B or E1) from the filename (in + # this case it is just the start of the file name, before the first "."). + fname = os.path.basename(filename) # filename without path. + experiment_label = fname.split(".")[0] - # Create a coordinate with the experiment label in it + # Create a coordinate with the experiment label in it... exp_coord = coords.AuxCoord( experiment_label, long_name="Experiment", units="no_unit" ) - # and add it to the cube + # ...and add it to the cube. cube.add_aux_coord(exp_coord) def main(): - # Load e1 and a1 using the callback to update the metadata - e1 = iris.load_cube( - iris.sample_data_path("E1.2098.pp"), callback=cop_metadata_callback - ) - a1b = iris.load_cube( - iris.sample_data_path("A1B.2098.pp"), callback=cop_metadata_callback - ) + # Load E1 and A1B scenarios using the callback to update the metadata. + scenario_files = [ + iris.sample_data_path(fname) for fname in ["E1.2098.pp", "A1B.2098.pp"] + ] + scenarios = iris.load(scenario_files, callback=cop_metadata_callback) - # Load the global average data and add an 'Experiment' coord it - global_avg = iris.load_cube(iris.sample_data_path("pre-industrial.pp")) + # Load the preindustrial reference data. + preindustrial = iris.load_cube(iris.sample_data_path("pre-industrial.pp")) # Define evenly spaced contour levels: -2.5, -1.5, ... 15.5, 16.5 with the - # specific colours + # specific colours. levels = np.arange(20) - 2.5 red = ( np.array( @@ -147,81 +145,67 @@ def main(): ) # Put those colours into an array which can be passed to contourf as the - # specific colours for each level - colors = np.array([red, green, blue]).T + # specific colours for each level. + colors = np.stack([red, green, blue], axis=1) - # Subtract the global + # Make a wider than normal figure to house two maps side-by-side. + fig, ax_array = plt.subplots(1, 2, figsize=(12, 5)) - # Iterate over each latitude longitude slice for both e1 and a1b scenarios - # simultaneously - for e1_slice, a1b_slice in zip( - e1.slices(["latitude", "longitude"]), - a1b.slices(["latitude", "longitude"]), + # Loop over our scenarios to make a plot for each. + for ax, experiment, label in zip( + ax_array, ["E1", "A1B"], ["E1", "A1B-Image"] ): - - time_coord = a1b_slice.coord("time") - - # Calculate the difference from the mean - delta_e1 = e1_slice - global_avg - delta_a1b = a1b_slice - global_avg - - # Make a wider than normal figure to house two maps side-by-side - fig = plt.figure(figsize=(12, 5)) - - # Get the time datetime from the coordinate - time = time_coord.units.num2date(time_coord.points[0]) - # Set a title for the entire figure, giving the time in a nice format - # of "MonthName Year". Also, set the y value for the title so that it - # is not tight to the top of the plot. - fig.suptitle( - "Annual Temperature Predictions for " + time.strftime("%Y"), - y=0.9, - fontsize=18, + exp_cube = scenarios.extract_cube( + iris.Constraint(Experiment=experiment) ) + time_coord = exp_cube.coord("time") - # Add the first subplot showing the E1 scenario - plt.subplot(121) - plt.title("HadGEM2 E1 Scenario", fontsize=10) - iplt.contourf(delta_e1, levels, colors=colors, extend="both") - plt.gca().coastlines() - # get the current axes' subplot for use later on - plt1_ax = plt.gca() + # Calculate the difference from the preindustial control run. + exp_anom_cube = exp_cube - preindustrial - # Add the second subplot showing the A1B scenario - plt.subplot(122) - plt.title("HadGEM2 A1B-Image Scenario", fontsize=10) + # Plot this anomaly. + plt.sca(ax) + ax.set_title(f"HadGEM2 {label} Scenario", fontsize=10) contour_result = iplt.contourf( - delta_a1b, levels, colors=colors, extend="both" + exp_anom_cube, levels, colors=colors, extend="both" ) plt.gca().coastlines() - # get the current axes' subplot for use later on - plt2_ax = plt.gca() - # Now add a colourbar who's leftmost point is the same as the leftmost - # point of the left hand plot and rightmost point is the rightmost - # point of the right hand plot + # Now add a colourbar who's leftmost point is the same as the leftmost + # point of the left hand plot and rightmost point is the rightmost + # point of the right hand plot. - # Get the positions of the 2nd plot and the left position of the 1st - # plot - left, bottom, width, height = plt2_ax.get_position().bounds - first_plot_left = plt1_ax.get_position().bounds[0] + # Get the positions of the 2nd plot and the left position of the 1st plot. + left, bottom, width, height = ax_array[1].get_position().bounds + first_plot_left = ax_array[0].get_position().bounds[0] - # the width of the colorbar should now be simple - width = left - first_plot_left + width + # The width of the colorbar should now be simple. + width = left - first_plot_left + width - # Add axes to the figure, to place the colour bar - colorbar_axes = fig.add_axes([first_plot_left, 0.18, width, 0.03]) + # Add axes to the figure, to place the colour bar. + colorbar_axes = fig.add_axes([first_plot_left, 0.18, width, 0.03]) - # Add the colour bar - cbar = plt.colorbar( - contour_result, colorbar_axes, orientation="horizontal" - ) + # Add the colour bar. + cbar = plt.colorbar( + contour_result, colorbar_axes, orientation="horizontal" + ) - # Label the colour bar and add ticks - cbar.set_label(e1_slice.units) - cbar.ax.tick_params(length=0) + # Label the colour bar and add ticks. + cbar.set_label(preindustrial.units) + cbar.ax.tick_params(length=0) + + # Get the time datetime from the coordinate. + time = time_coord.units.num2date(time_coord.points[0]) + # Set a title for the entire figure, using the year from the datetime + # object. Also, set the y value for the title so that it is not tight to + # the top of the plot. + fig.suptitle( + f"Annual Temperature Predictions for {time.year}", + y=0.9, + fontsize=18, + ) - iplt.show() + iplt.show() if __name__ == "__main__": diff --git a/docs/gallery_code/meteorology/plot_deriving_phenomena.py b/docs/gallery_code/meteorology/plot_deriving_phenomena.py index 0bb1fa53a4..b600941f35 100644 --- a/docs/gallery_code/meteorology/plot_deriving_phenomena.py +++ b/docs/gallery_code/meteorology/plot_deriving_phenomena.py @@ -26,14 +26,7 @@ def limit_colorbar_ticks(contour_object): number of ticks on the colorbar to 4. """ - # Under Matplotlib v1.2.x the colorbar attribute of a contour object is - # a tuple containing the colorbar and an axes object, whereas under - # Matplotlib v1.3.x it is simply the colorbar. - try: - colorbar = contour_object.colorbar[0] - except (AttributeError, TypeError): - colorbar = contour_object.colorbar - + colorbar = contour_object.colorbar colorbar.locator = matplotlib.ticker.MaxNLocator(4) colorbar.update_ticks() diff --git a/docs/src/common_links.inc b/docs/src/common_links.inc index 157444d65d..3c465b67dc 100644 --- a/docs/src/common_links.inc +++ b/docs/src/common_links.inc @@ -18,7 +18,7 @@ .. _issue: https://github.com/SciTools/iris/issues .. _issues: https://github.com/SciTools/iris/issues .. _legacy documentation: https://scitools.org.uk/iris/docs/v2.4.0/ -.. _matplotlib: https://matplotlib.org/ +.. _matplotlib: https://matplotlib.org/stable/ .. _napolean: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/sphinxcontrib.napoleon.html .. _nox: https://nox.thea.codes/en/stable/ .. _New Issue: https://github.com/scitools/iris/issues/new/choose diff --git a/docs/src/conf.py b/docs/src/conf.py index 843af17944..9bab5850b8 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -184,18 +184,18 @@ def autolog(message): # -- intersphinx extension ---------------------------------------------------- # See https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html intersphinx_mapping = { - "cartopy": ("http://scitools.org.uk/cartopy/docs/latest/", None), - "matplotlib": ("http://matplotlib.org/", None), - "numpy": ("http://docs.scipy.org/doc/numpy/", None), - "python": ("http://docs.python.org/2.7", None), - "scipy": ("http://docs.scipy.org/doc/scipy/reference/", None), + "cartopy": ("https://scitools.org.uk/cartopy/docs/latest/", None), + "matplotlib": ("https://matplotlib.org/stable/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "python": ("https://docs.python.org/3/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), } # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # -- plot_directive extension ------------------------------------------------- -# See https://matplotlib.org/3.1.3/devel/plot_directive.html#options +# See https://matplotlib.org/stable/api/sphinxext_plot_directive_api.html#options plot_formats = [ ("png", 100), ] diff --git a/docs/src/whatsnew/3.0.1.rst b/docs/src/whatsnew/3.0.1.rst index 163fe4ff3e..05bf41ce18 100644 --- a/docs/src/whatsnew/3.0.1.rst +++ b/docs/src/whatsnew/3.0.1.rst @@ -167,12 +167,12 @@ This document explains the changes made to Iris for this release ``volume`` are the only accepted values. (:pull:`3533`) #. `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use - `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot + :obj:`matplotlib.dates.date2num` to format date/time coordinates for use on a plot axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` did not include this behaviour). (:pull:`3762`) #. `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to - now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ + now **always** be based on the ``epoch`` used in :obj:`matplotlib.dates.date2num` (previously would take the unit from a time coordinate, if present, even though the coordinate's value had been changed via ``date2num``). (:pull:`3762`) @@ -189,7 +189,7 @@ This document explains the changes made to Iris for this release #. `@stephenworsley`_ changed the way tick labels are assigned from string coords. Previously, the first tick label would occasionally be duplicated. This also - removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) + removes the use of the deprecated `matplotlib`_ ``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`) @@ -295,11 +295,11 @@ This document explains the changes made to Iris for this release #. `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version - of `Matplotlib`_. (:pull:`3762`) + of `matplotlib`_. (:pull:`3762`) -#. `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. +#. `@bjlittle`_ unpinned Iris to use the latest version of `matplotlib`_. Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in - pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer + pinning our dependency on `matplotlib`_ at ``v2.x``. But this is no longer necessary now that ``Python2`` support has been dropped. (:pull:`3468`) #. `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version @@ -422,11 +422,11 @@ This document explains the changes made to Iris for this release grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) #. `@trexfeathers`_ added additional acceptable graphics test targets to account - for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and + for very minor changes in `matplotlib`_ version ``3.3`` (colormaps, fonts and axes borders). (:pull:`3762`) -#. `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore - `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. +#. `@rcomer`_ corrected the `matplotlib`_ backend in Iris tests to ignore + :obj:`matplotlib.rcdefaults`, instead the tests will **always** use ``agg``. (:pull:`3846`) #. `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. @@ -470,7 +470,6 @@ This document explains the changes made to Iris for this release 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 .. _CF Ancillary Data: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#ancillary-data .. _Quality Flags: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags @@ -480,7 +479,6 @@ This document explains the changes made to Iris for this release .. _Cartopy#1105: https://github.com/SciTools/cartopy/pull/1105 .. _Cartopy#1117: https://github.com/SciTools/cartopy/pull/1117 .. _Dask: https://github.com/dask/dask -.. _matplotlib.dates.date2num: https://matplotlib.org/api/dates_api.html#matplotlib.dates.date2num .. _Proj: https://github.com/OSGeo/PROJ .. _black: https://black.readthedocs.io/en/stable/ .. _Proj#1292: https://github.com/OSGeo/PROJ/pull/1292 @@ -510,7 +508,6 @@ This document explains the changes made to Iris for this release .. _numpy: https://github.com/numpy/numpy .. _xxHash: https://github.com/Cyan4973/xxHash .. _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/ diff --git a/docs/src/whatsnew/3.0.rst b/docs/src/whatsnew/3.0.rst index 0f61d62033..7fdc2e3400 100644 --- a/docs/src/whatsnew/3.0.rst +++ b/docs/src/whatsnew/3.0.rst @@ -150,12 +150,12 @@ This document explains the changes made to Iris for this release ``volume`` are the only accepted values. (:pull:`3533`) #. `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use - `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot + :obj:`matplotlib.dates.date2num` to format date/time coordinates for use on a plot axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` did not include this behaviour). (:pull:`3762`) #. `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to - now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ + now **always** be based on the ``epoch`` used in :obj:`matplotlib.dates.date2num` (previously would take the unit from a time coordinate, if present, even though the coordinate's value had been changed via ``date2num``). (:pull:`3762`) @@ -172,7 +172,7 @@ This document explains the changes made to Iris for this release #. `@stephenworsley`_ changed the way tick labels are assigned from string coords. Previously, the first tick label would occasionally be duplicated. This also - removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) + removes the use of the deprecated `matplotlib`_ ``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`) @@ -278,11 +278,11 @@ This document explains the changes made to Iris for this release #. `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version - of `Matplotlib`_. (:pull:`3762`) + of `matplotlib`_. (:pull:`3762`) -#. `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. +#. `@bjlittle`_ unpinned Iris to use the latest version of `matplotlib`_. Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in - pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer + pinning our dependency on `matplotlib`_ at ``v2.x``. But this is no longer necessary now that ``Python2`` support has been dropped. (:pull:`3468`) #. `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version @@ -405,11 +405,11 @@ This document explains the changes made to Iris for this release grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) #. `@trexfeathers`_ added additional acceptable graphics test targets to account - for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and + for very minor changes in `matplotlib`_ version ``3.3`` (colormaps, fonts and axes borders). (:pull:`3762`) -#. `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore - `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. +#. `@rcomer`_ corrected the `matplotlib`_ backend in Iris tests to ignore + :obj:`matplotlib.rcdefaults`, instead the tests will **always** use ``agg``. (:pull:`3846`) #. `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. @@ -453,7 +453,6 @@ This document explains the changes made to Iris for this release 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 .. _CF Ancillary Data: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#ancillary-data .. _Quality Flags: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags @@ -463,7 +462,6 @@ This document explains the changes made to Iris for this release .. _Cartopy#1105: https://github.com/SciTools/cartopy/pull/1105 .. _Cartopy#1117: https://github.com/SciTools/cartopy/pull/1117 .. _Dask: https://github.com/dask/dask -.. _matplotlib.dates.date2num: https://matplotlib.org/api/dates_api.html#matplotlib.dates.date2num .. _Proj: https://github.com/OSGeo/PROJ .. _black: https://black.readthedocs.io/en/stable/ .. _Proj#1292: https://github.com/OSGeo/PROJ/pull/1292 @@ -493,7 +491,6 @@ This document explains the changes made to Iris for this release .. _numpy: https://github.com/numpy/numpy .. _xxHash: https://github.com/Cyan4973/xxHash .. _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/ diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index c02b61341b..68872beb64 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -69,8 +69,8 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -#. `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. - (:pull:`3933`) +#. `@rcomer`_ updated the "Seasonal ensemble model plots" and "Global average + annual temperature maps" Gallery examples. (:pull:`3933` and :pull:`3934`) #. `@MHBalsmeier`_ described non-conda installation on Debian-based distros. (:pull:`3958`) @@ -86,6 +86,11 @@ This document explains the changes made to Iris for this release on :ref:`installing_iris` and to the footer of all pages. Also added the copyright years to the footer. (:pull:`3989`) +#. `@bjlittle`_ updated the ``intersphinx_mapping`` and fixed documentation + to use ``stable`` URLs for `matplotlib`_. (:pull:`4003`) + +#. `@bjlittle`_ added the |PyPI|_ badge to the `README.md`_. (:pull:`4004`) + 💼 Internal =========== @@ -120,4 +125,7 @@ This document explains the changes made to Iris for this release .. _GitHub: https://github.com/SciTools/iris/issues/new/choose .. _Met Office: https://www.metoffice.gov.uk/ .. _numpy: https://numpy.org/doc/stable/release/1.20.0-notes.html +.. |PyPI| image:: https://img.shields.io/pypi/v/scitools-iris?color=orange&label=pypi%7Cscitools-iris +.. _PyPI: https://pypi.org/project/scitools-iris/ .. _Python 3.8: https://www.python.org/downloads/release/python-380/ +.. _README.md: https://github.com/SciTools/iris#----- diff --git a/lib/iris/_representation.py b/lib/iris/_representation.py index 301f4a9a22..ee1e1a0d55 100644 --- a/lib/iris/_representation.py +++ b/lib/iris/_representation.py @@ -6,8 +6,10 @@ """ Provides objects describing cube summaries. """ +import re import iris.util +from iris.common.metadata import _hexdigest as quickhash class DimensionHeader: @@ -46,6 +48,35 @@ def __init__(self, cube, name_padding=35): self.dimension_header = DimensionHeader(cube) +def string_repr(text, quote_strings=False): + """Produce a one-line printable form of a text string.""" + if re.findall("[\n\t]", text) or quote_strings: + # Replace the string with its repr (including quotes). + text = repr(text) + return text + + +def array_repr(arr): + """Produce a single-line printable repr of an array.""" + # First take whatever numpy produces.. + text = repr(arr) + # ..then reduce any multiple spaces and newlines. + text = re.sub("[ \t\n]+", " ", text) + return text + + +def value_repr(value, quote_strings=False): + """ + Produce a single-line printable version of an attribute or scalar value. + """ + if hasattr(value, "dtype"): + value = array_repr(value) + elif isinstance(value, str): + value = string_repr(value, quote_strings=quote_strings) + value = str(value) + return value + + class CoordSummary: def _summary_coord_extra(self, cube, coord): # Returns the text needed to ensure this coordinate can be @@ -66,12 +97,21 @@ def _summary_coord_extra(self, cube, coord): vary.add(key) break value = similar_coord.attributes[key] - if attributes.setdefault(key, value) != value: + # Like "if attributes.setdefault(key, value) != value:" + # ..except setdefault fails if values are numpy arrays. + if key not in attributes: + attributes[key] = value + elif quickhash(attributes[key]) != quickhash(value): + # NOTE: fast and array-safe comparison, as used in + # :mod:`iris.common.metadata`. vary.add(key) break keys = sorted(vary & set(coord.attributes.keys())) bits = [ - "{}={!r}".format(key, coord.attributes[key]) for key in keys + "{}={}".format( + key, value_repr(coord.attributes[key], quote_strings=True) + ) + for key in keys ] if bits: extra = ", ".join(bits) @@ -105,13 +145,17 @@ def __init__(self, cube, coord): coord_cell = coord.cell(0) if isinstance(coord_cell.point, str): self.string_type = True + # 'lines' is value split on '\n', and _each one_ length-clipped. self.lines = [ iris.util.clip_string(str(item)) for item in coord_cell.point.split("\n") ] self.point = None self.bound = None - self.content = "\n".join(self.lines) + # 'content' contains a one-line printable version of the string, + content = string_repr(coord_cell.point) + content = iris.util.clip_string(content) + self.content = content else: self.string_type = False self.lines = None @@ -132,9 +176,6 @@ def __init__(self, cube, coord): class Section: - def _init_(self): - self.contents = [] - def is_empty(self): return self.contents == [] @@ -166,7 +207,8 @@ def __init__(self, title, attributes): self.values = [] self.contents = [] for name, value in sorted(attributes.items()): - value = iris.util.clip_string(str(value)) + value = value_repr(value) + value = iris.util.clip_string(value) self.names.append(name) self.values.append(value) content = "{}: {}".format(name, value) @@ -180,11 +222,13 @@ def __init__(self, title, cell_methods): class CubeSummary: + """ + This class provides a structure for output representations of an Iris cube. + TODO: use to produce the printout of :meth:`iris.cube.Cube.__str__`. + + """ + def __init__(self, cube, shorten=False, name_padding=35): - self.section_indent = 5 - self.item_indent = 10 - self.extra_indent = 13 - self.shorten = shorten self.header = FullHeader(cube, name_padding) # Cache the derived coords so we can rely on consistent @@ -249,9 +293,9 @@ def add_vector_section(title, contents, iscoord=True): add_vector_section("Dimension coordinates:", vector_dim_coords) add_vector_section("Auxiliary coordinates:", vector_aux_coords) add_vector_section("Derived coordinates:", vector_derived_coords) - add_vector_section("Cell Measures:", vector_cell_measures, False) + add_vector_section("Cell measures:", vector_cell_measures, False) add_vector_section( - "Ancillary Variables:", vector_ancillary_variables, False + "Ancillary variables:", vector_ancillary_variables, False ) self.scalar_sections = {} @@ -260,7 +304,7 @@ def add_scalar_section(section_class, title, *args): self.scalar_sections[title] = section_class(title, *args) add_scalar_section( - ScalarSection, "Scalar Coordinates:", cube, scalar_coords + ScalarSection, "Scalar coordinates:", cube, scalar_coords ) add_scalar_section( ScalarCellMeasureSection, diff --git a/lib/iris/tests/unit/representation/test_representation.py b/lib/iris/tests/unit/representation/test_representation.py index 212f454e70..69d2a71a97 100644 --- a/lib/iris/tests/unit/representation/test_representation.py +++ b/lib/iris/tests/unit/representation/test_representation.py @@ -54,8 +54,8 @@ def test_blank_cube(self): "Dimension coordinates:", "Auxiliary coordinates:", "Derived coordinates:", - "Cell Measures:", - "Ancillary Variables:", + "Cell measures:", + "Ancillary variables:", ] self.assertEqual( list(rep.vector_sections.keys()), expected_vector_sections @@ -66,7 +66,7 @@ def test_blank_cube(self): self.assertTrue(vector_section.is_empty()) expected_scalar_sections = [ - "Scalar Coordinates:", + "Scalar coordinates:", "Scalar cell measures:", "Attributes:", "Cell methods:", @@ -103,21 +103,28 @@ def test_scalar_coord(self): scalar_coord_with_bounds = AuxCoord( [10], long_name="foo", units="K", bounds=[(5, 15)] ) - scalar_coord_text = AuxCoord( - ["a\nb\nc"], long_name="foo", attributes={"key": "value"} + scalar_coord_simple_text = AuxCoord( + ["this and that"], + long_name="foo", + attributes={"key": 42, "key2": "value-str"}, + ) + scalar_coord_awkward_text = AuxCoord( + ["a is\nb\n and c"], long_name="foo_2" ) cube.add_aux_coord(scalar_coord_no_bounds) cube.add_aux_coord(scalar_coord_with_bounds) - cube.add_aux_coord(scalar_coord_text) + cube.add_aux_coord(scalar_coord_simple_text) + cube.add_aux_coord(scalar_coord_awkward_text) rep = iris._representation.CubeSummary(cube) - scalar_section = rep.scalar_sections["Scalar Coordinates:"] + scalar_section = rep.scalar_sections["Scalar coordinates:"] - self.assertEqual(len(scalar_section.contents), 3) + self.assertEqual(len(scalar_section.contents), 4) no_bounds_summary = scalar_section.contents[0] bounds_summary = scalar_section.contents[1] - text_summary = scalar_section.contents[2] + text_summary_simple = scalar_section.contents[2] + text_summary_awkward = scalar_section.contents[3] self.assertEqual(no_bounds_summary.name, "bar") self.assertEqual(no_bounds_summary.content, "10 K") @@ -127,9 +134,15 @@ def test_scalar_coord(self): self.assertEqual(bounds_summary.content, "10 K, bound=(5, 15) K") self.assertEqual(bounds_summary.extra, "") - self.assertEqual(text_summary.name, "foo") - self.assertEqual(text_summary.content, "a\nb\nc") - self.assertEqual(text_summary.extra, "key='value'") + self.assertEqual(text_summary_simple.name, "foo") + self.assertEqual(text_summary_simple.content, "this and that") + self.assertEqual(text_summary_simple.lines, ["this and that"]) + self.assertEqual(text_summary_simple.extra, "key=42, key2='value-str'") + + self.assertEqual(text_summary_awkward.name, "foo_2") + self.assertEqual(text_summary_awkward.content, r"'a is\nb\n and c'") + self.assertEqual(text_summary_awkward.lines, ["a is", "b", " and c"]) + self.assertEqual(text_summary_awkward.extra, "") def test_cell_measure(self): cube = self.cube @@ -137,7 +150,7 @@ def test_cell_measure(self): cube.add_cell_measure(cell_measure, 0) rep = iris._representation.CubeSummary(cube) - cm_section = rep.vector_sections["Cell Measures:"] + cm_section = rep.vector_sections["Cell measures:"] self.assertEqual(len(cm_section.contents), 1) cm_summary = cm_section.contents[0] @@ -150,7 +163,7 @@ def test_ancillary_variable(self): cube.add_ancillary_variable(cell_measure, 0) rep = iris._representation.CubeSummary(cube) - av_section = rep.vector_sections["Ancillary Variables:"] + av_section = rep.vector_sections["Ancillary variables:"] self.assertEqual(len(av_section.contents), 1) av_summary = av_section.contents[0] @@ -159,12 +172,14 @@ def test_ancillary_variable(self): def test_attributes(self): cube = self.cube - cube.attributes = {"a": 1, "b": "two"} + cube.attributes = {"a": 1, "b": "two", "c": " this \n that\tand."} rep = iris._representation.CubeSummary(cube) attribute_section = rep.scalar_sections["Attributes:"] attribute_contents = attribute_section.contents - expected_contents = ["a: 1", "b: two"] + expected_contents = ["a: 1", "b: two", "c: ' this \\n that\\tand.'"] + # Note: a string with \n or \t in it gets "repr-d". + # Other strings don't (though in coord 'extra' lines, they do.) self.assertEqual(attribute_contents, expected_contents) @@ -182,6 +197,108 @@ def test_cell_methods(self): expected_contents = ["mean: x, y", "mean: x"] self.assertEqual(cell_method_section.contents, expected_contents) + def test_scalar_cube(self): + cube = self.cube + while cube.ndim > 0: + cube = cube[0] + rep = iris._representation.CubeSummary(cube) + self.assertEqual(rep.header.nameunit, "air_temperature / (K)") + self.assertTrue(rep.header.dimension_header.scalar) + self.assertEqual(rep.header.dimension_header.dim_names, []) + self.assertEqual(rep.header.dimension_header.shape, []) + self.assertEqual(rep.header.dimension_header.contents, ["scalar cube"]) + self.assertEqual(len(rep.vector_sections), 5) + self.assertTrue( + all(sect.is_empty() for sect in rep.vector_sections.values()) + ) + self.assertEqual(len(rep.scalar_sections), 4) + self.assertEqual( + len(rep.scalar_sections["Scalar coordinates:"].contents), 1 + ) + self.assertTrue( + rep.scalar_sections["Scalar cell measures:"].is_empty() + ) + self.assertTrue(rep.scalar_sections["Attributes:"].is_empty()) + self.assertTrue(rep.scalar_sections["Cell methods:"].is_empty()) + + def test_coord_attributes(self): + cube = self.cube + co1 = cube.coord("latitude") + co1.attributes.update(dict(a=1, b=2)) + co2 = co1.copy() + co2.attributes.update(dict(a=7, z=77, text="ok", text2="multi\nline")) + cube.add_aux_coord(co2, cube.coord_dims(co1)) + rep = iris._representation.CubeSummary(cube) + co1_summ = rep.vector_sections["Dimension coordinates:"].contents[0] + co2_summ = rep.vector_sections["Auxiliary coordinates:"].contents[0] + # Notes: 'b' is same so does not appear; sorted order; quoted strings. + self.assertEqual(co1_summ.extra, "a=1") + self.assertEqual( + co2_summ.extra, "a=7, text='ok', text2='multi\\nline', z=77" + ) + + def test_array_attributes(self): + cube = self.cube + co1 = cube.coord("latitude") + co1.attributes.update(dict(a=1, array=np.array([1.2, 3]))) + co2 = co1.copy() + co2.attributes.update(dict(b=2, array=np.array([3.2, 1]))) + cube.add_aux_coord(co2, cube.coord_dims(co1)) + rep = iris._representation.CubeSummary(cube) + co1_summ = rep.vector_sections["Dimension coordinates:"].contents[0] + co2_summ = rep.vector_sections["Auxiliary coordinates:"].contents[0] + self.assertEqual(co1_summ.extra, "array=array([1.2, 3. ])") + self.assertEqual(co2_summ.extra, "array=array([3.2, 1. ]), b=2") + + def test_attributes_subtle_differences(self): + cube = Cube([0]) + + # Add a pair that differ only in having a list instead of an array. + co1a = DimCoord( + [0], + long_name="co1_list_or_array", + attributes=dict(x=1, arr1=np.array(2), arr2=np.array([1, 2])), + ) + co1b = co1a.copy() + co1b.attributes.update(dict(arr2=[1, 2])) + for co in (co1a, co1b): + cube.add_aux_coord(co) + + # Add a pair that differ only in an attribute array dtype. + co2a = AuxCoord( + [0], + long_name="co2_dtype", + attributes=dict(x=1, arr1=np.array(2), arr2=np.array([3, 4])), + ) + co2b = co2a.copy() + co2b.attributes.update(dict(arr2=np.array([3.0, 4.0]))) + assert co2b != co2a + for co in (co2a, co2b): + cube.add_aux_coord(co) + + # Add a pair that differ only in an attribute array shape. + co3a = DimCoord( + [0], + long_name="co3_shape", + attributes=dict(x=1, arr1=np.array([5, 6]), arr2=np.array([3, 4])), + ) + co3b = co3a.copy() + co3b.attributes.update(dict(arr1=np.array([[5], [6]]))) + for co in (co3a, co3b): + cube.add_aux_coord(co) + + rep = iris._representation.CubeSummary(cube) + co_summs = rep.scalar_sections["Scalar coordinates:"].contents + co1a_summ, co1b_summ = co_summs[0:2] + self.assertEqual(co1a_summ.extra, "arr2=array([1, 2])") + self.assertEqual(co1b_summ.extra, "arr2=[1, 2]") + co2a_summ, co2b_summ = co_summs[2:4] + self.assertEqual(co2a_summ.extra, "arr2=array([3, 4])") + self.assertEqual(co2b_summ.extra, "arr2=array([3., 4.])") + co3a_summ, co3b_summ = co_summs[4:6] + self.assertEqual(co3a_summ.extra, "arr1=array([5, 6])") + self.assertEqual(co3b_summ.extra, "arr1=array([[5], [6]])") + if __name__ == "__main__": tests.main() diff --git a/setup.py b/setup.py index b1c8939fdd..f4bfe4cf08 100644 --- a/setup.py +++ b/setup.py @@ -263,7 +263,7 @@ def long_description(): author="UK Met Office", author_email="scitools-iris-dev@googlegroups.com", description="A powerful, format-agnostic, community-driven Python " - "library for analysing and visualising Earth science data", + "package for analysing and visualising Earth science data", long_description=long_description(), long_description_content_type="text/markdown", packages=find_package_tree("lib/iris", "iris"),