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 nobt #3982

Closed
wants to merge 18 commits into from
Closed
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
9 changes: 9 additions & 0 deletions lib/iris/_representation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright Iris contributors
#
# This file is part of Iris and is released under the LGPL license.
# See COPYING and COPYING.LESSER in the root of the repository for full
# licensing details.
"""
Code to make printouts and other representations (e.g. html) of Iris objects.

"""
311 changes: 311 additions & 0 deletions lib/iris/_representation/cube_printout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
# Copyright Iris contributors
#
# This file is part of Iris and is released under the LGPL license.
# See COPYING and COPYING.LESSER in the root of the repository for full
# licensing details.
"""
Provides text printouts of Iris cubes.

"""
from copy import deepcopy


class Row:
def __init__(self, cols, aligns, i_col_unlimited=None):
assert len(cols) == len(aligns)
self.cols = cols
self.aligns = aligns
self.i_col_unlimited = i_col_unlimited
# This col + those after do not add to width
# - a crude alternative to proper column spanning


class Table:
def __init__(self, rows=None, col_widths=None):
if rows is None:
rows = []
self.rows = [deepcopy(row) for row in rows]
self.col_widths = col_widths

def copy(self):
return Table(self.rows, col_widths=self.col_widths)

@property
def n_columns(self):
if self.rows:
result = len(self.rows[0].cols)
else:
result = None
return result

def add_row(self, cols, aligns, i_col_unlimited=None):
n_cols = len(cols)
assert len(aligns) == n_cols
if self.n_columns is not None:
# For now, all rows must have same number of columns
assert n_cols == self.n_columns
row = Row(cols, aligns, i_col_unlimited)
self.rows.append(row)

def set_min_column_widths(self):
"""Set all column widths to minimum required for current content."""
if self.rows:
widths = [0] * self.n_columns
for row in self.rows:
cols, lim = row.cols, row.i_col_unlimited
if lim is not None:
cols = cols[:lim] # Ignore "unlimited" columns
for i_col, col in enumerate(cols):
widths[i_col] = max(widths[i_col], len(col))

self.col_widths = widths

def formatted_as_strings(self):
"""Return lines formatted to the set column widths."""
if self.col_widths is None:
# If not set, calculate minimum widths.
self.set_min_column_widths()
result_lines = []
for row in self.rows:
col_texts = []
for col, align, width in zip(
row.cols, row.aligns, self.col_widths
):
if align == "left":
col_text = col.ljust(width)
elif align == "right":
col_text = col.rjust(width)
else:
msg = (
f'Unknown alignment "{align}" '
'not in ("left", "right")'
)
raise ValueError(msg)
col_texts.append(col_text)
result_lines.append(" ".join(col_texts))
return result_lines


class CubePrinter:
"""
An object created from a
:class:`iris._representation.cube_summary.CubeSummary`, which provides
text printout of a :class:`iris.cube.Cube`.

The cube :meth:`iris.cube.Cube.__str__` and
:meth:`iris.cube.Cube.__repr__` methods, and
:meth:`iris.cube.Cube.summary` with 'oneline=True', also use this to
produce cube summary strings.

In principle, this class does not have any internal knowledge of
:class:`iris.cube.Cube`, but only of
:class:`iris._representation.cube_summary.CubeSummary`.

"""

def __init__(self, cube_summary):
# Create our internal table from the summary, to produce the printouts.
self.table = self._ingest_summary(cube_summary)

def _ingest_summary(
self,
cube_summary,
n_indent_section=4,
n_indent_item=4,
n_indent_extra=4,
):
"""Make a table of strings representing the cube-summary."""
sect_indent = " " * n_indent_section
item_indent = sect_indent + " " * n_indent_item
item_to_extra_indent = " " * n_indent_extra
extra_indent = item_indent + item_to_extra_indent
summ = cube_summary

fullheader = summ.header
nameunits_string = fullheader.nameunit
dimheader = fullheader.dimension_header
cube_is_scalar = dimheader.scalar

cube_shape = dimheader.shape # may be empty
dim_names = dimheader.dim_names # may be empty
n_dims = len(dim_names)
assert len(cube_shape) == n_dims

# First setup the columns
# - x1 @0 column-1 content : main title; headings; elements-names
# - x1 @1 "value" content (for scalar items)
# - OR x2n @1.. (name, length) for each of n dimensions
column_header_texts = [nameunits_string] # Note extra spacer here

if cube_is_scalar:
# We will put this in the column-1 position (replacing the dim-map)
column_header_texts.append("(scalar cube)")
else:
for dim_name, length in zip(dim_names, cube_shape):
column_header_texts.append(f"{dim_name}:")
column_header_texts.append(f"{length:d}")

n_cols = len(column_header_texts)

# Create a table : a (n_rows) list of (n_cols) strings

table = Table()

# Code for adding a row, with control options.
scalar_column_aligns = ["left"] * n_cols
vector_column_aligns = deepcopy(scalar_column_aligns)
if cube_is_scalar:
vector_column_aligns[1] = "left"
else:
vector_column_aligns[1:] = n_dims * ["right", "left"]

def add_row(col_texts, scalar=False):
aligns = scalar_column_aligns if scalar else vector_column_aligns
i_col_unlimited = 1 if scalar else None
n_missing = n_cols - len(col_texts)
col_texts += [" "] * n_missing
table.add_row(col_texts, aligns, i_col_unlimited=i_col_unlimited)

# Start with the header line
add_row(column_header_texts)

# Add rows from all the vector sections
for sect in summ.vector_sections.values():
if sect.contents:
sect_name = sect.title
column_texts = [sect_indent + sect_name]
add_row(column_texts)
for vec_summary in sect.contents:
element_name = vec_summary.name
dim_chars = vec_summary.dim_chars
extra_string = vec_summary.extra
column_texts = [item_indent + element_name]
for dim_char in dim_chars:
column_texts += ["", dim_char]
add_row(column_texts)
if extra_string:
column_texts = [extra_indent + extra_string]
add_row(column_texts)

# Similar for scalar sections
for sect in summ.scalar_sections.values():
if sect.contents:
# Add a row for the "section title" text.
sect_name = sect.title
add_row([sect_indent + sect_name])

def add_scalar_row(name, value=""):
column_texts = [item_indent + name, value]
add_row(column_texts, scalar=True)

# Add a row for each item
# NOTE: different section types handle differently
title = sect_name.lower()
if "scalar coordinate" in title:
for item in sect.contents:
add_scalar_row(item.name, item.content)
if item.extra:
add_scalar_row(item_to_extra_indent + item.extra)
elif "attribute" in title:
for title, value in zip(sect.names, sect.values):
add_scalar_row(title, value)
elif "scalar cell measure" in title or "cell method" in title:
# These are just strings: nothing in the 'value' column.
for name in sect.contents:
add_scalar_row(name)
# elif "mesh" in title:
# for line in sect.contents()
# add_scalar_row(line)
else:
msg = f"Unknown section type : {type(sect)}"
raise ValueError(msg)

return table

@staticmethod
def _decorated_table(table, name_padding=None):
"""Return a modified table with added characters in the header."""

# Copy the input table + extract the header + its columns.
table = table.copy()
header = table.rows[0]
cols = header.cols

if name_padding:
# Extend header column#0 to a given minimum width.
cols[0] = cols[0].ljust(name_padding)

# Add parentheses around the dim column texts, unless already present
# - e.g. "(scalar cube)".
if len(cols) > 1 and not cols[1].startswith("("):
# Add parentheses around the dim columns
cols[1] = "(" + cols[1]
cols[-1] = cols[-1] + ")"

# Add semicolons as dim column spacers
for i_col in range(2, len(cols) - 1, 2):
cols[i_col] += ";"

# Modify the new table to be returned, invalidate any stored widths.
header.cols = cols
table.rows[0] = header

# Recalc widths
table.set_min_column_widths()

# Add extra spacing between columns 0 and 1
# (Ok because col#0 is left-aligned)
table.col_widths[0] += 1

return table

def _oneline_string(self):
"""Produce a one-line summary string."""
# Copy existing content -- just the header line.
table = Table(rows=[self.table.rows[0]])
# Note: by excluding other columns, we get a minimum-width result.

# Add standard decorations.
table = self._decorated_table(table, name_padding=0)

# Format (with no extra spacing) --> one-line result
(oneline_result,) = table.formatted_as_strings()
return oneline_result

def _multiline_summary(self, max_width):
"""Produce a multi-line summary string."""
# Get a derived table with standard 'decorations' added.
table = self._decorated_table(self.table, name_padding=35)
result_lines = table.formatted_as_strings()
result = "\n".join(result_lines)
return result

def to_string(self, oneline=False, max_width=None):
"""
Produce a printable summary.

Args:
* oneline (bool):
If set, produce a one-line summary (without any extra spacings).
Default is False = produce full (multiline) summary.
* max_width (int):
If set, override the default maximum output width.
Default is None = use the default established at object creation.

Returns:
result (string)

"""
# if max_width is None:
# max_width = self.max_width

if oneline:
result = self._oneline_string()
else:
result = self._multiline_summary(max_width)

return result

def __str__(self):
"""Printout of self is the full multiline string."""
return self.to_string()
Loading