diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index cc51ff2d0a..a50331160d 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -47,6 +47,9 @@ This document explains the changes made to Iris for this release
#. `@ukmo-ccbunney`_ fixed loading and merging of masked data in scalar ``AuxCoords``.
(:issue:`3584`, :pull:`6468`)
+#. `@stephenworsley`_ fixed the html representation of cubes in Jupyter when coordinates
+ share the same name. (:pull:`6476`)
+
💣 Incompatible Changes
=======================
diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py
index 0648cc8e0d..4beac376ee 100644
--- a/lib/iris/experimental/representation.py
+++ b/lib/iris/experimental/representation.py
@@ -6,7 +6,8 @@
"""Definitions of how Iris objects should be represented."""
from html import escape
-import re
+
+from iris._representation.cube_summary import CubeSummary
class CubeRepresentation:
@@ -77,34 +78,8 @@ class CubeRepresentation:
def __init__(self, cube):
self.cube = cube
+ self.summary = CubeSummary(cube)
self.cube_id = id(self.cube)
- self.cube_str = escape(str(self.cube))
-
- # Define the expected vector and scalar sections in output, in expected
- # order of appearance.
- # NOTE: if we recoded this to use a CubeSummary, these section titles
- # would be available from that.
- self.vector_section_names = [
- "Dimension coordinates:",
- "Mesh coordinates:",
- "Auxiliary coordinates:",
- "Derived coordinates:",
- "Cell measures:",
- "Ancillary variables:",
- ]
- self.scalar_section_names = [
- "Mesh:",
- "Scalar coordinates:",
- "Scalar cell measures:",
- "Cell methods:",
- "Attributes:",
- ]
- self.sections_data = {
- name: None for name in self.vector_section_names + self.scalar_section_names
- }
- # 'Scalar-cell-measures' is currently alone amongst the scalar sections,
- # in displaying only a 'name' and no 'value' field.
- self.single_cell_section_names = ["Scalar cell measures:"]
# Important content that summarises a cube is defined here.
self.shapes = self.cube.shape
@@ -144,40 +119,6 @@ def _dim_names(self):
dim_names = self._get_dim_names()
return dim_names
- def _get_lines(self):
- return self.cube_str.split("\n")
-
- def _get_bits(self, bits):
- """Parse the body content (`bits`) of the cube string.
-
- Parse the body content (`bits`) of the cube string in preparation for
- being converted into table rows.
-
- """
- left_indent = re.split(r"\w+", bits[1])[0]
-
- # Get heading indices within the printout.
- start_inds = []
- for hdg in self.sections_data.keys():
- heading = "{}{}".format(left_indent, hdg)
- try:
- start_ind = bits.index(heading)
- except ValueError:
- continue
- else:
- start_inds.append(start_ind)
- # Mark the end of the file.
- start_inds.append(0)
-
- # Retrieve info for each heading from the printout.
- for i0, i1 in zip(start_inds[:-1], start_inds[1:]):
- str_heading_name = bits[i0].strip()
- if i1 != 0:
- content = bits[i0 + 1 : i1]
- else:
- content = bits[i0 + 1 :]
- self.sections_data[str_heading_name] = content
-
def _make_header(self):
"""Make the table header.
@@ -251,8 +192,7 @@ def _make_row(self, title, body=None, col_span=0):
content=sub_title,
)
)
- # One further item or more than that?
- if col_span != 0:
+ if not isinstance(body, list):
html_cls = ' class="{}" colspan="{}"'.format("iris-word-cell", col_span)
row.append(template.format(html_cls=html_cls, content=body))
else:
@@ -269,28 +209,55 @@ def _make_row(self, title, body=None, col_span=0):
def _make_content(self):
elements = []
- for k, v in self.sections_data.items():
- if v is not None:
- # Add the sub-heading title.
- elements.extend(self._make_row(k))
- for line in v:
- # Add every other row in the sub-heading.
- if k in self.vector_section_names:
- body = re.findall(r"[\w-]+", line)
- title = body.pop(0)
- colspan = 0
- else:
- colspan = self.ndims
- if k in self.single_cell_section_names:
- title = line.strip()
- body = ""
- else:
- line = line.strip()
- split_point = line.index(" ")
- title = line[:split_point].strip()
- body = line[split_point + 2 :].strip()
-
- elements.extend(self._make_row(title, body=body, col_span=colspan))
+ INDENT = 4 * " "
+ for sect in self.summary.vector_sections.values():
+ if sect.contents:
+ sect_title = sect.title
+ elements.extend(self._make_row(sect_title))
+
+ for content in sect.contents:
+ body = content.dim_chars
+
+ title = escape(content.name)
+ if content.extra:
+ title = title + "
" + INDENT + escape(content.extra)
+ elements.extend(self._make_row(title, body=body, col_span=0))
+ for sect in self.summary.scalar_sections.values():
+ if sect.contents:
+ sect_title = sect.title
+ elements.extend(self._make_row(sect_title))
+ st = sect_title.lower()
+ if st == "scalar coordinates:":
+ for item in sect.contents:
+ body = escape(item.content)
+ title = escape(item.name)
+ if item.extra:
+ title = title + "
" + INDENT + escape(item.extra)
+ elements.extend(
+ self._make_row(title, body=body, col_span=self.ndims)
+ )
+ elif st in ("attributes:", "cell methods:", "mesh:"):
+ for title, body in zip(sect.names, sect.values):
+ title = escape(title)
+ body = escape(body)
+ elements.extend(
+ self._make_row(title, body=body, col_span=self.ndims)
+ )
+ pass
+ elif st in (
+ "scalar ancillary variables:",
+ "scalar cell measures:",
+ ):
+ body = ""
+ # These are just strings: nothing in the 'value' column.
+ for title in sect.contents:
+ title = escape(title)
+ elements.extend(
+ self._make_row(title, body=body, col_span=self.ndims)
+ )
+ else:
+ msg = f"Unknown section type : {type(sect)}"
+ raise ValueError(msg)
return "\n".join(element for element in elements)
def repr_html(self):
@@ -305,16 +272,7 @@ def repr_html(self):
self.ndims = 1
else:
shape = self._make_shapes_row()
-
- # Now deal with the rest of the content.
- lines = self._get_lines()
- # If we only have a single line `cube_str` we have no coords / attrs!
- # We need to handle this case specially.
- if len(lines) == 1:
- content = ""
- else:
- self._get_bits(lines)
- content = self._make_content()
+ content = self._make_content()
return self._template.format(
header=header, id=self.cube_id, shape=shape, content=content
diff --git a/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py
index 7515fad08a..f47a1c9ef5 100644
--- a/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py
+++ b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py
@@ -4,6 +4,8 @@
# See LICENSE in the root of the repository for full licensing details.
"""Unit tests for the `iris.cube.CubeRepresentation` class."""
+import pytest
+
# Import iris.tests first so that some things can be initialised before
# importing anything else.
import iris.tests as tests # isort:skip
@@ -12,7 +14,7 @@
import numpy as np
-from iris.coords import AncillaryVariable, CellMeasure, CellMethod
+from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, CellMethod
from iris.cube import Cube
from iris.experimental.representation import CubeRepresentation
import iris.tests.stock as stock
@@ -27,12 +29,6 @@ def setUp(self):
def test_cube_attributes(self):
self.assertEqual(id(self.cube), self.representer.cube_id)
- self.assertMultiLineEqual(str(self.cube), self.representer.cube_str)
-
- def test__heading_contents(self):
- content = set(self.representer.sections_data.values())
- self.assertEqual(len(content), 1)
- self.assertIsNone(list(content)[0])
@tests.skip_data
@@ -111,96 +107,177 @@ def test_ndims(self):
self.assertEqual(expected, result)
-@tests.skip_data
-class Test__get_bits(tests.IrisTest):
- def setUp(self):
- self.cube = stock.realistic_4d()
- cmth = CellMethod("mean", "time", "6hr")
- self.cube.add_cell_method(cmth)
- cms = CellMeasure([0, 1, 2, 3, 4, 5], long_name="foo")
- self.cube.add_cell_measure(cms, 0)
- avr = AncillaryVariable([0, 1, 2, 3, 4, 5], long_name="bar")
- self.cube.add_ancillary_variable(avr, 0)
- scms = CellMeasure([0], long_name="baz")
- self.cube.add_cell_measure(scms)
- self.representer = CubeRepresentation(self.cube)
- self.representer._get_bits(self.representer._get_lines())
+@pytest.fixture
+def realistic_4d():
+ cube = stock.realistic_4d()
+ cmth = CellMethod("mean", "time", "6hr")
+ cube.add_cell_method(cmth)
+ cms = CellMeasure([0, 1, 2, 3, 4, 5], long_name="foo")
+ cube.add_cell_measure(cms, 0)
+ avr = AncillaryVariable([0, 1, 2, 3, 4, 5], long_name="bar")
+ cube.add_ancillary_variable(avr, 0)
+ scms = CellMeasure([0], long_name="baz")
+ cube.add_cell_measure(scms)
+ return cube
- def test_population(self):
- nonmesh_values = [
- value
- for key, value in self.representer.sections_data.items()
- if "Mesh" not in key
- ]
- for v in nonmesh_values:
- self.assertIsNotNone(v)
-
- def test_headings__dimcoords(self):
- contents = self.representer.sections_data["Dimension coordinates:"]
- content_str = ",".join(content for content in contents)
- dim_coords = [c.name() for c in self.cube.dim_coords]
- for coord in dim_coords:
- self.assertIn(coord, content_str)
-
- def test_headings__auxcoords(self):
- contents = self.representer.sections_data["Auxiliary coordinates:"]
- content_str = ",".join(content for content in contents)
- aux_coords = [c.name() for c in self.cube.aux_coords if c.shape != (1,)]
- for coord in aux_coords:
- self.assertIn(coord, content_str)
-
- def test_headings__derivedcoords(self):
- contents = self.representer.sections_data["Derived coordinates:"]
- content_str = ",".join(content for content in contents)
- derived_coords = [c.name() for c in self.cube.derived_coords]
- for coord in derived_coords:
- self.assertIn(coord, content_str)
-
- def test_headings__cellmeasures(self):
- contents = self.representer.sections_data["Cell measures:"]
- content_str = ",".join(content for content in contents)
- cell_measures = [c.name() for c in self.cube.cell_measures() if c.shape != (1,)]
- for coord in cell_measures:
- self.assertIn(coord, content_str)
-
- def test_headings__ancillaryvars(self):
- contents = self.representer.sections_data["Ancillary variables:"]
- content_str = ",".join(content for content in contents)
- ancillary_variables = [c.name() for c in self.cube.ancillary_variables()]
- for coord in ancillary_variables:
- self.assertIn(coord, content_str)
-
- def test_headings__scalarcellmeasures(self):
- contents = self.representer.sections_data["Scalar cell measures:"]
- content_str = ",".join(content for content in contents)
- scalar_cell_measures = [
- c.name() for c in self.cube.cell_measures() if c.shape == (1,)
- ]
- for coord in scalar_cell_measures:
- self.assertIn(coord, content_str)
-
- def test_headings__scalarcoords(self):
- contents = self.representer.sections_data["Scalar coordinates:"]
- content_str = ",".join(content for content in contents)
- scalar_coords = [c.name() for c in self.cube.coords() if c.shape == (1,)]
- for coord in scalar_coords:
- self.assertIn(coord, content_str)
-
- def test_headings__attributes(self):
- contents = self.representer.sections_data["Attributes:"]
- content_str = ",".join(content for content in contents)
- for attr_name, attr_value in self.cube.attributes.items():
- self.assertIn(attr_name, content_str)
- self.assertIn(attr_value, content_str)
-
- def test_headings__cellmethods(self):
- contents = self.representer.sections_data["Cell methods:"]
- content_str = ",".join(content for content in contents)
- for method in self.cube.cell_methods:
- name = method.method
- value = str(method)[len(name + ": ") :]
- self.assertIn(name, content_str)
- self.assertIn(value, content_str)
+
+@tests.skip_data
+def test_realistic(realistic_4d):
+ representer = CubeRepresentation(realistic_4d)
+ result = representer._make_content()
+ expected = (
+ '