From a136915a8421864a4bb18dcc06bade3b324ff1f3 Mon Sep 17 00:00:00 2001
From: Patrick Peglar
Date: Mon, 8 Feb 2021 11:20:36 +0000
Subject: [PATCH 01/12] Add abstract cube summary (#3987)
Co-authored-by: stephen.worsley
---
lib/iris/_representation.py | 273 ++++++++++++++++++
.../tests/unit/representation/__init__.py | 6 +
.../representation/test_representation.py | 187 ++++++++++++
3 files changed, 466 insertions(+)
create mode 100644 lib/iris/_representation.py
create mode 100644 lib/iris/tests/unit/representation/__init__.py
create mode 100644 lib/iris/tests/unit/representation/test_representation.py
diff --git a/lib/iris/_representation.py b/lib/iris/_representation.py
new file mode 100644
index 0000000000..301f4a9a22
--- /dev/null
+++ b/lib/iris/_representation.py
@@ -0,0 +1,273 @@
+# 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.
+"""
+Provides objects describing cube summaries.
+"""
+
+import iris.util
+
+
+class DimensionHeader:
+ def __init__(self, cube):
+ if cube.shape == ():
+ self.scalar = True
+ self.dim_names = []
+ self.shape = []
+ self.contents = ["scalar cube"]
+ else:
+ self.scalar = False
+ self.dim_names = []
+ for dim in range(len(cube.shape)):
+ dim_coords = cube.coords(
+ contains_dimension=dim, dim_coords=True
+ )
+ if dim_coords:
+ self.dim_names.append(dim_coords[0].name())
+ else:
+ self.dim_names.append("-- ")
+ self.shape = list(cube.shape)
+ self.contents = [
+ name + ": %d" % dim_len
+ for name, dim_len in zip(self.dim_names, self.shape)
+ ]
+
+
+class FullHeader:
+ def __init__(self, cube, name_padding=35):
+ self.name = cube.name()
+ self.unit = cube.units
+ self.nameunit = "{name} / ({units})".format(
+ name=self.name, units=self.unit
+ )
+ self.name_padding = name_padding
+ self.dimension_header = DimensionHeader(cube)
+
+
+class CoordSummary:
+ def _summary_coord_extra(self, cube, coord):
+ # Returns the text needed to ensure this coordinate can be
+ # distinguished from all others with the same name.
+ extra = ""
+ similar_coords = cube.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
+ vary = set()
+ attributes = {}
+ for key in keys:
+ 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:
+ vary.add(key)
+ break
+ keys = sorted(vary & set(coord.attributes.keys()))
+ bits = [
+ "{}={!r}".format(key, coord.attributes[key]) for key in keys
+ ]
+ if bits:
+ extra = ", ".join(bits)
+ return extra
+
+
+class VectorSummary(CoordSummary):
+ def __init__(self, cube, vector, iscoord):
+ self.name = iris.util.clip_string(vector.name())
+ dims = vector.cube_dims(cube)
+ self.dim_chars = [
+ "x" if dim in dims else "-" for dim in range(len(cube.shape))
+ ]
+ if iscoord:
+ extra = self._summary_coord_extra(cube, vector)
+ self.extra = iris.util.clip_string(extra)
+ else:
+ self.extra = ""
+
+
+class ScalarSummary(CoordSummary):
+ def __init__(self, cube, coord):
+ self.name = coord.name()
+ if (
+ coord.units in ["1", "no_unit", "unknown"]
+ or coord.units.is_time_reference()
+ ):
+ self.unit = ""
+ else:
+ self.unit = " {!s}".format(coord.units)
+ coord_cell = coord.cell(0)
+ if isinstance(coord_cell.point, str):
+ self.string_type = True
+ 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)
+ else:
+ self.string_type = False
+ self.lines = None
+ self.point = "{!s}".format(coord_cell.point)
+ coord_cell_cbound = coord_cell.bound
+ if coord_cell_cbound is not None:
+ self.bound = "({})".format(
+ ", ".join(str(val) for val in coord_cell_cbound)
+ )
+ self.content = "{}{}, bound={}{}".format(
+ self.point, self.unit, self.bound, self.unit
+ )
+ else:
+ self.bound = None
+ self.content = "{}{}".format(self.point, self.unit)
+ extra = self._summary_coord_extra(cube, coord)
+ self.extra = iris.util.clip_string(extra)
+
+
+class Section:
+ def _init_(self):
+ self.contents = []
+
+ def is_empty(self):
+ return self.contents == []
+
+
+class VectorSection(Section):
+ def __init__(self, title, cube, vectors, iscoord):
+ self.title = title
+ self.contents = [
+ VectorSummary(cube, vector, iscoord) for vector in vectors
+ ]
+
+
+class ScalarSection(Section):
+ def __init__(self, title, cube, scalars):
+ self.title = title
+ self.contents = [ScalarSummary(cube, scalar) for scalar in scalars]
+
+
+class ScalarCellMeasureSection(Section):
+ def __init__(self, title, cell_measures):
+ self.title = title
+ self.contents = [cm.name() for cm in cell_measures]
+
+
+class AttributeSection(Section):
+ def __init__(self, title, attributes):
+ self.title = title
+ self.names = []
+ self.values = []
+ self.contents = []
+ for name, value in sorted(attributes.items()):
+ value = iris.util.clip_string(str(value))
+ self.names.append(name)
+ self.values.append(value)
+ content = "{}: {}".format(name, value)
+ self.contents.append(content)
+
+
+class CellMethodSection(Section):
+ def __init__(self, title, cell_methods):
+ self.title = title
+ self.contents = [str(cm) for cm in cell_methods]
+
+
+class CubeSummary:
+ 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
+ # object IDs.
+ derived_coords = cube.derived_coords
+ # Determine the cube coordinates that are scalar (single-valued)
+ # AND non-dimensioned.
+ dim_coords = cube.dim_coords
+ aux_coords = cube.aux_coords
+ all_coords = dim_coords + aux_coords + derived_coords
+ scalar_coords = [
+ coord
+ for coord in all_coords
+ if not cube.coord_dims(coord) and coord.shape == (1,)
+ ]
+ # Determine the cube coordinates that are not scalar BUT
+ # dimensioned.
+ scalar_coord_ids = set(map(id, scalar_coords))
+ vector_dim_coords = [
+ coord for coord in dim_coords if id(coord) not in scalar_coord_ids
+ ]
+ vector_aux_coords = [
+ coord for coord in aux_coords if id(coord) not in scalar_coord_ids
+ ]
+ vector_derived_coords = [
+ coord
+ for coord in derived_coords
+ if id(coord) not in scalar_coord_ids
+ ]
+
+ # cell measures
+ vector_cell_measures = [
+ cm for cm in cube.cell_measures() if cm.shape != (1,)
+ ]
+
+ # Ancillary Variables
+ vector_ancillary_variables = [av for av in cube.ancillary_variables()]
+
+ # Sort scalar coordinates by name.
+ scalar_coords.sort(key=lambda coord: coord.name())
+ # Sort vector coordinates by data dimension and name.
+ vector_dim_coords.sort(
+ key=lambda coord: (cube.coord_dims(coord), coord.name())
+ )
+ vector_aux_coords.sort(
+ key=lambda coord: (cube.coord_dims(coord), coord.name())
+ )
+ vector_derived_coords.sort(
+ key=lambda coord: (cube.coord_dims(coord), coord.name())
+ )
+ scalar_cell_measures = [
+ cm for cm in cube.cell_measures() if cm.shape == (1,)
+ ]
+
+ self.vector_sections = {}
+
+ def add_vector_section(title, contents, iscoord=True):
+ self.vector_sections[title] = VectorSection(
+ title, cube, contents, iscoord
+ )
+
+ 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(
+ "Ancillary Variables:", vector_ancillary_variables, False
+ )
+
+ self.scalar_sections = {}
+
+ 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
+ )
+ add_scalar_section(
+ ScalarCellMeasureSection,
+ "Scalar cell measures:",
+ scalar_cell_measures,
+ )
+ add_scalar_section(AttributeSection, "Attributes:", cube.attributes)
+ add_scalar_section(
+ CellMethodSection, "Cell methods:", cube.cell_methods
+ )
diff --git a/lib/iris/tests/unit/representation/__init__.py b/lib/iris/tests/unit/representation/__init__.py
new file mode 100644
index 0000000000..e943ad149b
--- /dev/null
+++ b/lib/iris/tests/unit/representation/__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._representation` module."""
diff --git a/lib/iris/tests/unit/representation/test_representation.py b/lib/iris/tests/unit/representation/test_representation.py
new file mode 100644
index 0000000000..212f454e70
--- /dev/null
+++ b/lib/iris/tests/unit/representation/test_representation.py
@@ -0,0 +1,187 @@
+# 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._representation` module."""
+
+import numpy as np
+import iris.tests as tests
+import iris._representation
+from iris.cube import Cube
+from iris.coords import (
+ DimCoord,
+ AuxCoord,
+ CellMeasure,
+ AncillaryVariable,
+ CellMethod,
+)
+
+
+def example_cube():
+ cube = Cube(
+ np.arange(6).reshape([3, 2]),
+ standard_name="air_temperature",
+ long_name="screen_air_temp",
+ var_name="airtemp",
+ units="K",
+ )
+ lat = DimCoord([0, 1, 2], standard_name="latitude", units="degrees")
+ cube.add_dim_coord(lat, 0)
+ return cube
+
+
+class Test_CubeSummary(tests.IrisTest):
+ def setUp(self):
+ self.cube = example_cube()
+
+ def test_header(self):
+ rep = iris._representation.CubeSummary(self.cube)
+ header_left = rep.header.nameunit
+ header_right = rep.header.dimension_header.contents
+
+ self.assertEqual(header_left, "air_temperature / (K)")
+ self.assertEqual(header_right, ["latitude: 3", "-- : 2"])
+
+ def test_blank_cube(self):
+ cube = Cube([1, 2])
+ rep = iris._representation.CubeSummary(cube)
+
+ self.assertEqual(rep.header.nameunit, "unknown / (unknown)")
+ self.assertEqual(rep.header.dimension_header.contents, ["-- : 2"])
+
+ expected_vector_sections = [
+ "Dimension coordinates:",
+ "Auxiliary coordinates:",
+ "Derived coordinates:",
+ "Cell Measures:",
+ "Ancillary Variables:",
+ ]
+ self.assertEqual(
+ list(rep.vector_sections.keys()), expected_vector_sections
+ )
+ for title in expected_vector_sections:
+ vector_section = rep.vector_sections[title]
+ self.assertEqual(vector_section.contents, [])
+ self.assertTrue(vector_section.is_empty())
+
+ expected_scalar_sections = [
+ "Scalar Coordinates:",
+ "Scalar cell measures:",
+ "Attributes:",
+ "Cell methods:",
+ ]
+
+ self.assertEqual(
+ list(rep.scalar_sections.keys()), expected_scalar_sections
+ )
+ for title in expected_scalar_sections:
+ scalar_section = rep.scalar_sections[title]
+ self.assertEqual(scalar_section.contents, [])
+ self.assertTrue(scalar_section.is_empty())
+
+ def test_vector_coord(self):
+ rep = iris._representation.CubeSummary(self.cube)
+ dim_section = rep.vector_sections["Dimension coordinates:"]
+
+ self.assertEqual(len(dim_section.contents), 1)
+ self.assertFalse(dim_section.is_empty())
+
+ dim_summary = dim_section.contents[0]
+
+ name = dim_summary.name
+ dim_chars = dim_summary.dim_chars
+ extra = dim_summary.extra
+
+ self.assertEqual(name, "latitude")
+ self.assertEqual(dim_chars, ["x", "-"])
+ self.assertEqual(extra, "")
+
+ def test_scalar_coord(self):
+ cube = self.cube
+ scalar_coord_no_bounds = AuxCoord([10], long_name="bar", units="K")
+ 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"}
+ )
+ cube.add_aux_coord(scalar_coord_no_bounds)
+ cube.add_aux_coord(scalar_coord_with_bounds)
+ cube.add_aux_coord(scalar_coord_text)
+ rep = iris._representation.CubeSummary(cube)
+
+ scalar_section = rep.scalar_sections["Scalar Coordinates:"]
+
+ self.assertEqual(len(scalar_section.contents), 3)
+
+ no_bounds_summary = scalar_section.contents[0]
+ bounds_summary = scalar_section.contents[1]
+ text_summary = scalar_section.contents[2]
+
+ self.assertEqual(no_bounds_summary.name, "bar")
+ self.assertEqual(no_bounds_summary.content, "10 K")
+ self.assertEqual(no_bounds_summary.extra, "")
+
+ self.assertEqual(bounds_summary.name, "foo")
+ 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'")
+
+ def test_cell_measure(self):
+ cube = self.cube
+ cell_measure = CellMeasure([1, 2, 3], long_name="foo")
+ cube.add_cell_measure(cell_measure, 0)
+ rep = iris._representation.CubeSummary(cube)
+
+ cm_section = rep.vector_sections["Cell Measures:"]
+ self.assertEqual(len(cm_section.contents), 1)
+
+ cm_summary = cm_section.contents[0]
+ self.assertEqual(cm_summary.name, "foo")
+ self.assertEqual(cm_summary.dim_chars, ["x", "-"])
+
+ def test_ancillary_variable(self):
+ cube = self.cube
+ cell_measure = AncillaryVariable([1, 2, 3], long_name="foo")
+ cube.add_ancillary_variable(cell_measure, 0)
+ rep = iris._representation.CubeSummary(cube)
+
+ av_section = rep.vector_sections["Ancillary Variables:"]
+ self.assertEqual(len(av_section.contents), 1)
+
+ av_summary = av_section.contents[0]
+ self.assertEqual(av_summary.name, "foo")
+ self.assertEqual(av_summary.dim_chars, ["x", "-"])
+
+ def test_attributes(self):
+ cube = self.cube
+ cube.attributes = {"a": 1, "b": "two"}
+ rep = iris._representation.CubeSummary(cube)
+
+ attribute_section = rep.scalar_sections["Attributes:"]
+ attribute_contents = attribute_section.contents
+ expected_contents = ["a: 1", "b: two"]
+
+ self.assertEqual(attribute_contents, expected_contents)
+
+ def test_cell_methods(self):
+ cube = self.cube
+ x = AuxCoord(1, long_name="x")
+ y = AuxCoord(1, long_name="y")
+ cell_method_xy = CellMethod("mean", [x, y])
+ cell_method_x = CellMethod("mean", x)
+ cube.add_cell_method(cell_method_xy)
+ cube.add_cell_method(cell_method_x)
+
+ rep = iris._representation.CubeSummary(cube)
+ cell_method_section = rep.scalar_sections["Cell methods:"]
+ expected_contents = ["mean: x, y", "mean: x"]
+ self.assertEqual(cell_method_section.contents, expected_contents)
+
+
+if __name__ == "__main__":
+ tests.main()
From 7d73cf20a75ff55230bf108bc8689f9edd24bb76 Mon Sep 17 00:00:00 2001
From: Bill Little
Date: Tue, 9 Feb 2021 10:15:38 +0000
Subject: [PATCH 02/12] add nox session conda list (#3990)
---
.cirrus.yml | 10 +-
.../contributing_running_tests.rst | 8 +
docs/src/whatsnew/latest.rst | 4 +
noxfile.py | 148 +++++++-----------
4 files changed, 73 insertions(+), 97 deletions(-)
diff --git a/.cirrus.yml b/.cirrus.yml
index 971bd3b81b..007bab403e 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -107,7 +107,7 @@ linux_minimal_task:
tests_script:
- echo "[Resources]" > ${SITE_CFG}
- echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG}
- - nox --session tests
+ - nox --session tests -- --verbose
#
@@ -137,7 +137,7 @@ linux_task:
- echo "[Resources]" > ${SITE_CFG}
- echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG}
- echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG}
- - nox --session tests
+ - nox --session tests -- --verbose
#
@@ -167,7 +167,7 @@ gallery_task:
- echo "[Resources]" > ${SITE_CFG}
- echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG}
- echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG}
- - nox --session gallery
+ - nox --session gallery -- --verbose
#
@@ -201,7 +201,7 @@ doctest_task:
- mkdir -p ${MPL_RC_DIR}
- echo "backend : agg" > ${MPL_RC_FILE}
- echo "image.cmap : viridis" >> ${MPL_RC_FILE}
- - nox --session doctest
+ - nox --session doctest -- --verbose
#
@@ -224,4 +224,4 @@ link_task:
- mkdir -p ${MPL_RC_DIR}
- echo "backend : agg" > ${MPL_RC_FILE}
- echo "image.cmap : viridis" >> ${MPL_RC_FILE}
- - nox --session linkcheck
+ - nox --session linkcheck -- --verbose
diff --git a/docs/src/developers_guide/contributing_running_tests.rst b/docs/src/developers_guide/contributing_running_tests.rst
index 99ea4e831c..0fd9fa8486 100644
--- a/docs/src/developers_guide/contributing_running_tests.rst
+++ b/docs/src/developers_guide/contributing_running_tests.rst
@@ -175,6 +175,14 @@ For further `nox`_ command-line options::
nox --help
+.. tip::
+ For `nox`_ sessions that use the `conda`_ backend, you can use the ``-v`` or ``--verbose``
+ flag to display the `nox`_ `conda`_ environment package details and environment info.
+ For example::
+
+ nox --session tests -- --verbose
+
+
.. note:: `nox`_ will cache its testing environments in the `.nox` root ``iris`` project directory.
diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index fbb98cb1e3..ed11f60719 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -94,6 +94,10 @@ This document explains the changes made to Iris for this release
#. `@jamesp`_ updated a test to the latest numpy version (:pull:`3977`)
+#. `@bjlittle`_ rationalised the ``noxfile.py``, and added the ability for
+ each ``nox`` session to list its ``conda`` environment packages and
+ environment info. (:pull:`3990`)
+
.. comment
Whatsnew author names (@github name) in alphabetical order. Note that,
diff --git a/noxfile.py b/noxfile.py
index fc6175bdf0..b6f9480290 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -93,6 +93,58 @@ def cache_cartopy(session):
)
+def prepare_venv(session):
+ """
+ Create and cache the nox session conda environment, and additionally
+ provide conda environment package details and info.
+
+ Note that, iris is installed into the environment using pip.
+
+ 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{session.python.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.install("--no-deps", "--editable", ".")
+
+ # Determine whether verbose diagnostics have been requested
+ # from the command line.
+ verbose = "-v" in session.posargs or "--verbose" in session.posargs
+
+ if verbose:
+ session.run("conda", "info")
+ session.run("conda", "list", f"--prefix={session.virtualenv.location}")
+ session.run(
+ "conda",
+ "list",
+ f"--prefix={session.virtualenv.location}",
+ "--explicit",
+ )
+
+
@nox.session
def flake8(session):
"""
@@ -141,30 +193,8 @@ def tests(session):
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{session.python.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.install("--no-deps", "--editable", ".")
+ prepare_venv(session)
session.run(
"python",
"-m",
@@ -184,30 +214,8 @@ def gallery(session):
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{session.python.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.install("--no-deps", "--editable", ".")
+ prepare_venv(session)
session.run(
"python",
"-m",
@@ -226,30 +234,8 @@ def doctest(session):
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{session.python.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.install("--no-deps", "--editable", ".")
+ prepare_venv(session)
session.cd("docs")
session.run(
"make",
@@ -274,30 +260,8 @@ def linkcheck(session):
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{session.python.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.install("--no-deps", "--editable", ".")
+ prepare_venv(session)
session.cd("docs")
session.run(
"make",
From c51dab213b92b9e7eb1a95e5f650c9fec0f5b9d4 Mon Sep 17 00:00:00 2001
From: tkknight <2108488+tkknight@users.noreply.github.com>
Date: Tue, 9 Feb 2021 12:14:40 +0000
Subject: [PATCH 03/12] 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
---
docs/src/_templates/footer.html | 5 +++++
docs/src/conf.py | 14 +++++++++-----
.../contributing_documentation.rst | 3 +++
docs/src/installing.rst | 4 +++-
docs/src/whatsnew/latest.rst | 4 ++++
5 files changed, 24 insertions(+), 6 deletions(-)
create mode 100644 docs/src/_templates/footer.html
diff --git a/docs/src/_templates/footer.html b/docs/src/_templates/footer.html
new file mode 100644
index 0000000000..1d5fb08b78
--- /dev/null
+++ b/docs/src/_templates/footer.html
@@ -0,0 +1,5 @@
+{% extends "!footer.html" %}
+{% block extrafooter %}
+ Built using Python {{ python_version }}.
+ {{ super() }}
+{% endblock %}
diff --git a/docs/src/conf.py b/docs/src/conf.py
index 30e6150b39..843af17944 100644
--- a/docs/src/conf.py
+++ b/docs/src/conf.py
@@ -69,8 +69,8 @@ def autolog(message):
# define the copyright information for latex builds. Note, for html builds,
# the copyright exists directly inside "_templates/layout.html"
-upper_copy_year = datetime.datetime.now().year
-copyright = "Iris Contributors"
+copyright_years = f"2010 - {datetime.datetime.now().year}"
+copyright = f"{copyright_years}, Iris Contributors"
author = "Iris Developers"
# The version info for the project you're documenting, acts as replacement for
@@ -95,9 +95,12 @@ def autolog(message):
# Create a variable that can be inserted in the rst "|copyright_years|".
# You can add more variables here if needed.
+
+build_python_version = ".".join([str(i) for i in sys.version_info[:3]])
+
rst_epilog = f"""
-.. |copyright_years| replace:: 2010 - {upper_copy_year}
-.. |python_version| replace:: {'.'.join([str(i) for i in sys.version_info[:3]])}
+.. |copyright_years| replace:: {copyright_years}
+.. |python_version| replace:: {build_python_version}
.. |iris_version| replace:: v{version}
.. |build_date| replace:: ({datetime.datetime.now().strftime('%d %b %Y')})
"""
@@ -225,7 +228,8 @@ def autolog(message):
}
html_context = {
- "copyright_years": "2010 - {}".format(upper_copy_year),
+ "copyright_years": copyright_years,
+ "python_version": build_python_version,
# menu_links and menu_links_name are used in _templates/layout.html
# to include some nice icons. See http://fontawesome.io for a list of
# icons (used in the sphinx_rtd_theme)
diff --git a/docs/src/developers_guide/contributing_documentation.rst b/docs/src/developers_guide/contributing_documentation.rst
index 75e9dfe29c..167e8937b9 100644
--- a/docs/src/developers_guide/contributing_documentation.rst
+++ b/docs/src/developers_guide/contributing_documentation.rst
@@ -24,6 +24,9 @@ The documentation uses specific packages that need to be present. Please see
Building
~~~~~~~~
+This documentation was built using the latest Python version that Iris
+supports. For more information see :ref:`installing_iris`.
+
The build can be run from the documentation directory ``docs/src``.
The build output for the html is found in the ``_build/html`` sub directory.
diff --git a/docs/src/installing.rst b/docs/src/installing.rst
index 8b3ae8d3e7..31fc497b85 100644
--- a/docs/src/installing.rst
+++ b/docs/src/installing.rst
@@ -17,7 +17,9 @@ any WSL_ distributions.
.. _WSL: https://docs.microsoft.com/en-us/windows/wsl/install-win10
.. note:: Iris currently supports and is tested against **Python 3.6** and
- **Python 3.7**.
+ **Python 3.7**.
+
+.. note:: This documentation was built using Python |python_version|.
.. _installing_using_conda:
diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index ed11f60719..1efa08874a 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -83,6 +83,10 @@ This document explains the changes made to Iris for this release
#. `@bjlittle`_ added automated Iris version discovery for the ``latest.rst``
in the ``whatsnew`` documentation. (:pull:`3981`)
+#. `@tkknight`_ stated the Python version used to build the documentation
+ on :ref:`installing_iris` and to the footer of all pages. Also added the
+ copyright years to the footer. (:pull:`3989`)
+
💼 Internal
===========
From 8fb33bb8a9975f311795ae6c1f4301175b10307d Mon Sep 17 00:00:00 2001
From: Bill Little
Date: Tue, 9 Feb 2021 13:36:18 +0000
Subject: [PATCH 04/12] 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
---
.cirrus.yml | 12 +--
docs/src/common_links.inc | 2 +
.../contributing_running_tests.rst | 2 -
docs/src/further_topics/metadata.rst | 7 +-
docs/src/installing.rst | 4 +-
docs/src/whatsnew/latest.rst | 8 +-
lib/iris/coords.py | 77 +++++++++++++++++--
lib/iris/cube.py | 56 ++++++++++++++
lib/iris/tests/__init__.py | 4 +
noxfile.py | 2 +-
requirements/ci/iris.yml | 2 +-
requirements/ci/py38.yml | 51 ++++++++++++
12 files changed, 203 insertions(+), 24 deletions(-)
create mode 100644 requirements/ci/py38.yml
diff --git a/.cirrus.yml b/.cirrus.yml
index 007bab403e..da425a5691 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -98,6 +98,8 @@ linux_minimal_task:
PY_VER: 3.6
env:
PY_VER: 3.7
+ env:
+ PY_VER: 3.8
name: "${CIRRUS_OS}: py${PY_VER} tests (minimal)"
container:
image: gcc:latest
@@ -119,6 +121,8 @@ linux_task:
PY_VER: 3.6
env:
PY_VER: 3.7
+ env:
+ PY_VER: 3.8
name: "${CIRRUS_OS}: py${PY_VER} tests (full)"
container:
image: gcc:latest
@@ -146,9 +150,7 @@ linux_task:
gallery_task:
matrix:
env:
- PY_VER: 3.6
- env:
- PY_VER: 3.7
+ PY_VER: 3.8
name: "${CIRRUS_OS}: py${PY_VER} doc tests (gallery)"
container:
image: gcc:latest
@@ -176,7 +178,7 @@ gallery_task:
doctest_task:
matrix:
env:
- PY_VER: 3.7
+ PY_VER: 3.8
name: "${CIRRUS_OS}: py${PY_VER} doc tests"
container:
image: gcc:latest
@@ -210,7 +212,7 @@ doctest_task:
link_task:
matrix:
env:
- PY_VER: 3.7
+ PY_VER: 3.8
name: "${CIRRUS_OS}: py${PY_VER} doc link check"
container:
image: gcc:latest
diff --git a/docs/src/common_links.inc b/docs/src/common_links.inc
index 9f6a57f529..157444d65d 100644
--- a/docs/src/common_links.inc
+++ b/docs/src/common_links.inc
@@ -1,6 +1,7 @@
.. comment
Common resources in alphabetical order:
+.. _black: https://black.readthedocs.io/en/stable/
.. _.cirrus.yml: https://github.com/SciTools/iris/blob/master/.cirrus.yml
.. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8
.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris
@@ -19,6 +20,7 @@
.. _legacy documentation: https://scitools.org.uk/iris/docs/v2.4.0/
.. _matplotlib: https://matplotlib.org/
.. _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
.. _pull request: https://github.com/SciTools/iris/pulls
.. _pull requests: https://github.com/SciTools/iris/pulls
diff --git a/docs/src/developers_guide/contributing_running_tests.rst b/docs/src/developers_guide/contributing_running_tests.rst
index 0fd9fa8486..9bc2d797bd 100644
--- a/docs/src/developers_guide/contributing_running_tests.rst
+++ b/docs/src/developers_guide/contributing_running_tests.rst
@@ -186,8 +186,6 @@ For further `nox`_ command-line options::
.. 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/
diff --git a/docs/src/further_topics/metadata.rst b/docs/src/further_topics/metadata.rst
index e6d6ebc57a..ab6a6450b4 100644
--- a/docs/src/further_topics/metadata.rst
+++ b/docs/src/further_topics/metadata.rst
@@ -258,12 +258,12 @@ create a **new** instance directly from the metadata class itself,
>>> DimCoordMetadata._make(values)
DimCoordMetadata(standard_name=1, long_name=2, var_name=3, units=4, attributes=5, coord_system=6, climatological=7, circular=8)
-It is also possible to easily convert ``metadata`` to an `OrderedDict`_
+It is also possible to easily convert ``metadata`` to an `dict`_
using the `namedtuple._asdict`_ method. This can be particularly handy when a
standard Python built-in container is required to represent your ``metadata``,
>>> metadata._asdict()
- OrderedDict([('standard_name', 'longitude'), ('long_name', None), ('var_name', 'longitude'), ('units', Unit('degrees')), ('attributes', {'grinning face': '🙃'}), ('coord_system', GeogCS(6371229.0)), ('climatological', False), ('circular', False)])
+ {'standard_name': 'longitude', 'long_name': None, 'var_name': 'longitude', 'units': Unit('degrees'), 'attributes': {'grinning face': '🙃'}, 'coord_system': GeogCS(6371229.0), 'climatological': False, 'circular': False}
Using the `namedtuple._replace`_ method allows you to create a new metadata
class instance, but replacing specified members with **new** associated values,
@@ -943,7 +943,7 @@ such as a `dict`_,
>>> mapping = latitude.metadata._asdict()
>>> mapping
- OrderedDict([('standard_name', 'latitude'), ('long_name', None), ('var_name', 'latitude'), ('units', Unit('degrees')), ('attributes', {}), ('coord_system', GeogCS(6371229.0)), ('climatological', False), ('circular', False)])
+ {'standard_name': 'latitude', 'long_name': None, 'var_name': 'latitude', 'units': Unit('degrees'), 'attributes': {}, 'coord_system': GeogCS(6371229.0), 'climatological': False, 'circular': False}
>>> longitude.metadata = mapping
>>> longitude.metadata
DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False)
@@ -1000,7 +1000,6 @@ values. All other metadata members will be left unaltered.
.. _NetCDF: https://www.unidata.ucar.edu/software/netcdf/
.. _NetCDF CF Metadata Conventions: https://cfconventions.org/
.. _NumPy: https://github.com/numpy/numpy
-.. _OrderedDict: https://docs.python.org/3/library/collections.html#collections.OrderedDict
.. _Parametric Vertical Coordinate: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#parametric-vertical-coordinate
.. _rich comparison: https://www.python.org/dev/peps/pep-0207/
.. _SciTools/iris: https://github.com/SciTools/iris
diff --git a/docs/src/installing.rst b/docs/src/installing.rst
index 31fc497b85..8deb7043c5 100644
--- a/docs/src/installing.rst
+++ b/docs/src/installing.rst
@@ -16,8 +16,8 @@ any WSL_ distributions.
.. _WSL: https://docs.microsoft.com/en-us/windows/wsl/install-win10
-.. note:: Iris currently supports and is tested against **Python 3.6** and
- **Python 3.7**.
+.. note:: Iris is currently supported and tested against Python ``3.6``,
+ ``3.7``, and ``3.8``.
.. note:: This documentation was built using Python |python_version|.
diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index 1efa08874a..c02b61341b 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -12,7 +12,6 @@ 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/minor release of Iris include:
@@ -96,7 +95,10 @@ This document explains the changes made to Iris for this release
#. `@tkknight`_ moved the ``docs/iris`` directory to be in the parent
directory ``docs``. (:pull:`3975`)
-#. `@jamesp`_ updated a test to the latest numpy version (:pull:`3977`)
+#. `@jamesp`_ updated a test for `numpy`_ ``1.20.0``. (:pull:`3977`)
+
+#. `@bjlittle`_ and `@jamesp`_ extended the `cirrus-ci`_ testing and `nox`_
+ testing automation to support `Python 3.8`_. (:pull:`3976`)
#. `@bjlittle`_ rationalised the ``noxfile.py``, and added the ability for
each ``nox`` session to list its ``conda`` environment packages and
@@ -117,3 +119,5 @@ This document explains the changes made to Iris for this release
.. _abstract base class: https://docs.python.org/3/library/abc.html
.. _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
+.. _Python 3.8: https://www.python.org/downloads/release/python-380/
diff --git a/lib/iris/coords.py b/lib/iris/coords.py
index cfeb24cdcb..6129b35150 100644
--- a/lib/iris/coords.py
+++ b/lib/iris/coords.py
@@ -578,7 +578,21 @@ def shape(self):
return self._values_dm.shape
def xml_element(self, doc):
- """Return a DOM element describing this metadata."""
+ """
+ Create the :class:`xml.dom.minidom.Element` that describes this
+ :class:`_DimensionalMetadata`.
+
+ Args:
+
+ * doc:
+ The parent :class:`xml.dom.minidom.Document`.
+
+ Returns:
+ The :class:`xml.dom.minidom.Element` that will describe this
+ :class:`_DimensionalMetadata`, and the dictionary of attributes
+ that require to be added to this element.
+
+ """
# Create the XML element as the camelCaseEquivalent of the
# class name.
element_name = type(self).__name__
@@ -881,6 +895,20 @@ def cube_dims(self, cube):
return cube.cell_measure_dims(self)
def xml_element(self, doc):
+ """
+ Create the :class:`xml.dom.minidom.Element` that describes this
+ :class:`CellMeasure`.
+
+ Args:
+
+ * doc:
+ The parent :class:`xml.dom.minidom.Document`.
+
+ Returns:
+ The :class:`xml.dom.minidom.Element` that describes this
+ :class:`CellMeasure`.
+
+ """
# Create the XML element as the camelCaseEquivalent of the
# class name
element = super().xml_element(doc=doc)
@@ -2228,14 +2256,26 @@ def nearest_neighbour_index(self, point):
return result_index
def xml_element(self, doc):
- """Return a DOM element describing this Coord."""
+ """
+ Create the :class:`xml.dom.minidom.Element` that describes this
+ :class:`Coord`.
+
+ Args:
+
+ * doc:
+ The parent :class:`xml.dom.minidom.Document`.
+
+ Returns:
+ The :class:`xml.dom.minidom.Element` that will describe this
+ :class:`DimCoord`, and the dictionary of attributes that require
+ to be added to this element.
+
+ """
# Create the XML element as the camelCaseEquivalent of the
# class name
element = super().xml_element(doc=doc)
- element.setAttribute("points", self._xml_array_repr(self.points))
-
- # Add bounds handling
+ # Add bounds, points are handled by the parent class.
if self.has_bounds():
element.setAttribute("bounds", self._xml_array_repr(self.bounds))
@@ -2614,7 +2654,20 @@ def is_monotonic(self):
return True
def xml_element(self, doc):
- """Return DOM element describing this :class:`iris.coords.DimCoord`."""
+ """
+ Create the :class:`xml.dom.minidom.Element` that describes this
+ :class:`DimCoord`.
+
+ Args:
+
+ * doc:
+ The parent :class:`xml.dom.minidom.Document`.
+
+ Returns:
+ The :class:`xml.dom.minidom.Element` that describes this
+ :class:`DimCoord`.
+
+ """
element = super().xml_element(doc)
if self.circular:
element.setAttribute("circular", str(self.circular))
@@ -2794,7 +2847,17 @@ def __add__(self, other):
def xml_element(self, doc):
"""
- Return a dom element describing itself
+ Create the :class:`xml.dom.minidom.Element` that describes this
+ :class:`CellMethod`.
+
+ Args:
+
+ * doc:
+ The parent :class:`xml.dom.minidom.Document`.
+
+ Returns:
+ The :class:`xml.dom.minidom.Element` that describes this
+ :class:`CellMethod`.
"""
cellMethod_xml_element = doc.createElement("cellMethod")
diff --git a/lib/iris/cube.py b/lib/iris/cube.py
index 7c7d6c58e9..5578507d28 100644
--- a/lib/iris/cube.py
+++ b/lib/iris/cube.py
@@ -225,6 +225,7 @@ def __getslice__(self, start, stop):
def xml(self, checksum=False, order=True, byteorder=True):
"""Return a string of the XML that this list of cubes represents."""
+
doc = Document()
cubes_xml_element = doc.createElement("cubes")
cubes_xml_element.setAttribute("xmlns", XML_NAMESPACE_URI)
@@ -239,6 +240,7 @@ def xml(self, checksum=False, order=True, byteorder=True):
doc.appendChild(cubes_xml_element)
# return our newly created XML string
+ doc = Cube._sort_xml_attrs(doc)
return doc.toprettyxml(indent=" ")
def extract(self, constraints):
@@ -755,6 +757,59 @@ class Cube(CFVariableMixin):
#: is similar to Fortran or Matlab, but different than numpy.
__orthogonal_indexing__ = True
+ @classmethod
+ def _sort_xml_attrs(cls, doc):
+ """
+ Takes an xml document and returns a copy with all element
+ attributes sorted in alphabetical order.
+
+ This is a private utility method required by iris to maintain
+ legacy xml behaviour beyond python 3.7.
+
+ Args:
+
+ * doc:
+ The :class:`xml.dom.minidom.Document`.
+
+ Returns:
+ The :class:`xml.dom.minidom.Document` with sorted element
+ attributes.
+
+ """
+ from xml.dom.minidom import Document
+
+ def _walk_nodes(node):
+ """Note: _walk_nodes is called recursively on child elements."""
+
+ # we don't want to copy the children here, so take a shallow copy
+ new_node = node.cloneNode(deep=False)
+
+ # Versions of python <3.8 order attributes in alphabetical order.
+ # Python >=3.8 order attributes in insert order. For consistent behaviour
+ # across both, we'll go with alphabetical order always.
+ # Remove all the attribute nodes, then add back in alphabetical order.
+ attrs = [
+ new_node.getAttributeNode(attr_name).cloneNode(deep=True)
+ for attr_name in sorted(node.attributes.keys())
+ ]
+ for attr in attrs:
+ new_node.removeAttributeNode(attr)
+ for attr in attrs:
+ new_node.setAttributeNode(attr)
+
+ if node.childNodes:
+ children = [_walk_nodes(x) for x in node.childNodes]
+ for c in children:
+ new_node.appendChild(c)
+
+ return new_node
+
+ nodes = _walk_nodes(doc.documentElement)
+ new_doc = Document()
+ new_doc.appendChild(nodes)
+
+ return new_doc
+
def __init__(
self,
data,
@@ -3403,6 +3458,7 @@ def xml(self, checksum=False, order=True, byteorder=True):
doc.appendChild(cube_xml_element)
# Print our newly created XML
+ doc = self._sort_xml_attrs(doc)
return doc.toprettyxml(indent=" ")
def _xml_element(self, doc, checksum=False, order=True, byteorder=True):
diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py
index ac0d313d76..4a85e5cdb2 100644
--- a/lib/iris/tests/__init__.py
+++ b/lib/iris/tests/__init__.py
@@ -573,6 +573,10 @@ def assertXMLElement(self, obj, reference_filename):
"""
doc = xml.dom.minidom.Document()
doc.appendChild(obj.xml_element(doc))
+ # sort the attributes on xml elements before testing against known good state.
+ # this is to be compatible with stored test output where xml attrs are stored in alphabetical order,
+ # (which was default behaviour in python <3.8, but changed to insert order in >3.8)
+ doc = iris.cube.Cube._sort_xml_attrs(doc)
pretty_xml = doc.toprettyxml(indent=" ")
reference_path = self.get_result_path(reference_filename)
self._check_same(
diff --git a/noxfile.py b/noxfile.py
index b6f9480290..028da099dc 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.6", "3.7"])
+PY_VER = os.environ.get("PY_VER", ["3.6", "3.7", "3.8"])
#: Default cartopy cache directory.
CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy")
diff --git a/requirements/ci/iris.yml b/requirements/ci/iris.yml
index e9adb956db..a76932b56e 120000
--- a/requirements/ci/iris.yml
+++ b/requirements/ci/iris.yml
@@ -1 +1 @@
-py37.yml
\ No newline at end of file
+py38.yml
\ No newline at end of file
diff --git a/requirements/ci/py38.yml b/requirements/ci/py38.yml
new file mode 100644
index 0000000000..da29d30d71
--- /dev/null
+++ b/requirements/ci/py38.yml
@@ -0,0 +1,51 @@
+name: iris-dev
+
+channels:
+ - conda-forge
+
+dependencies:
+ - python=3.8
+
+# Setup dependencies.
+ - setuptools>=40.8.0
+ - pyke
+
+# Core dependencies.
+ - cartopy>=0.18
+ - cf-units>=2
+ - cftime<1.3.0
+ - dask>=2
+ - matplotlib
+ - netcdf4
+ - numpy>=1.14
+ - python-xxhash
+ - scipy
+
+# Optional dependencies.
+ - esmpy>=7.0
+ - graphviz
+ - iris-sample-data
+ - mo_pack
+ - nc-time-axis
+ - pandas
+ - python-stratify
+ - pyugrid
+
+# Test dependencies.
+ - asv
+ - black=20.8b1
+ - filelock
+ - flake8
+ - imagehash>=4.0
+ - nose
+ - pillow<7
+ - pre-commit
+ - requests
+
+# Documentation dependencies.
+ - sphinx
+ - sphinxcontrib-napoleon
+ - sphinx-copybutton
+ - sphinx-gallery
+ - sphinx-panels
+ - sphinx_rtd_theme
From 1549bae7a2e0b726745f28161d99f5dcb28a0ffd Mon Sep 17 00:00:00 2001
From: Bill Little
Date: Tue, 9 Feb 2021 13:36:55 +0000
Subject: [PATCH 05/12] normalise version to implicit development release
number (#3991)
---
lib/iris/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py
index a78d0a7682..e31c7b58d7 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.0dev0"
+__version__ = "3.1.dev0"
# Restrict the names imported when using "from iris import *"
__all__ = [
From 93171333a4fbc1a5484b7096382178e286fbc62e Mon Sep 17 00:00:00 2001
From: Ruth Comer
Date: Wed, 10 Feb 2021 09:25:45 +0000
Subject: [PATCH 06/12] Gallery: update COP maps example (#3934)
* update cop maps example
* comment tweaks
* minor comment tweak + whatsnew
* reinstate whatsnew addition
* remove duplicate whatsnew
---
.../gallery_code/meteorology/plot_COP_maps.py | 134 ++++++++----------
docs/src/whatsnew/latest.rst | 4 +-
2 files changed, 61 insertions(+), 77 deletions(-)
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/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index c02b61341b..b290b7ab5a 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`)
From e378eb8caaf869e4ce07ed9ff12b3b3f64148e2c Mon Sep 17 00:00:00 2001
From: Ruth Comer
Date: Wed, 10 Feb 2021 09:35:11 +0000
Subject: [PATCH 07/12] don't support mpl v1.2 (#3941)
---
docs/gallery_code/meteorology/plot_deriving_phenomena.py | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
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()
From e3c190543733ee53d1b5af84ec590a4c876b6071 Mon Sep 17 00:00:00 2001
From: Patrick Peglar
Date: Wed, 10 Feb 2021 11:38:22 +0000
Subject: [PATCH 08/12] 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.
---
lib/iris/_representation.py | 72 +++++++--
.../representation/test_representation.py | 149 ++++++++++++++++--
2 files changed, 191 insertions(+), 30 deletions(-)
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()
From b2cef9152998bf3e14262d67010554767916281f Mon Sep 17 00:00:00 2001
From: Bill Little
Date: Sun, 14 Feb 2021 16:05:33 +0000
Subject: [PATCH 09/12] update intersphinx mapping and matplotlib urls (#4003)
* update intersphinx mapping and matplotlib urls
* use matplotlib intersphinx where possible
* review actions
* review actions
---
.../general/plot_anomaly_log_colouring.py | 13 +++++-------
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 | 3 +++
6 files changed, 33 insertions(+), 39 deletions(-)
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/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 b290b7ab5a..f96cb8c7eb 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -86,6 +86,9 @@ 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`)
+
💼 Internal
===========
From e5cb25960e22f3befd36e78bc449add7ac16d859 Mon Sep 17 00:00:00 2001
From: Bill Little
Date: Sun, 14 Feb 2021 17:13:14 +0000
Subject: [PATCH 10/12] update readme badges (#4004)
* update readme badges
* pimp twitter badge
---
README.md | 20 +++++++++++++-------
docs/src/whatsnew/latest.rst | 5 +++++
2 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 0ceac7e089..c6cd4de47f 100644
--- a/README.md
+++ b/README.md
@@ -13,18 +13,24 @@
-
-
+
+
+
+
+
+
-
+
@@ -35,8 +41,8 @@
-
+
diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index f96cb8c7eb..68872beb64 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -89,6 +89,8 @@ This document explains the changes made to Iris for this release
#. `@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
===========
@@ -123,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#-----
From 343e7a4e84e3783e343a7b814f766f2d8973b342 Mon Sep 17 00:00:00 2001
From: Bill Little
Date: Sun, 14 Feb 2021 17:13:40 +0000
Subject: [PATCH 11/12] update readme logo img src and href (#4006)
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index c6cd4de47f..e460f4a01a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
- 
+
+ 
From f569a55e4644af1e1c0019c61a908bf3cda420ce Mon Sep 17 00:00:00 2001
From: Bill Little
Date: Sun, 14 Feb 2021 17:14:08 +0000
Subject: [PATCH 12/12] update setuptools description (#4008)
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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"),