From 1178590e4ebb39251ba2f3a1b997084e02780b47 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 15 Nov 2017 12:18:08 +0000 Subject: [PATCH 01/11] Add cube _repr_html_ functionality --- lib/iris/cube.py | 157 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 7ffb235c6d..3df79fb849 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -603,6 +603,159 @@ def _is_single_item(testee): not isinstance(testee, collections.Iterable)) +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. + + """ + _template = """ + +
+
+ +
+ {content} +
+
+
+ """ + + # Need to format the keywords: + # `emt_id`, `obj_id`, `str_heading`, `opened`, `content`. + _insert_content = """ +
+

+ +{str_heading} + +

+
+
+
+

{content}

+
+
+ """ + + def __init__(self, cube): + """ + Produce different representations of a :class:`~iris.cube.Cube`. + + Args: + + * cube + the cube to produce representations of. + + """ + + self.cube = cube + self.cube_id = id(self.cube) + self.cube_str = str(self.cube) + + self.summary = None + self.str_headings = { + 'Dimension coordinates:': None, + 'Auxiliary coordinates:': None, + 'Derived coordinates:': None, + 'Scalar coordinates:': None, + 'Attributes:': None, + 'Cell methods:': None, + } + self.major_headings = ['Dimension coordinates:', + 'Auxiliary coordinates:', + 'Attributes:'] + + def _get_bits(self): + """ + Parse the str representation of the cube to retrieve the elements + to add to an html representation of the cube. + + """ + bits = self.cube_str.split('\n') + self.summary = bits[0] + left_indent = bits[1].split('D')[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_content(self): + elements = [] + for k, v in self.str_headings.items(): + if v is not None: + html_id = k.split(' ')[0].lower().strip(':') + content = '\n'.join(line for line in v) + collapse = ' in' if k in self.major_headings else '' + element = self._insert_content.format(emt_id=html_id, + obj_id=self.cube_id, + str_heading=k, + opened=collapse, + content=content) + elements.append(element) + return '\n'.join(element for element in elements) + + def repr_html(self): + """Produce an html representation of a cube and return it.""" + self._get_bits() + summary = self.summary + content = self.make_content() + return self._template.format(summary=summary, + content=content, + obj_id=self.cube_id, + ) + + class Cube(CFVariableMixin): """ A single Iris cube of data and metadata. @@ -2064,6 +2217,10 @@ def __repr__(self): return "" % self.summary(shorten=True, name_padding=1) + def _repr_html_(self): + representer = _CubeRepresentation(self) + return representer.repr_html() + def __iter__(self): raise TypeError('Cube is not iterable') From 0f1dd68afe1473aa6d9e278685aeed7acdfff385 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 15 Nov 2017 15:06:16 +0000 Subject: [PATCH 02/11] Test suite added --- .../unit/cube/test_CubeRepresentation.py | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 lib/iris/tests/unit/cube/test_CubeRepresentation.py diff --git a/lib/iris/tests/unit/cube/test_CubeRepresentation.py b/lib/iris/tests/unit/cube/test_CubeRepresentation.py new file mode 100644 index 0000000000..163c1d81c9 --- /dev/null +++ b/lib/iris/tests/unit/cube/test_CubeRepresentation.py @@ -0,0 +1,166 @@ +# (C) British Crown Copyright 2017, 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 itertools import permutations + +import numpy as np +import numpy.ma as ma + +import iris.analysis +import iris.aux_factory +from iris.coords import CellMethod +import iris.exceptions +from iris import FUTURE +from iris.analysis import WeightedAggregator, Aggregator +from iris.analysis import MEAN +from iris.cube import _CubeRepresentation +from iris.coords import AuxCoord, DimCoord, CellMeasure +from iris.exceptions import (CoordinateNotFoundError, CellMeasureNotFoundError, + UnitConversionError) +from iris.tests import mock +import iris.tests.stock as stock + + +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_summary(self): + self.assertIsNone(self.representer.summary) + + def test__heading_contents(self): + content = set(self.representer.str_headings.values()) + self.assertEqual(len(content), 1) + self.assertIsNone(list(content)[0]) + + +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.summary = self.representer.summary + + def test_population(self): + self.assertIsNotNone(self.summary) + for v in self.representer.str_headings.values(): + self.assertIsNotNone(v) + + def test_summary(self): + expected = self.cube.summary(True) + result = self.summary + self.assertStringEqual(expected, result) + + 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) + + +class Test_make_content(tests.IrisTest): + def setUp(self): + self.cube = stock.simple_3d() + self.representer = _CubeRepresentation(self.cube) + self.representer._get_bits() + 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) + + +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_summary_added(self): + self.assertIn(self.cube.summary(True), self.result) + + 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() From 908e41c48028466cf5af6c642dee2dfc61899fc2 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 15 Nov 2017 15:30:20 +0000 Subject: [PATCH 03/11] Remove unused imports --- .../tests/unit/cube/test_CubeRepresentation.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/iris/tests/unit/cube/test_CubeRepresentation.py b/lib/iris/tests/unit/cube/test_CubeRepresentation.py index 163c1d81c9..afafeb9a94 100644 --- a/lib/iris/tests/unit/cube/test_CubeRepresentation.py +++ b/lib/iris/tests/unit/cube/test_CubeRepresentation.py @@ -23,23 +23,8 @@ # importing anything else. import iris.tests as tests -from itertools import permutations - -import numpy as np -import numpy.ma as ma - -import iris.analysis -import iris.aux_factory from iris.coords import CellMethod -import iris.exceptions -from iris import FUTURE -from iris.analysis import WeightedAggregator, Aggregator -from iris.analysis import MEAN from iris.cube import _CubeRepresentation -from iris.coords import AuxCoord, DimCoord, CellMeasure -from iris.exceptions import (CoordinateNotFoundError, CellMeasureNotFoundError, - UnitConversionError) -from iris.tests import mock import iris.tests.stock as stock From ec4072d977bac01cf6cf43008a489fe6f20a18b6 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Thu, 16 Nov 2017 16:52:55 +0000 Subject: [PATCH 04/11] py2k fixes (!) --- lib/iris/cube.py | 10 ++++++---- lib/iris/tests/unit/cube/test_CubeRepresentation.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 3df79fb849..0fd150ecad 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -718,19 +718,21 @@ def _get_bits(self): continue else: start_inds.append(start_ind) + # Make sure the indices are in order. + start_inds = sorted(start_inds) # Mark the end of the file. - start_inds.append(0) + start_inds.append(None) # 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: + if i1 is not None: content = bits[i0 + 1: i1] else: content = bits[i0 + 1:] self.str_headings[str_heading_name] = content - def make_content(self): + def _make_content(self): elements = [] for k, v in self.str_headings.items(): if v is not None: @@ -749,7 +751,7 @@ def repr_html(self): """Produce an html representation of a cube and return it.""" self._get_bits() summary = self.summary - content = self.make_content() + content = self._make_content() return self._template.format(summary=summary, content=content, obj_id=self.cube_id, diff --git a/lib/iris/tests/unit/cube/test_CubeRepresentation.py b/lib/iris/tests/unit/cube/test_CubeRepresentation.py index afafeb9a94..6bf06a38d9 100644 --- a/lib/iris/tests/unit/cube/test_CubeRepresentation.py +++ b/lib/iris/tests/unit/cube/test_CubeRepresentation.py @@ -109,12 +109,12 @@ def test_headings__cellmethods(self): self.assertIn(str(cell_method), content_str) -class Test_make_content(tests.IrisTest): +class Test__make_content(tests.IrisTest): def setUp(self): self.cube = stock.simple_3d() self.representer = _CubeRepresentation(self.cube) self.representer._get_bits() - self.result = self.representer.make_content() + self.result = self.representer._make_content() def test_included(self): included = 'Dimension coordinates:' From c2821ba61c71386c21a19b588cccdadcfe9564f4 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Thu, 18 Jan 2018 15:22:19 +0000 Subject: [PATCH 05/11] Add data skipper --- lib/iris/tests/unit/cube/test_CubeRepresentation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/iris/tests/unit/cube/test_CubeRepresentation.py b/lib/iris/tests/unit/cube/test_CubeRepresentation.py index 6bf06a38d9..958017b0be 100644 --- a/lib/iris/tests/unit/cube/test_CubeRepresentation.py +++ b/lib/iris/tests/unit/cube/test_CubeRepresentation.py @@ -28,6 +28,7 @@ import iris.tests.stock as stock +@tests.skip_data class Test__instantiation(tests.IrisTest): def setUp(self): self.cube = stock.simple_3d() @@ -46,6 +47,7 @@ def test__heading_contents(self): self.assertIsNone(list(content)[0]) +@tests.skip_data class Test__get_bits(tests.IrisTest): def setUp(self): self.cube = stock.realistic_4d() @@ -109,6 +111,7 @@ def test_headings__cellmethods(self): self.assertIn(str(cell_method), content_str) +@tests.skip_data class Test__make_content(tests.IrisTest): def setUp(self): self.cube = stock.simple_3d() @@ -131,6 +134,7 @@ def test_not_included(self): self.assertNotIn(heading, self.result) +@tests.skip_data class Test_repr_html(tests.IrisTest): def setUp(self): self.cube = stock.simple_3d() From 807424528fcc21237ee3a76a419ec4a166f79992 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Mon, 22 Jan 2018 16:40:11 +0000 Subject: [PATCH 06/11] License header dates etc --- lib/iris/tests/unit/cube/test_CubeRepresentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/cube/test_CubeRepresentation.py b/lib/iris/tests/unit/cube/test_CubeRepresentation.py index 958017b0be..067d0c5f5e 100644 --- a/lib/iris/tests/unit/cube/test_CubeRepresentation.py +++ b/lib/iris/tests/unit/cube/test_CubeRepresentation.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2017, Met Office +# (C) British Crown Copyright 2017 - 2018, Met Office # # This file is part of Iris. # From 63136efa5446f45ce13a4e131886e9c48144a591 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Tue, 1 May 2018 15:16:28 +0100 Subject: [PATCH 07/11] Extract html representation from cube module --- lib/iris/cube.py | 158 +--------------- lib/iris/experimental/representation.py | 176 ++++++++++++++++++ .../test_CubeRepresentation.py | 10 +- 3 files changed, 183 insertions(+), 161 deletions(-) create mode 100644 lib/iris/experimental/representation.py rename lib/iris/tests/unit/{cube => experimental/representation}/test_CubeRepresentation.py (95%) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 0fd150ecad..123ca50572 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -50,6 +50,7 @@ import iris.coord_systems import iris.coords import iris.exceptions +from iris.experimental.representation import CubeRepresentation import iris.util @@ -603,161 +604,6 @@ def _is_single_item(testee): not isinstance(testee, collections.Iterable)) -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. - - """ - _template = """ - -
-
- -
- {content} -
-
-
- """ - - # Need to format the keywords: - # `emt_id`, `obj_id`, `str_heading`, `opened`, `content`. - _insert_content = """ - -
-
-

{content}

-
-
- """ - - def __init__(self, cube): - """ - Produce different representations of a :class:`~iris.cube.Cube`. - - Args: - - * cube - the cube to produce representations of. - - """ - - self.cube = cube - self.cube_id = id(self.cube) - self.cube_str = str(self.cube) - - self.summary = None - self.str_headings = { - 'Dimension coordinates:': None, - 'Auxiliary coordinates:': None, - 'Derived coordinates:': None, - 'Scalar coordinates:': None, - 'Attributes:': None, - 'Cell methods:': None, - } - self.major_headings = ['Dimension coordinates:', - 'Auxiliary coordinates:', - 'Attributes:'] - - def _get_bits(self): - """ - Parse the str representation of the cube to retrieve the elements - to add to an html representation of the cube. - - """ - bits = self.cube_str.split('\n') - self.summary = bits[0] - left_indent = bits[1].split('D')[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) - # Make sure the indices are in order. - start_inds = sorted(start_inds) - # Mark the end of the file. - start_inds.append(None) - - # 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 is not None: - content = bits[i0 + 1: i1] - else: - content = bits[i0 + 1:] - self.str_headings[str_heading_name] = content - - def _make_content(self): - elements = [] - for k, v in self.str_headings.items(): - if v is not None: - html_id = k.split(' ')[0].lower().strip(':') - content = '\n'.join(line for line in v) - collapse = ' in' if k in self.major_headings else '' - element = self._insert_content.format(emt_id=html_id, - obj_id=self.cube_id, - str_heading=k, - opened=collapse, - content=content) - elements.append(element) - return '\n'.join(element for element in elements) - - def repr_html(self): - """Produce an html representation of a cube and return it.""" - self._get_bits() - summary = self.summary - content = self._make_content() - return self._template.format(summary=summary, - content=content, - obj_id=self.cube_id, - ) - - class Cube(CFVariableMixin): """ A single Iris cube of data and metadata. @@ -2220,7 +2066,7 @@ def __repr__(self): name_padding=1) def _repr_html_(self): - representer = _CubeRepresentation(self) + representer = CubeRepresentation(self) return representer.repr_html() def __iter__(self): diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py new file mode 100644 index 0000000000..5c27d3e90f --- /dev/null +++ b/lib/iris/experimental/representation.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 . + +""" +Definitions of how Iris objects should be represented. + +""" + + +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. + + """ + _template = """ + +
+
+ +
+ {content} +
+
+
+ """ + + # Need to format the keywords: + # `emt_id`, `obj_id`, `str_heading`, `opened`, `content`. + _insert_content = """ + +
+
+

{content}

+
+
+ """ + + def __init__(self, cube): + """ + Produce different representations of a :class:`~iris.cube.Cube`. + + Args: + + * cube + the cube to produce representations of. + + """ + + self.cube = cube + self.cube_id = id(self.cube) + self.cube_str = str(self.cube) + + self.summary = None + self.str_headings = { + 'Dimension coordinates:': None, + 'Auxiliary coordinates:': None, + 'Derived coordinates:': None, + 'Scalar coordinates:': None, + 'Attributes:': None, + 'Cell methods:': None, + } + self.major_headings = ['Dimension coordinates:', + 'Auxiliary coordinates:', + 'Attributes:'] + + def _get_bits(self): + """ + Parse the str representation of the cube to retrieve the elements + to add to an html representation of the cube. + + """ + bits = self.cube_str.split('\n') + self.summary = bits[0] + left_indent = bits[1].split('D')[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) + # Make sure the indices are in order. + start_inds = sorted(start_inds) + # Mark the end of the file. + start_inds.append(None) + + # 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 is not None: + content = bits[i0 + 1: i1] + else: + content = bits[i0 + 1:] + self.str_headings[str_heading_name] = content + + def _make_content(self): + elements = [] + for k, v in self.str_headings.items(): + if v is not None: + html_id = k.split(' ')[0].lower().strip(':') + content = '\n'.join(line for line in v) + collapse = ' in' if k in self.major_headings else '' + element = self._insert_content.format(emt_id=html_id, + obj_id=self.cube_id, + str_heading=k, + opened=collapse, + content=content) + elements.append(element) + return '\n'.join(element for element in elements) + + def repr_html(self): + """Produce an html representation of a cube and return it.""" + self._get_bits() + summary = self.summary + content = self._make_content() + return self._template.format(summary=summary, + content=content, + obj_id=self.cube_id, + ) diff --git a/lib/iris/tests/unit/cube/test_CubeRepresentation.py b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py similarity index 95% rename from lib/iris/tests/unit/cube/test_CubeRepresentation.py rename to lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py index 067d0c5f5e..77c72f8fcb 100644 --- a/lib/iris/tests/unit/cube/test_CubeRepresentation.py +++ b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py @@ -24,7 +24,7 @@ import iris.tests as tests from iris.coords import CellMethod -from iris.cube import _CubeRepresentation +from iris.experimental.representation import CubeRepresentation import iris.tests.stock as stock @@ -32,7 +32,7 @@ class Test__instantiation(tests.IrisTest): def setUp(self): self.cube = stock.simple_3d() - self.representer = _CubeRepresentation(self.cube) + self.representer = CubeRepresentation(self.cube) def test_cube_attributes(self): self.assertEqual(id(self.cube), self.representer.cube_id) @@ -53,7 +53,7 @@ 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 = CubeRepresentation(self.cube) self.representer._get_bits() self.summary = self.representer.summary @@ -115,7 +115,7 @@ def test_headings__cellmethods(self): class Test__make_content(tests.IrisTest): def setUp(self): self.cube = stock.simple_3d() - self.representer = _CubeRepresentation(self.cube) + self.representer = CubeRepresentation(self.cube) self.representer._get_bits() self.result = self.representer._make_content() @@ -138,7 +138,7 @@ def test_not_included(self): class Test_repr_html(tests.IrisTest): def setUp(self): self.cube = stock.simple_3d() - representer = _CubeRepresentation(self.cube) + representer = CubeRepresentation(self.cube) self.result = representer.repr_html() def test_summary_added(self): From 060d16336c982024ced22723a771bfde6e10b69b Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Tue, 1 May 2018 16:39:29 +0100 Subject: [PATCH 08/11] Add default imports --- lib/iris/experimental/representation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py index 5c27d3e90f..197793d32d 100644 --- a/lib/iris/experimental/representation.py +++ b/lib/iris/experimental/representation.py @@ -20,6 +20,9 @@ """ +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + class CubeRepresentation(object): """ From 793a9e6d96f28968c64a515726663c33919907f3 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Tue, 8 May 2018 14:57:16 +0100 Subject: [PATCH 09/11] Use table format instead --- lib/iris/experimental/representation.py | 238 ++++++++++++------ .../representation/test_CubeRepresentation.py | 167 +++++++++++- 2 files changed, 321 insertions(+), 84 deletions(-) diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py index 197793d32d..3be612e874 100644 --- a/lib/iris/experimental/representation.py +++ b/lib/iris/experimental/representation.py @@ -23,6 +23,8 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa +import re + class CubeRepresentation(object): """ @@ -31,23 +33,49 @@ class CubeRepresentation(object): This includes: * ``_html_repr_``: a representation of the cube as an html object, - available in jupyter notebooks. + available in Jupyter notebooks. Specifically, this is presented as an + html table. """ + _template = """ -
-
- -
- {content} -
-
-
+ + {header} + {shape} + {content} +
""" - # Need to format the keywords: - # `emt_id`, `obj_id`, `str_heading`, `opened`, `content`. - _insert_content = """ - -
-
-

{content}

-
-
- """ - def __init__(self, cube): - """ - Produce different representations of a :class:`~iris.cube.Cube`. - - Args: - - * cube - the cube to produce representations of. - - """ - self.cube = cube self.cube_id = id(self.cube) self.cube_str = str(self.cube) - self.summary = None + self.summary = self.cube.summary(True) self.str_headings = { 'Dimension coordinates:': None, 'Auxiliary coordinates:': None, @@ -115,18 +107,32 @@ def __init__(self, cube): 'Attributes:': None, 'Cell methods:': None, } - self.major_headings = ['Dimension coordinates:', - 'Auxiliary coordinates:', - 'Attributes:'] + self.dim_desc_coords = ['Dimension coordinates:', + 'Auxiliary coordinates:', + 'Derived coordinates:'] - def _get_bits(self): + def _summary_content(self): """ - Parse the str representation of the cube to retrieve the elements - to add to an html representation of the cube. + Deal with the content in the summary (the first line of printout). + + This contains: + * name (unit), + * dim name (len of dim) for each dim. """ + emts = re.findall(r'\w+', self.summary) + self.names = [' '.join(name.split('_')) for name in emts[::2]] + self.shapes = emts[1::2] + + # Name and unit are the first item in names and descs respectively. + self.name = self.names.pop(0).title() + self.units = self.shapes.pop(0) + self.ndims = self.cube.ndim + + def _get_bits(self): bits = self.cube_str.split('\n') - self.summary = bits[0] + # self.summary = bits[0] + self._summary_content() left_indent = bits[1].split('D')[0] # Get heading indices within the printout. @@ -139,41 +145,123 @@ def _get_bits(self): continue else: start_inds.append(start_ind) - # Make sure the indices are in order. - start_inds = sorted(start_inds) # Mark the end of the file. - start_inds.append(None) + 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 is not None: + 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: - html_id = k.split(' ')[0].lower().strip(':') - content = '\n'.join(line for line in v) - collapse = ' in' if k in self.major_headings else '' - element = self._insert_content.format(emt_id=html_id, - obj_id=self.cube_id, - str_heading=k, - opened=collapse, - content=content) - elements.append(element) + # 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): - """Produce an html representation of a cube and return it.""" + """The `repr` interface to Jupyter.""" self._get_bits() - summary = self.summary + header = self._make_header() + shape = self._make_shapes_row() content = self._make_content() - return self._template.format(summary=summary, - content=content, - obj_id=self.cube_id, - ) + 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 77c72f8fcb..1a3cda0d75 100644 --- a/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py +++ b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py @@ -38,15 +38,53 @@ 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_summary(self): - self.assertIsNone(self.representer.summary) - 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__summary_content(tests.IrisTest): + def setUp(self): + self.cube = stock.realistic_4d() + self.representer = CubeRepresentation(self.cube) + self.representer._summary_content() + + 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 = [str(s) for s in 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): @@ -111,6 +149,120 @@ def test_headings__cellmethods(self): 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.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.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() + + 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): @@ -120,7 +272,7 @@ def setUp(self): self.result = self.representer._make_content() def test_included(self): - included = 'Dimension coordinates:' + 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: @@ -141,13 +293,10 @@ def setUp(self): representer = CubeRepresentation(self.cube) self.result = representer.repr_html() - def test_summary_added(self): - self.assertIn(self.cube.summary(True), self.result) - def test_contents_added(self): - included = 'Dimension coordinates:' + included = 'Dimension coordinates' self.assertIn(included, self.result) - not_included = 'Auxiliary coordinates:' + not_included = 'Auxiliary coordinates' self.assertNotIn(not_included, self.result) From ffc0d4090a1afcfe7a1e2b9ebb182bfefc78183b Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Tue, 8 May 2018 15:13:26 +0100 Subject: [PATCH 10/11] Outstanding points --- .../contributions_2.1/newfeature_2018-May-08_repr-html.txt | 2 ++ lib/iris/cube.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 docs/iris/src/whatsnew/contributions_2.1/newfeature_2018-May-08_repr-html.txt 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 123ca50572..cd72a39f58 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -50,7 +50,6 @@ import iris.coord_systems import iris.coords import iris.exceptions -from iris.experimental.representation import CubeRepresentation import iris.util @@ -2066,6 +2065,7 @@ def __repr__(self): name_padding=1) def _repr_html_(self): + from iris.experimental.representation import CubeRepresentation representer = CubeRepresentation(self) return representer.repr_html() From 86edc2e5ac9813a301f7207ea15ae574eeb35ae6 Mon Sep 17 00:00:00 2001 From: Peter Killick Date: Wed, 30 May 2018 16:25:59 +0100 Subject: [PATCH 11/11] Improved cube handling --- lib/iris/experimental/representation.py | 85 ++++++--- .../experimental/test_CubeRepresentation.py | 176 ++++++++++++++++++ .../representation/test_CubeRepresentation.py | 59 ++++-- 3 files changed, 282 insertions(+), 38 deletions(-) create mode 100644 lib/iris/tests/integration/experimental/test_CubeRepresentation.py diff --git a/lib/iris/experimental/representation.py b/lib/iris/experimental/representation.py index 3be612e874..5adef1f06e 100644 --- a/lib/iris/experimental/representation.py +++ b/lib/iris/experimental/representation.py @@ -98,7 +98,6 @@ def __init__(self, cube): self.cube_id = id(self.cube) self.cube_str = str(self.cube) - self.summary = self.cube.summary(True) self.str_headings = { 'Dimension coordinates:': None, 'Auxiliary coordinates:': None, @@ -111,29 +110,54 @@ def __init__(self, cube): 'Auxiliary coordinates:', 'Derived coordinates:'] - def _summary_content(self): + # 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): """ - Deal with the content in the summary (the first line of printout). + Get dimension-describing coordinate names, or '--' if no coordinate] + describes the dimension. - This contains: - * name (unit), - * dim name (len of dim) for each dim. + Note: borrows from `cube.summary`. """ - emts = re.findall(r'\w+', self.summary) - self.names = [' '.join(name.split('_')) for name in emts[::2]] - self.shapes = emts[1::2] + # Create a set to contain the axis names for each data dimension. + dim_names = list(range(len(self.cube.shape))) - # Name and unit are the first item in names and descs respectively. - self.name = self.names.pop(0).title() - self.units = self.shapes.pop(0) - self.ndims = self.cube.ndim + # 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 = self.cube_str.split('\n') - # self.summary = bits[0] - self._summary_content() - left_indent = bits[1].split('D')[0] + 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 = [] @@ -256,11 +280,28 @@ def _make_content(self): return '\n'.join(element for element in elements) def repr_html(self): - """The `repr` interface to Jupyter.""" - self._get_bits() + """The `repr` interface for Jupyter.""" + # Deal with the header first. header = self._make_header() - shape = self._make_shapes_row() - content = self._make_content() + + # 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, 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 index 1a3cda0d75..c94742a49a 100644 --- a/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py +++ b/lib/iris/tests/unit/experimental/representation/test_CubeRepresentation.py @@ -24,9 +24,10 @@ import iris.tests as tests from iris.coords import CellMethod -from iris.experimental.representation import CubeRepresentation import iris.tests.stock as stock +from iris.experimental.representation import CubeRepresentation + @tests.skip_data class Test__instantiation(tests.IrisTest): @@ -45,11 +46,44 @@ def test__heading_contents(self): @tests.skip_data -class Test__summary_content(tests.IrisTest): +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) - self.representer._summary_content() def test_name(self): # Check the cube name is being set and formatted correctly. @@ -75,7 +109,7 @@ def test_units(self): def test_shapes(self): # Check cube dim lengths are split out correctly from the # summary string. - expected = [str(s) for s in self.cube.shape] + expected = self.cube.shape result = self.representer.shapes self.assertEqual(expected, result) @@ -92,19 +126,12 @@ def setUp(self): cm = CellMethod('mean', 'time', '6hr') self.cube.add_cell_method(cm) self.representer = CubeRepresentation(self.cube) - self.representer._get_bits() - self.summary = self.representer.summary + self.representer._get_bits(self.representer._get_lines()) def test_population(self): - self.assertIsNotNone(self.summary) for v in self.representer.str_headings.values(): self.assertIsNotNone(v) - def test_summary(self): - expected = self.cube.summary(True) - result = self.summary - self.assertStringEqual(expected, result) - def test_headings__dimcoords(self): contents = self.representer.str_headings['Dimension coordinates:'] content_str = ','.join(content for content in contents) @@ -154,7 +181,7 @@ 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_bits(self.representer._get_lines()) self.header_emts = self.representer._make_header().split('\n') def test_name_and_units(self): @@ -186,7 +213,7 @@ 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_bits(self.representer._get_lines()) self.result = self.representer._make_shapes_row().split('\n') def test_row_title(self): @@ -207,7 +234,7 @@ def setUp(self): cm = CellMethod('mean', 'time', '6hr') self.cube.add_cell_method(cm) self.representer = CubeRepresentation(self.cube) - self.representer._get_bits() + self.representer._get_bits(self.representer._get_lines()) def test__title_row(self): title = 'Wibble:' @@ -268,7 +295,7 @@ 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_bits(self.representer._get_lines()) self.result = self.representer._make_content() def test_included(self):