Repository: SciTools/iris
Pull request #6476 (2025-05-26 21:08:10)

Extract repr_html directly from cube summary

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/SciTools/iris/pull/6476?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools) Report
Attention: Patch coverage is `71.42857%` with `10 lines` in your changes missing coverage. Please review.
> Project coverage is 89.78%. Comparing base [(`75e09f5`)](https://app.codecov.io/gh/SciTools/iris/commit/75e09f59687d701ad792dbcec25fd53ef5e8290a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools) to head [(`2d3bd9e`)](https://app.codecov.io/gh/SciTools/iris/commit/2d3bd9e01f1df62f3839dcb7b2858ed3e7c6f551?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools).
> Report is 12 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/SciTools/iris/pull/6476?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools) | Patch % | Lines |
|---|---|---|
| [lib/iris/experimental/representation.py](https://app.codecov.io/gh/SciTools/iris/pull/6476?src=pr&el=tree&filepath=lib%2Firis%2Fexperimental%2Frepresentation.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools#diff-bGliL2lyaXMvZXhwZXJpbWVudGFsL3JlcHJlc2VudGF0aW9uLnB5) | 71.42% | [8 Missing and 2 partials :warning: ](https://app.codecov.io/gh/SciTools/iris/pull/6476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #6476      +/-   ##
==========================================
- Coverage   89.80%   89.78%   -0.03%     
==========================================
  Files          90       90              
  Lines       23755    23771      +16     
  Branches     4418     4430      +12     
==========================================
+ Hits        21334    21343       +9     
- Misses       1672     1678       +6     
- Partials      749      750       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/SciTools/iris/pull/6476?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SciTools).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py
index 0648cc8e0..4beac376e 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 @@ class CubeRepresentation:
             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 @@ class CubeRepresentation:
                     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 @@ class CubeRepresentation:
 
     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 * "&nbsp;"
+        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 + "<br>" + 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 + "<br>" + 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 @@ class CubeRepresentation:
             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


Test patch:
diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index cc51ff2d0..a50331160 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/tests/unit/experimental/representation/test_CubeRepresentation.py b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py
index 7515fad08..f47a1c9ef 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 @@ from html import escape
 
 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 @@ class Test__instantiation(tests.IrisTest):
 
     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 @@ class Test__summary_content(tests.IrisTest):
         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 = (
+        '<tr class="iris">\n'
+        '    <td class="iris-title iris-word-cell">Dimension coordinates</td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\ttime</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\t'
+        "model_level_number</td>\n"
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tgrid_latitude</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tgrid_longitude</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-title iris-word-cell">Auxiliary coordinates</td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tlevel_height</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tsigma</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tsurface_altitude</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-title iris-word-cell">Derived coordinates</td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\taltitude</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-title iris-word-cell">Cell measures</td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tfoo</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-title iris-word-cell">Ancillary variables</td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tbar</td>\n'
+        '    <td class="iris-inclusion-cell">x</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        '    <td class="iris-inclusion-cell">-</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-title iris-word-cell">Scalar coordinates</td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tforecast_period</td>\n'
+        '    <td class="iris-word-cell" colspan="4">0.0 hours</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-title iris-word-cell">Scalar cell measures</td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tbaz</td>\n'
+        '    <td class="iris-word-cell" colspan="4"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-title iris-word-cell">Cell methods</td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\t0</td>\n'
+        '    <td class="iris-word-cell" colspan="4">time: mean (interval: 6hr)</td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-title iris-word-cell">Attributes</td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        '    <td class="iris-title"></td>\n'
+        "</tr>\n"
+        '<tr class="iris">\n'
+        '    <td class="iris-word-cell iris-subheading-cell">\tsource</td>\n'
+        '    <td class="iris-word-cell" colspan="4">&#x27;Iris test case&#x27;</td>\n'
+        "</tr>"
+    )
+    assert expected == result
 
 
 @tests.skip_data
@@ -208,7 +285,6 @@ class Test__make_header(tests.IrisTest):
     def setUp(self):
         self.cube = stock.simple_3d()
         self.representer = CubeRepresentation(self.cube)
-        self.representer._get_bits(self.representer._get_lines())
         self.header_emts = self.representer._make_header().split("\n")
 
     def test_name_and_units(self):
@@ -241,7 +317,6 @@ class Test__make_shapes_row(tests.IrisTest):
     def setUp(self):
         self.cube = stock.simple_3d()
         self.representer = CubeRepresentation(self.cube)
-        self.representer._get_bits(self.representer._get_lines())
         self.result = self.representer._make_shapes_row().split("\n")
 
     def test_row_title(self):
@@ -262,7 +337,6 @@ class Test__make_row(tests.IrisTest):
         cm = CellMethod("mean", "time", "6hr")
         self.cube.add_cell_method(cm)
         self.representer = CubeRepresentation(self.cube)
-        self.representer._get_bits(self.representer._get_lines())
 
     def test__title_row(self):
         title = "Wibble:"
@@ -323,8 +397,20 @@ class Test__make_content(tests.IrisTest):
     def setUp(self):
         self.cube = stock.simple_3d()
         self.representer = CubeRepresentation(self.cube)
-        self.representer._get_bits(self.representer._get_lines())
         self.result = self.representer._make_content()
+        self.sections_keys = [
+            "Dimension coordinates:",
+            "Mesh coordinates:",
+            "Auxiliary coordinates:",
+            "Derived coordinates:",
+            "Cell measures:",
+            "Ancillary variables:",
+            "Mesh:",
+            "Scalar coordinates:",
+            "Scalar cell measures:",
+            "Cell methods:",
+            "Attributes:",
+        ]
 
         # Also provide an ultra-simple mesh cube, with only meshcoords.
         mesh = sample_mesh()
@@ -334,7 +420,6 @@ class Test__make_content(tests.IrisTest):
         mesh_cube.add_aux_coord(meshco_y, (0,))
         self.mesh_cube = mesh_cube
         self.mesh_representer = CubeRepresentation(self.mesh_cube)
-        self.mesh_representer._get_bits(self.mesh_representer._get_lines())
         self.mesh_result = self.mesh_representer._make_content()
 
     def test_included(self):
@@ -346,7 +431,7 @@ class Test__make_content(tests.IrisTest):
 
     def test_not_included(self):
         # `stock.simple_3d()` only contains the `Dimension coordinates` attr.
-        not_included = list(self.representer.sections_data.keys())
+        not_included = list(self.sections_keys)
         not_included.pop(not_included.index("Dimension coordinates:"))
         for heading in not_included:
             self.assertNotIn(heading, self.result)
@@ -367,7 +452,7 @@ class Test__make_content(tests.IrisTest):
 
     def test_mesh_not_included(self):
         # self.mesh_cube _only_ contains a `Mesh coordinates` section.
-        not_included = list(self.representer.sections_data.keys())
+        not_included = list(self.sections_keys)
         not_included.pop(not_included.index("Mesh coordinates:"))
         for heading in not_included:
             self.assertNotIn(heading, self.result)
@@ -391,7 +476,6 @@ class Test__make_content__string_attrs(tests.IrisTest):
         cube = Cube([0])
         cube.attributes[name] = attr
         representer = CubeRepresentation(cube)
-        representer._get_bits(representer._get_lines())
         result = representer._make_content()
         return result
 
@@ -414,6 +498,139 @@ class Test__make_content__string_attrs(tests.IrisTest):
         html = self._cube_stringattribute_html("multi-string", attr)
         self.assertString(html)
 
+    def test_coord_distinguishing_attributes(self):
+        # Printout of differing attributes to differentiate same-named coords.
+        # include : vector + scalar
+        cube = Cube([0, 1], long_name="name", units=1)
+        # Add a pair of vector coords with same name but different attributes.
+        cube.add_aux_coord(AuxCoord([0, 1], long_name="co1", attributes=dict(a=1)), 0)
+        cube.add_aux_coord(AuxCoord([0, 1], long_name="co1", attributes=dict(a=2)), 0)
+        # Likewise for scalar coords with same name but different attributes.
+        cube.add_aux_coord(AuxCoord([0], long_name="co2", attributes=dict(a=10, b=12)))
+        cube.add_aux_coord(AuxCoord([1], long_name="co2", attributes=dict(a=10, b=11)))
+
+        rep = CubeRepresentation(cube)
+        result = rep._make_content()
+        expected = (
+            '<tr class="iris">\n'
+            '    <td class="iris-title iris-word-cell">Auxiliary coordinates</td>\n'
+            '    <td class="iris-title"></td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\t'
+            "co1<br>&nbsp;&nbsp;&nbsp;&nbsp;a=1</td>\n"
+            '    <td class="iris-inclusion-cell">x</td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\t'
+            "co1<br>&nbsp;&nbsp;&nbsp;&nbsp;a=2</td>\n"
+            '    <td class="iris-inclusion-cell">x</td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-title iris-word-cell">Scalar coordinates</td>\n'
+            '    <td class="iris-title"></td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\t'
+            "co2<br>&nbsp;&nbsp;&nbsp;&nbsp;b=12</td>\n"
+            '    <td class="iris-word-cell" colspan="1">0</td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\t'
+            "co2<br>&nbsp;&nbsp;&nbsp;&nbsp;b=11</td>\n"
+            '    <td class="iris-word-cell" colspan="1">1</td>\n'
+            "</tr>"
+        )
+        self.assertEqual(result, expected)
+
+    def test_coord_extra_attributes__array(self):
+        cube = Cube(0, long_name="name", units=1)
+        # Add a pair of vector coords with same name but different attributes.
+        array1 = np.arange(0, 3)
+        array2 = np.arange(10, 13)
+        cube.add_aux_coord(
+            AuxCoord([1.2], long_name="co1", attributes=dict(a=1, arr=array1))
+        )
+        cube.add_aux_coord(
+            AuxCoord([3.4], long_name="co1", attributes=dict(a=1, arr=array2))
+        )
+
+        rep = CubeRepresentation(cube)
+        result = rep._make_content()
+        expected = (
+            '<tr class="iris">\n'
+            '    <td class="iris-title iris-word-cell">Scalar coordinates</td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\t'
+            "co1<br>&nbsp;&nbsp;&nbsp;&nbsp;arr=array([0, 1, 2])</td>\n"
+            '    <td class="iris-word-cell" colspan="0">1.2</td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\t'
+            "co1<br>&nbsp;&nbsp;&nbsp;&nbsp;arr=array([10, 11, 12])</td>\n"
+            '    <td class="iris-word-cell" colspan="0">3.4</td>\n'
+            "</tr>"
+        )
+        self.assertEqual(result, expected)
+
+    def test_coord_extra_attributes__array__long(self):
+        # Also test with a long array representation.
+        array = 10 + np.arange(24.0).reshape((2, 3, 4))
+        cube = Cube(0, long_name="name", units=1)
+        cube.add_aux_coord(AuxCoord([1], long_name="co"))
+        cube.add_aux_coord(
+            AuxCoord([2], long_name="co", attributes=dict(a=array + 1.0))
+        )
+
+        rep = CubeRepresentation(cube)
+        result = rep._make_content()
+        expected = (
+            '<tr class="iris">\n'
+            '    <td class="iris-title iris-word-cell">Scalar coordinates</td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\tco</td>\n'
+            '    <td class="iris-word-cell" colspan="0">1</td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\t'
+            "co<br>&nbsp;&nbsp;&nbsp;&nbsp;a=array([[[11., 12., 13., 14.], [15., 16., "
+            "17., 18.], [19., 20., 21., 22.]],...</td>\n"
+            '    <td class="iris-word-cell" colspan="0">2</td>\n'
+            "</tr>"
+        )
+        self.assertEqual(result, expected)
+
+    def test_coord_extra_attributes__string_escaped(self):
+        cube = Cube(0, long_name="name", units=1)
+        cube.add_aux_coord(AuxCoord([1], long_name="co"))
+        cube.add_aux_coord(
+            AuxCoord(
+                [2],
+                long_name="co",
+                attributes=dict(note="line 1\nline 2\t& ends."),
+            )
+        )
+        rep = CubeRepresentation(cube)
+        result = rep._make_content()
+        expected = (
+            '<tr class="iris">\n'
+            '    <td class="iris-title iris-word-cell">Scalar coordinates</td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\tco</td>\n'
+            '    <td class="iris-word-cell" colspan="0">1</td>\n'
+            "</tr>\n"
+            '<tr class="iris">\n'
+            '    <td class="iris-word-cell iris-subheading-cell">\t'
+            "co<br>&nbsp;&nbsp;&nbsp;&nbsp;note=&#x27;line 1\\nline 2\\t&amp; "
+            "ends.&#x27;</td>\n"
+            '    <td class="iris-word-cell" colspan="0">2</td>\n'
+            "</tr>"
+        )
+        self.assertEqual(result, expected)
+
 
 @tests.skip_data
 class Test_repr_html(tests.IrisTest):