Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cubesummary tidy #3988

Merged
merged 5 commits into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
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")
Copy link
Contributor

Choose a reason for hiding this comment

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

For the sake of testing numpy array equality, it might also be worth testing the case where two arrays are equal and the case where arrays have different shapes.


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()