diff --git a/docs/iris/src/whatsnew/contributions_2.1/newfeature_2018-May-08_repr-html.txt b/docs/iris/src/whatsnew/contributions_2.1/newfeature_2018-May-08_repr-html.txt new file mode 100644 index 0000000000..f64f2580e9 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_2.1/newfeature_2018-May-08_repr-html.txt @@ -0,0 +1,2 @@ +* Added ``repr_html`` functionality to the :class:`~iris.cube.Cube` to provide + a rich html representation of cubes in Jupyter notebooks. \ No newline at end of file diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 7ffb235c6d..cd72a39f58 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2064,6 +2064,11 @@ def __repr__(self): return "" % self.summary(shorten=True, name_padding=1) + def _repr_html_(self): + from iris.experimental.representation import CubeRepresentation + representer = CubeRepresentation(self) + return representer.repr_html() + def __iter__(self): raise TypeError('Cube is not iterable') diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py new file mode 100644 index 0000000000..5adef1f06e --- /dev/null +++ b/lib/iris/experimental/representation.py @@ -0,0 +1,308 @@ +# (C) British Crown Copyright 2018, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . + +""" +Definitions of how Iris objects should be represented. + +""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +import re + + +class CubeRepresentation(object): + """ + Produce representations of a :class:`~iris.cube.Cube`. + + This includes: + + * ``_html_repr_``: a representation of the cube as an html object, + available in Jupyter notebooks. Specifically, this is presented as an + html table. + + """ + + _template = """ + + + {header} + {shape} + {content} +
+ """ + + def __init__(self, cube): + self.cube = cube + self.cube_id = id(self.cube) + self.cube_str = str(self.cube) + + self.str_headings = { + 'Dimension coordinates:': None, + 'Auxiliary coordinates:': None, + 'Derived coordinates:': None, + 'Scalar coordinates:': None, + 'Attributes:': None, + 'Cell methods:': None, + } + self.dim_desc_coords = ['Dimension coordinates:', + 'Auxiliary coordinates:', + 'Derived coordinates:'] + + # Important content that summarises a cube is defined here. + self.shapes = self.cube.shape + self.scalar_cube = self.shapes == () + self.ndims = self.cube.ndim + + self.name = self.cube.name().title().replace('_', ' ') + self.names = self._dim_names() + self.units = self.cube.units + + def _get_dim_names(self): + """ + Get dimension-describing coordinate names, or '--' if no coordinate] + describes the dimension. + + Note: borrows from `cube.summary`. + + """ + # Create a set to contain the axis names for each data dimension. + dim_names = list(range(len(self.cube.shape))) + + # Add the dim_coord names that participate in the associated data + # dimensions. + for dim in range(len(self.cube.shape)): + dim_coords = self.cube.coords(contains_dimension=dim, + dim_coords=True) + if dim_coords: + dim_names[dim] = dim_coords[0].name() + else: + dim_names[dim] = '--' + return dim_names + + def _dim_names(self): + if self.scalar_cube: + dim_names = ['(scalar cube)'] + else: + 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 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.str_headings.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.str_headings[str_heading_name] = content + + def _make_header(self): + """ + Make the table header. This is similar to the summary of the cube, + but does not include dim shapes. These are included on the next table + row down, and produced with `make_shapes_row`. + + """ + # Header row. + tlc_template = \ + '{self.name} ({self.units})' + top_left_cell = tlc_template.format(self=self) + cells = ['', top_left_cell] + for dim_name in self.names: + cells.append( + '{}'.format(dim_name)) + cells.append('') + return '\n'.join(cell for cell in cells) + + def _make_shapes_row(self): + """Add a row to show data / dimensions shape.""" + title_cell = \ + 'Shape' + cells = ['', title_cell] + for shape in self.shapes: + cells.append( + '{}'.format(shape)) + cells.append('') + return '\n'.join(cell for cell in cells) + + def _make_row(self, title, body=None, col_span=0): + """ + Produce one row for the table body; i.e. + Coord namex-... + + `body` contains the content for each cell not in the left-most (title) + column. + If None, indicates this row is a title row (see below). + `title` contains the row heading. If `body` is None, indicates + that the row contains a sub-heading; + e.g. 'Dimension coordinates:'. + `col_span` indicates how many columns the string should span. + + """ + row = [''] + template = ' {content}' + if body is None: + # This is a title row. + # Strip off the trailing ':' from the title string. + title = title.strip()[:-1] + row.append( + template.format(html_cls=' class="iris-title iris-word-cell"', + content=title)) + # Add blank cells for the rest of the rows. + for _ in range(self.ndims): + row.append(template.format(html_cls=' class="iris-title"', + content='')) + else: + # This is not a title row. + # Deal with name of coord/attr etc. first. + sub_title = '\t{}'.format(title) + row.append(template.format( + html_cls=' class="iris-word-cell iris-subheading-cell"', + content=sub_title)) + # One further item or more than that? + if col_span != 0: + html_cls = ' class="{}" colspan="{}"'.format('iris-word-cell', + col_span) + row.append(template.format(html_cls=html_cls, content=body)) + else: + # "Inclusion" - `x` or `-`. + for itm in body: + row.append(template.format( + html_cls=' class="iris-inclusion-cell"', + content=itm)) + row.append('') + return row + + def _make_content(self): + elements = [] + for k, v in self.str_headings.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.dim_desc_coords: + body = re.findall(r'[\w-]+', line) + title = body.pop(0) + colspan = 0 + else: + split_point = line.index(':') + title = line[:split_point].strip() + body = line[split_point + 2:].strip() + colspan = self.ndims + elements.extend( + self._make_row(title, body=body, col_span=colspan)) + return '\n'.join(element for element in elements) + + def repr_html(self): + """The `repr` interface for Jupyter.""" + # Deal with the header first. + header = self._make_header() + + # Check if we have a scalar cube. + if self.scalar_cube: + shape = '' + # We still need a single content column! + 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() + + return self._template.format(header=header, + id=self.cube_id, + shape=shape, + content=content) diff --git a/lib/iris/tests/integration/experimental/test_CubeRepresentation.py b/lib/iris/tests/integration/experimental/test_CubeRepresentation.py new file mode 100644 index 0000000000..fa01ab37a7 --- /dev/null +++ b/lib/iris/tests/integration/experimental/test_CubeRepresentation.py @@ -0,0 +1,176 @@ +# (C) British Crown Copyright 2018, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Integration tests for cube html representation.""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from iris.cube import Cube +import iris.tests.stock as stock +import numpy as np + +from iris.experimental.representation import CubeRepresentation + + +@tests.skip_data +class TestNoMetadata(tests.IrisTest): + # Test the situation where we have a cube with no metadata at all. + def setUp(self): + self.shape = (2, 3, 4) + self.cube = Cube(np.arange(24).reshape(self.shape)) + self.representer = CubeRepresentation(self.cube) + self.representer.repr_html() + + def test_cube_name(self): + expected = 'Unknown' # This cube has no metadata. + result = self.representer.name + self.assertEqual(expected, result) + + def test_cube_units(self): + expected = 'unknown' # This cube has no metadata. + result = self.representer.units + self.assertEqual(expected, result) + + def test_dim_names(self): + expected = ['--'] * len(self.shape) + result = self.representer.names + self.assertEqual(expected, result) + + def test_shape(self): + result = self.representer.shapes + self.assertEqual(result, self.shape) + + +@tests.skip_data +class TestMissingMetadata(tests.IrisTest): + def setUp(self): + self.cube = stock.realistic_3d() + + def test_no_coords(self): + all_coords = [coord.name() for coord in self.cube.coords()] + for coord in all_coords: + self.cube.remove_coord(coord) + representer = CubeRepresentation(self.cube) + result = representer.repr_html().lower() + self.assertNotIn('dimension coordinates', result) + self.assertNotIn('auxiliary coordinates', result) + self.assertNotIn('scalar coordinates', result) + self.assertIn('attributes', result) + + def test_no_dim_coords(self): + dim_coords = [c.name() for c in self.cube.coords(dim_coords=True)] + for coord in dim_coords: + self.cube.remove_coord(coord) + representer = CubeRepresentation(self.cube) + result = representer.repr_html().lower() + self.assertNotIn('dimension coordinates', result) + self.assertIn('auxiliary coordinates', result) + self.assertIn('scalar coordinates', result) + self.assertIn('attributes', result) + + def test_no_aux_coords(self): + aux_coords = ['forecast_period'] + for coord in aux_coords: + self.cube.remove_coord(coord) + representer = CubeRepresentation(self.cube) + result = representer.repr_html().lower() + self.assertIn('dimension coordinates', result) + self.assertNotIn('auxiliary coordinates', result) + self.assertIn('scalar coordinates', result) + self.assertIn('attributes', result) + + def test_no_scalar_coords(self): + aux_coords = ['air_pressure'] + for coord in aux_coords: + self.cube.remove_coord(coord) + representer = CubeRepresentation(self.cube) + result = representer.repr_html().lower() + self.assertIn('dimension coordinates', result) + self.assertIn('auxiliary coordinates', result) + self.assertNotIn('scalar coordinates', result) + self.assertIn('attributes', result) + + def test_no_attrs(self): + self.cube.attributes = {} + representer = CubeRepresentation(self.cube) + result = representer.repr_html().lower() + self.assertIn('dimension coordinates', result) + self.assertIn('auxiliary coordinates', result) + self.assertIn('scalar coordinates', result) + self.assertNotIn('attributes', result) + + def test_no_cell_methods(self): + representer = CubeRepresentation(self.cube) + result = representer.repr_html().lower() + self.assertNotIn('cell methods', result) + + +@tests.skip_data +class TestScalarCube(tests.IrisTest): + def setUp(self): + self.cube = stock.realistic_3d()[0, 0, 0] + self.representer = CubeRepresentation(self.cube) + self.representer.repr_html() + + def test_identfication(self): + # Is this scalar cube accurately identified? + self.assertTrue(self.representer.scalar_cube) + + def test_header__name(self): + header = self.representer._make_header() + expected_name = self.cube.name().title().replace('_', ' ') + self.assertIn(expected_name, header) + + def test_header__units(self): + header = self.representer._make_header() + expected_units = self.cube.units.symbol + self.assertIn(expected_units, header) + + def test_header__scalar_str(self): + # Check that 'scalar cube' is placed in the header. + header = self.representer._make_header() + expected_str = '(scalar cube)' + self.assertIn(expected_str, header) + + def test_content__scalars(self): + # Check an element "Scalar coordinates" is present in the main content. + content = self.representer._make_content() + expected_str = 'Scalar coordinates' + self.assertIn(expected_str, content) + + def test_content__specific_scalar_coord(self): + # Check a specific scalar coord is present in the main content. + content = self.representer._make_content() + expected_coord = self.cube.coords()[0] + expected_coord_name = expected_coord.name() + self.assertIn(expected_coord_name, content) + expected_coord_val = str(expected_coord.points[0]) + self.assertIn(expected_coord_val, content) + + def test_content__attributes(self): + # Check an element "attributes" is present in the main content. + content = self.representer._make_content() + expected_str = 'Attributes' + self.assertIn(expected_str, content) + + +if __name__ == '__main__': + tests.main() diff --git a/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py new file mode 100644 index 0000000000..c94742a49a --- /dev/null +++ b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py @@ -0,0 +1,331 @@ +# (C) British Crown Copyright 2017 - 2018, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Unit tests for the `iris.cube.CubeRepresentation` class.""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from iris.coords import CellMethod +import iris.tests.stock as stock + +from iris.experimental.representation import CubeRepresentation + + +@tests.skip_data +class Test__instantiation(tests.IrisTest): + def setUp(self): + self.cube = stock.simple_3d() + self.representer = CubeRepresentation(self.cube) + + def test_cube_attributes(self): + self.assertEqual(id(self.cube), self.representer.cube_id) + self.assertStringEqual(str(self.cube), self.representer.cube_str) + + def test__heading_contents(self): + content = set(self.representer.str_headings.values()) + self.assertEqual(len(content), 1) + self.assertIsNone(list(content)[0]) + + +@tests.skip_data +class Test__get_dim_names(tests.IrisTest): + def setUp(self): + self.cube = stock.realistic_4d() + self.dim_names = [c.name() for c in self.cube.coords(dim_coords=True)] + self.representer = CubeRepresentation(self.cube) + + def test_basic(self): + result_names = self.representer._get_dim_names() + self.assertEqual(result_names, self.dim_names) + + def test_one_anonymous_dim(self): + self.cube.remove_coord('time') + expected_names = ['--'] + expected_names.extend(self.dim_names[1:]) + result_names = self.representer._get_dim_names() + self.assertEqual(result_names, expected_names) + + def test_anonymous_dims(self): + target_dims = [1, 3] + # Replicate this here as we're about to modify it. + expected_names = [c.name() for c in self.cube.coords(dim_coords=True)] + for dim in target_dims: + this_dim_coord, = self.cube.coords(contains_dimension=dim, + dim_coords=True) + self.cube.remove_coord(this_dim_coord) + expected_names[dim] = '--' + result_names = self.representer._get_dim_names() + self.assertEqual(result_names, expected_names) + + +@tests.skip_data +class Test__summary_content(tests.IrisTest): + def setUp(self): + self.cube = stock.lat_lon_cube() + # Check we're not tripped up by names containing spaces. + self.cube.rename('Electron density') + self.cube.units = '1e11 e/m^3' + self.representer = CubeRepresentation(self.cube) + + def test_name(self): + # Check the cube name is being set and formatted correctly. + expected = self.cube.name().replace('_', ' ').title() + result = self.representer.name + self.assertEqual(expected, result) + + def test_names(self): + # Check the dimension names used as column headings are split out and + # formatted correctly. + expected_coord_names = [c.name().replace('_', ' ') + for c in self.cube.coords(dim_coords=True)] + result_coord_names = self.representer.names[1:] + for result in result_coord_names: + self.assertIn(result, expected_coord_names) + + def test_units(self): + # Check the units is being set correctly. + expected = self.cube.units + result = self.representer.units + self.assertEqual(expected, result) + + def test_shapes(self): + # Check cube dim lengths are split out correctly from the + # summary string. + expected = self.cube.shape + result = self.representer.shapes + self.assertEqual(expected, result) + + def test_ndims(self): + expected = self.cube.ndim + result = self.representer.ndims + self.assertEqual(expected, result) + + +@tests.skip_data +class Test__get_bits(tests.IrisTest): + def setUp(self): + self.cube = stock.realistic_4d() + 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_population(self): + for v in self.representer.str_headings.values(): + self.assertIsNotNone(v) + + def test_headings__dimcoords(self): + contents = self.representer.str_headings['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.str_headings['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.str_headings['Auxiliary 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__scalarcoords(self): + contents = self.representer.str_headings['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.str_headings['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.str_headings['Cell methods:'] + content_str = ','.join(content for content in contents) + for cell_method in self.cube.cell_methods: + self.assertIn(str(cell_method), content_str) + + +@tests.skip_data +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): + # Check the correct name and units are being written into the top-left + # table cell. + # This is found in the first cell after the `` is defined. + name_and_units_cell = self.header_emts[1] + expected = '{name} ({units})'.format(name=self.cube.name(), + units=self.cube.units) + self.assertIn(expected.lower(), name_and_units_cell.lower()) + + def test_number_of_columns(self): + # There should be one headings column, plus a column per dimension. + # Ignore opening and closing tags. + result_cols = self.header_emts[1:-1] + expected = self.cube.ndim + 1 + self.assertEqual(len(result_cols), expected) + + def test_row_headings(self): + # Get only the dimension heading cells and not the headings column. + dim_coord_names = [c.name() for c in self.cube.coords(dim_coords=True)] + dim_col_headings = self.header_emts[2:-1] + for coord_name, col_heading in zip(dim_coord_names, dim_col_headings): + self.assertIn(coord_name, col_heading) + + +@tests.skip_data +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): + title_cell = self.result[1] + self.assertIn('Shape', title_cell) + + def test_shapes(self): + expected_shapes = self.cube.shape + result_shapes = self.result[2:-1] + for expected, result in zip(expected_shapes, result_shapes): + self.assertIn(str(expected), result) + + +@tests.skip_data +class Test__make_row(tests.IrisTest): + def setUp(self): + self.cube = stock.simple_3d() + 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:' + row = self.representer._make_row(title) + # A cell for the title, an empty cell for each cube dimension, plus row + # opening and closing tags. + expected_len = self.cube.ndim + 3 + self.assertEqual(len(row), expected_len) + # Check for specific content. + row_str = '\n'.join(element for element in row) + self.assertIn(title.strip(':'), row_str) + expected_html_class = 'iris-title' + self.assertIn(expected_html_class, row_str) + + def test__inclusion_row(self): + # An inclusion row has x/- to indicate whether a coordinate describes + # a dimension. + title = 'time' + body = ['x', '-', '-', '-'] + row = self.representer._make_row(title, body) + # A cell for the title, a cell for each cube dimension, plus row + # opening and closing tags. + expected_len = len(body) + 3 + self.assertEqual(len(row), expected_len) + # Check for specific content. + row_str = '\n'.join(element for element in row) + self.assertIn(title, row_str) + self.assertIn('x', row_str) + self.assertIn('-', row_str) + expected_html_class_1 = 'iris-word-cell' + expected_html_class_2 = 'iris-inclusion-cell' + self.assertIn(expected_html_class_1, row_str) + self.assertIn(expected_html_class_2, row_str) + # We do not expect a colspan to be set. + self.assertNotIn('colspan', row_str) + + def test__attribute_row(self): + # An attribute row does not contain inclusion indicators. + title = 'source' + body = 'Iris test case' + colspan = 5 + row = self.representer._make_row(title, body, colspan) + # We only expect two cells here: the row title cell and one other cell + # that spans a number of columns. We also need to open and close the + # tr html element, giving 4 bits making up the row. + self.assertEqual(len(row), 4) + # Check for specific content. + row_str = '\n'.join(element for element in row) + self.assertIn(title, row_str) + self.assertIn(body, row_str) + # We expect a colspan to be set. + colspan_str = 'colspan="{}"'.format(colspan) + self.assertIn(colspan_str, row_str) + + +@tests.skip_data +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() + + def test_included(self): + included = 'Dimension coordinates' + self.assertIn(included, self.result) + dim_coord_names = [c.name() for c in self.cube.dim_coords] + for coord_name in dim_coord_names: + self.assertIn(coord_name, self.result) + + def test_not_included(self): + # `stock.simple_3d()` only contains the `Dimension coordinates` attr. + not_included = list(self.representer.str_headings.keys()) + not_included.pop(not_included.index('Dimension coordinates:')) + for heading in not_included: + self.assertNotIn(heading, self.result) + + +@tests.skip_data +class Test_repr_html(tests.IrisTest): + def setUp(self): + self.cube = stock.simple_3d() + representer = CubeRepresentation(self.cube) + self.result = representer.repr_html() + + def test_contents_added(self): + included = 'Dimension coordinates' + self.assertIn(included, self.result) + not_included = 'Auxiliary coordinates' + self.assertNotIn(not_included, self.result) + + +if __name__ == '__main__': + tests.main()