Skip to content

Commit

Permalink
Cubesummary tidy (#3988)
Browse files Browse the repository at this point in the history
* Extra tests; fix for array attributes.

* Docstring for CubeSummary, and remove some unused parts.

* Fix section name capitalisation, in line with existing cube summary.

* Handle array differences; quote strings in extras and if 'awkward'-printing.

* Ensure scalar string coord 'content' prints on one line.
  • Loading branch information
pp-mo authored Feb 10, 2021
1 parent e378eb8 commit e3c1905
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 30 deletions.
72 changes: 58 additions & 14 deletions lib/iris/_representation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
"""
Provides objects describing cube summaries.
"""
import re

import iris.util
from iris.common.metadata import _hexdigest as quickhash


class DimensionHeader:
Expand Down Expand Up @@ -46,6 +48,35 @@ def __init__(self, cube, name_padding=35):
self.dimension_header = DimensionHeader(cube)


def string_repr(text, quote_strings=False):
"""Produce a one-line printable form of a text string."""
if re.findall("[\n\t]", text) or quote_strings:
# Replace the string with its repr (including quotes).
text = repr(text)
return text


def array_repr(arr):
"""Produce a single-line printable repr of an array."""
# First take whatever numpy produces..
text = repr(arr)
# ..then reduce any multiple spaces and newlines.
text = re.sub("[ \t\n]+", " ", text)
return text


def value_repr(value, quote_strings=False):
"""
Produce a single-line printable version of an attribute or scalar value.
"""
if hasattr(value, "dtype"):
value = array_repr(value)
elif isinstance(value, str):
value = string_repr(value, quote_strings=quote_strings)
value = str(value)
return value


class CoordSummary:
def _summary_coord_extra(self, cube, coord):
# Returns the text needed to ensure this coordinate can be
Expand All @@ -66,12 +97,21 @@ def _summary_coord_extra(self, cube, coord):
vary.add(key)
break
value = similar_coord.attributes[key]
if attributes.setdefault(key, value) != value:
# Like "if attributes.setdefault(key, value) != value:"
# ..except setdefault fails if values are numpy arrays.
if key not in attributes:
attributes[key] = value
elif quickhash(attributes[key]) != quickhash(value):
# NOTE: fast and array-safe comparison, as used in
# :mod:`iris.common.metadata`.
vary.add(key)
break
keys = sorted(vary & set(coord.attributes.keys()))
bits = [
"{}={!r}".format(key, coord.attributes[key]) for key in keys
"{}={}".format(
key, value_repr(coord.attributes[key], quote_strings=True)
)
for key in keys
]
if bits:
extra = ", ".join(bits)
Expand Down Expand Up @@ -105,13 +145,17 @@ def __init__(self, cube, coord):
coord_cell = coord.cell(0)
if isinstance(coord_cell.point, str):
self.string_type = True
# 'lines' is value split on '\n', and _each one_ length-clipped.
self.lines = [
iris.util.clip_string(str(item))
for item in coord_cell.point.split("\n")
]
self.point = None
self.bound = None
self.content = "\n".join(self.lines)
# 'content' contains a one-line printable version of the string,
content = string_repr(coord_cell.point)
content = iris.util.clip_string(content)
self.content = content
else:
self.string_type = False
self.lines = None
Expand All @@ -132,9 +176,6 @@ def __init__(self, cube, coord):


class Section:
def _init_(self):
self.contents = []

def is_empty(self):
return self.contents == []

Expand Down Expand Up @@ -166,7 +207,8 @@ def __init__(self, title, attributes):
self.values = []
self.contents = []
for name, value in sorted(attributes.items()):
value = iris.util.clip_string(str(value))
value = value_repr(value)
value = iris.util.clip_string(value)
self.names.append(name)
self.values.append(value)
content = "{}: {}".format(name, value)
Expand All @@ -180,11 +222,13 @@ def __init__(self, title, cell_methods):


class CubeSummary:
"""
This class provides a structure for output representations of an Iris cube.
TODO: use to produce the printout of :meth:`iris.cube.Cube.__str__`.
"""

def __init__(self, cube, shorten=False, name_padding=35):
self.section_indent = 5
self.item_indent = 10
self.extra_indent = 13
self.shorten = shorten
self.header = FullHeader(cube, name_padding)

# Cache the derived coords so we can rely on consistent
Expand Down Expand Up @@ -249,9 +293,9 @@ def add_vector_section(title, contents, iscoord=True):
add_vector_section("Dimension coordinates:", vector_dim_coords)
add_vector_section("Auxiliary coordinates:", vector_aux_coords)
add_vector_section("Derived coordinates:", vector_derived_coords)
add_vector_section("Cell Measures:", vector_cell_measures, False)
add_vector_section("Cell measures:", vector_cell_measures, False)
add_vector_section(
"Ancillary Variables:", vector_ancillary_variables, False
"Ancillary variables:", vector_ancillary_variables, False
)

self.scalar_sections = {}
Expand All @@ -260,7 +304,7 @@ def add_scalar_section(section_class, title, *args):
self.scalar_sections[title] = section_class(title, *args)

add_scalar_section(
ScalarSection, "Scalar Coordinates:", cube, scalar_coords
ScalarSection, "Scalar coordinates:", cube, scalar_coords
)
add_scalar_section(
ScalarCellMeasureSection,
Expand Down
149 changes: 133 additions & 16 deletions lib/iris/tests/unit/representation/test_representation.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def test_blank_cube(self):
"Dimension coordinates:",
"Auxiliary coordinates:",
"Derived coordinates:",
"Cell Measures:",
"Ancillary Variables:",
"Cell measures:",
"Ancillary variables:",
]
self.assertEqual(
list(rep.vector_sections.keys()), expected_vector_sections
Expand All @@ -66,7 +66,7 @@ def test_blank_cube(self):
self.assertTrue(vector_section.is_empty())

expected_scalar_sections = [
"Scalar Coordinates:",
"Scalar coordinates:",
"Scalar cell measures:",
"Attributes:",
"Cell methods:",
Expand Down Expand Up @@ -103,21 +103,28 @@ def test_scalar_coord(self):
scalar_coord_with_bounds = AuxCoord(
[10], long_name="foo", units="K", bounds=[(5, 15)]
)
scalar_coord_text = AuxCoord(
["a\nb\nc"], long_name="foo", attributes={"key": "value"}
scalar_coord_simple_text = AuxCoord(
["this and that"],
long_name="foo",
attributes={"key": 42, "key2": "value-str"},
)
scalar_coord_awkward_text = AuxCoord(
["a is\nb\n and c"], long_name="foo_2"
)
cube.add_aux_coord(scalar_coord_no_bounds)
cube.add_aux_coord(scalar_coord_with_bounds)
cube.add_aux_coord(scalar_coord_text)
cube.add_aux_coord(scalar_coord_simple_text)
cube.add_aux_coord(scalar_coord_awkward_text)
rep = iris._representation.CubeSummary(cube)

scalar_section = rep.scalar_sections["Scalar Coordinates:"]
scalar_section = rep.scalar_sections["Scalar coordinates:"]

self.assertEqual(len(scalar_section.contents), 3)
self.assertEqual(len(scalar_section.contents), 4)

no_bounds_summary = scalar_section.contents[0]
bounds_summary = scalar_section.contents[1]
text_summary = scalar_section.contents[2]
text_summary_simple = scalar_section.contents[2]
text_summary_awkward = scalar_section.contents[3]

self.assertEqual(no_bounds_summary.name, "bar")
self.assertEqual(no_bounds_summary.content, "10 K")
Expand All @@ -127,17 +134,23 @@ def test_scalar_coord(self):
self.assertEqual(bounds_summary.content, "10 K, bound=(5, 15) K")
self.assertEqual(bounds_summary.extra, "")

self.assertEqual(text_summary.name, "foo")
self.assertEqual(text_summary.content, "a\nb\nc")
self.assertEqual(text_summary.extra, "key='value'")
self.assertEqual(text_summary_simple.name, "foo")
self.assertEqual(text_summary_simple.content, "this and that")
self.assertEqual(text_summary_simple.lines, ["this and that"])
self.assertEqual(text_summary_simple.extra, "key=42, key2='value-str'")

self.assertEqual(text_summary_awkward.name, "foo_2")
self.assertEqual(text_summary_awkward.content, r"'a is\nb\n and c'")
self.assertEqual(text_summary_awkward.lines, ["a is", "b", " and c"])
self.assertEqual(text_summary_awkward.extra, "")

def test_cell_measure(self):
cube = self.cube
cell_measure = CellMeasure([1, 2, 3], long_name="foo")
cube.add_cell_measure(cell_measure, 0)
rep = iris._representation.CubeSummary(cube)

cm_section = rep.vector_sections["Cell Measures:"]
cm_section = rep.vector_sections["Cell measures:"]
self.assertEqual(len(cm_section.contents), 1)

cm_summary = cm_section.contents[0]
Expand All @@ -150,7 +163,7 @@ def test_ancillary_variable(self):
cube.add_ancillary_variable(cell_measure, 0)
rep = iris._representation.CubeSummary(cube)

av_section = rep.vector_sections["Ancillary Variables:"]
av_section = rep.vector_sections["Ancillary variables:"]
self.assertEqual(len(av_section.contents), 1)

av_summary = av_section.contents[0]
Expand All @@ -159,12 +172,14 @@ def test_ancillary_variable(self):

def test_attributes(self):
cube = self.cube
cube.attributes = {"a": 1, "b": "two"}
cube.attributes = {"a": 1, "b": "two", "c": " this \n that\tand."}
rep = iris._representation.CubeSummary(cube)

attribute_section = rep.scalar_sections["Attributes:"]
attribute_contents = attribute_section.contents
expected_contents = ["a: 1", "b: two"]
expected_contents = ["a: 1", "b: two", "c: ' this \\n that\\tand.'"]
# Note: a string with \n or \t in it gets "repr-d".
# Other strings don't (though in coord 'extra' lines, they do.)

self.assertEqual(attribute_contents, expected_contents)

Expand All @@ -182,6 +197,108 @@ def test_cell_methods(self):
expected_contents = ["mean: x, y", "mean: x"]
self.assertEqual(cell_method_section.contents, expected_contents)

def test_scalar_cube(self):
cube = self.cube
while cube.ndim > 0:
cube = cube[0]
rep = iris._representation.CubeSummary(cube)
self.assertEqual(rep.header.nameunit, "air_temperature / (K)")
self.assertTrue(rep.header.dimension_header.scalar)
self.assertEqual(rep.header.dimension_header.dim_names, [])
self.assertEqual(rep.header.dimension_header.shape, [])
self.assertEqual(rep.header.dimension_header.contents, ["scalar cube"])
self.assertEqual(len(rep.vector_sections), 5)
self.assertTrue(
all(sect.is_empty() for sect in rep.vector_sections.values())
)
self.assertEqual(len(rep.scalar_sections), 4)
self.assertEqual(
len(rep.scalar_sections["Scalar coordinates:"].contents), 1
)
self.assertTrue(
rep.scalar_sections["Scalar cell measures:"].is_empty()
)
self.assertTrue(rep.scalar_sections["Attributes:"].is_empty())
self.assertTrue(rep.scalar_sections["Cell methods:"].is_empty())

def test_coord_attributes(self):
cube = self.cube
co1 = cube.coord("latitude")
co1.attributes.update(dict(a=1, b=2))
co2 = co1.copy()
co2.attributes.update(dict(a=7, z=77, text="ok", text2="multi\nline"))
cube.add_aux_coord(co2, cube.coord_dims(co1))
rep = iris._representation.CubeSummary(cube)
co1_summ = rep.vector_sections["Dimension coordinates:"].contents[0]
co2_summ = rep.vector_sections["Auxiliary coordinates:"].contents[0]
# Notes: 'b' is same so does not appear; sorted order; quoted strings.
self.assertEqual(co1_summ.extra, "a=1")
self.assertEqual(
co2_summ.extra, "a=7, text='ok', text2='multi\\nline', z=77"
)

def test_array_attributes(self):
cube = self.cube
co1 = cube.coord("latitude")
co1.attributes.update(dict(a=1, array=np.array([1.2, 3])))
co2 = co1.copy()
co2.attributes.update(dict(b=2, array=np.array([3.2, 1])))
cube.add_aux_coord(co2, cube.coord_dims(co1))
rep = iris._representation.CubeSummary(cube)
co1_summ = rep.vector_sections["Dimension coordinates:"].contents[0]
co2_summ = rep.vector_sections["Auxiliary coordinates:"].contents[0]
self.assertEqual(co1_summ.extra, "array=array([1.2, 3. ])")
self.assertEqual(co2_summ.extra, "array=array([3.2, 1. ]), b=2")

def test_attributes_subtle_differences(self):
cube = Cube([0])

# Add a pair that differ only in having a list instead of an array.
co1a = DimCoord(
[0],
long_name="co1_list_or_array",
attributes=dict(x=1, arr1=np.array(2), arr2=np.array([1, 2])),
)
co1b = co1a.copy()
co1b.attributes.update(dict(arr2=[1, 2]))
for co in (co1a, co1b):
cube.add_aux_coord(co)

# Add a pair that differ only in an attribute array dtype.
co2a = AuxCoord(
[0],
long_name="co2_dtype",
attributes=dict(x=1, arr1=np.array(2), arr2=np.array([3, 4])),
)
co2b = co2a.copy()
co2b.attributes.update(dict(arr2=np.array([3.0, 4.0])))
assert co2b != co2a
for co in (co2a, co2b):
cube.add_aux_coord(co)

# Add a pair that differ only in an attribute array shape.
co3a = DimCoord(
[0],
long_name="co3_shape",
attributes=dict(x=1, arr1=np.array([5, 6]), arr2=np.array([3, 4])),
)
co3b = co3a.copy()
co3b.attributes.update(dict(arr1=np.array([[5], [6]])))
for co in (co3a, co3b):
cube.add_aux_coord(co)

rep = iris._representation.CubeSummary(cube)
co_summs = rep.scalar_sections["Scalar coordinates:"].contents
co1a_summ, co1b_summ = co_summs[0:2]
self.assertEqual(co1a_summ.extra, "arr2=array([1, 2])")
self.assertEqual(co1b_summ.extra, "arr2=[1, 2]")
co2a_summ, co2b_summ = co_summs[2:4]
self.assertEqual(co2a_summ.extra, "arr2=array([3, 4])")
self.assertEqual(co2b_summ.extra, "arr2=array([3., 4.])")
co3a_summ, co3b_summ = co_summs[4:6]
self.assertEqual(co3a_summ.extra, "arr1=array([5, 6])")
self.assertEqual(co3b_summ.extra, "arr1=array([[5], [6]])")


if __name__ == "__main__":
tests.main()

0 comments on commit e3c1905

Please sign in to comment.