Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import iris.coord_systems
import iris.coords
import iris.exceptions
from iris.experimental.representation import CubeRepresentation
import iris.util


Expand Down Expand Up @@ -2064,6 +2065,10 @@ def __repr__(self):
return "<iris 'Cube' of %s>" % 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')

Expand Down
179 changes: 179 additions & 0 deletions lib/iris/experimental/representation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# (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 <http://www.gnu.org/licenses/>.

"""
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


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 = """
<style>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we wouldn't re-define the style each time we do a repr. I think there is (should be) a hook for this somewhere, but I did a similar hack for the matplotlib notebook backend and it does work effectively.

a.iris {{
text-decoration: none !important;
}}
.iris {{
white-space: pre;
}}
.iris-panel-group {{
display: block;
overflow: visible;
width: max-content;
font-family: monaco, monospace;
}}
.iris-panel-body {{
padding-top: 0px;
}}
.iris-panel-title {{
padding-left: 3em;
}}
.iris-panel-title {{
margin-top: 7px;
}}
</style>
<div class="panel-group iris-panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="iris" data-toggle="collapse" href="#collapse1-{obj_id}">
{summary}
</a>
</h4>
</div>
<div id="collapse1-{obj_id}" class="panel-collapse collapse in">
{content}
</div>
</div>
</div>
"""

# Need to format the keywords:
# `emt_id`, `obj_id`, `str_heading`, `opened`, `content`.
_insert_content = """
<div class="panel-body iris-panel-body">
<h4 class="panel-title iris-panel-title">
<a class="iris" data-toggle="collapse" href="#{emt_id}-{obj_id}">
{str_heading}
</a>
</h4>
</div>
<div id="{emt_id}-{obj_id}" class="panel-collapse collapse{opened}">
<div class="panel-body iris-panel-body">
<p class="iris">{content}</p>
</div>
</div>
"""

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,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# (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 <http://www.gnu.org/licenses/>.
"""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
from iris.experimental.representation import CubeRepresentation
import iris.tests.stock as stock


@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_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__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)


@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.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_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()