Skip to content

Commit 8fb33bb

Browse files
bjlittlejamesp
andauthored
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 <[email protected]> * add jamesp to whatsnew + minor tweak Co-authored-by: James Penn <[email protected]>
1 parent c51dab2 commit 8fb33bb

File tree

12 files changed

+203
-24
lines changed

12 files changed

+203
-24
lines changed

.cirrus.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ linux_minimal_task:
9898
PY_VER: 3.6
9999
env:
100100
PY_VER: 3.7
101+
env:
102+
PY_VER: 3.8
101103
name: "${CIRRUS_OS}: py${PY_VER} tests (minimal)"
102104
container:
103105
image: gcc:latest
@@ -119,6 +121,8 @@ linux_task:
119121
PY_VER: 3.6
120122
env:
121123
PY_VER: 3.7
124+
env:
125+
PY_VER: 3.8
122126
name: "${CIRRUS_OS}: py${PY_VER} tests (full)"
123127
container:
124128
image: gcc:latest
@@ -146,9 +150,7 @@ linux_task:
146150
gallery_task:
147151
matrix:
148152
env:
149-
PY_VER: 3.6
150-
env:
151-
PY_VER: 3.7
153+
PY_VER: 3.8
152154
name: "${CIRRUS_OS}: py${PY_VER} doc tests (gallery)"
153155
container:
154156
image: gcc:latest
@@ -176,7 +178,7 @@ gallery_task:
176178
doctest_task:
177179
matrix:
178180
env:
179-
PY_VER: 3.7
181+
PY_VER: 3.8
180182
name: "${CIRRUS_OS}: py${PY_VER} doc tests"
181183
container:
182184
image: gcc:latest
@@ -210,7 +212,7 @@ doctest_task:
210212
link_task:
211213
matrix:
212214
env:
213-
PY_VER: 3.7
215+
PY_VER: 3.8
214216
name: "${CIRRUS_OS}: py${PY_VER} doc link check"
215217
container:
216218
image: gcc:latest

docs/src/common_links.inc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.. comment
22
Common resources in alphabetical order:
33

4+
.. _black: https://black.readthedocs.io/en/stable/
45
.. _.cirrus.yml: https://github.com/SciTools/iris/blob/master/.cirrus.yml
56
.. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8
67
.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris
@@ -19,6 +20,7 @@
1920
.. _legacy documentation: https://scitools.org.uk/iris/docs/v2.4.0/
2021
.. _matplotlib: https://matplotlib.org/
2122
.. _napolean: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/sphinxcontrib.napoleon.html
23+
.. _nox: https://nox.thea.codes/en/stable/
2224
.. _New Issue: https://github.com/scitools/iris/issues/new/choose
2325
.. _pull request: https://github.com/SciTools/iris/pulls
2426
.. _pull requests: https://github.com/SciTools/iris/pulls

docs/src/developers_guide/contributing_running_tests.rst

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,6 @@ For further `nox`_ command-line options::
186186
.. note:: `nox`_ will cache its testing environments in the `.nox` root ``iris`` project directory.
187187

188188

189-
.. _black: https://black.readthedocs.io/en/stable/
190-
.. _nox: https://nox.thea.codes/en/latest/
191189
.. _setuptools: https://setuptools.readthedocs.io/en/latest/
192190
.. _tox: https://tox.readthedocs.io/en/latest/
193191
.. _virtualenv: https://virtualenv.pypa.io/en/latest/

docs/src/further_topics/metadata.rst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,12 @@ create a **new** instance directly from the metadata class itself,
258258
>>> DimCoordMetadata._make(values)
259259
DimCoordMetadata(standard_name=1, long_name=2, var_name=3, units=4, attributes=5, coord_system=6, climatological=7, circular=8)
260260

261-
It is also possible to easily convert ``metadata`` to an `OrderedDict`_
261+
It is also possible to easily convert ``metadata`` to an `dict`_
262262
using the `namedtuple._asdict`_ method. This can be particularly handy when a
263263
standard Python built-in container is required to represent your ``metadata``,
264264

265265
>>> metadata._asdict()
266-
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)])
266+
{'standard_name': 'longitude', 'long_name': None, 'var_name': 'longitude', 'units': Unit('degrees'), 'attributes': {'grinning face': '🙃'}, 'coord_system': GeogCS(6371229.0), 'climatological': False, 'circular': False}
267267

268268
Using the `namedtuple._replace`_ method allows you to create a new metadata
269269
class instance, but replacing specified members with **new** associated values,
@@ -943,7 +943,7 @@ such as a `dict`_,
943943

944944
>>> mapping = latitude.metadata._asdict()
945945
>>> mapping
946-
OrderedDict([('standard_name', 'latitude'), ('long_name', None), ('var_name', 'latitude'), ('units', Unit('degrees')), ('attributes', {}), ('coord_system', GeogCS(6371229.0)), ('climatological', False), ('circular', False)])
946+
{'standard_name': 'latitude', 'long_name': None, 'var_name': 'latitude', 'units': Unit('degrees'), 'attributes': {}, 'coord_system': GeogCS(6371229.0), 'climatological': False, 'circular': False}
947947
>>> longitude.metadata = mapping
948948
>>> longitude.metadata
949949
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.
10001000
.. _NetCDF: https://www.unidata.ucar.edu/software/netcdf/
10011001
.. _NetCDF CF Metadata Conventions: https://cfconventions.org/
10021002
.. _NumPy: https://github.com/numpy/numpy
1003-
.. _OrderedDict: https://docs.python.org/3/library/collections.html#collections.OrderedDict
10041003
.. _Parametric Vertical Coordinate: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#parametric-vertical-coordinate
10051004
.. _rich comparison: https://www.python.org/dev/peps/pep-0207/
10061005
.. _SciTools/iris: https://github.com/SciTools/iris

docs/src/installing.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ any WSL_ distributions.
1616

1717
.. _WSL: https://docs.microsoft.com/en-us/windows/wsl/install-win10
1818

19-
.. note:: Iris currently supports and is tested against **Python 3.6** and
20-
**Python 3.7**.
19+
.. note:: Iris is currently supported and tested against Python ``3.6``,
20+
``3.7``, and ``3.8``.
2121

2222
.. note:: This documentation was built using Python |python_version|.
2323

docs/src/whatsnew/latest.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ This document explains the changes made to Iris for this release
1212
:title: text-primary text-center font-weight-bold
1313
:body: bg-light
1414
:animate: fade-in
15-
:open:
1615

1716
The highlights for this major/minor release of Iris include:
1817

@@ -96,7 +95,10 @@ This document explains the changes made to Iris for this release
9695
#. `@tkknight`_ moved the ``docs/iris`` directory to be in the parent
9796
directory ``docs``. (:pull:`3975`)
9897

99-
#. `@jamesp`_ updated a test to the latest numpy version (:pull:`3977`)
98+
#. `@jamesp`_ updated a test for `numpy`_ ``1.20.0``. (:pull:`3977`)
99+
100+
#. `@bjlittle`_ and `@jamesp`_ extended the `cirrus-ci`_ testing and `nox`_
101+
testing automation to support `Python 3.8`_. (:pull:`3976`)
100102

101103
#. `@bjlittle`_ rationalised the ``noxfile.py``, and added the ability for
102104
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
117119
.. _abstract base class: https://docs.python.org/3/library/abc.html
118120
.. _GitHub: https://github.com/SciTools/iris/issues/new/choose
119121
.. _Met Office: https://www.metoffice.gov.uk/
122+
.. _numpy: https://numpy.org/doc/stable/release/1.20.0-notes.html
123+
.. _Python 3.8: https://www.python.org/downloads/release/python-380/

lib/iris/coords.py

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,21 @@ def shape(self):
578578
return self._values_dm.shape
579579

580580
def xml_element(self, doc):
581-
"""Return a DOM element describing this metadata."""
581+
"""
582+
Create the :class:`xml.dom.minidom.Element` that describes this
583+
:class:`_DimensionalMetadata`.
584+
585+
Args:
586+
587+
* doc:
588+
The parent :class:`xml.dom.minidom.Document`.
589+
590+
Returns:
591+
The :class:`xml.dom.minidom.Element` that will describe this
592+
:class:`_DimensionalMetadata`, and the dictionary of attributes
593+
that require to be added to this element.
594+
595+
"""
582596
# Create the XML element as the camelCaseEquivalent of the
583597
# class name.
584598
element_name = type(self).__name__
@@ -881,6 +895,20 @@ def cube_dims(self, cube):
881895
return cube.cell_measure_dims(self)
882896

883897
def xml_element(self, doc):
898+
"""
899+
Create the :class:`xml.dom.minidom.Element` that describes this
900+
:class:`CellMeasure`.
901+
902+
Args:
903+
904+
* doc:
905+
The parent :class:`xml.dom.minidom.Document`.
906+
907+
Returns:
908+
The :class:`xml.dom.minidom.Element` that describes this
909+
:class:`CellMeasure`.
910+
911+
"""
884912
# Create the XML element as the camelCaseEquivalent of the
885913
# class name
886914
element = super().xml_element(doc=doc)
@@ -2228,14 +2256,26 @@ def nearest_neighbour_index(self, point):
22282256
return result_index
22292257

22302258
def xml_element(self, doc):
2231-
"""Return a DOM element describing this Coord."""
2259+
"""
2260+
Create the :class:`xml.dom.minidom.Element` that describes this
2261+
:class:`Coord`.
2262+
2263+
Args:
2264+
2265+
* doc:
2266+
The parent :class:`xml.dom.minidom.Document`.
2267+
2268+
Returns:
2269+
The :class:`xml.dom.minidom.Element` that will describe this
2270+
:class:`DimCoord`, and the dictionary of attributes that require
2271+
to be added to this element.
2272+
2273+
"""
22322274
# Create the XML element as the camelCaseEquivalent of the
22332275
# class name
22342276
element = super().xml_element(doc=doc)
22352277

2236-
element.setAttribute("points", self._xml_array_repr(self.points))
2237-
2238-
# Add bounds handling
2278+
# Add bounds, points are handled by the parent class.
22392279
if self.has_bounds():
22402280
element.setAttribute("bounds", self._xml_array_repr(self.bounds))
22412281

@@ -2614,7 +2654,20 @@ def is_monotonic(self):
26142654
return True
26152655

26162656
def xml_element(self, doc):
2617-
"""Return DOM element describing this :class:`iris.coords.DimCoord`."""
2657+
"""
2658+
Create the :class:`xml.dom.minidom.Element` that describes this
2659+
:class:`DimCoord`.
2660+
2661+
Args:
2662+
2663+
* doc:
2664+
The parent :class:`xml.dom.minidom.Document`.
2665+
2666+
Returns:
2667+
The :class:`xml.dom.minidom.Element` that describes this
2668+
:class:`DimCoord`.
2669+
2670+
"""
26182671
element = super().xml_element(doc)
26192672
if self.circular:
26202673
element.setAttribute("circular", str(self.circular))
@@ -2794,7 +2847,17 @@ def __add__(self, other):
27942847

27952848
def xml_element(self, doc):
27962849
"""
2797-
Return a dom element describing itself
2850+
Create the :class:`xml.dom.minidom.Element` that describes this
2851+
:class:`CellMethod`.
2852+
2853+
Args:
2854+
2855+
* doc:
2856+
The parent :class:`xml.dom.minidom.Document`.
2857+
2858+
Returns:
2859+
The :class:`xml.dom.minidom.Element` that describes this
2860+
:class:`CellMethod`.
27982861
27992862
"""
28002863
cellMethod_xml_element = doc.createElement("cellMethod")

lib/iris/cube.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ def __getslice__(self, start, stop):
225225

226226
def xml(self, checksum=False, order=True, byteorder=True):
227227
"""Return a string of the XML that this list of cubes represents."""
228+
228229
doc = Document()
229230
cubes_xml_element = doc.createElement("cubes")
230231
cubes_xml_element.setAttribute("xmlns", XML_NAMESPACE_URI)
@@ -239,6 +240,7 @@ def xml(self, checksum=False, order=True, byteorder=True):
239240
doc.appendChild(cubes_xml_element)
240241

241242
# return our newly created XML string
243+
doc = Cube._sort_xml_attrs(doc)
242244
return doc.toprettyxml(indent=" ")
243245

244246
def extract(self, constraints):
@@ -755,6 +757,59 @@ class Cube(CFVariableMixin):
755757
#: is similar to Fortran or Matlab, but different than numpy.
756758
__orthogonal_indexing__ = True
757759

760+
@classmethod
761+
def _sort_xml_attrs(cls, doc):
762+
"""
763+
Takes an xml document and returns a copy with all element
764+
attributes sorted in alphabetical order.
765+
766+
This is a private utility method required by iris to maintain
767+
legacy xml behaviour beyond python 3.7.
768+
769+
Args:
770+
771+
* doc:
772+
The :class:`xml.dom.minidom.Document`.
773+
774+
Returns:
775+
The :class:`xml.dom.minidom.Document` with sorted element
776+
attributes.
777+
778+
"""
779+
from xml.dom.minidom import Document
780+
781+
def _walk_nodes(node):
782+
"""Note: _walk_nodes is called recursively on child elements."""
783+
784+
# we don't want to copy the children here, so take a shallow copy
785+
new_node = node.cloneNode(deep=False)
786+
787+
# Versions of python <3.8 order attributes in alphabetical order.
788+
# Python >=3.8 order attributes in insert order. For consistent behaviour
789+
# across both, we'll go with alphabetical order always.
790+
# Remove all the attribute nodes, then add back in alphabetical order.
791+
attrs = [
792+
new_node.getAttributeNode(attr_name).cloneNode(deep=True)
793+
for attr_name in sorted(node.attributes.keys())
794+
]
795+
for attr in attrs:
796+
new_node.removeAttributeNode(attr)
797+
for attr in attrs:
798+
new_node.setAttributeNode(attr)
799+
800+
if node.childNodes:
801+
children = [_walk_nodes(x) for x in node.childNodes]
802+
for c in children:
803+
new_node.appendChild(c)
804+
805+
return new_node
806+
807+
nodes = _walk_nodes(doc.documentElement)
808+
new_doc = Document()
809+
new_doc.appendChild(nodes)
810+
811+
return new_doc
812+
758813
def __init__(
759814
self,
760815
data,
@@ -3403,6 +3458,7 @@ def xml(self, checksum=False, order=True, byteorder=True):
34033458
doc.appendChild(cube_xml_element)
34043459

34053460
# Print our newly created XML
3461+
doc = self._sort_xml_attrs(doc)
34063462
return doc.toprettyxml(indent=" ")
34073463

34083464
def _xml_element(self, doc, checksum=False, order=True, byteorder=True):

lib/iris/tests/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,10 @@ def assertXMLElement(self, obj, reference_filename):
573573
"""
574574
doc = xml.dom.minidom.Document()
575575
doc.appendChild(obj.xml_element(doc))
576+
# sort the attributes on xml elements before testing against known good state.
577+
# this is to be compatible with stored test output where xml attrs are stored in alphabetical order,
578+
# (which was default behaviour in python <3.8, but changed to insert order in >3.8)
579+
doc = iris.cube.Cube._sort_xml_attrs(doc)
576580
pretty_xml = doc.toprettyxml(indent=" ")
577581
reference_path = self.get_result_path(reference_filename)
578582
self._check_same(

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
PACKAGE = str("lib" / Path("iris"))
2020

2121
#: Cirrus-CI environment variable hook.
22-
PY_VER = os.environ.get("PY_VER", ["3.6", "3.7"])
22+
PY_VER = os.environ.get("PY_VER", ["3.6", "3.7", "3.8"])
2323

2424
#: Default cartopy cache directory.
2525
CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy")

0 commit comments

Comments
 (0)